新增设备状态页面及权限管理,优化管理员布局,提升用户体验

This commit is contained in:
yahaozhang
2025-09-16 01:52:58 +08:00
parent 843ff2a440
commit e1b3049053
5 changed files with 150 additions and 0 deletions

13
src/api/devices.js Normal file
View 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
}

View File

@@ -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> <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> <span>公告管理</span>
</el-menu-item> </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' }"> <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> <i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
<span>系统设置</span> <span>系统设置</span>
@@ -114,6 +118,7 @@ const pageTitleMap = {
'Links': '链接管理', 'Links': '链接管理',
'Refund': '退单管理', 'Refund': '退单管理',
'Announcements': '公告管理', 'Announcements': '公告管理',
'DeviceStatus': '设备状态',
'Settings': '系统设置' 'Settings': '系统设置'
} }
@@ -130,6 +135,7 @@ const canAccessUsers = computed(() => canAccessRoute('Users'))
const canAccessLinks = computed(() => canAccessRoute('Links')) const canAccessLinks = computed(() => canAccessRoute('Links'))
const canAccessRefund = computed(() => canAccessRoute('Refund')) const canAccessRefund = computed(() => canAccessRoute('Refund'))
const canAccessAnnouncements = computed(() => canAccessRoute('Announcements')) const canAccessAnnouncements = computed(() => canAccessRoute('Announcements'))
const canAccessDeviceStatus = computed(() => canAccessRoute('DeviceStatus'))
const canAccessSettings = computed(() => canAccessRoute('Settings')) const canAccessSettings = computed(() => canAccessRoute('Settings'))
// 获取积分余额 // 获取积分余额

View File

@@ -12,6 +12,7 @@ const AnnouncementList = () => import('@/views/announcements/AnnouncementList.vu
const RefundManagement = () => import('@/views/refund/RefundManagement.vue') const RefundManagement = () => import('@/views/refund/RefundManagement.vue')
const Play = () => import('@/views/Play.vue') const Play = () => import('@/views/Play.vue')
const NotFound = () => import('@/views/NotFound.vue') const NotFound = () => import('@/views/NotFound.vue')
const DeviceStatus = () => import('@/views/devices/DeviceStatus.vue')
export const routes = [ export const routes = [
{ path: '/login', name: 'Login', component: Login, meta: { public: true, title: '登录' } }, { 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: 'links', name: 'Links', component: LinkGenerate, meta: { title: '链接管理' } },
{ path: 'refund', name: 'Refund', component: RefundManagement, meta: { title: '退单管理' } }, { path: 'refund', name: 'Refund', component: RefundManagement, meta: { title: '退单管理' } },
{ path: 'announcements', name: 'Announcements', component: AnnouncementList, 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: '未找到' } }, { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound, meta: { public: true, title: '未找到' } },

View File

@@ -48,6 +48,9 @@ export const PERMISSIONS = {
REFUND_MANAGE: 'refund:manage', REFUND_MANAGE: 'refund:manage',
REFUND_EXECUTE: 'refund:execute', REFUND_EXECUTE: 'refund:execute',
REFUND_VIEW: 'refund:view', REFUND_VIEW: 'refund:view',
// 设备状态查看
DEVICE_VIEW: 'device:view',
} }
// 角色权限映射 // 角色权限映射
@@ -93,6 +96,7 @@ export const ROUTE_PERMISSIONS = {
'Links': [PERMISSIONS.LINK_VIEW], 'Links': [PERMISSIONS.LINK_VIEW],
'Refund': [PERMISSIONS.REFUND_VIEW], 'Refund': [PERMISSIONS.REFUND_VIEW],
'Announcements': [PERMISSIONS.ANNOUNCEMENT_VIEW], 'Announcements': [PERMISSIONS.ANNOUNCEMENT_VIEW],
'DeviceStatus': [PERMISSIONS.DEVICE_VIEW],
} }
// 获取当前用户信息 // 获取当前用户信息

View 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>