新增设备状态页面及权限管理,优化管理员布局,提升用户体验
This commit is contained in:
13
src/api/devices.js
Normal file
13
src/api/devices.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import http from '@/plugins/http'
|
||||
|
||||
/**
|
||||
* 获取所有设备状态快照
|
||||
* 响应示例见接口文档:/api/admin/devices/status
|
||||
* @returns {Promise<{devices: Record<string, any>, availableDevices: string[], totalDevices: number, availableCount: number}>}
|
||||
*/
|
||||
export async function getAllDeviceStatus() {
|
||||
const res = await http.get('/api/admin/devices/status')
|
||||
return res.data
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></i>
|
||||
<span>公告管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessDeviceStatus" index="DeviceStatus" :route="{ name: 'DeviceStatus' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M21 6h-7V4h-4v2H3c-1.1 0-2 .9-2 2v10a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2m0 12H3V8h18v10M8 10h2v6H8v-6m3 0h2v6h-2v-6m3 0h2v6h-2v-6Z"/></svg></i>
|
||||
<span>设备状态</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="canAccessSettings" index="Settings" :route="{ name: 'Settings' }">
|
||||
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
|
||||
<span>系统设置</span>
|
||||
@@ -114,6 +118,7 @@ const pageTitleMap = {
|
||||
'Links': '链接管理',
|
||||
'Refund': '退单管理',
|
||||
'Announcements': '公告管理',
|
||||
'DeviceStatus': '设备状态',
|
||||
'Settings': '系统设置'
|
||||
}
|
||||
|
||||
@@ -130,6 +135,7 @@ const canAccessUsers = computed(() => canAccessRoute('Users'))
|
||||
const canAccessLinks = computed(() => canAccessRoute('Links'))
|
||||
const canAccessRefund = computed(() => canAccessRoute('Refund'))
|
||||
const canAccessAnnouncements = computed(() => canAccessRoute('Announcements'))
|
||||
const canAccessDeviceStatus = computed(() => canAccessRoute('DeviceStatus'))
|
||||
const canAccessSettings = computed(() => canAccessRoute('Settings'))
|
||||
|
||||
// 获取积分余额
|
||||
|
||||
@@ -12,6 +12,7 @@ const AnnouncementList = () => import('@/views/announcements/AnnouncementList.vu
|
||||
const RefundManagement = () => import('@/views/refund/RefundManagement.vue')
|
||||
const Play = () => import('@/views/Play.vue')
|
||||
const NotFound = () => import('@/views/NotFound.vue')
|
||||
const DeviceStatus = () => import('@/views/devices/DeviceStatus.vue')
|
||||
|
||||
export const routes = [
|
||||
{ path: '/login', name: 'Login', component: Login, meta: { public: true, title: '登录' } },
|
||||
@@ -38,6 +39,7 @@ export const routes = [
|
||||
{ path: 'links', name: 'Links', component: LinkGenerate, meta: { title: '链接管理' } },
|
||||
{ path: 'refund', name: 'Refund', component: RefundManagement, meta: { title: '退单管理' } },
|
||||
{ path: 'announcements', name: 'Announcements', component: AnnouncementList, meta: { title: '公告管理' } },
|
||||
{ path: 'devices', name: 'DeviceStatus', component: DeviceStatus, meta: { title: '设备状态' } },
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { public: true, title: '未找到' } },
|
||||
|
||||
@@ -48,6 +48,9 @@ export const PERMISSIONS = {
|
||||
REFUND_MANAGE: 'refund:manage',
|
||||
REFUND_EXECUTE: 'refund:execute',
|
||||
REFUND_VIEW: 'refund:view',
|
||||
|
||||
// 设备状态查看
|
||||
DEVICE_VIEW: 'device:view',
|
||||
}
|
||||
|
||||
// 角色权限映射
|
||||
@@ -93,6 +96,7 @@ export const ROUTE_PERMISSIONS = {
|
||||
'Links': [PERMISSIONS.LINK_VIEW],
|
||||
'Refund': [PERMISSIONS.REFUND_VIEW],
|
||||
'Announcements': [PERMISSIONS.ANNOUNCEMENT_VIEW],
|
||||
'DeviceStatus': [PERMISSIONS.DEVICE_VIEW],
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
|
||||
125
src/views/devices/DeviceStatus.vue
Normal file
125
src/views/devices/DeviceStatus.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="device-status-view">
|
||||
<el-card class="header-card">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<h3 class="title">设备状态</h3>
|
||||
<div class="actions">
|
||||
<el-button type="primary" :loading="loading" @click="fetchData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-switch v-model="autoRefresh" active-text="自动刷新" @change="onToggleAuto" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stats">
|
||||
<el-statistic title="设备总数" :value="stats.totalDevices" />
|
||||
<el-statistic title="可分配数量" :value="stats.availableCount" />
|
||||
<el-tag type="success" class="inline-tag" v-if="stats.availableCount > 0">可用: {{ stats.availableCount }}</el-tag>
|
||||
<el-tag type="info" class="inline-tag" v-if="availableDevices.length">{{ availableDevices.join(', ') }}</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<el-table :data="tableData" v-loading="loading" border style="width: 100%">
|
||||
<el-table-column prop="deviceId" label="设备编号" width="120" />
|
||||
<el-table-column prop="series" label="系列" width="100" />
|
||||
<el-table-column prop="index" label="序号" width="80" />
|
||||
<el-table-column prop="val" label="原始状态" min-width="160" />
|
||||
<el-table-column prop="time" label="时间" width="180" />
|
||||
<el-table-column label="可用" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.available ? 'success' : 'danger'">{{ row.available ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getAllDeviceStatus } from '@/api/devices'
|
||||
import { isAdmin, canAccessRoute } from '@/utils/permission'
|
||||
|
||||
// 仅管理员可访问(路由守卫已做拦截,这里再次防御)
|
||||
const allowed = computed(() => isAdmin() || canAccessRoute('DeviceStatus'))
|
||||
|
||||
const loading = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
const timer = ref(null)
|
||||
|
||||
const stats = reactive({ totalDevices: 0, availableCount: 0 })
|
||||
const availableDevices = ref([])
|
||||
const devicesMap = ref({})
|
||||
|
||||
const tableData = computed(() => {
|
||||
const map = devicesMap.value || {}
|
||||
return Object.values(map).sort((a, b) => {
|
||||
if (a.series === b.series) return a.index - b.index
|
||||
return String(a.series).localeCompare(String(b.series))
|
||||
})
|
||||
})
|
||||
|
||||
async function fetchData() {
|
||||
if (!allowed.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const data = await getAllDeviceStatus()
|
||||
devicesMap.value = data?.devices || {}
|
||||
availableDevices.value = data?.availableDevices || []
|
||||
stats.totalDevices = data?.totalDevices || Object.keys(devicesMap.value).length
|
||||
stats.availableCount = data?.availableCount || availableDevices.value.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleAuto(val) {
|
||||
if (val) startAuto()
|
||||
else stopAuto()
|
||||
}
|
||||
|
||||
function startAuto() {
|
||||
stopAuto()
|
||||
timer.value = setInterval(fetchData, 2000)
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
if (autoRefresh.value) startAuto()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAuto()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-status-view { padding: 0; }
|
||||
.header-card { margin-bottom: 16px; }
|
||||
.header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.title { margin: 0; font-size: 18px; font-weight: 600; }
|
||||
.actions { display: flex; gap: 12px; align-items: center; }
|
||||
.stats { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
||||
.inline-tag { height: 24px; align-items: center; }
|
||||
.table-card { }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.title { font-size: 20px; }
|
||||
.actions { gap: 8px; }
|
||||
.stats { gap: 12px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user