Files
tiantian-system/app/pages/admin/index.vue
赵忠林 f9e1286ab1 refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
2026-04-09 07:35:34 +08:00

380 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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