feat: 添加历史设备管理功能,支持手动输入UUID加载设备

This commit is contained in:
SunWuyuan 2025-10-08 12:10:18 +08:00
parent ef29982de7
commit 7b0e1610d5
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
4 changed files with 224 additions and 67 deletions

View File

@ -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,19 +294,22 @@ 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>
<!-- 账户设备区域 -->
<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">
<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>
<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" />
注册新设备
@ -300,6 +345,28 @@ onUnmounted(() => {
</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>

View File

@ -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
}
}

View File

@ -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">

View File

@ -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"