mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-10-22 03:23:10 +00:00
feat: 添加历史设备管理功能,支持手动输入UUID加载设备
This commit is contained in:
parent
ef29982de7
commit
7b0e1610d5
@ -33,8 +33,10 @@ const newUuid = ref('')
|
||||
const deviceName = ref('')
|
||||
const bindToAccount = ref(false)
|
||||
const accountDevices = ref([])
|
||||
const historyDevices = ref([])
|
||||
const manualUuid = ref('')
|
||||
const loadingDevices = ref(false)
|
||||
const activeTab = ref('load') // 'load' 或 'register'
|
||||
const activeTab = ref('load') // 'load' | 'history' | 'register'
|
||||
const showLoginDialog = ref(false) // 登录对话框状态
|
||||
|
||||
const isOpen = computed({
|
||||
@ -47,6 +49,9 @@ watch(isOpen, (newVal) => {
|
||||
if (newVal && accountStore.isAuthenticated && activeTab.value === 'load') {
|
||||
loadAccountDevices()
|
||||
}
|
||||
if (newVal && activeTab.value === 'history') {
|
||||
loadHistoryDevices()
|
||||
}
|
||||
// 切换到注册选项卡时,自动生成UUID
|
||||
if (newVal && activeTab.value === 'register' && !newUuid.value) {
|
||||
generateRandomUuid()
|
||||
@ -58,6 +63,9 @@ watch(activeTab, (newVal) => {
|
||||
if (newVal === 'load' && accountStore.isAuthenticated && isOpen.value) {
|
||||
loadAccountDevices()
|
||||
}
|
||||
if (newVal === 'history' && isOpen.value) {
|
||||
loadHistoryDevices()
|
||||
}
|
||||
if (newVal === 'register' && !newUuid.value) {
|
||||
generateRandomUuid()
|
||||
}
|
||||
@ -121,12 +129,35 @@ const loadAccountDevices = async () => {
|
||||
// 加载选中的设备
|
||||
const loadDevice = (device) => {
|
||||
deviceStore.setDeviceUuid(device.uuid)
|
||||
// 写入历史
|
||||
deviceStore.addDeviceToHistory({ uuid: device.uuid, name: device.name })
|
||||
isOpen.value = false
|
||||
emit('confirm')
|
||||
resetForm()
|
||||
toast.success(`已切换到设备: ${device.name || device.uuid}`)
|
||||
}
|
||||
|
||||
// 手动输入UUID加载
|
||||
const loadByUuid = () => {
|
||||
const id = manualUuid.value?.trim()
|
||||
if (!id) {
|
||||
toast.error('请输入设备 UUID')
|
||||
return
|
||||
}
|
||||
// 可选:基本格式校验(宽松处理,避免误判合法UUID)
|
||||
const ok = /^[0-9a-fA-F-]{8,}$/.test(id)
|
||||
if (!ok) {
|
||||
toast.error('UUID 格式不正确')
|
||||
return
|
||||
}
|
||||
deviceStore.setDeviceUuid(id)
|
||||
deviceStore.addDeviceToHistory({ uuid: id })
|
||||
isOpen.value = false
|
||||
emit('confirm')
|
||||
resetForm()
|
||||
toast.success(`已切换到设备: ${id}`)
|
||||
}
|
||||
|
||||
// 注册新设备
|
||||
const registerDevice = async () => {
|
||||
if (!newUuid.value.trim()) {
|
||||
@ -142,6 +173,8 @@ const registerDevice = async () => {
|
||||
try {
|
||||
// 1. 保存UUID到本地
|
||||
deviceStore.setDeviceUuid(newUuid.value.trim())
|
||||
// 写入历史
|
||||
deviceStore.addDeviceToHistory({ uuid: newUuid.value.trim(), name: deviceName.value.trim() })
|
||||
|
||||
// 2. 调用设备注册接口(会自动在云端创建设备)
|
||||
await apiClient.registerDevice(
|
||||
@ -181,6 +214,7 @@ const resetForm = () => {
|
||||
bindToAccount.value = accountStore.isAuthenticated
|
||||
accountDevices.value = []
|
||||
activeTab.value = 'load'
|
||||
manualUuid.value = ''
|
||||
}
|
||||
|
||||
// 处理弹框关闭
|
||||
@ -211,6 +245,11 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown, true)
|
||||
})
|
||||
|
||||
// 加载本地历史设备
|
||||
const loadHistoryDevices = () => {
|
||||
historyDevices.value = deviceStore.getDeviceHistory()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -239,11 +278,14 @@ onUnmounted(() => {
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs v-model="activeTab" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="load">
|
||||
<Download class="h-4 w-4 mr-2" />
|
||||
加载设备
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="history">
|
||||
历史记录
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="register">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
注册设备
|
||||
@ -252,54 +294,79 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 加载设备选项卡 -->
|
||||
<TabsContent value="load" class="space-y-4 mt-4">
|
||||
<div v-if="!accountStore.isAuthenticated" class="text-center py-8">
|
||||
<p class="text-muted-foreground mb-4">请先登录以查看您的设备列表</p>
|
||||
<Button variant="outline" @click="handleOpenLogin">
|
||||
登录账户
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 账户设备区域 -->
|
||||
<div class="space-y-3">
|
||||
<div v-if="!accountStore.isAuthenticated" class="text-center py-6">
|
||||
<p class="text-muted-foreground mb-3">登录后可查看您账户绑定的设备</p>
|
||||
<Button variant="outline" @click="handleOpenLogin">
|
||||
登录账户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loadingDevices" class="text-center py-8">
|
||||
<p class="text-muted-foreground">加载中...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="loadingDevices" class="text-center py-6">
|
||||
<p class="text-muted-foreground">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="accountDevices.length === 0" class="text-center py-8">
|
||||
<p class="text-muted-foreground mb-4">您的账户暂未绑定任何设备</p>
|
||||
<Button variant="outline" @click="activeTab = 'register'">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
注册新设备
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="device in accountDevices"
|
||||
:key="device.uuid"
|
||||
class="p-4 rounded-lg border hover:bg-accent cursor-pointer transition-colors"
|
||||
@click="loadDevice(device)"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-base">
|
||||
{{ device.name || '未命名设备' }}
|
||||
</div>
|
||||
<code class="text-xs text-muted-foreground block mt-1">
|
||||
{{ device.uuid }}
|
||||
</code>
|
||||
<div class="text-xs text-muted-foreground mt-2">
|
||||
创建时间: {{ new Date(device.createdAt).toLocaleString('zh-CN') }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click.stop="loadDevice(device)"
|
||||
>
|
||||
加载
|
||||
<div v-else-if="accountDevices.length === 0" class="text-center py-6">
|
||||
<p class="text-muted-foreground mb-3">您的账户暂未绑定任何设备</p>
|
||||
<Button variant="outline" @click="activeTab = 'register'">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
注册新设备
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="device in accountDevices"
|
||||
:key="device.uuid"
|
||||
class="p-4 rounded-lg border hover:bg-accent cursor-pointer transition-colors"
|
||||
@click="loadDevice(device)"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-base">
|
||||
{{ device.name || '未命名设备' }}
|
||||
</div>
|
||||
<code class="text-xs text-muted-foreground block mt-1">
|
||||
{{ device.uuid }}
|
||||
</code>
|
||||
<div class="text-xs text-muted-foreground mt-2">
|
||||
创建时间: {{ new Date(device.createdAt).toLocaleString('zh-CN') }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click.stop="loadDevice(device)"
|
||||
>
|
||||
加载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- 手动输入 UUID 加载 -->
|
||||
<div class="space-y-2">
|
||||
<Label for="manualUuid">手动输入 UUID</Label>
|
||||
<div class="flex gap-2">
|
||||
<Input
|
||||
id="manualUuid"
|
||||
v-model="manualUuid"
|
||||
placeholder="输入设备 UUID 直接加载"
|
||||
class="flex-1"
|
||||
@keyup.enter="loadByUuid"
|
||||
/>
|
||||
<Button @click="loadByUuid" :disabled="!manualUuid.trim()">
|
||||
加载
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">无需注册或登录即可加载已有设备。</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 注册设备选项卡 -->
|
||||
@ -328,7 +395,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 设备名称输入 -->
|
||||
<div class="space-y-2">
|
||||
<Label for="deviceName">设备名称</Label>
|
||||
<Label for="deviceName">* 设备名称</Label>
|
||||
<Input
|
||||
id="deviceName"
|
||||
v-model="deviceName"
|
||||
@ -361,16 +428,6 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="text-sm text-muted-foreground bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 rounded-lg p-3">
|
||||
<p><strong>提示:</strong></p>
|
||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||
<li>UUID将保存到本地浏览器存储</li>
|
||||
<li v-if="deviceName">设备名称将帮助您快速识别不同的设备</li>
|
||||
<li v-if="bindToAccount && accountStore.isAuthenticated">绑定后可在任何设备上通过账户加载</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
@ -388,6 +445,42 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 历史设备选项卡 -->
|
||||
<TabsContent value="history" class="space-y-4 mt-4">
|
||||
<div v-if="historyDevices.length === 0" class="text-center py-8 text-muted-foreground">
|
||||
暂无历史设备
|
||||
</div>
|
||||
<div v-else class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="device in historyDevices"
|
||||
:key="device.uuid"
|
||||
class="p-4 rounded-lg border hover:bg-accent cursor-pointer transition-colors"
|
||||
@click="loadDevice(device)"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-base">
|
||||
{{ device.name || '未命名设备' }}
|
||||
</div>
|
||||
<code class="text-xs text-muted-foreground block mt-1">
|
||||
{{ device.uuid }}
|
||||
</code>
|
||||
<div class="text-xs text-muted-foreground mt-2">
|
||||
最近使用: {{ new Date(device.lastUsedAt).toLocaleString('zh-CN') }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click.stop="loadDevice(device)"
|
||||
>
|
||||
加载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -13,6 +13,7 @@ export const deviceStore = {
|
||||
STORAGE_KEY: 'device_uuid',
|
||||
BACKUP_KEY: 'device_uuid_backup',
|
||||
SESSION_KEY: 'device_uuid_session',
|
||||
HISTORY_KEY: 'device_history', // 本地历史设备记录
|
||||
|
||||
// 获取当前设备 UUID(从多个存储位置尝试读取)
|
||||
getDeviceUuid() {
|
||||
@ -189,3 +190,64 @@ export const deviceStore = {
|
||||
if (typeof window !== 'undefined') {
|
||||
deviceStore.tryRestoreFromIndexedDB()
|
||||
}
|
||||
|
||||
// 为 deviceStore 扩展历史设备管理功能
|
||||
// 记录结构:{ uuid: string, name?: string, lastUsedAt: number }
|
||||
deviceStore.getDeviceHistory = function () {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.HISTORY_KEY)
|
||||
const list = raw ? JSON.parse(raw) : []
|
||||
if (!Array.isArray(list)) return []
|
||||
// 排序:最近使用在前
|
||||
return list.sort((a, b) => (b.lastUsedAt || 0) - (a.lastUsedAt || 0))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
deviceStore.addDeviceToHistory = function (device) {
|
||||
try {
|
||||
if (!device || !device.uuid) return
|
||||
const maxItems = 20
|
||||
const now = Date.now()
|
||||
const list = this.getDeviceHistory()
|
||||
const idx = list.findIndex(d => d.uuid === device.uuid)
|
||||
const entry = {
|
||||
uuid: device.uuid,
|
||||
name: device.name || device.deviceName || '',
|
||||
lastUsedAt: now
|
||||
}
|
||||
if (idx >= 0) {
|
||||
// 更新名称和时间
|
||||
list[idx] = { ...list[idx], ...entry }
|
||||
} else {
|
||||
list.unshift(entry)
|
||||
}
|
||||
// 去重(按 uuid)并截断
|
||||
const uniqMap = new Map()
|
||||
for (const item of list) {
|
||||
if (!uniqMap.has(item.uuid)) uniqMap.set(item.uuid, item)
|
||||
}
|
||||
const next = Array.from(uniqMap.values()).slice(0, maxItems)
|
||||
localStorage.setItem(this.HISTORY_KEY, JSON.stringify(next))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
deviceStore.removeDeviceFromHistory = function (uuid) {
|
||||
try {
|
||||
const list = this.getDeviceHistory().filter(d => d.uuid !== uuid)
|
||||
localStorage.setItem(this.HISTORY_KEY, JSON.stringify(list))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
deviceStore.clearDeviceHistory = function () {
|
||||
try {
|
||||
localStorage.removeItem(this.HISTORY_KEY)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -218,6 +218,10 @@ const copyToClipboard = async (text, id) => {
|
||||
const updateUuid = () => {
|
||||
showRegisterDialog.value = false
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
// 记录到历史
|
||||
if (deviceUuid.value) {
|
||||
deviceStore.addDeviceToHistory({ uuid: deviceUuid.value, name: deviceInfo.value?.name || deviceInfo.value?.deviceName })
|
||||
}
|
||||
loadDeviceInfo()
|
||||
loadDeviceAccount()
|
||||
loadTokens()
|
||||
@ -400,6 +404,15 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 切换设备按钮 -->
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="showRegisterDialog = true"
|
||||
title="切换设备"
|
||||
>
|
||||
切换设备
|
||||
</Button>
|
||||
<!-- 账户状态 -->
|
||||
<template v-if="accountStore.isAuthenticated">
|
||||
<DropdownMenu v-model:open="showUserMenu" class="z-50">
|
||||
|
@ -398,22 +398,11 @@ onMounted(async () => {
|
||||
重置设备
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
重置或换新设备标识。此操作无法撤销,您将失去当前设备的所有授权。
|
||||
更换新的设备标识。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 rounded-lg bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertTriangle class="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-red-900 dark:text-red-100">警告:此操作不可逆</p>
|
||||
<p class="text-sm text-red-700 dark:text-red-300 mt-1">
|
||||
重置设备后,您将获得全新的设备标识,现有的所有授权将被撤销,无法恢复。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
|
Loading…
x
Reference in New Issue
Block a user