管理员列表加载 #73

Merged
pc8xi2fbj merged 2 commits from zhanghongwei_branch into develop 1 month ago

@ -2,6 +2,7 @@
"name": "web",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
@ -18,4 +19,4 @@
"vite": "^5.1.6",
"vue-tsc": "^1.8.27"
}
}
}

@ -1,10 +1,55 @@
npm<template>
<template>
<div id="app">
<!-- 全局提示组件可复用 -->
<div v-if="showToast" class="toast" :class="toastType">
{{ toastMessage }}
</div>
<!-- 路由视图出口 -->
<router-view />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
//
const showToast = ref(false)
const toastMessage = ref('')
const toastType = ref<'success' | 'error' | 'warning'>('success')
//
const showGlobalToast = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
toastMessage.value = message
toastType.value = type
showToast.value = true
// 3
setTimeout(() => {
showToast.value = false
}, 3000)
}
//
const app = document.querySelector('#app')
if (app) {
(app as any).showToast = showGlobalToast
}
//
const router = useRouter()
onMounted(() => {
router.beforeEach((to, from, next) => {
// 访
const isLoggedIn = localStorage.getItem('token') || sessionStorage.getItem('token')
if (to.path === '/login' && isLoggedIn) {
next('/home')
} else {
next()
}
})
})
</script>
<style>
@ -20,19 +65,27 @@ body {
-moz-osx-font-smoothing: grayscale;
background-color: #f8f9fa;
line-height: 1.6;
color: #333;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 全局样式 */
/* 全局组件样式 */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.stats-card {
@ -41,13 +94,15 @@ body {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
transition: transform 0.3s ease;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stats-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
/* 提示框样式 */
.alert-warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
@ -55,5 +110,111 @@ body {
padding: 12px 16px;
color: #856404;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
/* 全局提示样式 */
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9999;
transition: all 0.3s ease;
animation: slideIn 0.3s forwards;
}
.toast.success {
background-color: #52c41a;
}
.toast.error {
background-color: #f5222d;
}
.toast.warning {
background-color: #faad14;
}
/* 动画效果 */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 基础按钮样式 */
.btn {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
font-weight: 500;
}
.btn-primary {
background-color: #42b983;
color: white;
}
.btn-primary:hover {
background-color: #359e75;
}
.btn-secondary {
background-color: #f5f5f5;
color: #666;
}
.btn-secondary:hover {
background-color: #e8e8e8;
}
/* 表格基础样式 */
.base-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.base-table th,
.base-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
}
.base-table th {
background-color: #fafafa;
font-weight: 600;
color: #666;
}
.base-table tr:hover {
background-color: #f9f9f9;
}
/* 响应式调整 */
@media (max-width: 768px) {
.card, .stats-card {
padding: 16px;
}
.toast {
width: calc(100% - 40px);
text-align: center;
}
}
</style>

@ -74,8 +74,8 @@ const menuItems: MenuItem[] = [
{
id: 4,
name: '人员管理',
icon: '👥',
//
icon: '👥',
route: '/home/personnel/admin', //
children: [
{ name: '管理员', route: '/home/personnel/admin' },
{ name: '维修人员', route: '/home/personnel/maintenance' },

@ -17,8 +17,8 @@
:disabled="loading"
required
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="user">普通用户</option>
<option value="repairer">维修人员</option>
</select>
</div>
@ -84,7 +84,7 @@ const debugInfo = ref('')
const loginForm = reactive({
username: '',
password: '',
userType: 'user', //
userType: 'admin', //
rememberMe: false
})

@ -1,4 +1,3 @@
<!-- src/views/equipment/WaterSupplier.vue -->
<template>
<div class="water-supplier-page">
<!-- 页面标题和面包屑 -->
@ -10,22 +9,22 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add">添加供水机</button>
<div class="filters">
<!-- 搜索框 -->
<div class="search-box">
<input
type="text"
placeholder="搜索设备ID或位置..."
<input
type="text"
placeholder="搜索设备ID或位置..."
v-model="searchKeyword"
@input="handleSearch"
>
<button class="search-btn">搜索</button>
</div>
<!-- 片区筛选 -->
<select
v-model="selectedArea"
<select
v-model="selectedArea"
class="filter-select"
@change="handleSearch"
>
@ -33,10 +32,10 @@
<option value="市区">市区</option>
<option value="校区">校区</option>
</select>
<!-- 状态筛选 -->
<select
v-model="selectedStatus"
<select
v-model="selectedStatus"
class="filter-select"
@change="handleSearch"
>
@ -88,8 +87,8 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
@ -98,8 +97,8 @@
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -110,8 +109,9 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceStatusApi } from '@/api/deviceStatus'
//
type DeviceStatus = 'online' | 'offline' | 'warning' | 'error'
@ -125,47 +125,8 @@ interface WaterSupplierDevice {
lastUploadTime: string
}
//
const waterSupplierDevices: WaterSupplierDevice[] = [
{
id: 'WS-2023-001',
area: '市区',
location: '行政中心大楼1楼大厅',
status: 'online',
lastUploadTime: '2023-10-25 10:15:33'
},
{
id: 'WS-2023-002',
area: '校区',
location: '研究生公寓3号楼一层',
status: 'online',
lastUploadTime: '2023-10-25 09:30:22'
},
{
id: 'WS-2023-003',
area: '市区',
location: '科技园区A座大厅',
status: 'warning',
lastUploadTime: '2023-10-25 08:45:11'
},
{
id: 'WS-2023-004',
area: '校区',
location: '留学生公寓1楼',
status: 'offline',
lastUploadTime: '2023-10-24 23:05:47'
},
{
id: 'WS-2023-005',
area: '市区',
location: '图书馆新馆2楼',
status: 'error',
lastUploadTime: '2023-10-25 07:20:35'
}
]
//
const devices = ref<WaterSupplierDevice[]>(waterSupplierDevices)
const devices = ref<WaterSupplierDevice[]>([])
const searchKeyword = ref('')
const selectedArea = ref('') //
const selectedStatus = ref('') //
@ -173,16 +134,45 @@ const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
//
const loadWaterSuppliers = async () => {
try {
const params = {
status: selectedStatus.value,
areaId: selectedArea.value,
deviceType: 'water_supply' //
}
const response = await DeviceStatusApi.getDevicesByStatus(
params.status || 'all',
params.areaId,
params.deviceType
)
//
devices.value = response.data.map((device: any) => ({
id: device.deviceId,
area: device.areaId,
location: device.installLocation,
status: device.status,
lastUploadTime: device.lastUpdateTime
}))
} catch (error) {
console.error('加载供水机列表失败:', error)
alert('获取供水机列表失败,请检查网络连接')
}
}
//
const filteredDevices = computed(() => {
return devices.value.filter(device => {
const keywordMatch = searchKeyword.value.trim() === '' ||
device.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
device.location.toLowerCase().includes(searchKeyword.value.toLowerCase())
const areaMatch = selectedArea.value === '' || device.area === selectedArea.value
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
return keywordMatch && areaMatch && statusMatch
})
})
@ -205,13 +195,19 @@ const formatStatus = (status: DeviceStatus): string => {
//
const handleSearch = () => {
currentPage.value = 1 //
currentPage.value = 1 //
loadWaterSuppliers()
}
//
const viewDevice = (id: string) => {
router.push(`/home/equipment/water-supplier/${id}`)
}
//
onMounted(() => {
loadWaterSuppliers()
})
</script>
<style scoped>
@ -241,7 +237,7 @@ const viewDevice = (id: string) => {
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
flex-wrap:wrap;
gap: 16px;
}
@ -284,7 +280,7 @@ const viewDevice = (id: string) => {
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
cursor:pointer;
}
.filter-select {
@ -297,7 +293,7 @@ const viewDevice = (id: string) => {
.equipment-table {
width: 100%;
border-collapse: collapse;
border-collapse:collapse;
}
.equipment-table th,
@ -355,8 +351,8 @@ const viewDevice = (id: string) => {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
cursor:pointer;
border:none;
transition: opacity 0.3s;
}
@ -390,7 +386,7 @@ const viewDevice = (id: string) => {
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
cursor:pointer;
}
.page-btn:disabled {
@ -399,14 +395,14 @@ const viewDevice = (id: string) => {
}
/* 响应式调整 */
@media (max-width: 768px) {
@media (max-width: 768px) {
.filters {
flex-direction: column;
width: 100%;
}
.search-box, .filter-select {
width: 100%;
}
}
</style>
</style>

@ -1,4 +1,3 @@
<!-- src/views/personnel/Admin.vue -->
<template>
<div class="admin-page">
<!-- 页面标题和面包屑 -->
@ -10,11 +9,11 @@
<!-- 操作按钮区 -->
<div class="action-bar">
<button class="btn-add" @click="handleAddAdmin"></button>
<div class="search-box">
<input
type="text"
placeholder="搜索姓名或账号..."
<input
type="text"
placeholder="搜索姓名或账号..."
v-model="searchKeyword"
@input="handleSearch"
>
@ -47,14 +46,14 @@
</span>
</td>
<td class="operation-buttons">
<button
class="btn-edit"
<button
class="btn-edit"
@click="handleEdit(admin.id)"
>
编辑
</button>
<button
class="btn-status"
<button
class="btn-status"
:class="admin.status === 'active' ? 'btn-disable' : 'btn-enable'"
@click="handleStatusChange(admin.id, admin.status)"
>
@ -71,8 +70,8 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
@ -81,8 +80,8 @@
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -93,8 +92,14 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
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()
//
type AdminStatus = 'active' | 'disabled'
@ -109,40 +114,86 @@ interface Admin {
status: AdminStatus
}
//
const adminList: Admin[] = [
{
id: '1',
name: '张三',
account: 'admin01',
phone: '13800138000',
role: '超级管理员',
status: 'active'
},
{
id: '2',
name: '李四',
account: 'admin02',
phone: '13900139000',
role: '设备管理员',
status: 'active'
},
{
id: '3',
name: '王五',
account: 'admin03',
phone: '13700137000',
role: '系统管理员',
status: 'disabled'
}
]
//
const admins = ref<Admin[]>(adminList)
const admins = ref<Admin[]>([])
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const loading = ref(false)
//
// 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()}`, {
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 || '',
phone: admin.phone || '未知电话',
role: admin.role || '未知角色',
status: 'active' // 使
}))
} else {
// ""
const errorMsg = response.msg || `获取失败(错误码:${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 || '获取数据失败,请稍后重试'
alert(`获取管理员列表失败:${errorMsg}`)
// Token
if (error.message.includes('401')) {
authStore.logout() // Token
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const filteredAdmins = computed(() => {
@ -161,13 +212,19 @@ const totalPages = computed(() => {
//
const handleSearch = () => {
currentPage.value = 1 //
currentPage.value = 1
fetchAdminList()
}
//
onMounted(() => {
fetchAdminList()
})
//
const handleStatusChange = (id: string, currentStatus: AdminStatus) => {
const newStatus: AdminStatus = currentStatus === 'active' ? 'disabled' : 'active'
admins.value = admins.value.map(admin =>
admins.value = admins.value.map(admin =>
admin.id === id ? { ...admin, status: newStatus } : admin
)
// API
@ -359,13 +416,13 @@ const handleAddAdmin = () => {
flex-direction: column;
align-items: flex-start;
}
.search-box {
width: 100%;
}
.search-box input {
width: 100%;
}
}
</style>
</style>

@ -1,4 +1,3 @@
<!-- src/views/workorder/Pending.vue -->
<template>
<div class="order-to-claim-page">
<!-- 页面标题和面包屑 -->
@ -12,12 +11,12 @@
<!-- 工单号/设备ID搜索 -->
<div class="filter-item search-item">
<label>搜索</label>
<input
type="text"
v-model="searchKeyword"
class="search-input"
placeholder="输入工单号或设备ID搜索"
@input="handleSearch"
<input
type="text"
v-model="searchKeyword"
class="search-input"
placeholder="输入工单号或设备ID搜索"
@input="handleSearch"
>
</div>
@ -34,11 +33,11 @@
<!-- 日期筛选 -->
<div class="filter-item">
<label>创建日期</label>
<input
type="date"
v-model="filterForm.createDate"
class="filter-input"
@change="handleFilter"
<input
type="date"
v-model="filterForm.createDate"
class="filter-input"
@change="handleFilter"
>
</div>
@ -50,60 +49,62 @@
<div class="card">
<table class="order-table">
<thead>
<tr>
<th>工单号</th>
<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>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
<td>{{ order.area }}</td>
<td class="desc-cell">{{ order.problemDesc }}</td>
<td>
<tr v-for="order in paginatedOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
<div class="device-type">{{ order.deviceType }}</div>
<div class="device-id">{{ order.deviceId }}</div>
</div>
</td>
<td>{{ order.area }}</td>
<td class="desc-cell">{{ order.problemDesc }}</td>
<td>
<span :class="`status-tag ${order.status}`">
{{ formatStatus(order.status) }}
</span>
</td>
<td>{{ order.createTime }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewOrderDetail(order)"></button>
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">暂无待抢单工单</td>
</tr>
</td>
<td>{{ order.createTime }}</td>
<td class="operation-buttons">
<button class="btn-view" @click="viewOrderDetail(order)"></button>
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">
{{ loading ? '正在加载数据...' : '暂无待抢单工单' }}
</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 }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一页
</button>
@ -161,12 +162,13 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
//
type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled'
// -
type OrderStatus = 'pending' | 'grabbed' | 'processing' | 'completed' | 'closed' | 'timeout'
//
// WorkOrder
interface ToClaimOrder {
id: string
orderNo: string
@ -178,64 +180,15 @@ interface ToClaimOrder {
createTime: string //
lastUploadTime: string //
location: string //
description?: string //
priority?: string //
}
//
const orderList: ToClaimOrder[] = [
{
id: '1',
orderNo: 'ORD-20231025-001',
deviceType: '制水机',
deviceId: 'WM-2023-002',
area: '校区',
problemDesc: '出水速度慢,水压不足,需要检修水泵',
status: 'pending',
createTime: '2023-10-25 08:30:15',
lastUploadTime: '2023-10-25 08:25:30',
location: '教学区A栋一楼大厅'
},
{
id: '2',
orderNo: 'ORD-20231025-002',
deviceType: '供水机',
deviceId: 'WS-2023-005',
area: '市区',
problemDesc: '设备显示故障代码E12无法正常出水',
status: 'pending',
createTime: '2023-10-25 09:15:22',
lastUploadTime: '2023-10-25 09:10:05',
location: '市区图书馆二楼北侧'
},
{
id: '3',
orderNo: 'ORD-20231025-003',
deviceType: '制水机',
deviceId: 'WM-2023-003',
area: '市区',
problemDesc: '滤芯更换提醒,需要更换一级滤芯',
status: 'pending',
createTime: '2023-10-24 16:40:08',
lastUploadTime: '2023-10-24 16:35:22',
location: '行政楼一楼大厅'
},
{
id: '4',
orderNo: 'ORD-20231025-004',
deviceType: '供水机',
deviceId: 'WS-2023-004',
area: '校区',
problemDesc: '设备离线,无法上传数据,检查网络连接',
status: 'pending',
createTime: '2023-10-25 10:05:11',
lastUploadTime: '2023-10-25 09:50:45',
location: '学生宿舍3号楼一楼'
}
]
//
const orders = ref<ToClaimOrder[]>(orderList)
const orders = ref<ToClaimOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const loading = ref(false)
// /ID
const searchKeyword = ref('')
@ -250,15 +203,86 @@ const filterForm = ref({
const showDetailModal = ref(false)
const currentOrder = ref<ToClaimOrder | null>(null)
//
// -
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
pending: '待抢单',
grabbed: '已抢单',
processing: '处理中',
completed: '已完成',
cancelled: '已取消'
closed: '已关闭',
timeout: '超时未抢'
}
return statusMap[status] || status
}
//
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'));
return
}
// - /available
const params: any = {}
if (filterForm.value.area) {
params.areaId = filterForm.value.area
}
// ID
// 使使
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (!areaId) {
console.warn('未指定片区ID')
orders.value = []
return
}
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,
problemDesc: order.description || '暂无描述',
status: order.status,
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.updatedTime ? new Date(order.updatedTime).toLocaleString('zh-CN') : '未知时间',
location: '未知位置', // deviceId
description: order.description,
priority: order.priority
}))
} else {
console.error('获取待抢单工单失败:', response.data.msg)
alert('获取待抢单工单失败:' + response.data.msg)
}
} catch (error) {
console.error('请求异常:', error)
alert('网络错误,请检查网络连接')
} finally {
loading.value = false
}
return statusMap[status]
}
//
@ -266,20 +290,27 @@ const filteredOrders = computed(() => {
return orders.value.filter(order => {
// /ID
const keywordMatch = searchKeyword.value.trim() === '' ||
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
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 === '' ||
order.createTime.split(' ')[0] === 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)
@ -288,11 +319,14 @@ const totalPages = computed(() => {
//
const handleSearch = () => {
currentPage.value = 1 //
//
//
}
// /
const handleFilter = () => {
currentPage.value = 1 //
loadAvailableOrders() //
}
//
@ -303,6 +337,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadAvailableOrders()
}
//
@ -316,6 +351,11 @@ const closeDetailModal = () => {
showDetailModal.value = false
currentOrder.value = null
}
//
onMounted(() => {
loadAvailableOrders()
})
</script>
<style scoped>
@ -453,7 +493,7 @@ const closeDetailModal = () => {
position: relative;
}
/* 状态标签样式 */
/* 状态标签样式 - 适配后端状态 */
.status-tag {
display: inline-block;
padding: 4px 8px;
@ -467,21 +507,31 @@ const closeDetailModal = () => {
color: #d48806;
}
.status-tag.processing {
.status-tag.grabbed {
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag.processing {
background-color: #d6e4ff;
color: #1677ff;
}
.status-tag.completed {
background-color: #e6f7ee;
color: #00875a;
}
.status-tag.cancelled {
.status-tag.closed {
background-color: #f5f5f5;
color: #8c8c8c;
}
.status-tag.timeout {
background-color: #ffebe6;
color: #cf1322;
}
/* 操作按钮样式 */
.operation-buttons {
display: flex;
@ -634,11 +684,11 @@ const closeDetailModal = () => {
flex-direction: column;
align-items: flex-start;
}
.filter-item {
width: 100%;
}
.search-input, .filter-select, .filter-input {
width: 100%;
}

@ -1,4 +1,3 @@
<!-- src/views/workorder/Processing.vue -->
<template>
<div class="order-processing-page">
<!-- 页面标题和面包屑 -->
@ -12,9 +11,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 +33,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"
>
@ -46,7 +45,7 @@
<button class="btn-reset" @click="resetFilter"></button>
</div>
<!-- 表格 -->
<!-- 表格 -->
<div class="card">
<table class="order-table">
<thead>
@ -90,8 +89,8 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
@ -100,8 +99,8 @@
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -169,8 +168,9 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -183,7 +183,7 @@ interface ProcessingOrder {
deviceId: string // ID
area: string //
problemDesc: string //
status: OrderStatus //
status: OrderStatus // state
createTime: string //
lastUploadTime: string //
location: string //
@ -191,42 +191,10 @@ interface ProcessingOrder {
maintenancePhone: string //
}
//
const orderList: ProcessingOrder[] = [
{
id: '9',
orderNo: 'ORD-20231025-007',
deviceType: '制水机',
deviceId: 'WM-2023-003',
area: '市区',
problemDesc: '正在更换一级滤芯预计1小时内完成',
status: 'processing',
createTime: '2023-10-25 15:10:08',
lastUploadTime: '2023-10-25 15:45:22',
location: '市区商业广场B区3号柜位',
maintenanceName: '李师傅',
maintenancePhone: '13800138000'
},
{
id: '10',
orderNo: 'ORD-20231025-008',
deviceType: '供水机',
deviceId: 'WS-2023-004',
area: '校区',
problemDesc: '正在检查网络连接,设备离线问题排查中',
status: 'processing',
createTime: '2023-10-25 16:05:11',
lastUploadTime: '2023-10-25 13:30:05',
location: '校区图书馆2楼大厅',
maintenanceName: '王师傅',
maintenancePhone: '13900139000'
}
]
//
const orders = ref<ProcessingOrder[]>(orderList)
const orders = ref<ProcessingOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const pageSize = 10 //
const router = useRouter()
// /ID
@ -242,7 +210,7 @@ const filterForm = ref({
const showDetailModal = ref(false)
const currentOrder = ref<ProcessingOrder | null>(null)
//
// state
const formatStatus = (status: OrderStatus): string => {
const statusMap = {
timeout: '超时未抢',
@ -254,6 +222,59 @@ const formatStatus = (status: OrderStatus): string => {
return statusMap[status]
}
//
const loadProcessingOrders = async () => {
try {
const token = localStorage.getItem('token')
if (!token) {
console.warn('未登录或缺少令牌')
return
}
//
const params = new URLSearchParams()
if (filterForm.value.area) {
params.append('areaId', filterForm.value.area)
}
//
const response = await axios.get(`/api/work-orders/my`, {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
status: 'processing',
areaId: filterForm.value.area,
startDate: filterForm.value.createDate
}
})
if (response.data.code === 200) {
//
orders.value = response.data.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,
createTime: order.createdTime,
lastUploadTime: order.updatedTime,
location: order.location || '未知位置',
maintenanceName: order.assignedRepairmanName || '未分配',
maintenancePhone: order.assignedRepairmanPhone || '未知'
}))
} else {
console.error('获取处理中工单失败:', response.data.msg)
alert('获取处理中工单失败:' + response.data.msg)
}
} catch (error) {
console.error('请求异常:', error)
alert('网络错误,请检查网络连接')
}
}
//
const filteredOrders = computed(() => {
return orders.value.filter(order => {
@ -261,14 +282,14 @@ 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
})
})
@ -281,11 +302,13 @@ const totalPages = computed(() => {
//
const handleSearch = () => {
currentPage.value = 1 //
loadProcessingOrders()
}
// /
const handleFilter = () => {
currentPage.value = 1 //
loadProcessingOrders()
}
//
@ -296,6 +319,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadProcessingOrders()
}
//
@ -306,6 +330,11 @@ const viewOrderDetail = (id: string) => {
showDetailModal.value = true
}
}
//
onMounted(() => {
loadProcessingOrders()
})
</script>
<style scoped>
@ -318,33 +347,33 @@ const viewOrderDetail = (id: string) => {
.page-header h2 { font-size: 24px; font-weight: 600; color: #333; margin-bottom: 8px; }
.breadcrumb { color: #666; font-size: 14px; }
.filter-bar { display: flex; align-items: center; gap: 20px; padding: 16px; background-color: #f8f9fa; border-radius: 4px; margin-bottom: 16px; flex-wrap: wrap; }
.filter-item { display: flex; align-items: center; gap: 8px; }
.filter-item { display: flex;align-items: center; gap: 8px; }
.filter-item label { font-size: 14px; color: #4e5969; font-weight: 500; }
.search-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 240px; }
.filter-select, .filter-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 160px; }
.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 { 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; }
.device-info { display: flex; flex-direction: column; gap: 4px; }
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; }
.desc-cell { max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
.desc-cell:hover { white-space: normal; overflow: visible; background-color: white; z-index: 10; position: relative; }
.status-tag { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.desc-cell { max-width: 200px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;cursor:pointer; }
.desc-cell:hover { white-space: normal; overflow: visible;background-color: white;z-index: 10;position: relative; }
.status-tag { display: inline-block;padding: 4px 8px;border-radius: 4px;font-size: 12px;font-weight: 500; }
.status-tag.timeout { background-color: #ffebe6; color: #cf1322; }
.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; }
.operation-buttons { display: flex; gap: 8px; }
.btn-view { padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer; border: none; transition: opacity 0.3s; background-color: #e6f7ff; color: #1890ff; }
.operation-buttons { display:flex; gap: 8px; }
.btn-view { padding: 4px 8px; border-radius: 4px;font-size: 12px;cursor:pointer;border:none;transition: opacity 0.3s; background-color: #e6f7ff; color: #1890ff; }
.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; }
.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; }
/* 弹窗样式 */
@ -355,7 +384,7 @@ const viewOrderDetail = (id: string) => {
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
display:flex;
justify-content: center;
align-items: center;
z-index: 1000;
@ -372,7 +401,7 @@ const viewOrderDetail = (id: string) => {
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
display:flex;
justify-content: space-between;
align-items: center;
}
@ -384,10 +413,10 @@ const viewOrderDetail = (id: string) => {
}
.modal-close {
background: none;
border: none;
background:none;
border:none;
font-size: 20px;
cursor: pointer;
cursor:pointer;
color: #999;
padding: 0;
line-height: 1;
@ -403,7 +432,7 @@ const viewOrderDetail = (id: string) => {
.detail-item {
margin-bottom: 16px;
display: flex;
display:flex;
align-items: flex-start;
}
@ -423,17 +452,17 @@ const viewOrderDetail = (id: string) => {
.modal-footer {
padding: 12px 20px;
border-top: 1px solid #eee;
display: flex;
display:flex;
justify-content: flex-end;
}
.btn-close {
btn-close {
padding: 6px 16px;
background-color: #1890ff;
color: white;
border: none;
border:none;
border-radius: 4px;
cursor: pointer;
cursor:pointer;
font-size: 14px;
}
@ -443,18 +472,18 @@ const viewOrderDetail = (id: string) => {
/* 响应式调整 */
@media (max-width: 768px) {
.filter-bar { flex-direction: column; align-items: flex-start; }
.filter-bar { flex-direction: column;align-items: flex-start; }
.filter-item { width: 100%; }
.search-input, .filter-select, .filter-input { width: 100%; }
.detail-item {
flex-direction: column;
flex-direction:column;
}
.detail-label {
width: auto;
margin-bottom: 4px;
font-weight: 500;
}
}
</style>
</style>

Loading…
Cancel
Save