enterprise-saa-s-dashboard-.../src/app/pages/DeviceRegistration.vue

515 lines
22 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">
import { ref, computed } from 'vue'
import {
CheckCircle2,
AlertTriangle,
Camera,
Upload,
Check,
Download,
X,
FileSpreadsheet,
} from 'lucide-vue-next'
const showImportDialog = ref(false)
const importFile = ref<File | null>(null)
const showPhotoDialog = ref(false)
const photoDialogItemId = ref(0)
const photoFiles = ref<{ name: string; url: string }[]>([])
const photoNote = ref('')
const openPhotoDialog = (itemId: number) => {
photoDialogItemId.value = itemId
photoFiles.value = []
photoNote.value = ''
showPhotoDialog.value = true
}
const onPhotoSelect = (e: Event) => {
const target = e.target as HTMLInputElement
if (target.files) {
for (const f of Array.from(target.files)) {
photoFiles.value.push({ name: f.name, url: URL.createObjectURL(f) })
}
}
target.value = ''
}
const removePhoto = (index: number) => {
photoFiles.value.splice(index, 1)
}
const confirmPhotos = () => {
const item = checklistItems.value.find((i: any) => i.id === photoDialogItemId.value)
if (item) {
item.photos = photoFiles.value.length
item.completed = true
}
showPhotoDialog.value = false
}
const onFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
if (target.files && target.files.length > 0) importFile.value = target.files[0]
}
const selectedModel = ref('GD30')
const checklistItems = ref([
{ id: 1, text: '主板安装及固定', completed: true, photos: 3, needPhoto: true, versionCheck: true, versionMatch: true },
{ id: 2, text: '采集板连接', completed: true, photos: 2, needPhoto: true, versionCheck: true, versionMatch: true },
{ id: 3, text: '测控板安装', completed: true, photos: 2, needPhoto: true, versionCheck: true, versionMatch: false },
{ id: 4, text: '电源线连接检查', completed: false, photos: 0, needPhoto: true, versionCheck: false, versionMatch: true },
{ id: 5, text: '外壳密封性检测', completed: false, photos: 0, needPhoto: true, versionCheck: false, versionMatch: true },
])
const bomData = [
{ code: 'MB-2024-001', name: '主控板', sn: 'MB20240308001', spec: 'GD30-MB-V2.3', calibration: '无需校准', quantity: 1 },
{ code: 'RX-2024-002', name: '采集板', sn: 'RX20240308002', spec: 'GD30-RX-V1.8', calibration: '已校准', quantity: 2 },
{ code: 'MC-2024-003', name: '测控板', sn: 'MC20240308003', spec: 'GD30-MC-V1.5', calibration: '无需校准', quantity: 1 },
{ code: 'TX-2024-003', name: '发射板', sn: 'TX20240308003', spec: 'GD30-TX-V1.5', calibration: '无需校准', quantity: 1 },
{ code: 'BO-2024-004', name: '升压板', sn: 'BO20240308004', spec: 'BP600', calibration: '无需校准', quantity: 1 },
{ code: 'CS-2024-005', name: '外壳机箱', sn: '-', spec: 'AL6061-T6', calibration: '无需校准', quantity: 1 },
]
const completedCount = computed(() => checklistItems.value.filter((item: typeof checklistItems.value[number]) => item.completed).length)
const totalCount = computed(() => checklistItems.value.length)
const toggleChecklistItem = (id: number) => {
checklistItems.value = checklistItems.value.map((item: typeof checklistItems.value[number]) =>
item.id === id ? { ...item, completed: !item.completed } : item
)
}
</script>
<template>
<div class="p-6">
<!-- Page Header -->
<div class="mb-6">
<h2 class="text-2xl font-semibold mb-1">设备登记</h2>
<p class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.45)' }">登记新设备信息及装配记录</p>
</div>
<!-- Installation Info Card -->
<div class="bg-white p-6 rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }">
<h3 class="text-lg font-semibold mb-6">装机信息</h3>
<div class="grid grid-cols-3 gap-6">
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
设备型号 <span :style="{ color: '#FF4D4F' }">*</span>
</label>
<select
v-model="selectedModel"
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9', backgroundColor: '#fff' }"
>
<option value="GD30">GD30 高密度电法仪</option>
<option value="GT20">GT20 瞬变电磁仪</option>
<option value="GTXD">GM10 大地电磁仪</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
主机SN号 <span :style="{ color: '#FF4D4F' }">*</span>
</label>
<input
type="text"
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9' }"
placeholder="请输入主机SN号"
value="GD30-20240308-001"
/>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
主板SN号 <span :style="{ color: '#FF4D4F' }">*</span>
</label>
<div class="flex items-center gap-2">
<input
type="text"
class="flex-1 px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9' }"
placeholder="请输入主板SN号"
value="MB20240308001"
/>
<span
class="px-2 py-1 rounded text-xs whitespace-nowrap"
:style="{ backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }"
>
已绑定
</span>
</div>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
装机测试状态
</label>
<div class="flex items-center gap-2 px-3 py-2 border rounded" :style="{ borderColor: '#D9D9D9', backgroundColor: '#FAFAFA' }">
<select>
<option><span :style="{ color: '#52C41A' }">测试通过</span></option>
<option><span :style="{ color: '#c41a1aff' }">测试不通过</span></option>
</select>
</div>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
生产日期
</label>
<input
type="date"
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9' }"
value="2024-03-08"
/>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">
登记人
</label>
<input
type="text"
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9', backgroundColor: '#FAFAFA' }"
value="张工"
/>
</div>
</div>
</div>
<!-- Success Banner -->
<div
class="mb-4 p-4 rounded-lg flex items-start gap-3"
:style="{ backgroundColor: '#F6FFED', border: '1px solid #B7EB8F' }"
>
<CheckCircle2 :size="20" :style="{ color: '#52C41A', flexShrink: 0, marginTop: '2px' }" />
<div>
<div :style="{ color: '#389E0D', fontWeight: 500 }">型号已匹配GD30</div>
<div class="text-sm mt-1" :style="{ color: '#52C41A' }">
授权文件auth_gd30_v2.3.lic | 配置文件config_gd30_v1.5.json | 固件版本v2.3.5
</div>
</div>
</div>
<!-- Warning Banner (Optional - shown when model doesn't match) -->
<div
class="mb-6 p-4 rounded-lg flex items-start gap-3"
:style="{ backgroundColor: '#FFFBE6', border: '1px solid #FFE58F', display: 'none' }"
>
<AlertTriangle :size="20" :style="{ color: '#FAAD14', flexShrink: 0, marginTop: '2px' }" />
<div>
<div :style="{ color: '#D46B08', fontWeight: 500 }">未匹配到型号关联信息</div>
<div class="text-sm mt-1" :style="{ color: '#FAAD14' }">
请先在型号管理中配置该型号的授权文件、配置文件和固件版本
</div>
</div>
</div>
<!-- BOM List Card -->
<div class="bg-white rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }">
<div class="p-6 border-b" :style="{ borderColor: '#F0F0F0' }">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">设备BOM清单</h3>
<button
class="px-4 py-2 rounded flex items-center gap-2"
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
@click="showImportDialog = true"
>
<Upload :size="16" />
导入
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead :style="{ backgroundColor: '#FAFAFA' }">
<tr>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">物料编码</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">物料名称</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">SN号</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">规格型号</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">校准状态</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">数量</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in bomData"
:key="index"
class="border-b"
:style="{ borderColor: '#F0F0F0' }"
>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ item.code }}</td>
<td class="px-6 py-4">{{ item.name }}</td>
<td class="px-6 py-4">{{ item.sn }}</td>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ item.spec }}</td>
<td class="px-6 py-4">
<span
class="px-2 py-1 rounded text-xs"
:style="{
backgroundColor: item.calibration === '已校准' ? '#F6FFED' : '#F0F2F5',
color: item.calibration === '已校准' ? '#52C41A' : 'rgba(0, 0, 0, 0.65)',
border: `1px solid ${item.calibration === '已校准' ? '#B7EB8F' : '#D9D9D9'}`,
}"
>
{{ item.calibration }}
</span>
</td>
<td class="px-6 py-4">{{ item.quantity }}</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<button class="text-sm" :style="{ color: '#4a7c59' }">编辑</button>
<button class="text-sm" :style="{ color: '#FF4D4F' }">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Assembly Checklist Card -->
<div class="bg-white rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }">
<div class="p-6 border-b" :style="{ borderColor: '#F0F0F0' }">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">装配Checklist</h3>
<div class="flex items-center gap-2">
<span class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">完成进度</span>
<span class="text-sm font-semibold" :style="{ color: '#4a7c59' }">
{{ completedCount }}/{{ totalCount }}
</span>
</div>
</div>
</div>
<div class="p-6">
<div class="space-y-3">
<div
v-for="item in checklistItems"
:key="item.id"
class="flex items-center gap-4 p-4 rounded border"
:style="{
backgroundColor: item.completed ? '#F6FFED' : '#FAFAFA',
borderColor: item.completed ? '#B7EB8F' : '#F0F0F0',
}"
>
<button
@click="toggleChecklistItem(item.id)"
class="w-6 h-6 rounded border flex items-center justify-center flex-shrink-0 transition-colors"
:style="{
borderColor: item.completed ? '#52C41A' : '#D9D9D9',
backgroundColor: item.completed ? '#52C41A' : '#fff',
}"
>
<Check v-if="item.completed" :size="16" color="#fff" />
</button>
<div
class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0"
:style="{ backgroundColor: item.completed ? '#52C41A' : '#D9D9D9', color: 'white' }"
>
{{ item.id }}
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<span :style="{ color: item.completed ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.65)' }">
{{ item.text }}
</span>
<span
v-if="item.versionCheck && !item.versionMatch"
class="px-2 py-1 rounded text-xs flex items-center gap-1"
:style="{ backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }"
>
<AlertTriangle :size="12" />
版本不一致!
</span>
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="item.needPhoto && item.completed && item.photos > 0"
class="px-3 py-1 rounded text-xs flex items-center gap-1 cursor-pointer"
:style="{ backgroundColor: '#eef5f0', color: '#4a7c59' }"
@click="openPhotoDialog(item.id)"
>
<Camera :size="12" />
已上传 {{ item.photos }}张
</button>
<button
v-if="item.needPhoto && !item.completed"
class="px-3 py-1 rounded text-xs flex items-center gap-1"
:style="{ backgroundColor: '#4a7c59', color: '#fff' }"
@click="openPhotoDialog(item.id)"
>
<Camera :size="12" />
拍照上传
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Action Bar -->
<div
class="flex items-center justify-end gap-3 p-4 bg-white rounded-lg sticky bottom-0"
:style="{ boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.05)' }"
>
<button
class="px-6 py-2 rounded"
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
>
取消
</button>
<button
class="px-6 py-2 rounded"
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
>
更新
</button>
<button
class="px-6 py-2 rounded text-white"
:style="{ backgroundColor: '#4a7c59' }"
>
提交
</button>
</div>
<!-- Photo Upload Dialog -->
<div v-if="showPhotoDialog" class="fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0,0,0,0.45)">
<div class="bg-white rounded-lg w-[560px] max-h-[80vh] flex flex-col" style="box-shadow: 0 4px 12px rgba(0,0,0,0.15)">
<div class="flex items-center justify-between p-5 border-b" style="border-color: #F0F0F0">
<h3 class="text-lg font-semibold">上传照片</h3>
<button @click="showPhotoDialog = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.45)">
<X :size="18" />
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<!-- Photo grid -->
<div class="mb-4">
<div class="text-sm font-medium mb-2">照片({{ photoFiles.length }}张)</div>
<div class="flex flex-wrap gap-3">
<!-- Uploaded photos -->
<div v-for="(photo, i) in photoFiles" :key="i" class="relative w-24 h-24 rounded-lg overflow-hidden" style="border: 1px solid #F0F0F0">
<img :src="photo.url" :alt="photo.name" class="w-full h-full object-cover" />
<button
class="absolute top-1 right-1 w-5 h-5 rounded-full flex items-center justify-center"
style="background-color: rgba(0,0,0,0.5); color: #fff"
@click="removePhoto(i)"
>
<X :size="12" />
</button>
</div>
<!-- Add button -->
<div
class="w-24 h-24 rounded-lg flex flex-col items-center justify-center cursor-pointer transition-colors hover:border-blue-400"
style="border: 2px dashed #D9D9D9; color: rgba(0,0,0,0.45)"
@click="($refs.photoInput as HTMLInputElement)?.click()"
>
<Camera :size="24" />
<span class="text-xs mt-1">添加照片</span>
</div>
<input ref="photoInput" type="file" accept="image/*" multiple class="hidden" @change="onPhotoSelect" />
</div>
</div>
<!-- Note -->
<div>
<div class="text-sm font-medium mb-2">备注</div>
<textarea
v-model="photoNote"
class="w-full px-3 py-2 border rounded text-sm"
style="border-color: #D9D9D9; min-height: 80px; resize: vertical"
placeholder="输入备注信息(可选)"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showPhotoDialog = false">取消</button>
<button
class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: photoFiles.length > 0 ? '#4a7c59' : '#D9D9D9' }"
:disabled="photoFiles.length === 0"
@click="confirmPhotos"
>确认上传({{ photoFiles.length }}张)</button>
</div>
</div>
</div>
<!-- Import Excel Dialog -->
<div v-if="showImportDialog" class="fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0,0,0,0.45)">
<div class="bg-white rounded-lg w-[520px]" style="box-shadow: 0 4px 12px rgba(0,0,0,0.15)">
<!-- Dialog Header -->
<div class="flex items-center justify-between p-5 border-b" style="border-color: #F0F0F0">
<h3 class="text-lg font-semibold">导入BOM清单</h3>
<button @click="showImportDialog = false; importFile = null" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.45)">
<X :size="18" />
</button>
</div>
<!-- Dialog Body -->
<div class="p-6">
<!-- Step 1: Download Template -->
<div class="mb-6">
<div class="text-sm font-medium mb-2">第一步:下载导入模板</div>
<div class="p-4 rounded-lg flex items-center justify-between" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<div class="flex items-center gap-3">
<FileSpreadsheet :size="24" style="color: #52C41A" />
<div>
<div class="text-sm">BOM导入模板.xlsx</div>
<div class="text-xs" style="color: rgba(0,0,0,0.45)">包含物料编码、物料名称、SN号、规格型号等字段</div>
</div>
</div>
<button class="px-3 py-1.5 rounded text-sm flex items-center gap-1" style="border: 1px solid #4a7c59; color: #4a7c59">
<Download :size="14" />
下载模板
</button>
</div>
</div>
<!-- Step 2: Upload File -->
<div class="mb-6">
<div class="text-sm font-medium mb-2">第二步上传Excel文件</div>
<div
class="p-8 rounded-lg text-center cursor-pointer transition-colors"
:style="{
border: importFile ? '2px solid #52C41A' : '2px dashed #D9D9D9',
backgroundColor: importFile ? '#F6FFED' : '#FAFAFA',
}"
@click="($refs.fileInput as HTMLInputElement)?.click()"
>
<input ref="fileInput" type="file" accept=".xlsx,.xls,.csv" class="hidden" @change="onFileChange" />
<template v-if="importFile">
<FileSpreadsheet :size="32" style="color: #52C41A; margin: 0 auto 8px" />
<div class="text-sm" style="color: #52C41A">{{ importFile.name }}</div>
<div class="text-xs mt-1" style="color: rgba(0,0,0,0.45)">点击重新选择文件</div>
</template>
<template v-else>
<Upload :size="32" style="color: #D9D9D9; margin: 0 auto 8px" />
<div class="text-sm" style="color: rgba(0,0,0,0.65)">点击或拖拽文件到此处上传</div>
<div class="text-xs mt-1" style="color: rgba(0,0,0,0.45)">支持 .xlsx、.xls、.csv 格式</div>
</template>
</div>
</div>
<!-- Tips -->
<div class="p-3 rounded text-xs" style="background-color: #FFFBE6; border: 1px solid #FFE58F; color: #D46B08">
提示请确保Excel文件格式与模板一致物料编码和SN号为必填字段。导入后可在BOM清单中编辑。
</div>
</div>
<!-- Dialog Footer -->
<div class="flex items-center justify-end gap-3 p-5 border-t" style="border-color: #F0F0F0">
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showImportDialog = false; importFile = null">取消</button>
<button
class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: importFile ? '#4a7c59' : '#D9D9D9' }"
:disabled="!importFile"
@click="showImportDialog = false; importFile = null"
>确认导入</button>
</div>
</div>
</div>
</div>
</template>