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

618 lines
18 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 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>