refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue - 移除开发者文档页面及其导航与样式实现 - 清理开发者侧功能完善工作日志文件 - 删除全局.gitignore配置文件,清理无用忽略规则 - 优化应用配置页面的参数读取和路由结构,解决刷新404问题 - 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入 - 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
113
app/pages/admin/production/control.vue
Normal file
113
app/pages/admin/production/control.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-control'
|
||||
|
||||
const realTimeData = ref([
|
||||
{ id: 'WO2026040901', product: '精密轴承组件 A型', line: '产线1', planQty: 500, doneQty: 325, passQty: 318, passRate: 97.8, status: '生产中' },
|
||||
{ id: 'WO2026040703', product: '密封圈组件 D型', line: '产线3', planQty: 3000, doneQty: 2100, passQty: 2058, passRate: 98.0, status: '生产中' },
|
||||
])
|
||||
|
||||
const alerts = ref([
|
||||
{ id: 1, type: 'warning', msg: '产线1:主轴温度偏高,当前78°C', time: '10:30' },
|
||||
{ id: 2, type: 'info', msg: 'WO2026040901 完成500件,当前进度65%', time: '10:15' },
|
||||
{ id: 3, type: 'error', msg: '产线2:刀具寿命预警,请及时更换', time: '09:45' },
|
||||
])
|
||||
|
||||
const activeTab2 = ref('realtime')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">生产管控</h2>
|
||||
<a-space>
|
||||
<a-button @click="() => {}">导出报表</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab2">
|
||||
<a-tab-pane key="realtime" tab="实时监控">
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">实时生产进度</div>
|
||||
<a-table :dataSource="realTimeData" :pagination="false" size="small" rowKey="id">
|
||||
<a-table-column title="工单编号" dataIndex="id" width="150" />
|
||||
<a-table-column title="产品" dataIndex="product" />
|
||||
<a-table-column title="产线" dataIndex="line" width="80" align="center" />
|
||||
<a-table-column title="计划数量" dataIndex="planQty" width="100" align="center" />
|
||||
<a-table-column title="完成数量" dataIndex="doneQty" width="100" align="center">
|
||||
<template #default="{ record }">
|
||||
<span class="num-highlight">{{ record.doneQty }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="良品数量" dataIndex="passQty" width="100" align="center" />
|
||||
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text >= 97 ? 'rate-good' : 'rate-warn'">{{ text }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="进度" dataIndex="doneQty" width="150" align="center">
|
||||
<template #default="{ record }">
|
||||
<a-progress :percent="Math.round(record.doneQty / record.planQty * 100)" size="small" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-badge status="processing" :text="text" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">实时告警</div>
|
||||
<div class="alert-list">
|
||||
<div v-for="alert in alerts" :key="alert.id" class="alert-item" :class="alert.type">
|
||||
<span class="alert-icon">{{ alert.type === 'error' ? '🔴' : alert.type === 'warning' ? '🟡' : '🔵' }}</span>
|
||||
<div class="alert-body">
|
||||
<div class="alert-msg">{{ alert.msg }}</div>
|
||||
<div class="alert-time">{{ alert.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="history" tab="历史记录">
|
||||
<div class="card">
|
||||
<a-empty description="历史记录列表" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.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; }
|
||||
|
||||
.num-highlight { font-weight: 700; color: #4f46e5; }
|
||||
.rate-good { color: #10b981; font-weight: 600; }
|
||||
.rate-warn { color: #f59e0b; font-weight: 600; }
|
||||
|
||||
.alert-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.alert-item { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: 8px; }
|
||||
.alert-item.error { background: #fef2f2; }
|
||||
.alert-item.warning { background: #fffbeb; }
|
||||
.alert-item.info { background: #eff6ff; }
|
||||
.alert-icon { font-size: 14px; }
|
||||
.alert-msg { font-size: 13px; color: #374151; }
|
||||
.alert-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
||||
</style>
|
||||
111
app/pages/admin/production/energy.vue
Normal file
111
app/pages/admin/production/energy.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-energy'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '今日用电', value: 2856, unit: 'kWh', cost: '¥2,142', icon: '⚡', color: '#f59e0b' },
|
||||
{ label: '今日用水', value: 128, unit: 'm³', cost: '¥384', icon: '💧', color: '#3b82f6' },
|
||||
{ label: '今日用气', value: 560, unit: 'm³', cost: '¥840', icon: '🌬️', color: '#6366f1' },
|
||||
{ label: '碳排放', value: 1.8, unit: '吨', icon: '🌱', color: '#10b981' },
|
||||
])
|
||||
|
||||
const energyUsage = ref([
|
||||
{ name: 'CNC-01', electric: 450, water: 8, gas: 120, cost: 337.5 },
|
||||
{ name: 'CNC-02', electric: 420, water: 7, gas: 115, cost: 315.0 },
|
||||
{ name: 'CNC-03', electric: 0, water: 5, gas: 0, cost: 0 },
|
||||
{ name: 'MILL-01', electric: 0, water: 3, gas: 0, cost: 0 },
|
||||
{ name: 'MILL-02', electric: 380, water: 6, gas: 100, cost: 285.0 },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗管理</h2>
|
||||
<a-space>
|
||||
<a-select value="2026-04" style="width: 120px">
|
||||
<a-select-option value="2026-04">2026年4月</a-select-option>
|
||||
<a-select-option value="2026-03">2026年3月</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="() => {}">导出报表</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--c': stat.color }">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-sub">{{ stat.cost }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">设备能耗明细</div>
|
||||
<a-table :dataSource="energyUsage" :pagination="false" size="small" rowKey="name">
|
||||
<a-table-column title="设备" dataIndex="name" width="120" />
|
||||
<a-table-column title="用电(kWh)" dataIndex="electric" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="用水(m³)" dataIndex="water" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="用气(m³)" dataIndex="gas" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="能耗成本(元)" dataIndex="cost" width="130" align="right">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">¥{{ text.toFixed(1) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="占比" dataIndex="cost" align="center">
|
||||
<template #default="{ record, text }">
|
||||
<a-progress
|
||||
:percent="Math.round(text / energyUsage.reduce((a, b) => a + b.cost, 0) * 100)"
|
||||
size="small"
|
||||
:showInfo="false"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
<div class="total-row">
|
||||
<span>合计</span>
|
||||
<span>2856 kWh</span>
|
||||
<span>128 m³</span>
|
||||
<span>560 m³</span>
|
||||
<span>¥3,366.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.stat-icon { font-size: 28px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
|
||||
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
|
||||
.stat-sub { font-size: 13px; color: #6b7280; }
|
||||
.stat-label { font-size: 12px; color: #9ca3af; margin-top: 2px; }
|
||||
.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; }
|
||||
.val { font-weight: 600; color: #374151; }
|
||||
.dim { color: #d1d5db; }
|
||||
.total-row { display: flex; justify-content: space-between; padding: 12px 8px; border-top: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; color: #374151; }
|
||||
.total-row span:nth-child(2), .total-row span:nth-child(3), .total-row span:nth-child(4) { width: 120px; text-align: center; }
|
||||
.total-row span:nth-child(5) { width: 130px; text-align: right; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
617
app/pages/admin/production/equipment.vue
Normal file
617
app/pages/admin/production/equipment.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 设备统计数据
|
||||
const equipmentStats = ref([
|
||||
{ label: '设备总数', value: 128, icon: 'fa-cogs', color: 'blue', gradient: 'from-blue-500 to-purple-500', change: '+12%', up: true },
|
||||
{ label: '运行中', value: 96, icon: 'fa-play-circle', color: 'green', gradient: 'from-green-500 to-teal-500', change: '+8%', up: true },
|
||||
{ label: '待机中', value: 24, icon: 'fa-pause-circle', color: 'orange', gradient: 'from-orange-500 to-yellow-500', change: '-5%', up: false },
|
||||
{ label: '故障告警', value: 8, icon: 'fa-exclamation-triangle', color: 'red', gradient: 'from-red-500 to-pink-500', change: '+2', up: false },
|
||||
])
|
||||
|
||||
// 设备列表
|
||||
const equipmentList = ref([
|
||||
{ id: 'EQ-001', name: 'CNC加工中心 01', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 92, temp: 45, vibration: 0.8, lastMaintenance: '2026-04-01', nextMaintenance: '2026-05-01' },
|
||||
{ id: 'EQ-002', name: 'CNC加工中心 02', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 88, temp: 43, vibration: 0.7, lastMaintenance: '2026-04-02', nextMaintenance: '2026-05-02' },
|
||||
{ id: 'EQ-003', name: 'CNC加工中心 03', model: 'CNC-650', location: '1号车间 B区', status: 'warning', utilization: 0, temp: 78, vibration: 2.5, lastMaintenance: '2026-03-15', nextMaintenance: '2026-04-15' },
|
||||
{ id: 'EQ-004', name: '数控铣床 01', model: 'XK-500', location: '2号车间', status: 'idle', utilization: 0, temp: 28, vibration: 0.3, lastMaintenance: '2026-03-28', nextMaintenance: '2026-04-28' },
|
||||
{ id: 'EQ-005', name: '数控铣床 02', model: 'XK-500', location: '2号车间', status: 'running', utilization: 75, temp: 38, vibration: 0.6, lastMaintenance: '2026-04-03', nextMaintenance: '2026-05-03' },
|
||||
{ id: 'EQ-006', name: '冲压机 01', model: 'CP-200T', location: '3号车间', status: 'running', utilization: 85, temp: 42, vibration: 1.2, lastMaintenance: '2026-03-20', nextMaintenance: '2026-04-20' },
|
||||
{ id: 'EQ-007', name: '激光切割机 01', model: 'JC-3000', location: '3号车间', status: 'idle', utilization: 0, temp: 25, vibration: 0.2, lastMaintenance: '2026-04-05', nextMaintenance: '2026-05-05' },
|
||||
{ id: 'EQ-008', name: '空压机 01', model: 'KY-50', location: '动力站房', status: 'running', utilization: 68, temp: 55, vibration: 1.5, lastMaintenance: '2026-03-10', nextMaintenance: '2026-05-10' },
|
||||
])
|
||||
|
||||
// 筛选状态
|
||||
const statusFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 筛选后的列表
|
||||
const filteredList = computed(() => {
|
||||
return equipmentList.value.filter((item) => {
|
||||
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
|
||||
const matchSearch = !searchKeyword.value ||
|
||||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
return matchStatus && matchSearch
|
||||
})
|
||||
})
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
running: { label: '运行中', color: 'text-green-600', bg: 'bg-green-100' },
|
||||
idle: { label: '待机', color: 'text-orange-600', bg: 'bg-orange-100' },
|
||||
warning: { label: '告警', color: 'text-red-600', bg: 'bg-red-100' },
|
||||
offline: { label: '离线', color: 'text-gray-600', bg: 'bg-gray-100' },
|
||||
}
|
||||
|
||||
// 设备详情
|
||||
const selectedEquipment = ref<typeof equipmentList.value[0] | null>(null)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
function showDetail(item: typeof equipmentList.value[0]) {
|
||||
selectedEquipment.value = item
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 维保记录
|
||||
const maintenanceRecords = ref([
|
||||
{ date: '2026-04-01', type: '例行保养', technician: '李师傅', cost: 500, status: '完成' },
|
||||
{ date: '2026-03-01', type: '例行保养', technician: '李师傅', cost: 480, status: '完成' },
|
||||
{ date: '2026-02-01', type: '更换配件', technician: '王师傅', cost: 1200, status: '完成' },
|
||||
])
|
||||
|
||||
// 设备效率趋势(模拟数据)
|
||||
const efficiencyTrend = ref([85, 88, 82, 90, 92, 88, 91, 89, 93, 92])
|
||||
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日', '周一', '周二', '今天']
|
||||
|
||||
// 添加设备表单
|
||||
const addFormVisible = ref(false)
|
||||
const addForm = reactive({
|
||||
name: '',
|
||||
model: '',
|
||||
location: '',
|
||||
supplier: '',
|
||||
purchaseDate: '',
|
||||
warrantyEnd: '',
|
||||
})
|
||||
|
||||
function handleAdd() {
|
||||
// 模拟添加
|
||||
addFormVisible.value = false
|
||||
message.success('设备添加成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="equipment-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">设备管理</h2>
|
||||
<p class="text-gray-500 mt-1">实时监控设备状态,优化生产效率</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="addFormVisible = true">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
添加设备
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in equipmentStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-600 font-medium">状态筛选:</span>
|
||||
<a-radio-group v-model:value="statusFilter" button-style="solid">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="running">运行中</a-radio-button>
|
||||
<a-radio-button value="idle">待机</a-radio-button>
|
||||
<a-radio-button value="warning">告警</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索设备名称或编号..."
|
||||
style="width: 280px"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-list text-purple-500 mr-2"></i>
|
||||
设备列表
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">共 {{ filteredList.length }} 台设备</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredList"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<a-table-column title="设备编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="设备名称" dataIndex="name" width="180">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-xs"
|
||||
:class="statusMap[record.status]?.bg.replace('100', '500') || 'bg-gray-500'"
|
||||
>
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
<span class="font-medium">{{ record.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="型号" dataIndex="model" width="100" />
|
||||
<a-table-column title="位置" dataIndex="location" width="120" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<span
|
||||
:class="statusMap[text]?.color"
|
||||
class="px-2 py-1 rounded-lg text-sm font-medium"
|
||||
:class="statusMap[text]?.bg"
|
||||
>
|
||||
{{ statusMap[text]?.label }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="利用率" dataIndex="utilization" width="140">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2">
|
||||
<a-progress
|
||||
:percent="record.utilization"
|
||||
:status="record.utilization > 80 ? 'success' : 'active'"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
:stroke-color="record.utilization > 80 ? '#10b981' : '#6366f1'"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="text-sm text-gray-600">{{ record.utilization }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="温度(°C)" dataIndex="temp" width="100" align="center">
|
||||
<template #default="{ text, record }">
|
||||
<span :class="text > 60 ? 'text-red-500 font-medium' : 'text-gray-600'">
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="振动(mm/s)" dataIndex="vibration" width="110" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 2 ? 'text-red-500 font-medium' : 'text-gray-600'">
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="下次保养" dataIndex="nextMaintenance" width="120" />
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" @click="showDetail(record)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 设备详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="detailVisible"
|
||||
:title="selectedEquipment?.name"
|
||||
width="500"
|
||||
placement="right"
|
||||
>
|
||||
<template v-if="selectedEquipment">
|
||||
<!-- 基本信息 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-info-circle text-blue-500"></i>
|
||||
基本信息
|
||||
</h4>
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="设备编号">{{ selectedEquipment.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">{{ selectedEquipment.model }}</a-descriptions-item>
|
||||
<a-descriptions-item label="安装位置">{{ selectedEquipment.location }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前状态">
|
||||
<span
|
||||
:class="statusMap[selectedEquipment.status]?.color"
|
||||
class="px-2 py-0.5 rounded text-sm font-medium"
|
||||
:class="statusMap[selectedEquipment.status]?.bg"
|
||||
>
|
||||
{{ statusMap[selectedEquipment.status]?.label }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 实时监控 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-chart-line text-green-500"></i>
|
||||
实时监控
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ selectedEquipment.utilization }}%</div>
|
||||
<div class="text-xs text-gray-500 mt-1">设备利用率</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div
|
||||
class="text-2xl font-bold"
|
||||
:class="selectedEquipment.temp > 60 ? 'text-red-600' : 'text-orange-600'"
|
||||
>
|
||||
{{ selectedEquipment.temp }}°C
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">当前温度</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div
|
||||
class="text-2xl font-bold"
|
||||
:class="selectedEquipment.vibration > 2 ? 'text-red-600' : 'text-purple-600'"
|
||||
>
|
||||
{{ selectedEquipment.vibration }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">振动值</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 效率趋势图 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-chart-area text-purple-500"></i>
|
||||
效率趋势
|
||||
</h4>
|
||||
<div class="h-32 flex items-end gap-2">
|
||||
<div
|
||||
v-for="(value, idx) in efficiencyTrend"
|
||||
:key="idx"
|
||||
class="flex-1 flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="w-full bg-gradient-to-t from-purple-500 to-purple-300 rounded-t-lg transition-all hover:opacity-80"
|
||||
:style="{ height: value + '%' }"
|
||||
></div>
|
||||
<span class="text-xs text-gray-400 mt-1">{{ days[idx] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维保记录 -->
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-wrench text-orange-500"></i>
|
||||
维保记录
|
||||
</h4>
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="record in maintenanceRecords"
|
||||
:key="record.date"
|
||||
:color="record.status === '完成' ? 'green' : 'gray'"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{{ record.type }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ record.date }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
技师:{{ record.technician }} | 费用:¥{{ record.cost }}
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 flex gap-3">
|
||||
<a-button type="primary" block>发起保养</a-button>
|
||||
<a-button block>导出报表</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 添加设备弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="addFormVisible"
|
||||
title="添加新设备"
|
||||
@ok="handleAdd"
|
||||
ok-text="确认添加"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-form :model="addForm" layout="vertical">
|
||||
<a-form-item label="设备名称" required>
|
||||
<a-input v-model:value="addForm.name" placeholder="请输入设备名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="设备型号" required>
|
||||
<a-input v-model:value="addForm.model" placeholder="请输入设备型号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="安装位置" required>
|
||||
<a-input v-model:value="addForm.location" placeholder="请输入安装位置" />
|
||||
</a-form-item>
|
||||
<a-form-item label="供应商">
|
||||
<a-input v-model:value="addForm.supplier" placeholder="请输入供应商" />
|
||||
</a-form-item>
|
||||
<a-form-item label="采购日期">
|
||||
<a-date-picker v-model:value="addForm.purchaseDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="保修截止日期">
|
||||
<a-date-picker v-model:value="addForm.warrantyEnd" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.equipment-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-purple-600 {
|
||||
color: #9333ea;
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.bg-green-100 {
|
||||
background-color: #dcfce7;
|
||||
}
|
||||
|
||||
.bg-orange-100 {
|
||||
background-color: #ffedd5;
|
||||
}
|
||||
|
||||
.bg-red-100 {
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.from-blue-500 {
|
||||
--tw-gradient-from: #3b82f6;
|
||||
}
|
||||
|
||||
.to-purple-500 {
|
||||
--tw-gradient-to: #a855f7;
|
||||
}
|
||||
|
||||
.from-green-500 {
|
||||
--tw-gradient-from: #22c55e;
|
||||
}
|
||||
|
||||
.to-teal-500 {
|
||||
--tw-gradient-to: #14b8a6;
|
||||
}
|
||||
|
||||
.from-orange-500 {
|
||||
--tw-gradient-from: #f97316;
|
||||
}
|
||||
|
||||
.to-yellow-500 {
|
||||
--tw-gradient-to: #eab308;
|
||||
}
|
||||
|
||||
.from-red-500 {
|
||||
--tw-gradient-from: #ef4444;
|
||||
}
|
||||
|
||||
.to-pink-500 {
|
||||
--tw-gradient-to: #ec4899;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
98
app/pages/admin/production/quality.vue
Normal file
98
app/pages/admin/production/quality.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-quality'
|
||||
|
||||
const qualityRecords = ref([
|
||||
{ id: 'QC-2026040901', wo: 'WO2026040901', product: '精密轴承组件 A型', batch: 'B-20260409-01', inspector: '质检员A', checkTime: '2026-04-09 10:30', sampleSize: 50, passSize: 49, passRate: 98.0, result: '合格', issues: '1件尺寸超差' },
|
||||
{ id: 'QC-2026040801', wo: 'WO2026040703', product: '密封圈组件 D型', batch: 'B-20260408-03', inspector: '质检员B', checkTime: '2026-04-08 14:20', sampleSize: 100, passSize: 100, passRate: 100, result: '合格', issues: '-' },
|
||||
{ id: 'QC-2026040702', wo: 'WO2026040702', product: '弹簧组件 E型', batch: 'B-20260407-02', inspector: '质检员A', checkTime: '2026-04-07 09:00', sampleSize: 80, passSize: 72, passRate: 90.0, result: '不合格', issues: '8件弹性不足' },
|
||||
])
|
||||
|
||||
const resultColor: Record<string, string> = { '合格': 'success', '不合格': 'error' }
|
||||
const activeTab3 = ref('records')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">质量管理</h2>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建质检
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#10b981">
|
||||
<div class="stat-num">98.5%</div>
|
||||
<div class="stat-lbl">今日良品率</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#3b82f6">
|
||||
<div class="stat-num">156</div>
|
||||
<div class="stat-lbl">今日检验数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#f59e0b">
|
||||
<div class="stat-num">3</div>
|
||||
<div class="stat-lbl">待处理异常</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#6366f1">
|
||||
<div class="stat-num">12</div>
|
||||
<div class="stat-lbl">质检标准数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab3">
|
||||
<a-tab-pane key="records" tab="检验记录">
|
||||
<div class="card">
|
||||
<a-table :dataSource="qualityRecords" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="质检编号" dataIndex="id" width="150" />
|
||||
<a-table-column title="工单号" dataIndex="wo" width="150" />
|
||||
<a-table-column title="产品" dataIndex="product" />
|
||||
<a-table-column title="批次" dataIndex="batch" width="160" />
|
||||
<a-table-column title="检验员" dataIndex="inspector" width="90" align="center" />
|
||||
<a-table-column title="检验时间" dataIndex="checkTime" width="160" />
|
||||
<a-table-column title="抽样数" dataIndex="sampleSize" width="80" align="center" />
|
||||
<a-table-column title="良品数" dataIndex="passSize" width="80" align="center" />
|
||||
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
|
||||
<template #default="{ text, record }">
|
||||
<span :class="record.result === '合格' ? 'ok' : 'warn'">{{ text }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="结果" dataIndex="result" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="问题描述" dataIndex="issues" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="standards" tab="质检标准">
|
||||
<div class="card"><a-empty description="质检标准管理" /></div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 20px; text-align: center; 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: 100%; height: 3px; background: var(--c); }
|
||||
.stat-num { font-size: 28px; font-weight: 700; color: #1f2937; }
|
||||
.stat-lbl { font-size: 13px; color: #9ca3af; margin-top: 4px; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.ok { color: #10b981; font-weight: 600; }
|
||||
.warn { color: #ef4444; font-weight: 600; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
95
app/pages/admin/production/safety.vue
Normal file
95
app/pages/admin/production/safety.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-safety'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '安全生产天数', value: 156, unit: '天', icon: '🛡️', color: '#10b981' },
|
||||
{ label: '本月巡检次数', value: 28, unit: '次', icon: '🔍', color: '#3b82f6' },
|
||||
{ label: '安全隐患', value: 3, unit: '条', icon: '⚠️', color: '#f59e0b' },
|
||||
{ label: '应急演练', value: 2, unit: '次', icon: '🚨', color: '#6366f1' },
|
||||
])
|
||||
|
||||
const safetyLogs = ref([
|
||||
{ id: 'SL-001', type: '巡检', area: '生产车间A区', inspector: '安全员A', result: '正常', time: '2026-04-09 08:30', remark: '-' },
|
||||
{ id: 'SL-002', type: '巡检', area: '电工房', inspector: '安全员B', result: '隐患', time: '2026-04-09 09:15', remark: '发现1处接线松动' },
|
||||
{ id: 'SL-003', type: '设备检查', area: 'CNC-03区域', inspector: '安全员A', result: '告警', time: '2026-04-09 10:00', remark: '设备温度异常,已停机' },
|
||||
{ id: 'SL-004', type: '巡检', area: '仓储区', inspector: '安全员C', result: '正常', time: '2026-04-08 14:00', remark: '-' },
|
||||
])
|
||||
|
||||
const resultColor: Record<string, string> = { '正常': 'success', '隐患': 'warning', '告警': 'error' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">安全生产</h2>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建巡检
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--c': stat.color }">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">安全巡检记录</div>
|
||||
<a-table :dataSource="safetyLogs" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="记录编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="类型" dataIndex="type" width="100" align="center" />
|
||||
<a-table-column title="区域" dataIndex="area" />
|
||||
<a-table-column title="检查人" dataIndex="inspector" width="100" align="center" />
|
||||
<a-table-column title="结果" dataIndex="result" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="备注" dataIndex="remark" />
|
||||
<a-table-column title="时间" dataIndex="time" width="160" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">安全知识库</div>
|
||||
<div class="doc-list">
|
||||
<div class="doc-item">📄 安全生产管理制度 v3.2</div>
|
||||
<div class="doc-item">📄 应急救援预案</div>
|
||||
<div class="doc-item">📄 特种设备操作规程</div>
|
||||
<div class="doc-item">📄 危险源辨识清单</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--c); }
|
||||
.stat-icon { font-size: 28px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
|
||||
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
|
||||
.stat-label { font-size: 12px; 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; }
|
||||
.doc-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.doc-item { padding: 10px; background: #fafafa; border-radius: 6px; font-size: 13px; color: #374151; cursor: pointer; }
|
||||
.doc-item:hover { background: #f0f0f0; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
155
app/pages/admin/production/schedule.vue
Normal file
155
app/pages/admin/production/schedule.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-schedule'
|
||||
|
||||
const selectedDate = ref('2026-04-09')
|
||||
|
||||
const scheduleData = ref([
|
||||
{ time: '08:00', tasks: [
|
||||
{ wo: 'WO2026040901', product: '精密轴承组件 A型', qty: 500, line: '产线1', status: 'running' },
|
||||
]},
|
||||
{ time: '10:00', tasks: [
|
||||
{ wo: 'WO2026040802', product: '液压缸体 B型', qty: 200, line: '产线2', status: 'pending' },
|
||||
]},
|
||||
{ time: '14:00', tasks: [
|
||||
{ wo: 'WO2026040703', product: '密封圈组件 D型', qty: 3000, line: '产线3', status: 'pending' },
|
||||
]},
|
||||
])
|
||||
|
||||
const productionLines = ref([
|
||||
{ id: 'L1', name: '产线1', status: 'running', utilization: 92, output: 245, target: 260 },
|
||||
{ id: 'L2', name: '产线2', status: 'idle', utilization: 0, output: 0, target: 200 },
|
||||
{ id: 'L3', name: '产线3', status: 'running', utilization: 78, output: 180, target: 220 },
|
||||
{ id: 'L4', name: '产线4', status: 'maintenance', utilization: 0, output: 0, target: 180 },
|
||||
])
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
running: '#10b981',
|
||||
pending: '#f59e0b',
|
||||
idle: '#9ca3af',
|
||||
maintenance: '#ef4444',
|
||||
}
|
||||
|
||||
const lineStatusMap: Record<string, string> = {
|
||||
running: { status: 'running', text: '运行中' },
|
||||
idle: { status: 'default', text: '空闲' },
|
||||
maintenance: { status: 'exception', text: '维护中' },
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">计划排程</h2>
|
||||
<a-space>
|
||||
<a-date-picker v-model:value="selectedDate" />
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建排程
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<!-- 排程甘特图 -->
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">生产排程</div>
|
||||
<div class="gantt">
|
||||
<div class="gantt-header">
|
||||
<div class="gantt-time">时间段</div>
|
||||
<div class="gantt-tasks">排程任务</div>
|
||||
</div>
|
||||
<div v-for="slot in scheduleData" :key="slot.time" class="gantt-row">
|
||||
<div class="gantt-time">{{ slot.time }}</div>
|
||||
<div class="gantt-tasks">
|
||||
<div v-for="task in slot.tasks" :key="task.wo" class="task-card" :class="task.status">
|
||||
<div class="task-wo">{{ task.wo }}</div>
|
||||
<div class="task-info">{{ task.product }} · {{ task.qty }}件</div>
|
||||
<div class="task-line">{{ task.line }}</div>
|
||||
</div>
|
||||
<div v-if="slot.tasks.length === 0" class="task-empty">暂无排程</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 产线状态 -->
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">产线状态</div>
|
||||
<div class="line-list">
|
||||
<div v-for="line in productionLines" :key="line.id" class="line-item">
|
||||
<div class="line-header">
|
||||
<span class="line-name">{{ line.name }}</span>
|
||||
<a-badge :status="lineStatusMap[line.status].status as any" :text="lineStatusMap[line.status].text" />
|
||||
</div>
|
||||
<div class="line-stats">
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.utilization }}%</span>
|
||||
<span class="line-lbl">利用率</span>
|
||||
</div>
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.output }}</span>
|
||||
<span class="line-lbl">实际产量</span>
|
||||
</div>
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.target }}</span>
|
||||
<span class="line-lbl">目标产量</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="line.utilization"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
:status="line.status === 'running' ? 'active' : 'exception'"
|
||||
:strokeColor="statusColor[line.status]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; margin-bottom: 20px; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
|
||||
.gantt { border: 1px solid #f0f0f0; border-radius: 8px; overflow: hidden; }
|
||||
.gantt-header { display: flex; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; }
|
||||
.gantt-time { width: 80px; padding: 10px 12px; border-right: 1px solid #f0f0f0; }
|
||||
.gantt-tasks { flex: 1; padding: 10px 12px; }
|
||||
.gantt-row { display: flex; border-bottom: 1px solid #f0f0f0; }
|
||||
.gantt-row:last-child { border-bottom: none; }
|
||||
.gantt-row .gantt-time { padding: 16px 12px; font-size: 13px; color: #6b7280; }
|
||||
.gantt-row .gantt-tasks { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px; min-height: 60px; align-items: center; }
|
||||
|
||||
.task-card {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-card.running { background: #ecfdf5; border-left: 3px solid #10b981; }
|
||||
.task-card.pending { background: #fffbeb; border-left: 3px solid #f59e0b; }
|
||||
.task-wo { font-weight: 600; color: #374151; }
|
||||
.task-info { color: #6b7280; margin: 2px 0; }
|
||||
.task-line { color: #9ca3af; font-size: 11px; }
|
||||
.task-empty { color: #d1d5db; font-size: 13px; }
|
||||
|
||||
.line-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.line-item { padding: 14px; background: #fafafa; border-radius: 8px; }
|
||||
.line-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.line-name { font-weight: 600; color: #374151; font-size: 14px; }
|
||||
.line-stats { display: flex; justify-content: space-between; margin-bottom: 10px; }
|
||||
.line-stat { text-align: center; }
|
||||
.line-num { display: block; font-size: 16px; font-weight: 700; color: #1f2937; }
|
||||
.line-lbl { font-size: 11px; color: #9ca3af; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user