mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-12-07 18:13:09 +00:00
396 lines
12 KiB
Vue
396 lines
12 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import { useAccountStore } from '@/stores/account'
|
|
import { deviceStore } from '@/lib/deviceStore'
|
|
import { apiClient } from '@/lib/api'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
import DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue'
|
|
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
|
|
import LoginDialog from '@/components/LoginDialog.vue'
|
|
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
|
|
import {
|
|
ChevronDown,
|
|
Plus,
|
|
Monitor,
|
|
Search,
|
|
Clock,
|
|
User,
|
|
Check,
|
|
Settings,
|
|
Layers
|
|
} from 'lucide-vue-next'
|
|
import { toast } from 'vue-sonner'
|
|
|
|
const props = defineProps({
|
|
deviceInfo: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
deviceUuid: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['device-changed'])
|
|
|
|
const accountStore = useAccountStore()
|
|
|
|
// 状态
|
|
const showDropdown = ref(false)
|
|
const showLoginDialog = ref(false)
|
|
const showRegisterDialog = ref(false)
|
|
const showManualInputDialog = ref(false)
|
|
const searchQuery = ref('')
|
|
const manualUuid = ref('')
|
|
const isLoading = ref(false)
|
|
|
|
// 设备列表
|
|
const accountDevices = ref([])
|
|
const historyDevices = ref([])
|
|
|
|
// 当前设备的显示信息
|
|
const currentDevice = computed(() => {
|
|
if (props.deviceInfo) {
|
|
return {
|
|
name: props.deviceInfo.name || props.deviceInfo.deviceName || '未命名设备',
|
|
namespace: props.deviceInfo.namespace,
|
|
uuid: props.deviceUuid,
|
|
isOwned: !!props.deviceInfo.account
|
|
}
|
|
}
|
|
return {
|
|
name: '未选择设备',
|
|
namespace: props.deviceUuid,
|
|
uuid: props.deviceUuid,
|
|
isOwned: false
|
|
}
|
|
})
|
|
|
|
// 过滤后的账户设备
|
|
const filteredAccountDevices = computed(() => {
|
|
if (!searchQuery.value) return accountDevices.value
|
|
const query = searchQuery.value.toLowerCase()
|
|
return accountDevices.value.filter(device =>
|
|
(device.name || '').toLowerCase().includes(query) ||
|
|
device.uuid.toLowerCase().includes(query) ||
|
|
(device.namespace || '').toLowerCase().includes(query)
|
|
)
|
|
})
|
|
|
|
// 过滤后的历史设备
|
|
const filteredHistoryDevices = computed(() => {
|
|
if (!searchQuery.value) return historyDevices.value
|
|
const query = searchQuery.value.toLowerCase()
|
|
return historyDevices.value.filter(device =>
|
|
(device.name || '').toLowerCase().includes(query) ||
|
|
device.uuid.toLowerCase().includes(query)
|
|
)
|
|
})
|
|
|
|
// 加载账户设备
|
|
const loadAccountDevices = async () => {
|
|
if (!accountStore.isAuthenticated) return
|
|
|
|
isLoading.value = true
|
|
try {
|
|
const response = await apiClient.getAccountDevices()
|
|
accountDevices.value = response.data || []
|
|
} catch (error) {
|
|
console.error('Failed to load account devices:', error)
|
|
toast.error('加载设备列表失败')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 加载历史设备
|
|
const loadHistoryDevices = () => {
|
|
historyDevices.value = deviceStore.getDeviceHistory()
|
|
}
|
|
|
|
// 切换设备
|
|
const switchToDevice = async (device) => {
|
|
try {
|
|
deviceStore.setDeviceUuid(device.uuid)
|
|
deviceStore.addDeviceToHistory({
|
|
uuid: device.uuid,
|
|
name: device.name || device.deviceName
|
|
})
|
|
|
|
showDropdown.value = false
|
|
searchQuery.value = ''
|
|
|
|
emit('device-changed', device.uuid)
|
|
toast.success(`已切换到: ${device.name || device.uuid}`)
|
|
} catch (error) {
|
|
toast.error('切换设备失败')
|
|
}
|
|
}
|
|
|
|
// 手动输入UUID
|
|
const handleManualInput = () => {
|
|
const uuid = manualUuid.value.trim()
|
|
if (!uuid) {
|
|
toast.error('请输入设备UUID')
|
|
return
|
|
}
|
|
|
|
if (!/^[0-9a-fA-F-]{8,}$/.test(uuid)) {
|
|
toast.error('UUID格式不正确')
|
|
return
|
|
}
|
|
|
|
switchToDevice({ uuid, name: '' })
|
|
showManualInputDialog.value = false
|
|
manualUuid.value = ''
|
|
}
|
|
|
|
// 登录成功回调
|
|
const handleLoginSuccess = async (token) => {
|
|
showLoginDialog.value = false
|
|
await accountStore.login(token)
|
|
await loadAccountDevices()
|
|
toast.success('登录成功')
|
|
}
|
|
|
|
// 注册设备成功回调
|
|
const handleDeviceRegistered = () => {
|
|
showRegisterDialog.value = false
|
|
loadAccountDevices()
|
|
loadHistoryDevices()
|
|
emit('device-changed')
|
|
}
|
|
|
|
// 监听下拉菜单打开,加载数据
|
|
watch(showDropdown, (isOpen) => {
|
|
if (isOpen) {
|
|
loadHistoryDevices()
|
|
if (accountStore.isAuthenticated) {
|
|
loadAccountDevices()
|
|
}
|
|
} else {
|
|
searchQuery.value = ''
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
loadHistoryDevices()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="relative">
|
|
<!-- 设备切换器触发按钮 -->
|
|
<DropdownMenu v-model:open="showDropdown">
|
|
<template #trigger="{ toggle, open }">
|
|
<Button
|
|
variant="ghost"
|
|
class="h-8 px-3 max-w-[300px] justify-start font-normal hover:bg-accent/50 border border-border"
|
|
@click="toggle"
|
|
>
|
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
<Monitor class="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
<div class="flex flex-col items-start min-w-0 flex-1">
|
|
<div class="truncate text-sm font-medium max-w-[180px]">
|
|
{{ currentDevice.name }}
|
|
</div>
|
|
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-auto">
|
|
<Badge v-if="!currentDevice.isOwned" variant="secondary" class="h-4 px-1 text-[10px]">
|
|
未绑定
|
|
</Badge>
|
|
<ChevronDown
|
|
class="h-3 w-3 text-muted-foreground flex-shrink-0 transition-transform duration-200"
|
|
:class="{ 'rotate-180': open }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Button>
|
|
</template>
|
|
|
|
<!-- 下拉菜单内容 -->
|
|
<div class="w-auto p-0">
|
|
<!-- 搜索框 -->
|
|
<div class="p-3 border-b">
|
|
<div class="relative">
|
|
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
v-model="searchQuery"
|
|
placeholder="搜索设备..."
|
|
class="pl-9 h-8"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 当前设备信息 -->
|
|
<div class="p-3 border-b bg-muted/20">
|
|
<div class="text-xs font-medium text-muted-foreground mb-1">当前设备</div>
|
|
<div class="flex items-center gap-2">
|
|
<Monitor class="h-4 w-4 text-primary" />
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-medium text-sm truncate">{{ currentDevice.name }}</div>
|
|
<code class="text-xs text-muted-foreground truncate block">
|
|
{{ currentDevice.namespace }}
|
|
</code>
|
|
</div>
|
|
<Check class="h-4 w-4 text-green-500" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-h-80 overflow-y-auto">
|
|
<!-- 账户设备 -->
|
|
<div v-if="accountStore.isAuthenticated">
|
|
<div class="px-3 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2">
|
|
<User class="h-3 w-3" />
|
|
账户设备
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="px-3 py-4 text-center text-sm text-muted-foreground">
|
|
加载中...
|
|
</div>
|
|
|
|
<div v-else-if="filteredAccountDevices.length === 0" class="px-3 py-2">
|
|
<div class="text-sm text-muted-foreground text-center py-2">
|
|
{{ searchQuery ? '未找到匹配的设备' : '暂无绑定设备' }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<DropdownItem
|
|
v-for="device in filteredAccountDevices"
|
|
:key="device.uuid"
|
|
@click="switchToDevice(device)"
|
|
class="cursor-pointer"
|
|
>
|
|
<div class="flex items-center gap-2 w-full">
|
|
<Monitor class="h-4 w-4 text-muted-foreground" />
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-medium text-sm truncate">
|
|
{{ device.name || '未命名设备' }}
|
|
</div>
|
|
<div class="text-xs text-muted-foreground truncate">
|
|
{{ device.namespace}}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</DropdownItem>
|
|
</div>
|
|
|
|
<Separator class="my-1" />
|
|
</div>
|
|
|
|
<!-- 历史设备 -->
|
|
<div v-if="filteredHistoryDevices.length > 0">
|
|
<div class="px-3 py-2 text-xs font-medium text-muted-foreground flex items-center gap-2">
|
|
<Clock class="h-3 w-3" />
|
|
最近使用
|
|
</div>
|
|
|
|
<DropdownItem
|
|
v-for="device in filteredHistoryDevices.slice(0, 5)"
|
|
:key="device.uuid"
|
|
@click="switchToDevice(device)"
|
|
class="cursor-pointer"
|
|
>
|
|
<div class="flex items-center gap-2 w-full">
|
|
<Monitor class="h-4 w-4 text-muted-foreground" />
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-medium text-sm truncate">
|
|
{{ device.name || '未命名设备' }}
|
|
</div>
|
|
<div class="text-xs text-muted-foreground truncate">
|
|
{{ device.namespace }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DropdownItem>
|
|
|
|
<Separator class="my-1" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 操作按钮 -->
|
|
<div class="p-2 border-t bg-muted/20 space-y-1">
|
|
<DropdownItem
|
|
v-if="!accountStore.isAuthenticated"
|
|
@click="showLoginDialog = true"
|
|
class="cursor-pointer text-primary"
|
|
>
|
|
<User class="h-4 w-4" />
|
|
登录账户
|
|
</DropdownItem>
|
|
|
|
<DropdownItem
|
|
@click="showManualInputDialog = true"
|
|
class="cursor-pointer"
|
|
>
|
|
<Settings class="h-4 w-4" />
|
|
手动输入UUID
|
|
</DropdownItem>
|
|
|
|
<DropdownItem
|
|
@click="showRegisterDialog = true"
|
|
class="cursor-pointer text-primary"
|
|
>
|
|
<Plus class="h-4 w-4" />
|
|
注册新设备
|
|
</DropdownItem> <DropdownItem
|
|
@click="showRegisterDialog = true"
|
|
class="cursor-pointer text-primary"
|
|
>
|
|
<Plus class="h-4 w-4" />
|
|
高级选项
|
|
</DropdownItem>
|
|
</div>
|
|
</div>
|
|
</DropdownMenu>
|
|
|
|
<!-- 手动输入UUID对话框 -->
|
|
<Dialog v-model:open="showManualInputDialog">
|
|
<DialogContent class="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>手动输入设备UUID</DialogTitle>
|
|
<DialogDescription>
|
|
输入已存在的设备UUID来快速切换
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div class="space-y-4 py-4">
|
|
<Input
|
|
v-model="manualUuid"
|
|
placeholder="输入设备UUID"
|
|
@keyup.enter="handleManualInput"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<Button variant="outline" @click="showManualInputDialog = false">
|
|
取消
|
|
</Button>
|
|
<Button @click="handleManualInput" :disabled="!manualUuid.trim()">
|
|
确定
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<!-- 登录对话框 -->
|
|
<LoginDialog
|
|
v-model="showLoginDialog"
|
|
:on-success="handleLoginSuccess"
|
|
/>
|
|
|
|
<!-- 设备注册对话框 -->
|
|
<DeviceRegisterDialog
|
|
v-model="showRegisterDialog"
|
|
@confirm="handleDeviceRegistered"
|
|
/>
|
|
</div>
|
|
</template> |