2025-08-21 14:11:58 +08:00
|
|
|
|
<template>
|
2026-07-30 10:26:02 +08:00
|
|
|
|
<div class="home-dashboard">
|
|
|
|
|
|
<section class="hero-panel">
|
|
|
|
|
|
<div class="hero-content">
|
|
|
|
|
|
<div class="platform-state">
|
|
|
|
|
|
<span class="state-dot"></span>
|
|
|
|
|
|
CMVR 智能测试平台
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1>机器人智能测试管理系统</h1>
|
|
|
|
|
|
<p class="hero-description">
|
|
|
|
|
|
连接设备、编排流程、执行测试与巡检任务,集中管理机器人全生命周期测试业务。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="hero-actions">
|
|
|
|
|
|
<el-button type="primary" size="large" @click="router.push('/flow')">
|
|
|
|
|
|
<el-icon><Connection /></el-icon>
|
|
|
|
|
|
流程编排
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button size="large" class="cockpit-button" @click="router.push('/inspection/cockpit')">
|
|
|
|
|
|
<el-icon><Monitor /></el-icon>
|
|
|
|
|
|
巡检驾驶舱
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="hero-time" aria-label="当前时间">
|
|
|
|
|
|
<span class="time-label">{{ greeting }},{{ displayName }}</span>
|
|
|
|
|
|
<strong>{{ currentTime }}</strong>
|
|
|
|
|
|
<span>{{ currentDate }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="overview-section" aria-labelledby="overview-title">
|
|
|
|
|
|
<div class="section-heading">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="section-kicker">SYSTEM OVERVIEW</span>
|
|
|
|
|
|
<h2 id="overview-title">系统概览</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-tooltip content="刷新首页数据" placement="top">
|
|
|
|
|
|
<el-button class="refresh-button" circle :loading="loading" @click="loadDashboardData">
|
|
|
|
|
|
<el-icon v-if="!loading"><Refresh /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="metric-grid">
|
|
|
|
|
|
<article v-for="item in metrics" :key="item.key" class="metric-card">
|
|
|
|
|
|
<div class="metric-icon" :class="`metric-icon--${item.tone}`">
|
|
|
|
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="metric-content">
|
|
|
|
|
|
<span>{{ item.label }}</span>
|
|
|
|
|
|
<strong>{{ displayMetric(item.key) }}</strong>
|
|
|
|
|
|
<small>{{ item.description }}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-if="stats[item.key].restricted" class="metric-state">无权限</span>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="workspace-grid">
|
|
|
|
|
|
<div class="dashboard-panel task-panel">
|
|
|
|
|
|
<div class="panel-heading">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="section-kicker">LIVE OPERATIONS</span>
|
|
|
|
|
|
<h2>任务运行态</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="text-action" type="button" @click="navigateToFeature(features[2])">
|
|
|
|
|
|
查看全部
|
|
|
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="activeTasks.length" class="task-list">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="task in activeTasks"
|
|
|
|
|
|
:key="task.id"
|
|
|
|
|
|
class="task-row"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="navigateToFeature(features[2])"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="task-status-dot" :class="`status-${task.status}`"></span>
|
|
|
|
|
|
<span class="task-main">
|
|
|
|
|
|
<strong>{{ task.taskName || '未命名任务' }}</strong>
|
|
|
|
|
|
<small>{{ task.robotName || '暂未分配机器人' }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="task-meta">
|
|
|
|
|
|
<el-tag :type="inspectionStatus(task.status).type" effect="light" size="small">
|
|
|
|
|
|
{{ inspectionStatus(task.status).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<small>{{ formatTime(task.startTime || task.createTime) }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-icon class="row-arrow"><ArrowRight /></el-icon>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
|
|
|
<div class="empty-icon"><el-icon><CircleCheck /></el-icon></div>
|
|
|
|
|
|
<strong>{{ activeTasksRestricted ? '暂无任务查看权限' : '当前没有运行中的任务' }}</strong>
|
|
|
|
|
|
<span>{{ activeTasksRestricted ? '请联系管理员分配巡检任务权限' : '新的巡检任务启动后会显示在这里' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="dashboard-panel capability-panel">
|
|
|
|
|
|
<div class="panel-heading">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="section-kicker">CAPABILITIES</span>
|
|
|
|
|
|
<h2>能力中心</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="feature-grid">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="feature in features"
|
|
|
|
|
|
:key="feature.title"
|
|
|
|
|
|
class="feature-item"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
@click="navigateToFeature(feature)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="feature-icon" :style="{ color: feature.color, backgroundColor: feature.background }">
|
|
|
|
|
|
<el-icon><component :is="feature.icon" /></el-icon>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="feature-copy">
|
|
|
|
|
|
<strong>{{ feature.title }}</strong>
|
|
|
|
|
|
<small>{{ feature.description }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-icon class="feature-arrow"><ArrowRight /></el-icon>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="dashboard-panel recent-panel">
|
|
|
|
|
|
<div class="panel-heading">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span class="section-kicker">RECENT EXECUTIONS</span>
|
|
|
|
|
|
<h2>最近测试实例</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="text-action" type="button" @click="navigateToFeature(features[4])">
|
|
|
|
|
|
执行记录
|
|
|
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="recentInstances.length" class="recent-list">
|
|
|
|
|
|
<div v-for="instance in recentInstances" :key="instance.id" class="recent-item">
|
|
|
|
|
|
<div class="recent-order">{{ String(instance.order).padStart(2, '0') }}</div>
|
|
|
|
|
|
<div class="recent-info">
|
|
|
|
|
|
<strong>{{ instance.taskName || '未命名测试实例' }}</strong>
|
|
|
|
|
|
<span>{{ instance.remark || '机器人测试任务' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="recent-time">{{ formatTime(instance.createTime) }}</span>
|
|
|
|
|
|
<el-tag :type="testStatus(instance.status).type" effect="plain" size="small">
|
|
|
|
|
|
{{ testStatus(instance.status).label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="recent-empty">
|
|
|
|
|
|
<el-icon><DocumentChecked /></el-icon>
|
|
|
|
|
|
<span>{{ recentInstancesRestricted ? '暂无测试实例查看权限' : '暂无测试执行记录' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2025-08-21 14:11:58 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup name="Index">
|
2026-07-30 10:26:02 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ArrowRight,
|
|
|
|
|
|
CircleCheck,
|
|
|
|
|
|
Connection,
|
|
|
|
|
|
Cpu,
|
|
|
|
|
|
DataAnalysis,
|
|
|
|
|
|
DocumentChecked,
|
|
|
|
|
|
Grid,
|
|
|
|
|
|
Headset,
|
|
|
|
|
|
Monitor,
|
|
|
|
|
|
Odometer,
|
|
|
|
|
|
Operation,
|
|
|
|
|
|
Refresh,
|
|
|
|
|
|
SetUp,
|
|
|
|
|
|
} from '@element-plus/icons-vue'
|
|
|
|
|
|
import { listRegister } from '@/api/device/register'
|
|
|
|
|
|
import { getTaskList } from '@/api/inspection/task'
|
|
|
|
|
|
import { getRunTaskList } from '@/api/inspection/runTask'
|
|
|
|
|
|
import { listConfig } from '@/api/test/config'
|
|
|
|
|
|
import { listInst } from '@/api/test/log'
|
|
|
|
|
|
import auth from '@/plugins/auth'
|
|
|
|
|
|
import usePermissionStore from '@/store/modules/permission'
|
|
|
|
|
|
import useUserStore from '@/store/modules/user'
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
const permissionStore = usePermissionStore()
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const now = ref(new Date())
|
|
|
|
|
|
const activeTasks = ref([])
|
|
|
|
|
|
const recentInstances = ref([])
|
|
|
|
|
|
const activeTasksRestricted = ref(false)
|
|
|
|
|
|
const recentInstancesRestricted = ref(false)
|
|
|
|
|
|
let clockTimer
|
|
|
|
|
|
|
|
|
|
|
|
const stats = reactive({
|
|
|
|
|
|
devices: { value: null, restricted: false },
|
|
|
|
|
|
workflows: { value: null, restricted: false },
|
|
|
|
|
|
inspections: { value: null, restricted: false },
|
|
|
|
|
|
instances: { value: null, restricted: false },
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const metrics = [
|
|
|
|
|
|
{ key: 'devices', label: '接入设备', description: '已注册设备总数', icon: Cpu, tone: 'blue' },
|
|
|
|
|
|
{ key: 'workflows', label: '测试流程', description: '可用测试编排', icon: Connection, tone: 'green' },
|
|
|
|
|
|
{ key: 'inspections', label: '巡检任务', description: '已配置巡检任务', icon: Odometer, tone: 'orange' },
|
|
|
|
|
|
{ key: 'instances', label: '测试实例', description: '累计执行实例', icon: DataAnalysis, tone: 'red' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const features = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '设备接入',
|
|
|
|
|
|
description: '配置机器人与终端设备',
|
|
|
|
|
|
icon: SetUp,
|
|
|
|
|
|
color: '#2563eb',
|
|
|
|
|
|
background: '#eaf2ff',
|
|
|
|
|
|
menuTitles: ['设备注册', '设备配置管理', '设备管理'],
|
|
|
|
|
|
fallback: '/device/register',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '流程编排',
|
|
|
|
|
|
description: '构建可视化测试工作流',
|
|
|
|
|
|
icon: Operation,
|
|
|
|
|
|
color: '#0f8a73',
|
|
|
|
|
|
background: '#e7f6f2',
|
|
|
|
|
|
menuTitles: ['任务流程编排', '流程编排'],
|
|
|
|
|
|
fallback: '/flow',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '巡检执行',
|
|
|
|
|
|
description: '调度机器人巡检任务',
|
|
|
|
|
|
icon: Monitor,
|
|
|
|
|
|
color: '#c56a16',
|
|
|
|
|
|
background: '#fff3e5',
|
|
|
|
|
|
menuTitles: ['执行任务', '运行任务', '巡检任务执行'],
|
|
|
|
|
|
fallback: '/inspection/runTask',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '巡检配置',
|
|
|
|
|
|
description: '管理地图、点位与任务',
|
|
|
|
|
|
icon: Grid,
|
|
|
|
|
|
color: '#6b57c7',
|
|
|
|
|
|
background: '#f0edff',
|
|
|
|
|
|
menuTitles: ['巡检任务', '巡检地图', '地图管理'],
|
|
|
|
|
|
fallback: '/inspection/task',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '测试记录',
|
|
|
|
|
|
description: '追踪执行结果与日志',
|
|
|
|
|
|
icon: DocumentChecked,
|
|
|
|
|
|
color: '#d14343',
|
|
|
|
|
|
background: '#ffeded',
|
|
|
|
|
|
menuTitles: ['任务历史', '测试记录', '任务实例', '执行记录'],
|
|
|
|
|
|
fallback: '/test/history',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '语料中心',
|
|
|
|
|
|
description: '管理语音与合成资源',
|
|
|
|
|
|
icon: Headset,
|
|
|
|
|
|
color: '#087f9c',
|
|
|
|
|
|
background: '#e8f7fa',
|
|
|
|
|
|
menuTitles: ['语料库', '语料管理', '车载语料'],
|
|
|
|
|
|
fallback: '/corpusManger/corpus',
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
2026-07-30 10:26:02 +08:00
|
|
|
|
const displayName = computed(() => userStore.nickName || userStore.name || '用户')
|
|
|
|
|
|
const currentTime = computed(() => new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false,
|
|
|
|
|
|
}).format(now.value))
|
|
|
|
|
|
const currentDate = computed(() => new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: 'long',
|
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
|
}).format(now.value))
|
|
|
|
|
|
const greeting = computed(() => {
|
|
|
|
|
|
const hour = now.value.getHours()
|
|
|
|
|
|
if (hour < 6) return '夜深了'
|
|
|
|
|
|
if (hour < 12) return '上午好'
|
|
|
|
|
|
if (hour < 18) return '下午好'
|
|
|
|
|
|
return '晚上好'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function displayMetric(key) {
|
|
|
|
|
|
const metric = stats[key]
|
|
|
|
|
|
if (metric.restricted) return '--'
|
|
|
|
|
|
return metric.value === null ? '--' : metric.value.toLocaleString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasPermission(permission) {
|
|
|
|
|
|
return auth.hasPermi(permission)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createMetricRequest(key, permission, request) {
|
|
|
|
|
|
if (!hasPermission(permission)) {
|
|
|
|
|
|
stats[key].restricted = true
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
stats[key].restricted = false
|
|
|
|
|
|
return request()
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
stats[key].value = Number(response.total ?? response.rows?.length ?? 0)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
stats[key].value = null
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDashboardData() {
|
|
|
|
|
|
if (loading.value) return
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
|
|
|
|
|
|
const requests = [
|
|
|
|
|
|
createMetricRequest('devices', 'device:register:list', () => listRegister({ pageNum: 1, pageSize: 1 })),
|
|
|
|
|
|
createMetricRequest('workflows', 'test:config:list', () => listConfig({ pageNum: 1, pageSize: 1 })),
|
|
|
|
|
|
createMetricRequest('inspections', 'inspection:task:list', () => getTaskList({ pageNum: 1, pageSize: 1 })),
|
|
|
|
|
|
createMetricRequest('instances', 'test:inst:list', () => listInst({ pageNum: 1, pageSize: 5 })),
|
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
if (hasPermission('inspection:taskInstance:list')) {
|
|
|
|
|
|
activeTasksRestricted.value = false
|
|
|
|
|
|
requests.push(
|
|
|
|
|
|
getRunTaskList({ pageNum: 1, pageSize: 5, statusList: ['1', '3', '5'] })
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
activeTasks.value = (response.rows || []).slice(0, 4)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
activeTasks.value = []
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
activeTasksRestricted.value = true
|
|
|
|
|
|
activeTasks.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasPermission('test:inst:list')) {
|
|
|
|
|
|
recentInstancesRestricted.value = false
|
|
|
|
|
|
requests.push(
|
|
|
|
|
|
listInst({ pageNum: 1, pageSize: 4 })
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
recentInstances.value = (response.rows || []).slice(0, 4).map((item, index) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
order: index + 1,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
recentInstances.value = []
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
recentInstancesRestricted.value = true
|
|
|
|
|
|
recentInstances.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.allSettled(requests)
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inspectionStatus(status) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'0': { label: '已完成', type: 'success' },
|
|
|
|
|
|
'1': { label: '执行中', type: 'warning' },
|
|
|
|
|
|
'2': { label: '失败', type: 'danger' },
|
|
|
|
|
|
'3': { label: '已暂停', type: 'primary' },
|
|
|
|
|
|
'4': { label: '已终止', type: 'info' },
|
|
|
|
|
|
'5': { label: '等待执行', type: 'info' },
|
|
|
|
|
|
}[String(status)] || { label: '未知', type: 'info' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testStatus(status) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'0': { label: '已完成', type: 'success' },
|
|
|
|
|
|
'1': { label: '执行中', type: 'warning' },
|
|
|
|
|
|
'2': { label: '失败', type: 'danger' },
|
|
|
|
|
|
}[String(status)] || { label: '未知', type: 'info' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTime(value) {
|
|
|
|
|
|
if (!value) return '尚未开始'
|
|
|
|
|
|
const date = new Date(String(value).replace(/-/g, '/'))
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) return String(value)
|
|
|
|
|
|
return new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false,
|
|
|
|
|
|
}).format(date)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function joinPath(parentPath, routePath) {
|
|
|
|
|
|
if (!routePath) return parentPath || '/'
|
|
|
|
|
|
if (routePath.startsWith('/')) return routePath
|
|
|
|
|
|
const parent = parentPath && parentPath !== '/' ? parentPath.replace(/\/$/, '') : ''
|
|
|
|
|
|
return `${parent}/${routePath}`.replace(/\/+/g, '/')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function flattenMenus(routes, parentPath = '', result = []) {
|
|
|
|
|
|
for (const route of routes || []) {
|
|
|
|
|
|
const path = joinPath(parentPath, route.path)
|
|
|
|
|
|
if (!route.hidden && route.meta?.title) {
|
|
|
|
|
|
result.push({ title: route.meta.title, path })
|
|
|
|
|
|
}
|
|
|
|
|
|
if (route.children?.length) flattenMenus(route.children, path, result)
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveFeaturePath(feature) {
|
|
|
|
|
|
const menus = flattenMenus(permissionStore.sidebarRouters)
|
|
|
|
|
|
const exact = menus.find((menu) => feature.menuTitles.includes(menu.title))
|
|
|
|
|
|
if (exact) return exact.path
|
|
|
|
|
|
const partial = menus.find((menu) => feature.menuTitles.some((title) => menu.title.includes(title)))
|
|
|
|
|
|
return partial?.path || feature.fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function navigateToFeature(feature) {
|
|
|
|
|
|
router.push(resolveFeaturePath(feature))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadDashboardData()
|
|
|
|
|
|
clockTimer = window.setInterval(() => {
|
|
|
|
|
|
now.value = new Date()
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
window.clearInterval(clockTimer)
|
|
|
|
|
|
})
|
2025-08-21 14:11:58 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
2026-07-30 10:26:02 +08:00
|
|
|
|
.home-dashboard {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 0 2px 24px;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-panel {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
min-height: 236px;
|
|
|
|
|
|
padding: 36px 42px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 32px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background-image: url('../assets/images/index-background.jpg');
|
|
|
|
|
|
background-position: center 48%;
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-panel::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(8, 24, 37, 0.72);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-content,
|
|
|
|
|
|
.hero-time {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-content {
|
|
|
|
|
|
max-width: 760px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.platform-state {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
margin-bottom: 13px;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.82);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.state-dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #35d2a0;
|
|
|
|
|
|
box-shadow: 0 0 0 5px rgba(53, 210, 160, 0.16);
|
|
|
|
|
|
animation: status-pulse 2.2s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-content h1 {
|
2025-11-20 16:44:51 +08:00
|
|
|
|
margin: 0;
|
2026-07-30 10:26:02 +08:00
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
line-height: 1.28;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
letter-spacing: 0;
|
2025-11-20 16:44:51 +08:00
|
|
|
|
}
|
2026-07-30 10:26:02 +08:00
|
|
|
|
|
|
|
|
|
|
.hero-description {
|
|
|
|
|
|
max-width: 670px;
|
|
|
|
|
|
margin: 12px 0 22px;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.76);
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-actions :deep(.el-button) {
|
|
|
|
|
|
min-width: 126px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-actions :deep(.el-button--primary) {
|
|
|
|
|
|
border-color: #1688e7;
|
|
|
|
|
|
background: #1688e7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cockpit-button {
|
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.42) !important;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.1) !important;
|
|
|
|
|
|
color: #fff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cockpit-button:hover {
|
|
|
|
|
|
border-color: #fff !important;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.18) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-time {
|
|
|
|
|
|
min-width: 190px;
|
|
|
|
|
|
padding-left: 26px;
|
|
|
|
|
|
border-left: 1px solid rgba(255, 255, 255, 0.24);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-time strong {
|
|
|
|
|
|
margin: 6px 0 2px;
|
|
|
|
|
|
font-size: 42px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-time > span:last-child,
|
|
|
|
|
|
.time-label {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.72);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.overview-section {
|
|
|
|
|
|
padding: 24px 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-heading,
|
|
|
|
|
|
.panel-heading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-heading {
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-kicker {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-heading h2,
|
|
|
|
|
|
.panel-heading h2 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-button {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-card {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
min-height: 116px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
|
|
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-card:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
border-color: var(--el-border-color);
|
|
|
|
|
|
box-shadow: 0 10px 24px rgba(28, 44, 64, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-icon {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
flex: 0 0 48px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-icon--blue { color: #2563eb; background: #eaf2ff; }
|
|
|
|
|
|
.metric-icon--green { color: #0f8a73; background: #e7f6f2; }
|
|
|
|
|
|
.metric-icon--orange { color: #c56a16; background: #fff3e5; }
|
|
|
|
|
|
.metric-icon--red { color: #d14343; background: #ffeded; }
|
|
|
|
|
|
|
|
|
|
|
|
.metric-content {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr auto;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
column-gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-content > span {
|
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-content strong {
|
|
|
|
|
|
grid-column: 2;
|
|
|
|
|
|
grid-row: 1 / span 2;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
2025-11-20 16:44:51 +08:00
|
|
|
|
}
|
2026-07-30 10:26:02 +08:00
|
|
|
|
|
|
|
|
|
|
.metric-content small {
|
|
|
|
|
|
margin-top: 7px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-state {
|
2025-11-20 16:44:51 +08:00
|
|
|
|
position: absolute;
|
2026-07-30 10:26:02 +08:00
|
|
|
|
right: 12px;
|
|
|
|
|
|
bottom: 8px;
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workspace-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr);
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dashboard-panel {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-heading {
|
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.text-action {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.text-action:hover {
|
|
|
|
|
|
color: var(--el-color-primary-light-3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-row {
|
2025-11-20 16:44:51 +08:00
|
|
|
|
width: 100%;
|
2026-07-30 10:26:02 +08:00
|
|
|
|
min-height: 63px;
|
|
|
|
|
|
padding: 11px 6px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-extra-light);
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 10px minmax(130px, 1fr) auto 18px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-row:first-child {
|
|
|
|
|
|
border-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-row:hover {
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-status-dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--el-color-info);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-status-dot.status-1 { background: var(--el-color-warning); }
|
|
|
|
|
|
.task-status-dot.status-3 { background: var(--el-color-primary); }
|
|
|
|
|
|
.task-status-dot.status-5 { background: var(--el-color-info); }
|
|
|
|
|
|
|
|
|
|
|
|
.task-main,
|
|
|
|
|
|
.task-meta {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-main strong {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-main small,
|
|
|
|
|
|
.task-meta small {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-meta {
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.row-arrow,
|
|
|
|
|
|
.feature-arrow {
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
|
min-height: 184px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-icon {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
background: var(--el-color-success-light-9);
|
|
|
|
|
|
color: var(--el-color-success);
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state strong {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state > span {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-item {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
min-height: 76px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 38px minmax(0, 1fr) 14px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
background: var(--el-fill-color-extra-light);
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: border-color 160ms ease, background-color 160ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-item:hover {
|
|
|
|
|
|
border-color: var(--el-border-color);
|
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-icon {
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-copy {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-copy strong {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.feature-copy small {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-panel {
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
column-gap: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-item {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
min-height: 62px;
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-extra-light);
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 32px minmax(0, 1fr) auto auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-item:nth-child(-n + 2) {
|
|
|
|
|
|
border-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-order {
|
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
|
font-size: 17px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
2025-11-20 16:44:51 +08:00
|
|
|
|
}
|
2026-07-30 10:26:02 +08:00
|
|
|
|
|
|
|
|
|
|
.recent-info {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-info strong,
|
|
|
|
|
|
.recent-info span {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-info strong {
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-info span,
|
|
|
|
|
|
.recent-time {
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-empty {
|
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-empty .el-icon {
|
|
|
|
|
|
font-size: 19px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:global(html.dark) .hero-panel::before {
|
|
|
|
|
|
background: rgba(4, 13, 22, 0.82);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:global(html.dark) .metric-card:hover {
|
|
|
|
|
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:global(html.dark) .metric-icon,
|
|
|
|
|
|
:global(html.dark) .feature-icon {
|
|
|
|
|
|
background: var(--el-fill-color) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes status-pulse {
|
|
|
|
|
|
0%, 100% { box-shadow: 0 0 0 4px rgba(53, 210, 160, 0.12); }
|
|
|
|
|
|
50% { box-shadow: 0 0 0 7px rgba(53, 210, 160, 0.04); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
|
.metric-grid {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workspace-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
@media (max-width: 768px) {
|
2026-07-30 10:26:02 +08:00
|
|
|
|
.home-dashboard {
|
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-panel {
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
padding: 28px 22px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-content h1 {
|
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-description {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-time {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 18px 0 0;
|
|
|
|
|
|
border-top: 1px solid rgba(255, 255, 255, 0.24);
|
|
|
|
|
|
border-left: 0;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hero-time strong {
|
|
|
|
|
|
font-size: 34px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-grid,
|
|
|
|
|
|
.feature-grid,
|
|
|
|
|
|
.recent-list {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-item:nth-child(2) {
|
|
|
|
|
|
border-top: 1px solid var(--el-border-color-extra-light);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
2026-07-30 10:26:02 +08:00
|
|
|
|
@media (max-width: 520px) {
|
|
|
|
|
|
.metric-grid {
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-card {
|
|
|
|
|
|
min-height: 104px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-content {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.metric-content strong {
|
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
|
grid-row: auto;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-row {
|
|
|
|
|
|
grid-template-columns: 10px minmax(0, 1fr) 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-meta {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-item {
|
|
|
|
|
|
grid-template-columns: 28px minmax(0, 1fr) auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.recent-time {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|