管理界面增删改 #86

Merged
pc8xi2fbj merged 6 commits from zhanghongwei_branch into develop 4 weeks ago

@ -6,11 +6,19 @@ export async function request<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
console.log(`🌐 发送请求: ${API_BASE_URL}${url}`, {
method: options.method || 'GET',
// 处理日志数据GET/HEAD 方法不显示 body
const method = options.method?.toUpperCase() || 'GET';
const logData: Record<string, any> = {
method,
headers: options.headers,
body: options.body ? JSON.parse(options.body as string) : undefined,
})
};
// 只对非 GET/HEAD 方法显示 body
if (!['GET', 'HEAD'].includes(method)) {
logData.body = options.body ? JSON.parse(options.body as string) : undefined;
}
console.log(`🌐 发送请求: ${API_BASE_URL}${url}`, logData)
const defaultOptions: RequestInit = {
headers: {
@ -39,10 +47,13 @@ export async function request<T>(
}
try {
const response = await fetch(`${API_BASE_URL}${url}`, {
...defaultOptions,
...options,
})
// 确保 GET/HEAD 请求不包含 body
const fetchOptions = { ...defaultOptions, ...options };
if (['GET', 'HEAD'].includes(method)) {
delete fetchOptions.body;
}
const response = await fetch(`${API_BASE_URL}${url}`, fetchOptions)
console.log('📥 响应状态:', response.status, response.statusText)

@ -0,0 +1,2 @@
// src/api/types/repairman.ts
export type RepairmanStatus = 'idle' | 'busy' | 'vacation'

@ -0,0 +1,10 @@
// src/api/types/workorder.ts
export interface WorkOrder {
orderId: string
deviceId: string
areaId: string
description: string
status: 'pending' | 'processing' | 'reviewing' | 'completed' | 'timeout'
createdTime?: string
assignedRepairmanId?: string
}

@ -4,6 +4,7 @@ import LoginView from '../views/LoginView.vue'
import MainLayout from '../components/layout/MainLayout.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@ -122,22 +123,42 @@ const router = createRouter({
}
},
// 人员管理相关路由
// 在 personnel/admin 路由下添加子路由
{
path: 'personnel/admin',
name: 'personnel-admin',
component: () => import('../views/personnel/Admin.vue'),
meta: {
title: '管理员管理'
}
path: 'personnel/admin',
name: 'personnel-admin',
component: () => import('../views/personnel/Admin.vue'),
meta: {
title: '管理员管理'
},
children: [
{
path: 'add',
name: 'admin-add',
component: () => import('../views/personnel/addAdmin.vue'),
meta: {
title: '新增管理员'
}
}
]
},
{
path: 'personnel/maintenance',
name: 'personnel-maintenance',
component: () => import('../views/personnel/Maintenance.vue'),
component: () => import('@/views/personnel/Maintenance.vue'),
meta: {
title: '运维人员管理'
title: '人员管理'
}
},
{
path: 'personnel/maintenance/records/:id',
name: 'MaintenanceRecord',
component: () => import('@/views/personnel/MaintenanceRecord.vue'),
meta: {
title: '维修记录详情'
}
}
,
{
path: 'personnel/user',
name: 'personnel-user',

@ -7,9 +7,10 @@ import type { LoginRequest, LoginVO, ResultVO } from '@/api/types/auth'
interface UserInfo {
id: number
username: string
realName: string
role: string
realName?: string
role?: string
avatar?: string
areaId?: string
}
export const useAuthStore = defineStore('auth', () => {

@ -15,19 +15,19 @@
<!-- 搜索框 -->
<div class="search-box">
<input
type="text"
placeholder="搜索设备ID或位置..."
v-model="searchKeyword"
@input="handleSearch"
type="text"
placeholder="搜索设备ID或位置..."
v-model="searchKeyword"
@input="handleSearch"
>
<button class="search-btn" @click="handleSearch"></button>
</div>
<!-- 片区筛选 -->
<select
v-model="selectedArea"
class="filter-select"
@change="handleSearch"
v-model="selectedArea"
class="filter-select"
@change="handleSearch"
>
<option value="">全部片区</option>
<option value="市区">市区</option>
@ -36,9 +36,9 @@
<!-- 状态筛选 -->
<select
v-model="selectedStatus"
class="filter-select"
@change="handleSearch"
v-model="selectedStatus"
class="filter-select"
@change="handleSearch"
>
<option value="">全部状态</option>
<option value="online">在线</option>
@ -53,57 +53,57 @@
<div class="card">
<table class="equipment-table">
<thead>
<tr>
<th>设备ID</th>
<th>设备机型</th> <!-- 新增机型列 -->
<th>所属片区</th>
<th>详细位置</th>
<th>状态</th>
<th>最后上传时间</th>
<th>操作</th>
</tr>
<tr>
<th>设备ID</th>
<th>设备机型</th> <!-- 新增机型列 -->
<th>所属片区</th>
<th>详细位置</th>
<th>状态</th>
<th>最后上传时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="device in paginatedDevices" :key="device.deviceId">
<td>{{ device.deviceId }}</td>
<td>{{ device.deviceType === 'WATER_MAKER' ? '制水机' : device.deviceType }}</td>
<td>{{ device.areaId }}</td>
<td>{{ device.installLocation }}</td>
<td>
<tr v-for="device in paginatedDevices" :key="device.deviceId">
<td>{{ device.deviceId }}</td>
<td>{{ device.deviceType === 'WATER_MAKER' ? '制水机' : device.deviceType }}</td>
<td>{{ device.areaId }}</td>
<td>{{ device.installLocation }}</td>
<td>
<span :class="`status-tag ${device.status}`">
{{ formatStatus(device.status) }}
</span>
</td>
<td>{{ formatDate(device.lastHeartbeatTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.deviceId)"></button>
<button
</td>
<td>{{ formatDate(device.lastHeartbeatTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewDevice(device.deviceId)"></button>
<button
class="btn-online"
@click="updateDeviceStatus(device.deviceId, 'online')"
:disabled="device.status === 'online'"
>
设为在线
</button>
<button
>
设为在线
</button>
<button
class="btn-offline"
@click="showOfflineModal(device.deviceId)"
:disabled="device.status === 'offline'"
>
设为离线
</button>
<button
>
设为离线
</button>
<button
class="btn-fault"
@click="showFaultModalFunc(device.deviceId)"
:disabled="device.status === 'fault'"
>
>
设为故障
</button>
</button>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td> <!-- colspan从6改为7 -->
</tr>
</td>
</tr>
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td>
</tr>
</tbody>
</table>
</div>
@ -111,9 +111,9 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
@ -121,9 +121,9 @@
{{ currentPage }} / {{ totalPages }} ( {{ filteredDevices.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
@ -204,7 +204,8 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceStatusApi } from '@/api/deviceStatus'
import { useAuthStore } from '@/stores/auth' // auth store
import { request } from '@/api/request' //
//
type DeviceStatus = 'online' | 'offline' | 'fault'
@ -229,6 +230,7 @@ const selectedStatus = ref('') // 状态筛选值
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore() // auth store
//
const showAddModal = ref(false)
@ -254,16 +256,24 @@ const faultInfo = ref({
})
//
// - loadDevices
// loadDevices deviceType
const loadDevices = async (): Promise<WaterMakerDevice[]> => {
try {
//
if (!authStore.isLoggedIn) {
router.push('/login')
return []
}
const statuses = ['online', 'offline', 'fault']
const allDevices: WaterMakerDevice[] = []
for (const status of statuses) {
// deviceType
const result = await DeviceStatusApi.getDevicesByStatus(status, undefined, 'water_maker')
// 使token
const result = await request<{
code: number
message: string
data: any[]
}>(`/api/device/status/${status}?deviceType=water_maker`)
if (result.code === 200 && result.data && Array.isArray(result.data)) {
allDevices.push(...result.data.map((item: any) => ({
@ -282,19 +292,21 @@ const loadDevices = async (): Promise<WaterMakerDevice[]> => {
return allDevices
} catch (error) {
console.error('加载设备数据失败:', error)
//
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
return []
}
}
//
const filteredDevices = computed(() => {
return devices.value.filter(device => {
const keywordMatch = searchKeyword.value.trim() === '' ||
device.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.installLocation.toLowerCase().includes(searchKeyword.value.toLowerCase())
device.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.installLocation.toLowerCase().includes(searchKeyword.value.toLowerCase())
const areaMatch = selectedArea.value === '' || device.areaId === selectedArea.value
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
@ -315,7 +327,6 @@ const totalPages = computed(() => {
})
//
// - script setup
const formatStatus = (status: DeviceStatus): string => {
const statusMap: Record<string, string> = {
online: '在线',
@ -325,7 +336,6 @@ const formatStatus = (status: DeviceStatus): string => {
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
@ -358,10 +368,13 @@ const showFaultModalFunc = (deviceId: string) => {
// 线
const confirmOffline = async () => {
try {
const result = await DeviceStatusApi.markDeviceOffline(
currentDeviceId.value,
offlineReason.value
)
const result = await request<{
code: number
message: string
}>(`/api/device/${currentDeviceId.value}/offline`, {
method: 'POST',
body: JSON.stringify({ reason: offlineReason.value })
})
if (result.code === 200) {
const device = devices.value.find(d => d.deviceId === currentDeviceId.value)
@ -373,17 +386,23 @@ const confirmOffline = async () => {
}
} catch (error) {
console.error('设置设备离线失败:', error)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const confirmFault = async () => {
try {
const result = await DeviceStatusApi.markDeviceFault(
currentDeviceId.value,
faultInfo.value.faultType,
faultInfo.value.description
)
const result = await request<{
code: number
message: string
}>(`/api/device/${currentDeviceId.value}/fault`, {
method: 'POST',
body: JSON.stringify(faultInfo.value)
})
if (result.code === 200) {
const device = devices.value.find(d => d.deviceId === currentDeviceId.value)
@ -395,25 +414,47 @@ const confirmFault = async () => {
}
} catch (error) {
console.error('设置设备故障失败:', error)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
// - updateDeviceStatus
const updateDeviceStatus = async (deviceId: string, status: string, remark: string = '') => {
try {
let result: any;
let result;
switch (status) {
case 'online':
result = await DeviceStatusApi.markDeviceOnline(deviceId)
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/online`, {
method: 'POST'
})
break
case 'offline':
result = await DeviceStatusApi.markDeviceOffline(deviceId, remark)
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/offline`, {
method: 'POST',
body: JSON.stringify({ reason: remark })
})
break
case 'fault':
//
result = await DeviceStatusApi.markDeviceFault(deviceId, 'MANUAL_FAULT', remark || '手动设置故障')
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/fault`, {
method: 'POST',
body: JSON.stringify({
faultType: 'MANUAL_FAULT',
description: remark || '手动设置故障'
})
})
break
default:
throw new Error('不支持的状态类型')
@ -431,11 +472,14 @@ const updateDeviceStatus = async (deviceId: string, status: string, remark: stri
}
} catch (error) {
console.error('更新设备状态失败:', error)
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
throw error
}
}
//
const addDevice = async () => {
try {
@ -448,14 +492,14 @@ const addDevice = async () => {
deviceType: newDevice.value.deviceType
};
// API
const result = await fetch('/api/web/device/add', {
// 使token
const result = await request<{
code: number
message: string
}>('/api/web/device/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(deviceToAdd)
}).then(response => response.json());
})
if (result.code === 200) {
//
@ -468,7 +512,7 @@ const addDevice = async () => {
deviceName: '',
areaId: '市区',
installLocation: '',
deviceType: 'WATER_MAKER'
deviceType: 'water_maker'
};
console.log('设备添加成功');
@ -477,21 +521,29 @@ const addDevice = async () => {
}
} catch (error) {
console.error('添加设备失败:', error);
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
};
//
// onMounted
onMounted(async () => {
console.log('🚀 开始加载设备数据...')
//
if (!authStore.isLoggedIn) {
router.push('/login')
return
}
try {
const result = await loadDevices()
console.log('🌐 API返回 data:', result)
if (result.length === 0) {
console.warn('⚠️ 数据库中无设备数据')
console.log('⚠️ 数据库中无设备数据')
} else {
console.log('✅ 成功加载设备数据:', result)
}
@ -499,9 +551,6 @@ onMounted(async () => {
console.error('❌ 加载设备数据失败:', error)
}
})
</script>
<style scoped>
@ -801,4 +850,4 @@ onMounted(async () => {
min-width: auto;
}
}
</style>
</style>

@ -1,3 +1,4 @@
<!-- src/views/personnel/Admin.vue -->
<template>
<div class="admin-page">
<!-- 页面标题和面包屑 -->
@ -8,14 +9,14 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="handleAddAdmin"></button>
<button class="btn-add" @click="showAddModal = true">新增管理</button>
<div class="search-box">
<input
type="text"
placeholder="搜索姓名或账号..."
v-model="searchKeyword"
@input="handleSearch"
type="text"
placeholder="搜索姓名或账号..."
v-model="searchKeyword"
@input="handleSearch"
>
<button class="search-btn" @click="handleSearch"></button>
</div>
@ -25,45 +26,51 @@
<div class="card">
<table class="admin-table">
<thead>
<tr>
<th>姓名</th>
<th>账号</th>
<th>联系电话</th>
<th>身份</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr>
<th>姓名</th>
<th>账号</th>
<th>联系电话</th>
<th>身份</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="admin in filteredAdmins" :key="admin.id">
<td>{{ admin.name }}</td>
<td>{{ admin.account }}</td>
<td>{{ admin.phone }}</td>
<td>{{ admin.role }}</td>
<td>
<tr v-for="admin in filteredAdmins" :key="admin.adminId">
<td>{{ admin.name }}</td>
<td>{{ admin.account }}</td>
<td>{{ admin.phone }}</td>
<td>{{ formatRole(admin.role) }}</td>
<td>
<span :class="`status-tag ${admin.status}`">
{{ admin.status === 'active' ? '启用' : '禁用' }}
</span>
</td>
<td class="operation-buttons">
<button
</td>
<td class="operation-buttons">
<button
class="btn-edit"
@click="handleEdit(admin.id)"
>
编辑
</button>
<button
@click="handleEdit(admin.adminId)"
>
编辑
</button>
<button
class="btn-delete"
@click="handleDelete(admin.adminId, admin.name)"
>
删除
</button>
<button
class="btn-status"
:class="admin.status === 'active' ? 'btn-disable' : 'btn-enable'"
@click="handleStatusChange(admin.id, admin.status)"
>
{{ admin.status === 'active' ? '禁用' : '启用' }}
</button>
</td>
</tr>
<tr v-if="filteredAdmins.length === 0">
<td colspan="6" class="no-data">暂无管理员数据</td>
</tr>
@click="handleStatusChange(admin.adminId, admin.status)"
>
{{ admin.status === 'active' ? '禁用' : '启用' }}
</button>
</td>
</tr>
<tr v-if="filteredAdmins.length === 0">
<td colspan="6" class="no-data">暂无管理员数据</td>
</tr>
</tbody>
</table>
</div>
@ -71,9 +78,9 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
@ -81,32 +88,123 @@
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
</div>
<!-- 添加管理员弹窗 -->
<div class="modal-overlay" v-if="showAddModal">
<div class="modal-container">
<div class="modal-header">
<h3>新增管理员</h3>
<button class="modal-close" @click="showAddModal = false">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="name" class="form-label required">姓名</label>
<input type="text" id="name" v-model="formData.name" required>
</div>
<div class="form-group">
<label for="account" class="form-label required">账号</label>
<input type="text" id="account" v-model="formData.account" required>
</div>
<div class="form-group">
<label for="phone" class="form-label required">联系电话</label>
<input type="tel" id="phone" v-model="formData.phone" required>
</div>
<div class="form-group">
<label for="role" class="form-label required">身份</label>
<select id="role" v-model="formData.role" required>
<option value="ROLE_SUPER_ADMIN">超级管理员</option>
<option value="ROLE_AREA_ADMIN">区域管理员</option>
<option value="ROLE_VIEWER">查看者</option>
</select>
</div>
<div class="form-group">
<label for="password" class="form-label">初始密码</label>
<input
type="password"
id="password"
v-model="formData.password"
placeholder="不填默认为123456"
>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showAddModal = false">取消</button>
<button type="submit" class="btn-submit">创建</button>
</div>
</form>
</div>
</div>
</div>
<!-- 编辑管理员弹窗 -->
<div class="modal-overlay" v-if="showEditModal">
<div class="modal-container">
<div class="modal-header">
<h3>编辑管理员</h3>
<button class="modal-close" @click="showEditModal = false">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="handleEditSubmit">
<div class="form-group">
<label for="edit-name" class="form-label required">姓名</label>
<input type="text" id="edit-name" v-model="editFormData.name" required>
</div>
<div class="form-group">
<label for="edit-account" class="form-label required">账号</label>
<input type="text" id="edit-account" v-model="editFormData.account" required disabled>
</div>
<div class="form-group">
<label for="edit-phone" class="form-label required">联系电话</label>
<input type="tel" id="edit-phone" v-model="editFormData.phone" required>
</div>
<div class="form-group">
<label for="edit-role" class="form-label required">身份</label>
<select id="edit-role" v-model="editFormData.role" required>
<option value="ROLE_SUPER_ADMIN">超级管理员</option>
<option value="ROLE_AREA_ADMIN">区域管理员</option>
<option value="ROLE_VIEWER">查看者</option>
</select>
</div>
<div class="form-group">
<label for="edit-password" class="form-label">重置密码</label>
<input
type="password"
id="edit-password"
v-model="editFormData.password"
placeholder="不修改密码请留空"
>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showEditModal = false">取消</button>
<button type="submit" class="btn-submit">保存</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import { request } from '@/api/request'
// 1. useAuthStore
import { useAuthStore } from '@/stores/auth'
// 2. authStore
const authStore = useAuthStore()
import type { ResultVO } from '@/api/types/auth'
//
type AdminStatus = 'active' | 'disabled'
//
interface Admin {
id: string
adminId: string
name: string
account: string
phone: string
@ -114,79 +212,102 @@ interface Admin {
status: AdminStatus
}
//
interface FormData {
name: string
account: string
phone: string
role: string
password?: string
}
//
interface EditFormData {
adminId: string
name: string
account: string
phone: string
role: string
password?: string
}
const authStore = useAuthStore()
const router = useRouter()
//
const admins = ref<Admin[]>([])
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const loading = ref(false)
const showAddModal = ref(false)
const showEditModal = ref(false)
//
const formData = ref<FormData>({
name: '',
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
})
//
const editFormData = ref<EditFormData>({
adminId: '',
name: '',
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
})
//
// fetchAdminList
// fetchAdminList Token
const fetchAdminList = async () => {
loading.value = true
try {
// 1. Pinia Token
const token = authStore.token
// Token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
// 2.
const params = new URLSearchParams()
if (searchKeyword.value.trim()) {
params.append('name', searchKeyword.value.trim())
}
// 3. 使 request 使 axios
const response = await request<{
code: number
msg: string
data: any[]
}>(`/api/web/admin/list?${params.toString()}`, {
const response = await request<ResultVO<any[]>>(`/api/web/admin/list?${params.toString()}`, {
method: 'GET',
// Authorization request
})
// 4.
if (response.code === 200) {
//
admins.value = (response.data || []).map((admin: any) => ({
id: admin.adminId || '', //
name: admin.adminName || '未知姓名',
account: admin.adminName || '',
adminId: admin.adminId || '',
name: admin.adminName || '未知姓名', //
account: admin.account || '',
phone: admin.phone || '未知电话',
role: admin.role || '未知角色',
status: 'active' // 使
status: 'active' as AdminStatus
}))
} else {
// ""
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
const errorMsg = response.message || `获取失败(错误码:${response.code}`
console.error('获取管理员列表失败:', errorMsg)
alert(`获取管理员列表失败:${errorMsg}`)
}
} catch (error: any) {
// 5. Token
console.error('请求异常:', error)
//
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
: error.message.includes('Net')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取管理员列表失败:${errorMsg}`)
// Token
if (error.message.includes('401')) {
authStore.logout() // Token
authStore.logout()
router.push('/login')
}
} finally {
@ -194,13 +315,22 @@ const fetchAdminList = async () => {
}
}
//
const formatRole = (role: string): string => {
const roleMap: Record<string, string> = {
'ROLE_SUPER_ADMIN': '超级管理员',
'ROLE_AREA_ADMIN': '区域管理员',
'ROLE_VIEWER': '查看者'
}
return roleMap[role] || role
}
//
const filteredAdmins = computed(() => {
return admins.value.filter(admin => {
const keywordMatch = searchKeyword.value.trim() === '' ||
admin.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
admin.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
admin.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
admin.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
return keywordMatch
})
})
@ -222,22 +352,174 @@ onMounted(() => {
})
//
const handleStatusChange = (id: string, currentStatus: AdminStatus) => {
const newStatus: AdminStatus = currentStatus === 'active' ? 'disabled' : 'active'
admins.value = admins.value.map(admin =>
admin.id === id ? { ...admin, status: newStatus } : admin
)
// API
const handleStatusChange = async (id: string, currentStatus: AdminStatus) => {
try {
const newStatus: AdminStatus = currentStatus === 'active' ? 'disabled' : 'active'
// API
const response = await request<ResultVO>(`/api/web/admin/status/${id}`, {
method: 'PUT',
body: JSON.stringify({ status: newStatus })
})
if (response.code === 200) {
//
admins.value = admins.value.map(admin =>
admin.adminId === id ? { ...admin, status: newStatus } : admin
)
} else {
alert(`状态更新失败: ${response.message}`)
}
} catch (error: any) {
console.error('更新状态失败:', error)
alert(`更新状态失败: ${error.message}`)
}
}
//
const handleEdit = (id: string) => {
router.push(`/home/personnel/admin/edit/${id}`)
const handleEdit = async (id: string) => {
try {
//
const adminToEdit = admins.value.find(admin => admin.adminId === id);
if (adminToEdit) {
//
editFormData.value = {
adminId: adminToEdit.adminId,
name: adminToEdit.name,
account: adminToEdit.account,
phone: adminToEdit.phone,
role: adminToEdit.role,
password: ''
};
showEditModal.value = true;
}
} catch (error: any) {
console.error('获取管理员信息失败:', error);
alert(`获取管理员信息失败:${error.message}`);
}
}
//
const handleAddAdmin = () => {
router.push('/home/personnel/admin/add')
//
const handleEditSubmit = async () => {
try {
const submitData = {
adminId: editFormData.value.adminId,
adminName: editFormData.value.name,
account: editFormData.value.account,
phone: editFormData.value.phone,
role: editFormData.value.role,
password: editFormData.value.password || undefined //
};
const response = await request<ResultVO>(`/api/web/admin/save`, {
method: 'POST',
body: JSON.stringify(submitData)
});
if (response.code === 200) {
alert('管理员信息更新成功');
showEditModal.value = false;
//
editFormData.value = {
adminId: '',
name: '',
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
};
//
fetchAdminList();
} else {
alert(`更新失败:${response.message}`);
}
} catch (error: any) {
console.error('更新管理员失败:', error);
alert(`更新管理员失败:${error.message}`);
}
};
//
const handleDelete = (id: string, name: string) => {
if (confirm(`确定要删除管理员 "${name}" 吗?此操作不可恢复!`)) {
performDelete(id);
}
}
//
const performDelete = async (id: string) => {
try {
const token = authStore.token;
if (!token) {
console.warn('未获取到 Token跳转到登录页');
router.push('/login');
return;
}
const response = await request<ResultVO>(`/api/web/admin/${id}`, {
method: 'DELETE',
});
if (response.code === 200) {
alert('管理员删除成功');
//
fetchAdminList();
} else {
const errorMsg = response.message || `删除失败(错误码:${response.code}`;
console.error('删除管理员失败:', errorMsg);
alert(`删除管理员失败:${errorMsg}`);
}
} catch (error: any) {
console.error('请求异常:', error);
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '删除失败,请稍后重试';
alert(`删除管理员失败:${errorMsg}`);
if (error.message.includes('401')) {
authStore.logout();
router.push('/login');
}
}
}
//
const handleSubmit = async () => {
try {
//
const submitData = {
...formData.value,
password: formData.value.password || '123456'
}
const response = await request<ResultVO>(`/api/web/admin/save`, {
method: 'POST',
body: JSON.stringify(submitData)
})
if (response.code === 200) {
alert('管理员添加成功')
showAddModal.value = false
//
formData.value = {
name: '',
account: '',
phone: '',
role: 'ROLE_SUPER_ADMIN',
password: ''
}
//
fetchAdminList()
} else {
alert(`添加失败:${response.message}`)
}
} catch (error: any) {
console.error('添加管理员失败:', error)
alert(`添加管理员失败:${error.message}`)
}
}
</script>
@ -371,6 +653,15 @@ const handleAddAdmin = () => {
color: #1890ff;
}
.btn-delete {
background-color: #ffebe6;
color: #cf1322;
}
.btn-delete:hover {
background-color: #ffccc7;
}
.btn-enable {
background-color: #e6f7ee;
color: #00875a;
@ -410,6 +701,120 @@ const handleAddAdmin = () => {
cursor: not-allowed;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-container {
background-color: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-label.required::after {
content: '*';
color: #cf1322;
margin-left: 4px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.form-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 24px;
}
.btn-submit {
background: #42b983;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-submit:hover {
background: #359e75;
}
.btn-cancel {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-cancel:hover {
background: #e0e0e0;
}
/* 响应式调整 */
@media (max-width: 768px) {
.action-bar {

@ -9,16 +9,40 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="handleAddMaintenance"></button>
<div class="search-box">
<input
type="text"
placeholder="搜索姓名或账号..."
v-model="searchKeyword"
@input="handleSearch"
>
<button class="btn-add" @click="openAddForm"></button>
<div class="filters">
<!-- 姓名搜索 -->
<div class="filter-item">
<input
type="text"
placeholder="搜索姓名..."
v-model="searchFilters.name"
@input="handleSearch"
>
</div>
<!-- 区域筛选 -->
<div class="filter-item">
<select v-model="searchFilters.areaId" @change="handleSearch">
<option value="">全部区域</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
</select>
</div>
<!-- 状态筛选 -->
<div class="filter-item">
<select v-model="searchFilters.status" @change="handleSearch">
<option value="">全部状态</option>
<option value="idle">空闲</option>
<option value="busy">忙碌</option>
<option value="vacation">休假</option>
</select>
</div>
<button class="search-btn" @click="handleSearch"></button>
<button class="reset-btn" @click="resetFilters"></button>
</div>
</div>
@ -26,173 +50,456 @@
<div class="card">
<table class="maintenance-table">
<thead>
<tr>
<th>姓名</th>
<th>账号</th>
<th>联系电话</th>
<th>维修片区</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr>
<th>姓名</th>
<th>联系电话</th>
<th>维修片区</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="staff in filteredStaff" :key="staff.id">
<td>{{ staff.name }}</td>
<td>{{ staff.account }}</td>
<td>{{ staff.phone }}</td>
<td>{{ staff.area }}</td>
<td>
<tr v-for="staff in paginatedStaff" :key="staff.repairmanId">
<td>{{ staff.repairmanName }}</td>
<td>{{ staff.phone }}</td>
<td>{{ staff.areaId }}</td>
<td>
<span :class="`status-tag ${staff.status}`">
{{ staff.status === 'active' ? '启用' : '禁用' }}
{{ getStatusText(staff.status) }}
</span>
</td>
<td class="operation-buttons">
<button
class="btn-view"
@click="handleViewRecords(staff.id)"
>
查看维修记录
</button>
<button
class="btn-edit"
@click="handleEdit(staff.id)"
>
编辑
</button>
<button
class="btn-status"
:class="staff.status === 'active' ? 'btn-disable' : 'btn-enable'"
@click="handleStatusChange(staff.id, staff.status)"
>
{{ staff.status === 'active' ? '禁用' : '启用' }}
</button>
</td>
</tr>
<tr v-if="filteredStaff.length === 0">
<td colspan="6" class="no-data">暂无维修人员数据</td>
</tr>
</td>
<td class="operation-buttons">
<button
class="btn-view"
@click="handleViewRecords(staff.repairmanId)"
>
查看维修记录
</button>
<button
class="btn-edit"
@click="openEditForm(staff)"
>
编辑
</button>
<button
class="btn-delete"
@click="confirmDelete(staff.repairmanId, staff.repairmanName)"
>
删除
</button>
</td>
</tr>
<tr v-if="paginatedStaff.length === 0">
<td colspan="5" class="no-data">暂无维修人员数据</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredStaff.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
</div>
<!-- 新增/编辑弹窗 -->
<div v-if="isModalOpen" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ isEditing ? '编辑维修人员' : '新增维修人员' }}</h3>
<button class="close-btn" @click="closeModal">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveRepairman">
<!-- ID字段编辑时显示 -->
<div v-if="isEditing" class="form-group">
<label>ID</label>
<input
type="text"
v-model="form.repairmanId"
disabled
class="disabled-field"
/>
</div>
<div class="form-group">
<label class="form-label required">姓名</label>
<input
type="text"
v-model="form.repairmanName"
required
placeholder="请输入姓名"
/>
</div>
<div class="form-group">
<label class="form-label required">联系电话</label>
<input
type="tel"
v-model="form.phone"
required
placeholder="请输入联系电话"
/>
</div>
<div class="form-group">
<label class="form-label required">维修片区</label>
<select v-model="form.areaId" required>
<option value="">请选择片区</option>
<option value="市区">市区</option>
<option value="校区">校区</option>
</select>
</div>
<div class="form-group">
<label class="form-label required">状态</label>
<select v-model="form.status" required>
<option value="idle">空闲</option>
<option value="busy">忙碌</option>
<option value="vacation">休假</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="closeModal"></button>
<button type="submit" class="btn-submit">{{ isEditing ? '保存' : '创建' }}</button>
</div>
</form>
</div>
</div>
</div>
<!-- 删除确认弹窗 -->
<div v-if="isDeleteConfirmOpen" class="modal-overlay" @click="closeDeleteConfirm">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>确认删除</h3>
<button class="close-btn" @click="closeDeleteConfirm">×</button>
</div>
<div class="modal-body">
<p>确定要删除维修人员 "{{ deleteName }}" 此操作不可恢复</p>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="closeDeleteConfirm"></button>
<button type="button" class="btn-submit" @click="deleteRepairman"></button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import type { ResultVO } from '@/api/types/auth'
//
type StaffStatus = 'active' | 'disabled'
type RepairmanStatus = 'idle' | 'busy' | 'vacation'
//
//
interface MaintenanceStaff {
id: string
repairmanId: string
repairmanName: string
phone: string
areaId: string
status: RepairmanStatus
}
//
interface SearchFilters {
name: string
account: string
areaId: string
status: string
}
//
interface FormData {
repairmanId: string
repairmanName: string
phone: string
area: string
status: StaffStatus
}
//
const staffList: MaintenanceStaff[] = [
{
id: '1',
name: '赵六',
account: 'repair01',
phone: '13500135000',
area: '市区',
status: 'active'
},
{
id: '2',
name: '孙七',
account: 'repair02',
phone: '13600136000',
area: '校区',
status: 'active'
},
{
id: '3',
name: '周八',
account: 'repair03',
phone: '13400134000',
area: '市区',
status: 'disabled'
}
]
areaId: string
status: RepairmanStatus
}
const authStore = useAuthStore()
const router = useRouter()
// ========== ==========
//
const staff = ref<MaintenanceStaff[]>(staffList)
const searchKeyword = ref('')
const staffList = ref<MaintenanceStaff[]>([])
const searchFilters = ref<SearchFilters>({
name: '',
areaId: '',
status: ''
})
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const loading = ref(false)
//
const isModalOpen = ref(false)
const isEditing = ref(false)
const isDeleteConfirmOpen = ref(false)
const deleteId = ref<string>('')
const deleteName = ref<string>('')
//
const form = ref<FormData>({
repairmanId: '',
repairmanName: '',
phone: '',
areaId: '',
status: 'idle'
})
//
const fetchMaintenanceStaff = async () => {
loading.value = true
try {
// token
if (!authStore.token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const params = new URLSearchParams()
if (searchFilters.value.name.trim()) {
params.append('name', searchFilters.value.name.trim())
}
if (searchFilters.value.areaId) {
params.append('areaId', searchFilters.value.areaId)
}
if (searchFilters.value.status) {
params.append('status', searchFilters.value.status)
}
// 使request
const response = await request<ResultVO<MaintenanceStaff[]>>(
`/api/web/repairman/list?${params.toString()}`,
{
method: 'GET'
}
)
//
if (response.code === 200) {
staffList.value = response.data || []
} else {
const errorMsg = response.message || `获取失败(错误码:${response.code}`
console.error('获取维修人员列表失败:', errorMsg)
alert(`获取维修人员列表失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取维修人员列表失败:${errorMsg}`)
// Token
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const filteredStaff = computed(() => {
return staff.value.filter(person => {
const keywordMatch = searchKeyword.value.trim() === '' ||
person.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
person.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
return keywordMatch
//
return staffList.value.filter(person => {
const nameMatch = searchFilters.value.name.trim() === '' ||
person.repairmanName.toLowerCase().includes(searchFilters.value.name.toLowerCase())
const areaMatch = searchFilters.value.areaId === '' ||
person.areaId === searchFilters.value.areaId
const statusMatch = searchFilters.value.status === '' ||
person.status === searchFilters.value.status
return nameMatch && areaMatch && statusMatch
})
})
//
const paginatedStaff = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredStaff.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredStaff.value.length / pageSize)
})
//
const handleSearch = () => {
currentPage.value = 1 //
currentPage.value = 1
fetchMaintenanceStaff()
}
//
const resetFilters = () => {
searchFilters.value = {
name: '',
areaId: '',
status: ''
}
currentPage.value = 1
fetchMaintenanceStaff()
}
//
const handleStatusChange = (id: string, currentStatus: StaffStatus) => {
const newStatus: StaffStatus = currentStatus === 'active' ? 'disabled' : 'active'
staff.value = staff.value.map(person =>
person.id === id ? { ...person, status: newStatus } : person
)
// API
//
const getStatusText = (status: RepairmanStatus) => {
const statusMap: Record<RepairmanStatus, string> = {
'idle': '空闲',
'busy': '忙碌',
'vacation': '休假'
}
return statusMap[status] || status
}
// ========== ==========
const openEditForm = (staff: MaintenanceStaff) => {
//
form.value = JSON.parse(JSON.stringify(staff))
isEditing.value = true
isModalOpen.value = true
}
// ========== ==========
const openAddForm = () => {
//
form.value = {
repairmanId: '',
repairmanName: '',
phone: '',
areaId: '',
status: 'idle'
}
isEditing.value = false
isModalOpen.value = true
}
//
const closeModal = () => {
isModalOpen.value = false
isEditing.value = false
}
//
const saveRepairman = async () => {
try {
//
if (!form.value.repairmanName.trim()) {
alert('请输入姓名')
return
}
if (!form.value.phone.trim()) {
alert('请输入联系电话')
return
}
if (!form.value.areaId) {
alert('请选择维修片区')
return
}
//
const response = await request<ResultVO<MaintenanceStaff>>(
`/api/web/repairman/save`,
{
method: 'POST',
body: JSON.stringify(form.value)
}
)
if (response.code === 200) {
alert(isEditing.value ? '维修人员更新成功' : '维修人员新增成功')
closeModal()
fetchMaintenanceStaff() //
} else {
//
alert(`保存失败:${response.message}`)
}
} catch (error: any) {
console.error('保存失败:', error)
if (error.message.includes('403')) {
alert('权限不足:您没有权限执行此操作,请联系管理员')
} else {
alert('保存失败,请稍后重试')
}
}
}
//
const handleEdit = (id: string) => {
router.push(`/home/personnel/maintenance/edit/${id}`)
//
const confirmDelete = (id: string, name: string) => {
deleteId.value = id
deleteName.value = name
isDeleteConfirmOpen.value = true
}
//
const closeDeleteConfirm = () => {
isDeleteConfirmOpen.value = false
deleteId.value = ''
deleteName.value = ''
}
//
const deleteRepairman = async () => {
if (!deleteId.value) return
try {
const response = await request<ResultVO>(
`/api/web/repairman/${deleteId.value}`,
{
method: 'DELETE'
}
)
if (response.code === 200) {
alert('删除成功')
closeDeleteConfirm()
fetchMaintenanceStaff() //
} else {
alert(`删除失败:${response.message}`)
}
} catch (error: any) {
console.error('删除失败:', error)
alert('删除失败,请稍后重试')
}
}
//
const handleViewRecords = (id: string) => {
// id
console.log('跳转到维修记录:', id)
router.push(`/home/personnel/maintenance/records/${id}`)
}
//
const handleAddMaintenance = () => {
router.push('/home/personnel/maintenance/add')
}
//
onMounted(() => {
fetchMaintenanceStaff()
})
</script>
<style scoped>
@ -240,16 +547,27 @@ const handleAddMaintenance = () => {
background: #359e75;
}
.search-box {
.filters {
display: flex;
gap: 8px;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box input {
.filter-item input,
.filter-item select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 240px;
font-size: 14px;
}
.filter-item input {
width: 180px;
}
.filter-item select {
min-width: 120px;
}
.search-btn {
@ -261,6 +579,15 @@ const handleAddMaintenance = () => {
cursor: pointer;
}
.reset-btn {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.maintenance-table {
width: 100%;
border-collapse: collapse;
@ -292,13 +619,18 @@ const handleAddMaintenance = () => {
font-weight: 500;
}
.status-tag.active {
.status-tag.idle {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.disabled {
background-color: #f5f5f5;
.status-tag.busy {
background-color: #fffbe6;
color: #d48806;
}
.status-tag.vacation {
background-color: #f0f0f0;
color: #8c8c8c;
}
@ -330,12 +662,7 @@ const handleAddMaintenance = () => {
color: #1890ff;
}
.btn-enable {
background-color: #e6f7ee;
color: #00875a;
}
.btn-disable {
.btn-delete {
background-color: #ffebe6;
color: #cf1322;
}
@ -375,13 +702,138 @@ const handleAddMaintenance = () => {
flex-direction: column;
align-items: flex-start;
}
.search-box {
.filters {
width: 100%;
}
.search-box input {
.filter-item {
width: 100%;
}
.filter-item input,
.filter-item select {
width: 100%;
}
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 4px;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-label.required::after {
content: '*';
color: #cf1322;
margin-left: 4px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.disabled-field {
background-color: #f5f5f5;
cursor: not-allowed;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.btn-submit {
background: #42b983;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.btn-cancel {
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-submit:hover {
background: #359e75;
}
.btn-cancel:hover {
background: #e0e0e0;
}
</style>

@ -0,0 +1,569 @@
<!-- src/views/personnel/MaintenanceRecord.vue -->
<template>
<div class="record-page">
<!-- 页面标题和面包屑 -->
<div class="page-header">
<h2>维修记录详情</h2>
<div class="breadcrumb">校园矿化水平台 / 人员管理 / 维修人员 / 维修记录</div>
</div>
<!-- 维修人员信息 -->
<div class="repairman-info card">
<h3>维修人员信息</h3>
<div class="info-content">
<div class="info-item">
<span class="label">姓名</span>
<span>{{ repairmanInfo.repairmanName }}</span>
</div>
<div class="info-item">
<span class="label">联系电话</span>
<span>{{ repairmanInfo.phone }}</span>
</div>
<div class="info-item">
<span class="label">维修片区</span>
<span>{{ repairmanInfo.areaId }}</span>
</div>
<div class="info-item">
<span class="label">状态</span>
<span :class="`status-tag ${repairmanInfo.status}`">
{{ getStatusText(repairmanInfo.status) }}
</span>
</div>
</div>
</div>
<!-- 工单统计 -->
<div class="order-stats">
<div class="stat-card">
<div class="stat-number">{{ processingOrders.length }}</div>
<div class="stat-label">处理中工单</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ completedOrders.length }}</div>
<div class="stat-label">已完成工单</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ totalOrders.length }}</div>
<div class="stat-label">总工单数</div>
</div>
</div>
<!-- 工单分类标签 -->
<div class="order-tabs">
<button
:class="{ active: activeTab === 'all' }"
@click="activeTab = 'all'"
>
全部工单
</button>
<button
:class="{ active: activeTab === 'processing' }"
@click="activeTab = 'processing'"
>
处理中 ({{ processingOrders.length }})
</button>
<button
:class="{ active: activeTab === 'completed' }"
@click="activeTab = 'completed'"
>
已完成 ({{ completedOrders.length }})
</button>
</div>
<!-- 工单表格 -->
<div class="card">
<table class="order-table">
<thead>
<tr>
<th>工单号</th>
<th>设备ID</th>
<th>片区</th>
<th>问题描述</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="order in displayedOrders" :key="order.orderId">
<td>{{ order.orderId }}</td>
<td>{{ order.deviceId }}</td>
<td>{{ order.areaId }}</td>
<td class="desc-cell">{{ order.description }}</td>
<td>
<span :class="`status-tag ${order.status}`">
{{ formatOrderStatus(order.status) }}
</span>
</td>
<td>{{ formatDate(order.createdTime) }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewOrderDetail(order.orderId)">
查看详情
</button>
</td>
</tr>
<tr v-if="displayedOrders.length === 0">
<td colspan="7" class="no-data">暂无工单记录</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import type { WorkOrder } from '@/api/types/workorder'
//
interface RepairmanInfo {
repairmanId: string
repairmanName: string
phone: string
areaId: string
status: RepairmanStatus //
}
//
type OrderStatus = 'pending' | 'processing' | 'reviewing' | 'completed' | 'timeout'
type RepairmanStatus = 'idle' | 'busy' | 'vacation'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
//
const repairmanInfo = ref<RepairmanInfo>({
repairmanId: '',
repairmanName: '',
phone: '',
areaId: '',
status: 'idle'
})
const allOrders = ref<WorkOrder[]>([])
const activeTab = ref<'all' | 'processing' | 'completed'>('all')
const currentPage = ref(1)
const pageSize = 10
const loading = ref(false)
//
const fetchRepairmanData = async () => {
loading.value = true
try {
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const repairmanId = route.params.id as string
//
//
repairmanInfo.value.repairmanId = repairmanId
repairmanInfo.value.repairmanName = '维修人员姓名'
repairmanInfo.value.phone = '13800138000'
repairmanInfo.value.areaId = '市区'
repairmanInfo.value.status = 'idle'
//
const response = await request<{
code: number
msg: string
data: WorkOrder[]
}>(`/api/work-orders/my?repairmanId=${repairmanId}`, {
method: 'GET'
})
if (response.code === 200) {
allOrders.value = response.data || []
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取工单列表失败:', errorMsg)
alert(`获取工单列表失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取数据失败:${errorMsg}`)
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const processingOrders = computed(() => {
return allOrders.value.filter(order =>
order.status === 'processing' ||
order.status === 'pending' ||
order.status === 'reviewing'
)
})
//
const completedOrders = computed(() => {
return allOrders.value.filter(order => order.status === 'completed')
})
//
const totalOrders = computed(() => {
return allOrders.value
})
//
const filteredOrders = computed(() => {
switch (activeTab.value) {
case 'processing':
return processingOrders.value
case 'completed':
return completedOrders.value
default:
return totalOrders.value
}
})
//
const displayedOrders = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredOrders.value.slice(start, end)
})
//
const totalPages = computed(() => {
return Math.ceil(filteredOrders.value.length / pageSize)
})
//
const getStatusText = (status: RepairmanStatus): string => {
const statusMap: Record<RepairmanStatus, string> = {
'idle': '空闲',
'busy': '忙碌',
'vacation': '休假'
}
return statusMap[status] || status
}
//
const formatOrderStatus = (status: OrderStatus): string => {
const statusMap: Record<OrderStatus, string> = {
'pending': '待抢单',
'processing': '处理中',
'reviewing': '待审核',
'completed': '已完成',
'timeout': '超时未抢'
}
return statusMap[status] || status
}
//
const formatDate = (dateString?: string): string => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
//
const viewOrderDetail = (orderId: string) => {
//
router.push(`/home/work-order/detail/${orderId}`)
}
//
// MaintenanceRecord.vue
onMounted(() => {
const repairmanId = route.params.id as string
if (!repairmanId) {
alert('维修人员ID不能为空')
router.push('/home/personnel/maintenance')
return
}
fetchRepairmanData()
})
</script>
<style scoped>
.record-page {
padding: 20px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.breadcrumb {
color: #666;
font-size: 14px;
}
.card {
background: #fff;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.repairman-info h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 18px;
color: #333;
}
.info-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
align-items: center;
}
.label {
font-weight: 500;
color: #666;
margin-right: 8px;
min-width: 80px;
}
.order-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: #42b983;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #666;
}
.order-tabs {
display: flex;
gap: 1px;
background: #ddd;
border-radius: 4px;
margin-bottom: 20px;
overflow: hidden;
}
.order-tabs button {
flex: 1;
padding: 12px 16px;
border: none;
background: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.order-tabs button.active {
background: #42b983;
color: white;
}
.order-table {
width: 100%;
border-collapse: collapse;
}
.order-table th,
.order-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.order-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #4e5969;
font-size: 14px;
}
.order-table tbody tr:hover {
background-color: #f8f9fa;
}
.desc-cell {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-tag.pending {
background-color: #fff7e6;
color: #d48806;
}
.status-tag.processing {
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag.reviewing {
background-color: #f6f7ff;
color: #667eea;
}
.status-tag.completed {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.timeout {
background-color: #ffebe6;
color: #cf1322;
}
.status-tag.idle {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.busy {
background-color: #fffbe6;
color: #d48806;
}
.status-tag.vacation {
background-color: #f0f0f0;
color: #8c8c8c;
}
.operation-buttons {
display: flex;
gap: 8px;
}
.btn-view {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
background-color: #e6f7ff;
color: #1890ff;
transition: opacity 0.3s;
}
.btn-view:hover {
opacity: 0.9;
}
.no-data {
text-align: center;
padding: 40px 0;
color: #8c8c8c;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 24px;
color: #666;
font-size: 14px;
}
.page-btn {
padding: 4px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.page-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 响应式调整 */
@media (max-width: 768px) {
.order-stats {
flex-direction: column;
gap: 12px;
}
.info-content {
grid-template-columns: 1fr;
}
.order-tabs {
flex-direction: column;
}
}
</style>

@ -1,20 +1,18 @@
<!-- src/views/personnel/User.vue -->
<template>
<div class="user-page">
<!-- 页面标题和面包屑恢复与管理员/维修人员页面一致 -->
<!-- 页面标题和面包屑 -->
<div class="page-header">
<h2>用户管理</h2>
<div class="breadcrumb">校园矿化水平台 / 人员管理 / 用户</div>
</div>
<!-- 操作按钮区移除新增按钮保留搜索功能 -->
<!-- 操作按钮区 -->
<div class="action-bar">
<!-- 移除新增用户按钮 -->
<div class="search-box">
<input
type="text"
placeholder="搜索姓名或账号..."
<input
type="text"
placeholder="搜索姓名..."
v-model="searchKeyword"
@input="handleSearch"
>
@ -22,13 +20,13 @@
</div>
</div>
<!-- 用户表格保持与管理员/维修人员页面一致的列结构 -->
<!-- 用户表格 -->
<div class="card">
<table class="user-table">
<thead>
<tr>
<th>姓名</th>
<th></th>
<th></th>
<th>联系电话</th>
<th>身份</th>
<th>状态</th>
@ -36,13 +34,13 @@
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.name }}</td>
<td>{{ user.account }}</td>
<tr v-for="user in paginatedUsers" :key="user.studentId">
<td>{{ user.studentName }}</td>
<td>{{ user.studentId }}</td>
<td>{{ user.phone }}</td>
<td>
<span :class="`role-tag ${user.role}`">
{{ formatRole(user.role) }}
<span class="role-tag student">
学生
</span>
</td>
<td>
@ -51,38 +49,31 @@
</span>
</td>
<td class="operation-buttons">
<button
class="btn-view"
@click="handleView(user.id)"
<button
class="btn-view"
@click="handleView(user.studentId)"
>
查看
</button>
<button
class="btn-edit"
@click="handleEdit(user.id)"
<button
class="btn-edit"
@click="handleEdit(user.studentId)"
>
编辑
</button>
<button
class="btn-status"
:class="user.status === 'active' ? 'btn-disable' : 'btn-enable'"
@click="handleStatusChange(user.id, user.status)"
>
{{ user.status === 'active' ? '禁用' : '启用' }}
</button>
</td>
</tr>
<tr v-if="filteredUsers.length === 0">
<tr v-if="paginatedUsers.length === 0">
<td colspan="6" class="no-data">暂无用户数据</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控件与管理员/维修人员页面样式一致 -->
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
@ -91,8 +82,8 @@
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -103,95 +94,115 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type UserRole = 'student' | 'teacher' | 'visitor'
type UserStatus = 'active' | 'disabled'
//
type UserStatus = 'active' | 'inactive'
//
interface User {
id: string
name: string
account: string
studentId: string
studentName: string
phone: string
role: UserRole
status: UserStatus
}
//
const userList: User[] = [
{
id: '1',
name: '吴九',
account: 'user01',
phone: '13100131000',
role: 'student', //
status: 'active'
},
{
id: '2',
name: '郑十',
account: 'user02',
phone: '13200132000',
role: 'teacher', //
status: 'active'
},
{
id: '3',
name: '王十一',
account: 'user03',
phone: '13300133000',
role: 'visitor', //
status: 'disabled'
}
]
const authStore = useAuthStore()
const router = useRouter()
// /
const users = ref<User[]>(userList)
//
const users = ref<User[]>([])
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = 10
const router = useRouter()
//
const formatRole = (role: UserRole) => {
switch(role) {
case 'student': return '学生';
case 'teacher': return '老师';
case 'visitor': return '游客';
default: return '未知身份';
const loading = ref(false)
//
const fetchUserList = async () => {
loading.value = true
try {
// token
if (!authStore.token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const params = new URLSearchParams()
if (searchKeyword.value.trim()) {
params.append('studentName', searchKeyword.value.trim())
}
// 使request
const response = await request<{
code: number
msg: string
data: User[]
}>(`/api/web/user/list?${params.toString()}`, {
method: 'GET'
})
//
if (response.code === 200) {
users.value = response.data || []
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取用户列表失败:', errorMsg)
alert(`获取用户列表失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
const errorMsg = error.message.includes('401')
? '登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取用户列表失败:${errorMsg}`)
// Token
if (error.message.includes('401')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
//
const filteredUsers = computed(() => {
return users.value.filter(user => {
const keywordMatch = searchKeyword.value.trim() === '' ||
user.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
user.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
user.studentName.toLowerCase().includes(searchKeyword.value.toLowerCase())
return keywordMatch
})
})
// /
//
const paginatedUsers = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredUsers.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredUsers.value.length / pageSize)
})
//
//
const handleSearch = () => {
currentPage.value = 1 //
currentPage.value = 1
fetchUserList()
}
//
const handleStatusChange = (id: string, currentStatus: UserStatus) => {
const newStatus: UserStatus = currentStatus === 'active' ? 'disabled' : 'active'
users.value = users.value.map(user =>
user.id === id ? { ...user, status: newStatus } : user
)
}
//
onMounted(() => {
fetchUserList()
})
//
const handleView = (id: string) => {
@ -205,7 +216,6 @@ const handleEdit = (id: string) => {
</script>
<style scoped>
/* 完全复用管理员/维修人员页面的样式,仅调整角色标签颜色 */
.user-page {
padding: 20px;
}
@ -279,7 +289,7 @@ const handleEdit = (id: string) => {
background-color: #f8f9fa;
}
/* 状态标签样式(与管理员/维修人员页面一致) */
/* 状态标签样式 */
.status-tag {
display: inline-block;
padding: 4px 8px;
@ -293,12 +303,12 @@ const handleEdit = (id: string) => {
color: #00875a;
}
.status-tag.disabled {
.status-tag.inactive {
background-color: #f5f5f5;
color: #8c8c8c;
}
/* 角色标签样式(新增,区分学生/老师/游客) */
/* 角色标签样式 */
.role-tag {
display: inline-block;
padding: 4px 8px;
@ -312,16 +322,6 @@ const handleEdit = (id: string) => {
color: #1890ff;
}
.role-tag.teacher {
background-color: #f6f7ff;
color: #667eea;
}
.role-tag.visitor {
background-color: #fff7e6;
color: #d48806;
}
.operation-buttons {
display: flex;
gap: 8px;
@ -350,16 +350,6 @@ const handleEdit = (id: string) => {
color: #1890ff;
}
.btn-enable {
background-color: #e6f7ee;
color: #00875a;
}
.btn-disable {
background-color: #ffebe6;
color: #cf1322;
}
.no-data {
text-align: center;
padding: 40px 0;
@ -389,19 +379,19 @@ const handleEdit = (id: string) => {
cursor: not-allowed;
}
/* 响应式调整(与管理员/维修人员页面一致) */
/* 响应式调整 */
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
align-items: flex-start;
}
.search-box {
width: 100%;
}
.search-box input {
width: 100%;
}
}
</style>
</style>

@ -1,4 +1,4 @@
<!-- src/views/order/OrderCompleted.vue -->
<!-- src/views/workorder/Completed.vue -->
<template>
<div class="order-completed-page">
<!-- 页面标题和面包屑 -->
@ -12,9 +12,9 @@
<!-- 工单号/设备ID搜索 -->
<div class="filter-item search-item">
<label>搜索</label>
<input
type="text"
v-model="searchKeyword"
<input
type="text"
v-model="searchKeyword"
class="search-input"
placeholder="输入工单号或设备ID搜索"
@input="handleSearch"
@ -34,9 +34,9 @@
<!-- 日期筛选 -->
<div class="filter-item">
<label>创建日期</label>
<input
type="date"
v-model="filterForm.createDate"
<input
type="date"
v-model="filterForm.createDate"
class="filter-input"
@change="handleFilter"
>
@ -61,7 +61,7 @@
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<tr v-for="order in paginatedOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
@ -82,7 +82,9 @@
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">暂无已结单工单</td>
<td colspan="7" class="no-data">
{{ loading ? '正在加载数据...' : '暂无已结单工单' }}
</td>
</tr>
</tbody>
</table>
@ -90,18 +92,18 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -112,8 +114,10 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -130,45 +134,13 @@ interface CompletedOrder {
createTime: string //
}
//
const orderList: CompletedOrder[] = [
{
id: '11',
orderNo: 'ORD-20231023-001',
deviceType: '制水机',
deviceId: 'WM-2023-001',
area: '市区',
problemDesc: '更换密封垫后漏水问题已解决,设备运行正常',
status: 'completed',
createTime: '2023-10-23 08:10:05'
},
{
id: '12',
orderNo: 'ORD-20231023-002',
deviceType: '供水机',
deviceId: 'WS-2023-001',
area: '校区',
problemDesc: '水质检测合格,出水口感异常问题已解决',
status: 'completed',
createTime: '2023-10-23 14:20:33'
},
{
id: '13',
orderNo: 'ORD-20231024-003',
deviceType: '制水机',
deviceId: 'WM-2023-002',
area: '校区',
problemDesc: '水泵检修完成,出水速度恢复正常,已审核通过',
status: 'completed',
createTime: '2023-10-24 11:30:15'
}
]
//
const orders = ref<CompletedOrder[]>(orderList)
const orders = ref<CompletedOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
// /ID
const searchKeyword = ref('')
@ -179,6 +151,84 @@ const filterForm = ref({
createDate: '' //
})
//
const loadCompletedOrders = async () => {
loading.value = true
try {
// Token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('当前 Token:', token.substring(0, 20) + '...')
//
let url = '/api/work-orders/by-status?status=completed'
const params = new URLSearchParams()
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
//
const queryString = params.toString()
if (queryString) {
url += `&${queryString}`
}
// 使 request
const response = await request<{
code: number
msg: string
data: any[]
}>(url, {
method: 'GET',
})
//
if (response.code === 200) {
orders.value = (response.data || []).map((order: any) => ({
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status || 'completed',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间'
}))
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取已结单工单失败:', errorMsg)
alert(`获取已结单工单失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取已结单工单失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
@ -188,7 +238,7 @@ const formatStatus = (status: OrderStatus): string => {
reviewing: '待审核',
completed: '已结单'
}
return statusMap[status]
return statusMap[status] || status
}
//
@ -198,18 +248,25 @@ const filteredOrders = computed(() => {
const keywordMatch = searchKeyword.value.trim() === '' ||
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
const dateMatch = filterForm.value.createDate === '' ||
const dateMatch = filterForm.value.createDate === '' ||
order.createTime.split(' ')[0] === filterForm.value.createDate
return keywordMatch && areaMatch && dateMatch
})
})
//
const paginatedOrders = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredOrders.value.slice(start, end)
})
//
const totalPages = computed(() => {
return Math.ceil(filteredOrders.value.length / pageSize)
@ -223,6 +280,7 @@ const handleSearch = () => {
// /
const handleFilter = () => {
currentPage.value = 1 //
loadCompletedOrders() //
}
//
@ -233,12 +291,19 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadCompletedOrders()
}
//
const viewOrderDetail = (id: string) => {
router.push(`/home/work-order/completed/${id}`)
}
//
onMounted(() => {
console.log('Token:', authStore.token)
loadCompletedOrders()
})
</script>
<style scoped>
@ -287,4 +352,4 @@ const viewOrderDetail = (id: string) => {
.filter-item { width: 100%; }
.search-input, .filter-select, .filter-input { width: 100%; }
}
</style>
</style>

@ -163,7 +163,14 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
// 1.
import { useRouter } from 'vue-router'
import { request } from '@/api/request' //
import { useAuthStore } from '@/stores/auth' // authStore
// 2. authStore
const router = useRouter()
const authStore = useAuthStore()
// -
type OrderStatus = 'pending' | 'grabbed' | 'processing' | 'completed' | 'closed' | 'timeout'
@ -217,70 +224,90 @@ const formatStatus = (status: OrderStatus): string => {
}
//
// 3. loadAvailableOrders Admin.vue Token
const loadAvailableOrders = async () => {
loading.value = true
try {
const token = localStorage.getItem('token') || sessionStorage.getItem('token')
if (!token) {
console.warn('未登录或缺少令牌')
// Pending.vue loadAvailableOrders
console.log('Token:', token);
console.log('localStorage:', localStorage.getItem('token'));
console.log('sessionStorage:', sessionStorage.getItem('token'));
// Pinia Token Admin.vue
const token = authStore.token
// Token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
// - /available
const params: any = {}
if (filterForm.value.area) {
params.areaId = filterForm.value.area
}
console.log('当前 Token:', token.substring(0, 20) + '...') //
// ID
// 使使
//
let url = '/api/work-orders/available'
const params = new URLSearchParams()
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (!areaId) {
console.warn('未指定片区ID')
orders.value = []
return
if (areaId) {
params.append('areaId', areaId)
}
//
const queryString = params.toString()
if (queryString) {
url += `?${queryString}`
}
const response = await axios.get(`/api/work-orders/available`, {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
areaId: areaId
}
})
if (response.data.code === 200) {
//
orders.value = response.data.data.map((order: any) => ({
id: order.orderId,
orderNo: order.orderId,
deviceType: '未知设备', // deviceId
deviceId: order.deviceId,
area: order.areaId,
// 使 request 使 axios
// loadAvailableOrders
const response = await request<{
code: number
msg: string
data: any[]
}>(url,{
method: 'GET',
})
//
if (response.code === 200) {
orders.value = (response.data || []).map((order: any) => ({
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status,
status: order.status || 'pending',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.updatedTime ? new Date(order.updatedTime).toLocaleString('zh-CN') : '未知时间',
location: '未知位置', // deviceId
location: '未知位置',
description: order.description,
priority: order.priority
}))
} else {
console.error('获取待抢单工单失败:', response.data.msg)
alert('获取待抢单工单失败:' + response.data.msg)
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取待抢单工单失败:', errorMsg)
alert(`获取待抢单工单失败:${errorMsg}`)
}
} catch (error) {
console.error('请求异常:', error)
alert('网络错误,请检查网络连接')
} finally {
} catch (error: any) {
console.error('请求异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取待抢单工单失败:${errorMsg}`)
/*if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}*/
}
finally {
loading.value = false
}
}
@ -354,6 +381,7 @@ const closeDetailModal = () => {
//
onMounted(() => {
console.log('Token:', authStore.token)
loadAvailableOrders()
})
</script>

@ -60,7 +60,7 @@
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<tr v-for="order in paginatedOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
@ -81,7 +81,7 @@
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">暂无处理中工单</td>
<td colspan="7" class="no-data">{{ loading ? '正在加载数据...' : '暂无处理中工单' }}</td>
</tr>
</tbody>
</table>
@ -97,7 +97,7 @@
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
@ -109,11 +109,11 @@
</div>
<!-- 详情弹窗 -->
<div v-if="showDetailModal" class="modal-overlay">
<div class="modal-container">
<div v-if="showDetailModal" class="modal-overlay" @click="closeDetailModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3>工单详情</h3>
<button class="modal-close" @click="showDetailModal = false">×</button>
<button class="modal-close" @click="closeDetailModal">×</button>
</div>
<div class="modal-body">
<div class="detail-item">
@ -160,7 +160,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn-close" @click="showDetailModal = false"></button>
<button class="btn-close" @click="closeDetailModal"></button>
</div>
</div>
</div>
@ -170,7 +170,8 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -196,6 +197,8 @@ const orders = ref<ProcessingOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
// /ID
const searchKeyword = ref('')
@ -224,54 +227,66 @@ const formatStatus = (status: OrderStatus): string => {
//
const loadProcessingOrders = async () => {
loading.value = true
try {
const token = localStorage.getItem('token')
const token = authStore.token
if (!token) {
console.warn('未登录或缺少令牌')
router.push('/login')
return
}
//
const params = new URLSearchParams()
params.append('status', 'processing')
if (filterForm.value.area) {
params.append('areaId', filterForm.value.area)
}
if (filterForm.value.createDate) {
params.append('startDate', filterForm.value.createDate)
}
const queryString = params.toString()
//
const url = `/api/work-orders/by-status?status=processing${queryString ? `&${queryString}` : ''}`
//
const response = await axios.get(`/api/work-orders/my`, {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
status: 'processing',
areaId: filterForm.value.area,
startDate: filterForm.value.createDate
}
const response = await request<{
code: number
msg: string
data: any[]
}>(url, {
method: 'GET'
})
if (response.data.code === 200) {
if (response.code === 200) {
//
orders.value = response.data.data.map((order: any) => ({
orders.value = response.data.map((order: any) => ({
id: order.orderId,
orderNo: order.orderId,
deviceType: order.deviceType,
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId,
area: order.areaId,
problemDesc: order.description,
problemDesc: order.description || '暂无描述',
status: order.status,
createTime: order.createdTime,
lastUploadTime: order.updatedTime,
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.updatedTime ? new Date(order.updatedTime).toLocaleString('zh-CN') : '未知时间',
location: order.location || '未知位置',
maintenanceName: order.assignedRepairmanName || '未分配',
maintenancePhone: order.assignedRepairmanPhone || '未知'
}))
} else {
console.error('获取处理中工单失败:', response.data.msg)
alert('获取处理中工单失败:' + response.data.msg)
console.error('获取处理中工单失败:', response.msg)
alert('获取处理中工单失败:' + response.msg)
}
} catch (error) {
console.error('请求异常:', error)
alert('网络错误,请检查网络连接')
} finally {
loading.value = false
}
}
@ -299,10 +314,16 @@ const totalPages = computed(() => {
return Math.ceil(filteredOrders.value.length / pageSize)
})
//
const paginatedOrders = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredOrders.value.slice(start, end)
})
//
const handleSearch = () => {
currentPage.value = 1 //
loadProcessingOrders()
}
// /
@ -331,6 +352,12 @@ const viewOrderDetail = (id: string) => {
}
}
//
const closeDetailModal = () => {
showDetailModal.value = false
currentOrder.value = null
}
//
onMounted(() => {
loadProcessingOrders()
@ -354,9 +381,9 @@ onMounted(() => {
.btn-reset { padding: 8px 16px; border: 1px solid #ddd; background-color: white; border-radius: 4px;cursor: pointer;font-size: 14px;color: #666; transition: all 0.3s; }
.btn-reset:hover { background-color: #f0f0f0; }
.order-table { width: 100%; border-collapse: collapse; }
order-table th, order-table td { padding: 12px 16px;text-align: left;border-bottom: 1px solid #f0f0f0; }
order-table th { background-color: #f8f9fa; font-weight: 600; color: #4e5969; font-size: 14px; }
order-table tbody tr:hover { background-color: #f8f9fa; }
.order-table th, .order-table td { padding: 12px 16px;text-align: left;border-bottom: 1px solid #f0f0f0; }
.order-table th { background-color: #f8f9fa; font-weight: 600; color: #4e5969; font-size: 14px; }
.order-table tbody tr:hover { background-color: #f8f9fa; }
.device-info { display: flex;flex-direction: column;gap: 4px; }
.device-type { font-weight: 500; color: #333; }
.device-id { font-size: 12px; color: #666; }
@ -456,7 +483,7 @@ order-table tbody tr:hover { background-color: #f8f9fa; }
justify-content: flex-end;
}
btn-close {
.btn-close {
padding: 6px 16px;
background-color: #1890ff;
color: white;

@ -1,4 +1,4 @@
<!-- src/views/order/OrderReview.vue -->
<!-- src/views/workorder/Review.vue -->
<template>
<div class="order-review-page">
<!-- 页面标题和面包屑 -->
@ -12,9 +12,9 @@
<!-- 工单号/设备ID搜索 -->
<div class="filter-item search-item">
<label>搜索</label>
<input
type="text"
v-model="searchKeyword"
<input
type="text"
v-model="searchKeyword"
class="search-input"
placeholder="输入工单号或设备ID搜索"
@input="handleSearch"
@ -34,9 +34,9 @@
<!-- 日期筛选 -->
<div class="filter-item">
<label>创建日期</label>
<input
type="date"
v-model="filterForm.createDate"
<input
type="date"
v-model="filterForm.createDate"
class="filter-input"
@change="handleFilter"
>
@ -61,7 +61,7 @@
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<tr v-for="order in paginatedOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
@ -82,7 +82,9 @@
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">暂无待审核工单</td>
<td colspan="7" class="no-data">
{{ loading ? '正在加载数据...' : '暂无待审核工单' }}
</td>
</tr>
</tbody>
</table>
@ -90,18 +92,18 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -112,8 +114,10 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -130,35 +134,13 @@ interface ReviewOrder {
createTime: string //
}
//
const orderList: ReviewOrder[] = [
{
id: '7',
orderNo: 'ORD-20231025-005',
deviceType: '制水机',
deviceId: 'WM-2023-002',
area: '校区',
problemDesc: '已完成水泵检修,出水速度恢复正常',
status: 'reviewing',
createTime: '2023-10-25 11:30:15'
},
{
id: '8',
orderNo: 'ORD-20231025-006',
deviceType: '供水机',
deviceId: 'WS-2023-005',
area: '市区',
problemDesc: '已修复故障代码E12设备出水正常',
status: 'reviewing',
createTime: '2023-10-25 14:20:22'
}
]
//
const orders = ref<ReviewOrder[]>(orderList)
const orders = ref<ReviewOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
// /ID
const searchKeyword = ref('')
@ -169,6 +151,84 @@ const filterForm = ref({
createDate: '' //
})
//
const loadReviewOrders = async () => {
loading.value = true
try {
// Token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('当前 Token:', token.substring(0, 20) + '...')
//
let url = '/api/work-orders/by-status?status=reviewing'
const params = new URLSearchParams()
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
//
const queryString = params.toString()
if (queryString) {
url += `&${queryString}`
}
// 使 request
const response = await request<{
code: number
msg: string
data: any[]
}>(url, {
method: 'GET',
})
//
if (response.code === 200) {
orders.value = (response.data || []).map((order: any) => ({
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status || 'reviewing',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间'
}))
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取待审核工单失败:', errorMsg)
alert(`获取待审核工单失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取待审核工单失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
@ -178,7 +238,7 @@ const formatStatus = (status: OrderStatus): string => {
reviewing: '待审核',
completed: '已结单'
}
return statusMap[status]
return statusMap[status] || status
}
//
@ -188,18 +248,25 @@ const filteredOrders = computed(() => {
const keywordMatch = searchKeyword.value.trim() === '' ||
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
const dateMatch = filterForm.value.createDate === '' ||
const dateMatch = filterForm.value.createDate === '' ||
order.createTime.split(' ')[0] === filterForm.value.createDate
return keywordMatch && areaMatch && dateMatch
})
})
//
const paginatedOrders = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredOrders.value.slice(start, end)
})
//
const totalPages = computed(() => {
return Math.ceil(filteredOrders.value.length / pageSize)
@ -213,6 +280,7 @@ const handleSearch = () => {
// /
const handleFilter = () => {
currentPage.value = 1 //
loadReviewOrders() //
}
//
@ -223,6 +291,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadReviewOrders()
}
//
@ -230,6 +299,12 @@ const viewOrderDetail = (id: string) => {
//
router.push(`/home/work-order/review/${id}`)
}
//
onMounted(() => {
console.log('Token:', authStore.token)
loadReviewOrders()
})
</script>
<style scoped>
@ -278,4 +353,4 @@ const viewOrderDetail = (id: string) => {
.filter-item { width: 100%; }
.search-input, .filter-select, .filter-input { width: 100%; }
}
</style>
</style>

@ -3,12 +3,33 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': '/src'
plugins: [
vue(),
],
resolve: {
alias: {
'@': '/src'
},
},
},
})
server: {
proxy: {
// 代理所有以 /api 开头的请求到后端
'/api': {
target: 'http://localhost:8080', // Spring Boot 后端地址
changeOrigin: true, // 改变请求来源
secure: false, // 如果是https可能需要设置为false
// 如果需要重写路径,可以取消下面的注释
// rewrite: (path) => path.replace(/^\/api/, '')
},
// 如果需要代理其他路径,可以继续添加
// '/ws': {
// target: 'ws://localhost:8080',
// ws: true
// }
},
// 可选:设置端口
port: 5173, // Vite默认端口
// 可选:自动打开浏览器
open: true
}
})
Loading…
Cancel
Save