- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue - 移除开发者文档页面及其导航与样式实现 - 清理开发者侧功能完善工作日志文件 - 删除全局.gitignore配置文件,清理无用忽略规则 - 优化应用配置页面的参数读取和路由结构,解决刷新404问题 - 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入 - 移除对后端配置加密字段的 secret 标记,修正加密异常问题
380 lines
10 KiB
Vue
380 lines
10 KiB
Vue
<script setup lang="ts">
|
||
definePageMeta({ layout: 'admin' })
|
||
|
||
const { activeTab } = useNav()
|
||
|
||
const stats = ref([
|
||
{ label: '生产工单', value: 128, unit: '单', change: '+12%', up: true, icon: '📋', color: '#6366f1' },
|
||
{ label: '在制品数量', value: 3, unit: '万', change: '+8%', up: true, icon: '🏭', color: '#10b981' },
|
||
{ label: '设备利用率', value: 87, unit: '%', change: '+3%', up: true, icon: '⚙️', color: '#f59e0b' },
|
||
{ label: '订单交付率', value: 96, unit: '%', change: '+1%', up: true, icon: '🚚', color: '#3b82f6' },
|
||
])
|
||
|
||
const quickActions = ref([
|
||
{ label: '计划排程', icon: '📅', color: 'from-indigo-500 to-purple-500', path: '/admin/production/schedule' },
|
||
{ label: '生产管控', icon: '🎛️', color: 'from-emerald-500 to-teal-500', path: '/admin/production/control' },
|
||
{ label: '质量检测', icon: '🔍', color: 'from-amber-500 to-orange-500', path: '/admin/production/quality' },
|
||
{ label: '设备监控', icon: '📊', color: 'from-blue-500 to-cyan-500', path: '/admin/production/equipment' },
|
||
{ label: '采购申请', icon: '🛒', color: 'from-pink-500 to-rose-500', path: '/admin/supply/purchase' },
|
||
{ label: '库存查询', icon: '📦', color: 'from-violet-500 to-purple-500', path: '/admin/supply/warehouse' },
|
||
])
|
||
|
||
const recentOrders = ref([
|
||
{ id: 'WO2026040901', product: '精密轴承组件 A型', quantity: 500, status: '生产中', progress: 65, startDate: '2026-04-09' },
|
||
{ id: 'WO2026040802', product: '液压缸体 B型', quantity: 200, status: '待排产', progress: 0, startDate: '2026-04-08' },
|
||
{ id: 'WO2026040801', product: '传动齿轮组 C型', quantity: 1000, status: '已完成', progress: 100, startDate: '2026-04-07' },
|
||
{ id: 'WO2026040703', product: '密封圈组件 D型', quantity: 3000, status: '已完成', progress: 100, startDate: '2026-04-06' },
|
||
{ id: 'WO2026040702', product: '弹簧组件 E型', quantity: 800, status: '已取消', progress: 30, startDate: '2026-04-05' },
|
||
])
|
||
|
||
const qualityAlerts = ref([
|
||
{ level: 'warning', title: '质检异常:批次 B-20260408-03', desc: '尺寸超出公差范围,已自动隔离', time: '10分钟前' },
|
||
{ level: 'info', title: '质检报告生成', desc: '批次 A-20260409-01 质检完成', time: '30分钟前' },
|
||
{ level: 'warning', title: '设备报警:CNC-03', desc: '主轴温度异常,请及时处理', time: '1小时前' },
|
||
])
|
||
|
||
const equipmentStatus = ref([
|
||
{ name: 'CNC-01', status: '运行中', utilization: 92 },
|
||
{ name: 'CNC-02', status: '运行中', utilization: 88 },
|
||
{ name: 'CNC-03', status: '告警', utilization: 0 },
|
||
{ name: '铣床-01', status: '待机', utilization: 0 },
|
||
{ name: '铣床-02', status: '运行中', utilization: 75 },
|
||
])
|
||
|
||
const statusMap: Record<string, string> = {
|
||
'生产中': 'processing',
|
||
'待排产': 'warning',
|
||
'已完成': 'success',
|
||
'已取消': 'default',
|
||
}
|
||
const equipStatusMap: Record<string, string> = {
|
||
'运行中': 'success',
|
||
'告警': 'error',
|
||
'待机': 'default',
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="dashboard">
|
||
<!-- 统计卡片 -->
|
||
<a-row :gutter="[20, 20]" class="mb-6">
|
||
<a-col :xs="24" :sm="12" :xl="6" v-for="stat in stats" :key="stat.label">
|
||
<div class="stat-card" :style="{ '--accent': stat.color }">
|
||
<div class="stat-header">
|
||
<span class="stat-icon">{{ stat.icon }}</span>
|
||
<span :class="['stat-change', stat.up ? 'up' : 'down']">{{ stat.change }}</span>
|
||
</div>
|
||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||
<div class="stat-label">{{ stat.label }}</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<a-row :gutter="[20, 20]">
|
||
<!-- 左侧主体 -->
|
||
<a-col :xs="24" :xl="16">
|
||
<!-- 快捷入口 -->
|
||
<div class="card mb-6">
|
||
<div class="card-title">快捷入口</div>
|
||
<div class="quick-grid">
|
||
<NuxtLink
|
||
v-for="action in quickActions"
|
||
:key="action.label"
|
||
:to="action.path"
|
||
class="quick-item"
|
||
>
|
||
<div class="quick-icon" :class="action.color">{{ action.icon }}</div>
|
||
<span class="quick-label">{{ action.label }}</span>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工单列表 -->
|
||
<div class="card">
|
||
<div class="card-title">近期工单</div>
|
||
<a-table
|
||
:dataSource="recentOrders"
|
||
:pagination="false"
|
||
size="small"
|
||
rowKey="id"
|
||
:scroll="{ x: 600 }"
|
||
>
|
||
<a-table-column title="工单编号" dataIndex="id" width="140" />
|
||
<a-table-column title="产品名称" dataIndex="product" />
|
||
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
|
||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||
<template #default="{ text }">
|
||
<a-tag :color="statusMap[text] === 'success' ? 'success' : statusMap[text] === 'processing' ? 'processing' : statusMap[text] === 'warning' ? 'warning' : 'default'">
|
||
{{ text }}
|
||
</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="进度" dataIndex="progress" width="140" align="center">
|
||
<template #default="{ record }">
|
||
<a-progress
|
||
:percent="record.progress"
|
||
:status="record.progress === 100 ? 'success' : 'active'"
|
||
:showInfo="false"
|
||
size="small"
|
||
/>
|
||
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="开始日期" dataIndex="startDate" width="120" />
|
||
</a-table>
|
||
</div>
|
||
</a-col>
|
||
|
||
<!-- 右侧 -->
|
||
<a-col :xs="24" :xl="8">
|
||
<!-- 质量告警 -->
|
||
<div class="card mb-6">
|
||
<div class="card-title">质量告警</div>
|
||
<div class="alert-list">
|
||
<div v-for="(alert, idx) in qualityAlerts" :key="idx" class="alert-item" :class="alert.level">
|
||
<div class="alert-header">
|
||
<span class="alert-dot" :class="alert.level"></span>
|
||
<span class="alert-title">{{ alert.title }}</span>
|
||
</div>
|
||
<div class="alert-desc">{{ alert.desc }}</div>
|
||
<div class="alert-time">{{ alert.time }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备状态 -->
|
||
<div class="card">
|
||
<div class="card-title">设备状态</div>
|
||
<div class="equip-list">
|
||
<div v-for="equip in equipmentStatus" :key="equip.name" class="equip-item">
|
||
<span class="equip-name">{{ equip.name }}</span>
|
||
<a-tag :color="equipStatusMap[equip.status] === 'success' ? 'success' : equipStatusMap[equip.status] === 'error' ? 'error' : 'default'" size="small">
|
||
{{ equip.status }}
|
||
</a-tag>
|
||
<span class="equip-util">{{ equip.utilization }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dashboard {
|
||
padding: 24px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
border: 1px solid #f0f0f0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stat-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 4px;
|
||
height: 100%;
|
||
background: var(--accent);
|
||
}
|
||
|
||
.stat-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.stat-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.stat-change {
|
||
font-size: 12px;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.stat-change.up {
|
||
color: #10b981;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.stat-change.down {
|
||
color: #ef4444;
|
||
background: #fef2f2;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-unit {
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
color: #6b7280;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 13px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.quick-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.quick-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16px 8px;
|
||
border-radius: 10px;
|
||
background: #fafafa;
|
||
text-decoration: none;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.quick-item:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.quick-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(168,85,247,0.1));
|
||
}
|
||
|
||
.quick-label {
|
||
font-size: 13px;
|
||
color: #374151;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.alert-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.alert-item {
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.alert-item.warning {
|
||
background: #fffbeb;
|
||
border-left: 3px solid #f59e0b;
|
||
}
|
||
|
||
.alert-item.info {
|
||
background: #eff6ff;
|
||
border-left: 3px solid #3b82f6;
|
||
}
|
||
|
||
.alert-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.alert-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.alert-dot.warning {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.alert-dot.info {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.alert-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.alert-desc {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.alert-time {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.equip-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.equip-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background: #fafafa;
|
||
border-radius: 8px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.equip-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
flex: 1;
|
||
}
|
||
|
||
.equip-util {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
width: 40px;
|
||
text-align: right;
|
||
}
|
||
</style>
|