更新授权文件管理页面

This commit is contained in:
徐星 2026-04-01 11:55:43 +08:00
parent e93000b0c2
commit cbfd253720
4 changed files with 550 additions and 247 deletions

View File

@ -5,13 +5,6 @@
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-2xl font-semibold">设备列表</h2> <h2 class="text-2xl font-semibold">设备列表</h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button
class="px-4 py-2 rounded flex items-center gap-2"
style="border: 1px solid #D9D9D9; color: rgba(0, 0, 0, 0.85)"
>
<Upload :size="16" />
BOM导入
</button>
<button <button
class="px-4 py-2 rounded flex items-center gap-2" class="px-4 py-2 rounded flex items-center gap-2"
style="border: 1px solid #D9D9D9; color: rgba(0, 0, 0, 0.85)" style="border: 1px solid #D9D9D9; color: rgba(0, 0, 0, 0.85)"

View File

@ -9,8 +9,19 @@ import {
Plus, Plus,
Trash2, Trash2,
GripVertical, GripVertical,
X,
} from 'lucide-vue-next' } from 'lucide-vue-next'
const showDrawer = ref(false)
const drawerForm = ref({
name: '',
code: '',
authFile: '',
configFile: '',
firmwareVersion: '',
status: '在产' as '在产' | '停产',
})
interface ChecklistItem { interface ChecklistItem {
id: number id: number
text: string text: string
@ -209,9 +220,11 @@ const boardVersionData = [
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">型号列表</h3> <h3 class="text-lg font-semibold">型号列表</h3>
<button <button
class="px-4 py-2 rounded text-white" class="px-4 py-2 rounded text-white flex items-center gap-2"
:style="{ backgroundColor: '#1890FF' }" :style="{ backgroundColor: '#1890FF' }"
@click="showDrawer = true"
> >
<Plus :size="16" />
新增型号 新增型号
</button> </button>
</div> </div>
@ -369,47 +382,161 @@ const boardVersionData = [
</div> </div>
</div> </div>
<!-- Board Version Requirements -->
<div class="bg-white rounded-lg" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }"> <!-- New Model Drawer -->
<div class="p-6 border-b" :style="{ borderColor: '#F0F0F0' }"> <div v-if="showDrawer" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showDrawer = false">
<h3 class="text-lg font-semibold">板卡版本要求</h3> <div class="bg-white w-[480px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
</div> <!-- Drawer Header -->
<div class="overflow-x-auto"> <div class="flex items-center justify-between p-5 border-b" style="border-color: #F0F0F0">
<table class="w-full"> <h3 class="text-lg font-semibold">新增设备型号</h3>
<thead :style="{ backgroundColor: '#FAFAFA' }"> <button @click="showDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.45)">
<tr> <X :size="18" />
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">板卡类型</th> </button>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">要求固件版本</th> </div>
<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> <!-- Drawer Body -->
</tr> <div class="flex-1 overflow-y-auto p-6">
</thead> <div class="space-y-5">
<tbody> <div>
<tr <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">型号名称 <span style="color: #FF4D4F">*</span></label>
v-for="(board, index) in boardVersionData" <input v-model="drawerForm.name" type="text" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9" placeholder="如GD30 高密度电法仪" />
:key="index" </div>
class="border-b"
:style="{ borderColor: '#F0F0F0' }" <div>
> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">型号编码 <span style="color: #FF4D4F">*</span></label>
<td class="px-6 py-4">{{ board.boardType }}</td> <select v-model="drawerForm.code" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<td class="px-6 py-4"> <option value="">请选择编码</option>
<span <option>GD10-2025</option>
class="px-2 py-1 rounded text-xs" <option>GD20-2025</option>
:style="{ backgroundColor: '#F0F2F5', color: 'rgba(0, 0, 0, 0.85)' }" <option>GD30-2025</option>
> <option>GT20-2025</option>
{{ board.requiredVersion }} <option>GM10-2025</option>
</span> </select>
</td> </div>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ board.validationRule }}</td>
<td class="px-6 py-4"> <div>
<div class="flex items-center gap-3"> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定授权文件</label>
<button class="text-sm" :style="{ color: '#1890FF' }">编辑</button> <select v-model="drawerForm.authFile" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<button class="text-sm" :style="{ color: '#1890FF' }">测试</button> <option value="">请选择授权文件</option>
<option>auth_gd10_v1.0.lic</option>
<option>auth_gd20_v2.0.lic</option>
<option>auth_gd30_v2.3.lic</option>
<option>auth_gt20_v1.8.lic</option>
<option>auth_gm10_v1.5.lic</option>
</select>
<div class="text-xs mt-1" style="color: rgba(0,0,0,0.45)">授权文件按型号绑定设备激活时自动下载</div>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定配置文件</label>
<select v-model="drawerForm.configFile" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择配置文件</option>
<option>config_gd10_v1.0.json</option>
<option>config_gd20_v1.2.json</option>
<option>config_gd30_v1.5.json</option>
<option>config_gt20_v1.2.json</option>
<option>config_gm10_v1.0.json</option>
</select>
<div class="text-xs mt-1" style="color: rgba(0,0,0,0.45)">配置文件包含发射参数采集参数网络参数等</div>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定主机固件版本</label>
<select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择主机固件版本</option>
<option>v2.3.5</option>
<option>v2.3.0</option>
<option>v2.2.0</option>
<option>v2.1.0</option>
<option>v1.8.2</option>
<option>v1.5.1</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定主协板固件版本</label>
<select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择主协板固件版本</option>
<option>v2.3.5</option>
<option>v2.3.0</option>
<option>v2.2.0</option>
<option>v2.1.0</option>
<option>v1.8.2</option>
<option>v1.5.1</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定发射板固件版本</label>
<select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择发射板固件版本</option>
<option>v2.3.5</option>
<option>v2.3.0</option>
<option>v2.2.0</option>
<option>v2.1.0</option>
<option>v1.8.2</option>
<option>v1.5.1</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">绑定采集板固件版本</label>
<select v-model="drawerForm.firmwareVersion" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option value="">请选择采集板固件版本</option>
<option>v2.3.5</option>
<option>v2.3.0</option>
<option>v2.2.0</option>
<option>v2.1.0</option>
<option>v1.8.2</option>
<option>v1.5.1</option>
</select>
</div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">状态</label>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer text-sm">
<input type="radio" v-model="drawerForm.status" value="在产" style="accent-color: #1890FF" />
<span>在产</span>
</label>
<label class="flex items-center gap-2 cursor-pointer text-sm">
<input type="radio" v-model="drawerForm.status" value="停产" style="accent-color: #1890FF" />
<span>停产</span>
</label>
</div>
</div>
<!-- Preview -->
<div class="p-4 rounded-lg" style="background-color: #FAFAFA; border: 1px solid #F0F0F0">
<div class="text-sm font-medium mb-3">绑定预览</div>
<div class="space-y-2 text-sm" style="color: rgba(0,0,0,0.65)">
<div class="flex justify-between">
<span>授权文件</span>
<span :style="{ color: drawerForm.authFile ? '#52C41A' : '#FF4D4F' }">{{ drawerForm.authFile || '未绑定' }}</span>
</div> </div>
</td> <div class="flex justify-between">
</tr> <span>配置文件</span>
</tbody> <span :style="{ color: drawerForm.configFile ? '#52C41A' : '#FF4D4F' }">{{ drawerForm.configFile || '未绑定' }}</span>
</table> </div>
<div class="flex justify-between">
<span>固件版本</span>
<span :style="{ color: drawerForm.firmwareVersion ? '#52C41A' : '#FF4D4F' }">{{ drawerForm.firmwareVersion || '未绑定' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Drawer 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="showDrawer = false">取消</button>
<button
class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: drawerForm.name && drawerForm.code ? '#1890FF' : '#D9D9D9' }"
:disabled="!drawerForm.name || !drawerForm.code"
@click="showDrawer = false"
>确认创建</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,8 +6,54 @@ import {
Camera, Camera,
Upload, Upload,
Check, Check,
Download,
X,
FileSpreadsheet,
} from 'lucide-vue-next' } 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 selectedModel = ref('GD30')
const checklistItems = ref([ const checklistItems = ref([
{ id: 1, text: '主板安装及固定', completed: true, photos: 3, needPhoto: true, versionCheck: true, versionMatch: true }, { id: 1, text: '主板安装及固定', completed: true, photos: 3, needPhoto: true, versionCheck: true, versionMatch: true },
@ -124,8 +170,7 @@ const toggleChecklistItem = (id: number) => {
type="text" type="text"
class="w-full px-3 py-2 border rounded" class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9', backgroundColor: '#FAFAFA' }" :style="{ borderColor: '#D9D9D9', backgroundColor: '#FAFAFA' }"
value="张工程师" value="张工"
readonly
/> />
</div> </div>
</div> </div>
@ -167,6 +212,7 @@ const toggleChecklistItem = (id: number) => {
<button <button
class="px-4 py-2 rounded flex items-center gap-2" class="px-4 py-2 rounded flex items-center gap-2"
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }" :style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
@click="showImportDialog = true"
> >
<Upload :size="16" /> <Upload :size="16" />
导入 导入
@ -220,9 +266,6 @@ const toggleChecklistItem = (id: number) => {
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="p-4 border-t" :style="{ borderColor: '#F0F0F0' }">
<button class="text-sm" :style="{ color: '#1890FF' }">+ 添加物料</button>
</div>
</div> </div>
<!-- Assembly Checklist Card --> <!-- Assembly Checklist Card -->
@ -281,18 +324,20 @@ const toggleChecklistItem = (id: number) => {
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <button
v-if="item.needPhoto && item.completed && item.photos > 0" v-if="item.needPhoto && item.completed && item.photos > 0"
class="px-3 py-1 rounded text-xs flex items-center gap-1" class="px-3 py-1 rounded text-xs flex items-center gap-1 cursor-pointer"
:style="{ backgroundColor: '#E6F7FF', color: '#1890FF' }" :style="{ backgroundColor: '#E6F7FF', color: '#1890FF' }"
@click="openPhotoDialog(item.id)"
> >
<Camera :size="12" /> <Camera :size="12" />
已上传 {{ item.photos }} 已上传 {{ item.photos }}
</span> </button>
<button <button
v-if="item.needPhoto && !item.completed" v-if="item.needPhoto && !item.completed"
class="px-3 py-1 rounded text-xs flex items-center gap-1" class="px-3 py-1 rounded text-xs flex items-center gap-1"
:style="{ backgroundColor: '#1890FF', color: '#fff' }" :style="{ backgroundColor: '#1890FF', color: '#fff' }"
@click="openPhotoDialog(item.id)"
> >
<Camera :size="12" /> <Camera :size="12" />
拍照上传 拍照上传
@ -327,5 +372,143 @@ const toggleChecklistItem = (id: number) => {
提交 提交
</button> </button>
</div> </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 ? '#1890FF' : '#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 #1890FF; color: #1890FF">
<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 ? '#1890FF' : '#D9D9D9' }"
:disabled="!importFile"
@click="showImportDialog = false; importFile = null"
>确认导入</button>
</div>
</div>
</div>
</div> </div>
</template> </template>

View File

@ -1,205 +1,155 @@
<script setup lang="ts"> <script setup lang="ts">
import { Plus, Upload, Download, Info } from 'lucide-vue-next' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { Plus, Download, Info, X } from 'lucide-vue-next'
const router = useRouter() const showDrawer = ref(false)
const selectedModel = ref('GD-10 Supreme')
const expiryType = ref('永久')
const expiryDate = ref('')
interface License { interface AuthItem { name: string,gd10: boolean, gd20: boolean, gd30: boolean }
sn: string const authItems: AuthItem[] = [
licenseId: string { name: '1D SP',gd10: true,gd20: true, gd30: true },
status: '已激活' | '已生成' | '待激活' { name: '2D SP',gd10: true, gd20: true, gd30: true },
generationDate: string { name: '3D SP',gd10: false, gd20: true, gd30: true },
expiryDate: string { name: '1D VES',gd10: true, gd20: true, gd30: true },
{ name: '2D ERT',gd10: true, gd20: true, gd30: true },
{ name: '3D ERT',gd10: false, gd20: true, gd30: true },
{ name: '1D IP',gd10: true, gd20: true, gd30: true },
{ name: '2D IP' ,gd10: true, gd20: true, gd30: true },
{ name: '3D IP',gd10: false, gd20: true, gd30: true },
{ name: '跨孔Cross-Hole',gd10: false, gd20: true, gd30: true },
{ name: '水上Marine',gd10: false, gd20: true, gd30: true },
]
const checkedItems = ref(new Set<string>())
const availableItems = computed(() =>
authItems.map(item => {
let preset = false
if (selectedModel.value === 'GD-10 Supreme') preset = item.gd10
else if (selectedModel.value === 'GD-20 Supreme') preset = item.gd20
else preset = item.gd30
return { ...item, preset }
})
)
const selectedCount = computed(() => checkedItems.value.size)
const totalAvailable = computed(() => authItems.length)
const toggleItem = (name: string) => { checkedItems.value.has(name) ? checkedItems.value.delete(name) : checkedItems.value.add(name) }
const selectAll = () => { authItems.forEach(i => checkedItems.value.add(i.name)) }
const clearAll = () => { checkedItems.value.clear() }
const openDrawer = () => {
checkedItems.value.clear()
// Auto-check items that match the selected model
authItems.forEach(item => {
let preset = false
if (selectedModel.value === 'GD-10 Supreme') preset = item.gd10
else if (selectedModel.value === 'GD-20 Supreme') preset = item.gd20
else preset = item.gd30
if (preset) checkedItems.value.add(item.name)
})
showDrawer.value = true
}
const onModelChange = () => {
checkedItems.value.clear()
authItems.forEach(item => {
let preset = false
if (selectedModel.value === 'GD-10 Supreme') preset = item.gd10
else if (selectedModel.value === 'GD-20 Supreme') preset = item.gd20
else preset = item.gd30
if (preset) checkedItems.value.add(item.name)
})
} }
interface License { licenseId: string; model: string; status: '已发布' | '草稿' | '已停用'; modules: string; expiry: string; createdDate: string }
const licenses: License[] = [ const licenses: License[] = [
{ { licenseId: 'LIC-GD10-Supreme-v1.0', model: 'GD-10 Supreme', status: '已发布', modules: '1D SP, 2D SP, 1D VES, 2D ERT, 1D IP, 2D IP', expiry: '永久', createdDate: '2025-02-01' },
sn: 'GD30-2025-000001', { licenseId: 'LIC-GD20-Supreme-v1.0', model: 'GD-20 Supreme', status: '已发布', modules: '全部模块', expiry: '永久', createdDate: '2025-02-05' },
licenseId: 'LIC-2025-0001', { licenseId: 'LIC-GD30-Supreme-v1.0', model: 'GD-30 Supreme', status: '草稿', modules: '全部模块(不含水上)', expiry: '1年', createdDate: '2025-02-08' },
status: '已激活',
generationDate: '2025-02-01',
expiryDate: '2026-02-01',
},
{
sn: 'GT20-2025-000045',
licenseId: 'LIC-2025-0045',
status: '已生成',
generationDate: '2025-02-05',
expiryDate: '2026-02-05',
},
{
sn: 'GTXD-2025-000023',
licenseId: 'LIC-2025-0023',
status: '待激活',
generationDate: '2025-02-08',
expiryDate: '2026-02-08',
},
] ]
const getStatusStyle = (s: License['status']) => {
const getStatusStyle = (status: License['status']) => { if (s === '已发布') return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
switch (status) { if (s === '草稿') return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
case '已激活': return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
return {
backgroundColor: '#F6FFED',
color: '#52C41A',
border: '1px solid #B7EB8F',
}
case '已生成':
return {
backgroundColor: '#E6F7FF',
color: '#1890FF',
border: '1px solid #91D5FF',
}
case '待激活':
return {
backgroundColor: '#FFFBE6',
color: '#FAAD14',
border: '1px solid #FFE58F',
}
}
} }
</script> </script>
<template> <template>
<div class="p-6"> <div class="p-6">
<!-- Page Header --> <!-- Header -->
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-2xl font-semibold">授权管理</h2> <h2 class="text-2xl font-semibold">授权管理</h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button class="px-4 py-2 rounded flex items-center gap-2" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)">
class="px-4 py-2 rounded flex items-center gap-2" <Download :size="16" /> 导出
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
>
<Upload :size="16" />
批量生成
</button> </button>
<button <button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #1890FF" @click="openDrawer">
class="px-4 py-2 rounded flex items-center gap-2" <Plus :size="16" /> 选择授权项生成
:style="{ border: '1px solid #D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }"
>
<Download :size="16" />
导出
</button>
<button
class="px-4 py-2 rounded text-white flex items-center gap-2"
:style="{ backgroundColor: '#1890FF' }"
@click="router.push('/licenses/generate')"
>
<Plus :size="16" />
选择授权项生成
</button> </button>
</div> </div>
</div> </div>
<p class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.45)' }">管理设备授权文件</p> <p class="text-sm" style="color: rgba(0,0,0,0.45)">管理设备授权文件</p>
</div> </div>
<!-- Info Banner --> <!-- Info Banner -->
<div <div class="mb-6 p-4 rounded-lg flex items-start gap-3" style="background-color: #E6FFFB; border: 1px solid #87E8DE">
class="mb-6 p-4 rounded-lg flex items-start gap-3" <Info :size="20" style="color: #13C2C2; flex-shrink: 0; margin-top: 2px" />
:style="{ backgroundColor: '#E6FFFB', border: '1px solid #87E8DE' }" <div style="color: #006D75">
>
<Info :size="20" :style="{ color: '#13C2C2', flexShrink: 0, marginTop: '2px' }" />
<div :style="{ color: '#006D75' }">
<div class="font-medium mb-1">授权说明</div> <div class="font-medium mb-1">授权说明</div>
<div class="text-sm"> <div class="text-sm">授权文件按设备型号管理每个型号对应一套授权模块配置点击"选择授权项生成"可按型号勾选功能模块并生成授权文件设备在APP激活时自动下载对应型号的授权文件</div>
授权文件按型号关联点击选择授权项生成可勾选功能模块和有效期生产装配阶段有配置无授权授权在出厂阶段生成
</div>
</div> </div>
</div> </div>
<!-- Filter Card --> <!-- Filter -->
<div class="bg-white p-6 rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }"> <div class="bg-white p-6 rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="grid grid-cols-4 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.65)' }"> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.65)">设备型号</label>
设备SN号 <select class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9; background-color: #fff">
</label> <option>全部</option><option>GD-10 Supreme</option><option>GD-20 Supreme</option><option>GD-30 Supreme</option>
<input
type="text"
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9' }"
placeholder="输入设备SN号搜索"
/>
</div>
<div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">
授权状态
</label>
<select
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9', backgroundColor: '#fff' }"
>
<option>全部</option>
<option>已激活</option>
<option>已生成</option>
<option>待激活</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm mb-2" :style="{ color: 'rgba(0, 0, 0, 0.65)' }"> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.65)">状态</label>
设备型号 <select class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9; background-color: #fff">
</label> <option>全部</option><option>已发布</option><option>草稿</option><option>已停用</option>
<select
class="w-full px-3 py-2 border rounded"
:style="{ borderColor: '#D9D9D9', backgroundColor: '#fff' }"
>
<option>全部</option>
<option>GD30 高密度电法仪</option>
<option>GT20 瞬变电磁仪</option>
<option>GM10 大地电磁仪</option>
</select> </select>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button <button class="w-full px-4 py-2 rounded text-white" style="background-color: #1890FF">查询</button>
class="w-full px-4 py-2 rounded text-white"
:style="{ backgroundColor: '#1890FF' }"
>
查询
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- License List --> <!-- License Table -->
<div class="bg-white rounded-lg mb-6" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }"> <div class="bg-white rounded-lg mb-6" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead :style="{ backgroundColor: '#FAFAFA' }"> <thead style="background-color: #FAFAFA">
<tr> <tr>
<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)">授权文件ID</th>
<th class="px-6 py-3 text-left text-sm font-medium" :style="{ color: 'rgba(0, 0, 0, 0.85)' }">授权ID</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> <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> <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> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr v-for="(lic, i) in licenses" :key="i" class="border-b" style="border-color: #F0F0F0">
v-for="(license, index) in licenses" <td class="px-6 py-4" style="color: #1890FF">{{ lic.licenseId }}</td>
:key="index" <td class="px-6 py-4">{{ lic.model }}</td>
class="border-b" <td class="px-6 py-4 text-sm" style="color: rgba(0,0,0,0.65); max-width: 200px">{{ lic.modules }}</td>
:style="{ borderColor: '#F0F0F0' }" <td class="px-6 py-4" style="color: rgba(0,0,0,0.65)">{{ lic.expiry }}</td>
> <td class="px-6 py-4" style="color: rgba(0,0,0,0.65)">{{ lic.createdDate }}</td>
<td class="px-6 py-4">{{ license.sn }}</td> <td class="px-6 py-4"><span class="px-2 py-1 rounded text-xs" :style="getStatusStyle(lic.status)">{{ lic.status }}</span></td>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ license.licenseId }}</td>
<td class="px-6 py-4">
<span
class="px-2 py-1 rounded text-xs"
:style="getStatusStyle(license.status)"
>
{{ license.status }}
</span>
</td>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ license.generationDate }}</td>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ license.expiryDate }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" :style="{ color: '#1890FF' }">详情</button> <button class="text-sm" style="color: #1890FF">详情</button>
<button class="text-sm" :style="{ color: '#1890FF' }">下载</button> <button class="text-sm" style="color: #1890FF">下载</button>
<button class="text-sm" :style="{ color: '#1890FF' }">续期</button> <button v-if="lic.status === '草稿'" class="text-sm" style="color: #52C41A">发布</button>
<button class="text-sm" :style="{ color: '#FF4D4F' }">撤销</button> <button v-if="lic.status === '已发布'" class="text-sm" style="color: #FF4D4F">停用</button>
</div> </div>
</td> </td>
</tr> </tr>
@ -209,42 +159,92 @@ const getStatusStyle = (status: License['status']) => {
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="bg-white p-4 rounded-lg flex items-center justify-between" :style="{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }"> <div class="bg-white p-4 rounded-lg flex items-center justify-between" style="box-shadow: 0 1px 2px rgba(0,0,0,0.05)">
<div class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.65)' }"> <div class="text-sm" style="color: rgba(0,0,0,0.65)">显示 1-10 / 156 </div>
显示 1-10 / 156
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.45)" disabled>上一页</button>
class="px-3 py-1 rounded border" <button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button>
:style="{ borderColor: '#D9D9D9', color: 'rgba(0, 0, 0, 0.45)' }" <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">2</button>
disabled <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">3</button>
> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">下一页</button>
上一页 </div>
</button> </div>
<button
class="px-3 py-1 rounded" <!-- Authorization Generate Drawer -->
:style="{ backgroundColor: '#1890FF', color: '#fff' }" <div v-if="showDrawer" class="fixed inset-0 z-50 flex justify-end" style="background-color: rgba(0,0,0,0.45)" @click.self="showDrawer = false">
> <div class="bg-white w-[640px] h-full flex flex-col" style="box-shadow: -4px 0 12px rgba(0,0,0,0.1)">
1 <!-- Header -->
</button> <div class="flex items-center justify-between p-5 border-b" style="border-color: #F0F0F0">
<button <h3 class="text-lg font-semibold">选择授权项生成</h3>
class="px-3 py-1 rounded border" <button @click="showDrawer = false" class="p-1 rounded hover:bg-gray-100" style="color: rgba(0,0,0,0.45)">
:style="{ borderColor: '#D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }" <X :size="18" />
> </button>
2 </div>
</button>
<button <!-- Body -->
class="px-3 py-1 rounded border" <div class="flex-1 overflow-y-auto p-6">
:style="{ borderColor: '#D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }" <!-- Model & Expiry -->
> <div class="grid grid-cols-2 gap-4 mb-6">
3 <div>
</button> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">设备型号 <span style="color: #FF4D4F">*</span></label>
<button <select v-model="selectedModel" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff" @change="onModelChange()">
class="px-3 py-1 rounded border" <option>GD-10 Supreme</option>
:style="{ borderColor: '#D9D9D9', color: 'rgba(0, 0, 0, 0.85)' }" <option>GD-20 Supreme</option>
> <option>GD-30 Supreme</option>
下一页 </select>
</button> </div>
<div>
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">授权有效期</label>
<select v-model="expiryType" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9; background-color: #fff">
<option>永久</option><option>1</option><option>2</option><option>自定义</option>
</select>
</div>
<div v-if="expiryType === '自定义'">
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">到期日期</label>
<input type="date" v-model="expiryDate" class="w-full px-3 py-2 border rounded text-sm" style="border-color: #D9D9D9" />
</div>
</div>
<!-- Auth Items Table -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<span class="text-sm font-medium">功能授权项</span>
<span class="text-xs" style="color: rgba(0,0,0,0.45)">已选 {{ selectedCount }} / {{ totalAvailable }} </span>
</div>
<div class="flex items-center gap-3">
<button class="text-xs" style="color: #1890FF" @click="selectAll">全选</button>
<button class="text-xs" style="color: rgba(0,0,0,0.45)" @click="clearAll">清空</button>
</div>
</div>
<table class="w-full text-sm">
<thead style="background-color: #FAFAFA">
<tr>
<th class="px-3 py-2 text-left" style="width: 36px"></th>
<th class="px-3 py-2 text-left">功能授权项名称</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in availableItems" :key="i" class="border-b" style="border-color: #F0F0F0">
<td class="px-3 py-2">
<input type="checkbox" class="w-3.5 h-3.5" style="accent-color: #1890FF" :checked="checkedItems.has(item.name)" @change="toggleItem(item.name)" />
</td>
<td class="px-3 py-2">{{ item.name }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 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="showDrawer = false">取消</button>
<button
class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: selectedCount > 0 ? '#52C41A' : '#D9D9D9' }"
:disabled="selectedCount === 0"
@click="showDrawer = false"
>生成授权文件{{ selectedCount }}</button>
</div>
</div> </div>
</div> </div>
</div> </div>