mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-12-07 18:13:09 +00:00
Refactor API client to remove password handling in token revocation and authorization methods; update UI components to eliminate password input fields and related logic; streamline device management and password management functionalities.
This commit is contained in:
parent
95dedb6e29
commit
d9d62e8f8c
@ -24,7 +24,7 @@ const props = defineProps({
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '请输入设备 UUID 和密码'
|
||||
default: '请输入设备 UUID'
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
@ -35,9 +35,9 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
|
||||
const deviceUuid = ref('')
|
||||
const devicePassword = ref('')
|
||||
|
||||
|
||||
// 监听对话框打开,自动填充 UUID
|
||||
watch(() => props.modelValue, (isOpen) => {
|
||||
@ -62,21 +62,17 @@ const handleAuth = async () => {
|
||||
toast.error('请输入设备 UUID')
|
||||
return
|
||||
}
|
||||
if (!devicePassword.value) {
|
||||
toast.error('请输入设备密码')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 尝试通过 UUID 和密码验证设备
|
||||
const deviceInfo = await apiClient.verifyDevicePassword(deviceUuid.value, devicePassword.value)
|
||||
// 尝试通过 UUID 验证设备
|
||||
const deviceInfo = await apiClient.getDeviceInfo(deviceUuid.value)
|
||||
|
||||
// 验证成功,保存到 deviceStore
|
||||
deviceStore.setDeviceUuid(deviceUuid.value)
|
||||
|
||||
toast.success('认证成功')
|
||||
emit('success', deviceUuid.value, devicePassword.value, deviceInfo)
|
||||
emit('success', deviceUuid.value, deviceInfo)
|
||||
} catch (error) {
|
||||
toast.error('认证失败:' + error.message)
|
||||
} finally {
|
||||
@ -84,10 +80,7 @@ const handleAuth = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换密码可见性
|
||||
const togglePasswordVisibility = () => {
|
||||
showPassword.value = !showPassword.value
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -110,30 +103,7 @@ const togglePasswordVisibility = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<div class="space-y-2">
|
||||
<Label for="device-password">设备密码 *</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="device-password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
v-model="devicePassword"
|
||||
placeholder="输入设备密码"
|
||||
@keyup.enter="handleAuth"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
||||
@click="togglePasswordVisibility"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Eye v-if="!showPassword" class="h-4 w-4 text-muted-foreground" />
|
||||
<EyeOff v-else class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
396
src/components/DeviceSwitcher.vue
Normal file
396
src/components/DeviceSwitcher.vue
Normal file
@ -0,0 +1,396 @@
|
||||
<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>
|
||||
@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Edit } from 'lucide-vue-next'
|
||||
import { toast } from 'vue-sonner'
|
||||
import PasswordInput from './PasswordInput.vue'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -23,10 +23,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasPassword: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
@ -34,7 +31,6 @@ const emit = defineEmits(['update:modelValue', 'success'])
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const deviceName = ref('')
|
||||
const password = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const isOpen = computed({
|
||||
@ -42,15 +38,12 @@ const isOpen = computed({
|
||||
set: (val) => {
|
||||
if (val) {
|
||||
deviceName.value = props.currentName || ''
|
||||
password.value = ''
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
const needsPassword = computed(() => {
|
||||
return props.hasPassword && !accountStore.isAuthenticated
|
||||
})
|
||||
|
||||
|
||||
const updateDeviceName = async () => {
|
||||
if (!deviceName.value.trim()) {
|
||||
@ -63,7 +56,6 @@ const updateDeviceName = async () => {
|
||||
await apiClient.setDeviceName(
|
||||
props.deviceUuid,
|
||||
deviceName.value.trim(),
|
||||
needsPassword.value ? password.value : null,
|
||||
accountStore.isAuthenticated ? accountStore.token : null
|
||||
)
|
||||
|
||||
@ -104,23 +96,7 @@ const updateDeviceName = async () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="needsPassword">
|
||||
<PasswordInput
|
||||
v-model="password"
|
||||
label="设备密码"
|
||||
placeholder="输入设备密码"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="accountStore.isAuthenticated && hasPassword" class="p-3 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
您已登录绑定的账户,无需输入密码
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@ -50,15 +50,6 @@ const features = [
|
||||
iconBg: 'bg-orange-500/10',
|
||||
iconColor: 'text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
{
|
||||
title: '高级设置',
|
||||
description: '密码管理、安全设置和其他高级功能',
|
||||
icon: Settings,
|
||||
path: '/password-manager',
|
||||
color: 'from-gray-500 to-slate-500',
|
||||
iconBg: 'bg-gray-500/10',
|
||||
iconColor: 'text-gray-600 dark:text-gray-400',
|
||||
},
|
||||
]
|
||||
|
||||
const navigateTo = (path) => {
|
||||
|
||||
@ -1,274 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAccountStore } from '@/stores/account'
|
||||
import { apiClient } from '@/lib/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import PasswordInput from './PasswordInput.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
deviceUuid: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
deviceName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const password = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const showDeleteConfirm = ref(false)
|
||||
const showHintDialog = ref(false)
|
||||
const passwordHint = ref('')
|
||||
const isSettingHint = ref(false)
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
if (!val) {
|
||||
password.value = ''
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
const resetPassword = async () => {
|
||||
if (!password.value.trim()) {
|
||||
toast.error('请输入新密码')
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
// 账户拥有者使用专门的重置接口,无需当前密码
|
||||
if (accountStore.isAuthenticated) {
|
||||
await apiClient.resetDevicePasswordAsOwner(
|
||||
props.deviceUuid,
|
||||
password.value,
|
||||
null, // passwordHint 可以后续单独设置
|
||||
accountStore.token
|
||||
)
|
||||
} else {
|
||||
// 非账户拥有者使用普通设置密码接口
|
||||
await apiClient.setDevicePassword(
|
||||
props.deviceUuid,
|
||||
{ password: password.value }
|
||||
)
|
||||
}
|
||||
|
||||
toast.success('密码重置成功')
|
||||
isOpen.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
toast.error('重置密码失败:' + error.message)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeletePassword = () => {
|
||||
// 先关闭主弹框,避免重叠
|
||||
isOpen.value = false
|
||||
// 延迟打开删除确认弹框,确保主弹框完全关闭
|
||||
setTimeout(() => {
|
||||
showDeleteConfirm.value = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const deletePassword = async () => {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await apiClient.deleteDevicePassword(props.deviceUuid, null, accountStore.token)
|
||||
toast.success('密码已删除')
|
||||
showDeleteConfirm.value = false
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
toast.error('删除密码失败:' + error.message)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openHintDialog = () => {
|
||||
// 先关闭主弹框,避免重叠
|
||||
isOpen.value = false
|
||||
// 延迟打开设置提示弹框,确保主弹框完全关闭
|
||||
setTimeout(() => {
|
||||
showHintDialog.value = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const setPasswordHint = async () => {
|
||||
isSettingHint.value = true
|
||||
try {
|
||||
await apiClient.setDevicePasswordHint(
|
||||
props.deviceUuid,
|
||||
passwordHint.value,
|
||||
null,
|
||||
accountStore.token
|
||||
)
|
||||
toast.success('密码提示已设置')
|
||||
showHintDialog.value = false
|
||||
passwordHint.value = ''
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
toast.error('设置密码提示失败:' + error.message)
|
||||
} finally {
|
||||
isSettingHint.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
showDeleteConfirm.value = false
|
||||
// 延迟打开主弹框,避免重叠
|
||||
setTimeout(() => {
|
||||
isOpen.value = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleHintCancel = () => {
|
||||
showHintDialog.value = false
|
||||
passwordHint.value = ''
|
||||
// 延迟打开主弹框,避免重叠
|
||||
setTimeout(() => {
|
||||
isOpen.value = true
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>重置设备密码</DialogTitle>
|
||||
<DialogDescription>
|
||||
为设备 {{ deviceName || deviceUuid }} 设置新密码
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="p-3 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
您已登录绑定的账户,可以直接重置密码而无需输入当前密码
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="password"
|
||||
label="新密码"
|
||||
placeholder="输入新密码"
|
||||
:show-hint="false"
|
||||
:show-strength="true"
|
||||
:min-length="8"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="flex-col gap-2 sm:flex-row sm:justify-between">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
@click="confirmDeletePassword"
|
||||
:disabled="isSubmitting"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
删除密码
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="openHintDialog"
|
||||
:disabled="isSubmitting"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
设置提示
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" @click="isOpen = false" :disabled="isSubmitting">
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="resetPassword" :disabled="isSubmitting || !password.trim()">
|
||||
{{ isSubmitting ? '重置中...' : '确认重置' }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 删除密码确认对话框 -->
|
||||
<AlertDialog v-model:open="showDeleteConfirm">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除密码</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除设备 "{{ deviceName || deviceUuid }}" 的密码吗?删除后任何人都可以访问该设备。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="handleDeleteCancel">取消</AlertDialogCancel>
|
||||
<AlertDialogAction @click="deletePassword" :disabled="isSubmitting">
|
||||
{{ isSubmitting ? '删除中...' : '确认删除' }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 设置密码提示对话框 -->
|
||||
<Dialog v-model:open="showHintDialog">
|
||||
<DialogContent class="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>设置密码提示</DialogTitle>
|
||||
<DialogDescription>
|
||||
为设备 {{ deviceName || deviceUuid }} 设置密码提示
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div>
|
||||
<Label for="hint">密码提示</Label>
|
||||
<Input
|
||||
id="hint"
|
||||
v-model="passwordHint"
|
||||
placeholder="输入密码提示(可选)"
|
||||
:disabled="isSettingHint"
|
||||
class="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="handleHintCancel" :disabled="isSettingHint">
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="setPasswordHint" :disabled="isSettingHint">
|
||||
{{ isSettingHint ? '设置中...' : '确认设置' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
168
src/lib/api.js
168
src/lib/api.js
@ -84,7 +84,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
async revokeToken(targetToken, authOptions = {}) {
|
||||
const { deviceUuid, password, usePathParam = true, bearerToken } = authOptions;
|
||||
const { deviceUuid, usePathParam = true, bearerToken } = authOptions;
|
||||
|
||||
if (usePathParam) {
|
||||
// 使用路径参数方式 (推荐)
|
||||
@ -94,9 +94,6 @@ class ApiClient {
|
||||
headers['Authorization'] = `Bearer ${bearerToken}`;
|
||||
} else if (deviceUuid) {
|
||||
headers['x-device-uuid'] = deviceUuid;
|
||||
if (password) {
|
||||
headers['x-device-password'] = password;
|
||||
}
|
||||
}
|
||||
|
||||
return this.fetch(`/apps/tokens/${targetToken}`, {
|
||||
@ -112,9 +109,6 @@ class ApiClient {
|
||||
headers['Authorization'] = `Bearer ${bearerToken}`;
|
||||
} else if (deviceUuid) {
|
||||
headers['x-device-uuid'] = deviceUuid;
|
||||
if (password) {
|
||||
headers['x-device-password'] = password;
|
||||
}
|
||||
}
|
||||
|
||||
return this.fetch(`/apps/tokens?${params}`, {
|
||||
@ -126,19 +120,18 @@ class ApiClient {
|
||||
|
||||
// 应用安装接口 (对应后端的 /apps/devices/:uuid/install/:appId)
|
||||
async authorizeApp(appId, deviceUuid, options = {}) {
|
||||
const { password, note, token } = options;
|
||||
const { note, token } = options;
|
||||
|
||||
const headers = {
|
||||
'x-device-uuid': deviceUuid,
|
||||
};
|
||||
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 使用新的安装接口
|
||||
return this.fetch(`/apps/devices/${deviceUuid}/install/${appId}?password=${password}`, {
|
||||
return this.fetch(`/apps/devices/${deviceUuid}/install/${appId}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ note: note || '应用授权' }),
|
||||
@ -146,14 +139,10 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// 设备级别的应用卸载,使用新的 uninstall 接口
|
||||
async revokeDeviceToken(deviceUuid, installId, password = null, token = null) {
|
||||
async revokeDeviceToken(deviceUuid, installId, token = null) {
|
||||
const params = new URLSearchParams({ uuid: deviceUuid });
|
||||
const headers = {};
|
||||
|
||||
if (password) {
|
||||
params.set('password', password);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
@ -164,76 +153,9 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
// 设备密码管理 API
|
||||
async setDevicePassword(deviceUuid, data, token = null) {
|
||||
const { newPassword, currentPassword, passwordHint } = data;
|
||||
|
||||
// 检查设备是否已设置密码
|
||||
const deviceInfo = await this.getDeviceInfo(deviceUuid);
|
||||
const hasPassword = deviceInfo.hasPassword;
|
||||
|
||||
if (hasPassword) {
|
||||
// 使用PUT修改密码
|
||||
const params = new URLSearchParams();
|
||||
params.set('uuid', deviceUuid);
|
||||
params.set('newPassword', newPassword);
|
||||
if (currentPassword) {
|
||||
params.set('currentPassword', currentPassword);
|
||||
}
|
||||
if (passwordHint !== undefined) {
|
||||
params.set('passwordHint', passwordHint);
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
});
|
||||
} else {
|
||||
// 使用POST初次设置密码
|
||||
const params = new URLSearchParams();
|
||||
params.set('newPassword', newPassword);
|
||||
if (passwordHint !== undefined) {
|
||||
params.set('passwordHint', passwordHint);
|
||||
}
|
||||
|
||||
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDevicePassword(deviceUuid, password, token = null) {
|
||||
const params = new URLSearchParams({ uuid: deviceUuid });
|
||||
const headers = {};
|
||||
|
||||
// 如果提供了账户token,使用JWT认证
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
} else if (password) {
|
||||
params.set('password', password);
|
||||
}
|
||||
|
||||
return this.fetch(`/devices/${deviceUuid}/password?${params}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async setDevicePasswordHint(deviceUuid, hint, password = null, token = null) {
|
||||
return this.authenticatedFetch(`/devices/${deviceUuid}/password-hint`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ hint, password }),
|
||||
}, token)
|
||||
}
|
||||
|
||||
async getDevicePasswordHint(deviceUuid) {
|
||||
return this.fetch(`/devices/${deviceUuid}/password-hint`)
|
||||
}
|
||||
|
||||
// 设备授权相关 API
|
||||
async bindDeviceCode(deviceCode, token) {
|
||||
@ -292,32 +214,7 @@ class ApiClient {
|
||||
return this.fetch(`/apps/devices/${deviceUuid}/apps`)
|
||||
}
|
||||
|
||||
// 密码提示管理 API
|
||||
async getPasswordHint(deviceUuid) {
|
||||
try {
|
||||
const response = await this.fetch(`/devices/${deviceUuid}`)
|
||||
return { hint: response.device?.passwordHint || '' }
|
||||
} catch (error) {
|
||||
// 如果接口不存在,返回空提示
|
||||
return { hint: '' }
|
||||
}
|
||||
}
|
||||
|
||||
async setPasswordHint(deviceUuid, hint, password) {
|
||||
try {
|
||||
return await this.fetch(`/devices/${deviceUuid}/password-hint?password=${encodeURIComponent(password)}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'x-device-uuid': deviceUuid,
|
||||
},
|
||||
body: JSON.stringify({ passwordHint: hint }),
|
||||
})
|
||||
} catch (error) {
|
||||
// 如果接口不存在,忽略错误
|
||||
console.log('Password hint API not available')
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
// 账户相关 API(Authorization 由 axios 拦截器统一注入)
|
||||
async getOAuthProviders() {
|
||||
@ -391,16 +288,13 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// 设备名称管理 API
|
||||
async setDeviceName(deviceUuid, name, password = null, token = null) {
|
||||
async setDeviceName(deviceUuid, name, token = null) {
|
||||
const headers = {
|
||||
'x-device-uuid': deviceUuid,
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (password) {
|
||||
headers['x-device-password'] = password;
|
||||
}
|
||||
|
||||
return this.fetch(`/devices/${deviceUuid}/name`, {
|
||||
method: 'PUT',
|
||||
@ -409,42 +303,7 @@ class ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
// 修改设备密码 API
|
||||
async updateDevicePassword(deviceUuid, currentPassword, newPassword, passwordHint = null, token = null) {
|
||||
const headers = {
|
||||
'x-device-uuid': deviceUuid,
|
||||
};
|
||||
|
||||
// 如果提供了账户token,使用JWT认证(账户拥有者无需当前密码)
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
} else if (currentPassword) {
|
||||
headers['x-device-password'] = currentPassword;
|
||||
}
|
||||
|
||||
const body = { newPassword, passwordHint };
|
||||
// 只有在非账户拥有者时才需要发送当前密码
|
||||
if (!token && currentPassword) {
|
||||
body.currentPassword = currentPassword;
|
||||
}
|
||||
|
||||
return this.fetch(`/devices/${deviceUuid}/password`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
// 验证设备密码 API
|
||||
async verifyDevicePassword(deviceUuid, password) {
|
||||
return this.fetch(`/devices/${deviceUuid}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-device-uuid': deviceUuid,
|
||||
'x-device-password': password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 设备注册 API
|
||||
async registerDevice(uuid, deviceName, token = null) {
|
||||
@ -454,17 +313,7 @@ class ApiClient {
|
||||
}, token)
|
||||
}
|
||||
|
||||
// 账户拥有者重置设备密码 API
|
||||
async resetDevicePasswordAsOwner(deviceUuid, newPassword, passwordHint = null, token) {
|
||||
return this.fetch(`/devices/${deviceUuid}/password`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'x-device-uuid': deviceUuid,
|
||||
},
|
||||
body: JSON.stringify({ newPassword, passwordHint }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 兼容性方法 - 保持旧的API调用方式
|
||||
async getTokens(deviceUuid, options = {}) {
|
||||
@ -476,11 +325,10 @@ class ApiClient {
|
||||
return this.revokeToken(targetToken, { deviceUuid, usePathParam: true });
|
||||
}
|
||||
|
||||
// 便捷方法:使用设备UUID和密码删除token
|
||||
async revokeTokenByDevice(targetToken, deviceUuid, password = null) {
|
||||
// 便捷方法:使用设备UUID删除token
|
||||
async revokeTokenByDevice(targetToken, deviceUuid) {
|
||||
return this.revokeToken(targetToken, {
|
||||
deviceUuid,
|
||||
password,
|
||||
usePathParam: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CheckCircle2, XCircle, Loader2, Shield, Key, AlertCircle, User, Plus, Check } from 'lucide-vue-next'
|
||||
import AppCard from '@/components/AppCard.vue'
|
||||
import PasswordInput from '@/components/PasswordInput.vue'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
@ -46,12 +45,8 @@ const customDeviceUuid = ref('')
|
||||
const showRegisterDialog = ref(false)
|
||||
const deviceRequired = ref(false)
|
||||
|
||||
// 计算属性获取是否有密码
|
||||
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
|
||||
|
||||
// 表单数据
|
||||
const inputDeviceCode = ref('')
|
||||
const authPassword = ref('')
|
||||
const authNote = ref('')
|
||||
|
||||
// 应用信息
|
||||
@ -153,10 +148,6 @@ const authorizeWithDeviceCode = async () => {
|
||||
note: authNote.value || '设备代码授权',
|
||||
}
|
||||
|
||||
if (hasPassword.value && authPassword.value) {
|
||||
authData.password = authPassword.value
|
||||
}
|
||||
|
||||
const authResult = await apiClient.authorizeApp(appId.value, deviceUuid.value, authData)
|
||||
const token = authResult.token
|
||||
|
||||
@ -182,10 +173,6 @@ const authorizeWithCallback = async () => {
|
||||
note: authNote.value || '回调授权',
|
||||
}
|
||||
|
||||
if (hasPassword.value && authPassword.value) {
|
||||
authData.password = authPassword.value
|
||||
}
|
||||
|
||||
const authResult = await apiClient.authorizeApp(appId.value, deviceUuid.value, authData)
|
||||
const token = authResult.token
|
||||
|
||||
@ -221,7 +208,7 @@ const goHome = () => {
|
||||
const retry = () => {
|
||||
step.value = 'input'
|
||||
errorMessage.value = ''
|
||||
authPassword.value = ''
|
||||
|
||||
}
|
||||
|
||||
// 加载设备信息
|
||||
@ -314,10 +301,7 @@ onMounted(async () => {
|
||||
<code class="text-xs font-mono bg-muted px-3 py-2 rounded flex-1 truncate">
|
||||
{{ deviceUuid }}
|
||||
</code>
|
||||
<Badge v-if="hasPassword" variant="secondary" class="shrink-0">
|
||||
<Shield class="h-3 w-3 mr-1" />
|
||||
已保护
|
||||
</Badge>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 设备绑定状态 -->
|
||||
@ -379,18 +363,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 密码输入(使用统一组件) -->
|
||||
<div v-if="hasPassword">
|
||||
<PasswordInput
|
||||
v-model="authPassword"
|
||||
label="设备密码"
|
||||
placeholder="输入设备密码以确认授权"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
:error="step === 'error' && errorMessage.includes('密码') ? '密码错误' : ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 授权按钮 -->
|
||||
<div class="space-y-3 pt-2">
|
||||
@ -398,7 +371,7 @@ onMounted(async () => {
|
||||
@click="handleSubmit"
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="(isDeviceCodeMode && !currentDeviceCode) || (hasPassword && !authPassword)"
|
||||
:disabled="(isDeviceCodeMode && !currentDeviceCode)"
|
||||
>
|
||||
<Key class="mr-2 h-4 w-4" />
|
||||
确认授权
|
||||
|
||||
@ -27,7 +27,6 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { toast } from 'vue-sonner'
|
||||
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
|
||||
import ResetDevicePasswordDialog from '@/components/ResetDevicePasswordDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const accountStore = useAccountStore()
|
||||
@ -35,7 +34,6 @@ const accountStore = useAccountStore()
|
||||
const devices = ref([])
|
||||
const isLoading = ref(false)
|
||||
const showEditNameDialog = ref(false)
|
||||
const showResetPasswordDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const currentDevice = ref(null)
|
||||
|
||||
@ -84,22 +82,11 @@ const editDeviceName = (device) => {
|
||||
showEditNameDialog.value = true
|
||||
}
|
||||
|
||||
// 重置设备密码
|
||||
const resetPassword = (device) => {
|
||||
currentDevice.value = device
|
||||
showResetPasswordDialog.value = true
|
||||
}
|
||||
|
||||
// 设备名称更新成功
|
||||
const handleDeviceNameUpdated = async () => {
|
||||
await loadDevices()
|
||||
}
|
||||
|
||||
// 密码重置成功
|
||||
const handlePasswordReset = async () => {
|
||||
toast.success('密码重置成功')
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
@ -209,15 +196,7 @@ onMounted(() => {
|
||||
<Edit class="h-3 w-3 mr-1" />
|
||||
重命名
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="resetPassword(device)"
|
||||
class="flex-1"
|
||||
>
|
||||
<Lock class="h-3 w-3 mr-1" />
|
||||
重置密码
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@ -245,14 +224,7 @@ onMounted(() => {
|
||||
@success="handleDeviceNameUpdated"
|
||||
/>
|
||||
|
||||
<!-- 重置密码弹框 -->
|
||||
<ResetDevicePasswordDialog
|
||||
v-if="currentDevice"
|
||||
v-model="showResetPasswordDialog"
|
||||
:device-uuid="currentDevice.uuid"
|
||||
:device-name="currentDevice.name || ''"
|
||||
@success="handlePasswordReset"
|
||||
/>
|
||||
|
||||
|
||||
<!-- 解绑确认对话框 -->
|
||||
<AlertDialog v-model:open="showDeleteDialog">
|
||||
|
||||
@ -10,14 +10,14 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Plus, Trash2, Key, Shield, RefreshCw, Copy, CheckCircle2, Settings, Package, Clock, AlertCircle, Lock, Info, User, LogOut, Layers, ChevronDown, TestTube2 } from 'lucide-vue-next'
|
||||
import { Plus, Trash2, Key, Shield, RefreshCw, Copy, CheckCircle2, Settings, Package, Clock, AlertCircle, Lock, Info, User, LogOut, Layers, ChevronDown, TestTube2, Edit } from 'lucide-vue-next'
|
||||
import DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue'
|
||||
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
|
||||
import AppCard from '@/components/AppCard.vue'
|
||||
import TokenList from '@/components/TokenList.vue'
|
||||
import PasswordInput from '@/components/PasswordInput.vue'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
|
||||
import DeviceSwitcher from '@/components/DeviceSwitcher.vue'
|
||||
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
|
||||
import EditNamespaceDialog from '@/components/EditNamespaceDialog.vue'
|
||||
import FeatureNavigation from '@/components/FeatureNavigation.vue'
|
||||
@ -36,7 +36,7 @@ const appInfoCache = ref({}) // 缓存应用信息
|
||||
const showAuthorizeDialog = ref(false)
|
||||
const showRevokeDialog = ref(false)
|
||||
const showRegisterDialog = ref(false)
|
||||
const showPasswordDialog = ref(false)
|
||||
|
||||
const showLoginDialog = ref(false)
|
||||
const showEditNameDialog = ref(false)
|
||||
const showEditNamespaceDialog = ref(false)
|
||||
@ -47,19 +47,14 @@ const showTokenDialog = ref(false)
|
||||
|
||||
// Form data
|
||||
const appIdToAuthorize = ref('')
|
||||
const authPassword = ref('')
|
||||
|
||||
const authNote = ref('')
|
||||
const devicePassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const currentPassword = ref('')
|
||||
const passwordHint = ref('')
|
||||
const revokePassword = ref('') // 撤销授权时的密码
|
||||
|
||||
|
||||
// 使用OAuth回调处理
|
||||
const { handleOAuthCallback } = useOAuthCallback()
|
||||
|
||||
// 使用计算属性来获取是否有密码
|
||||
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
|
||||
|
||||
|
||||
// 检查 namespace 是否等于 UUID(需要提示用户修改)
|
||||
const namespaceEqualsUuid = computed(() => {
|
||||
@ -90,28 +85,13 @@ const loadDeviceInfo = async () => {
|
||||
try {
|
||||
const info = await apiClient.getDeviceInfo(deviceUuid.value)
|
||||
deviceInfo.value = info
|
||||
// 如果有密码提示,设置它
|
||||
if (info.passwordHint) {
|
||||
passwordHint.value = info.passwordHint
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to load device info:', error)
|
||||
// 设备不存在时,deviceInfo为null,hasPassword会返回false
|
||||
deviceInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 加载密码提示
|
||||
const loadPasswordHint = async () => {
|
||||
try {
|
||||
const data = await apiClient.getPasswordHint(deviceUuid.value)
|
||||
if (data.hint) {
|
||||
passwordHint.value = data.hint
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to load password hint')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const loadTokens = async () => {
|
||||
if (!deviceUuid.value) return
|
||||
@ -157,10 +137,6 @@ const authorizeApp = async () => {
|
||||
note: authNote.value || '授权访问',
|
||||
}
|
||||
|
||||
if (hasPassword.value && authPassword.value) {
|
||||
options.password = authPassword.value
|
||||
}
|
||||
|
||||
// 账户已登录时无需显式传 token,axios 会自动注入 Authorization
|
||||
|
||||
// 调用授权接口
|
||||
@ -172,7 +148,6 @@ const authorizeApp = async () => {
|
||||
|
||||
showAuthorizeDialog.value = false
|
||||
appIdToAuthorize.value = ''
|
||||
authPassword.value = ''
|
||||
authNote.value = ''
|
||||
|
||||
await loadTokens()
|
||||
@ -190,23 +165,15 @@ const confirmRevoke = (token) => {
|
||||
const revokeToken = async () => {
|
||||
if (!selectedToken.value) return
|
||||
|
||||
// 如果没有登录账户且设备有密码,检查是否输入了密码
|
||||
if (!accountStore.isAuthenticated && hasPassword.value && !revokePassword.value) {
|
||||
alert('请输入设备密码')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用安装记录ID撤销授权
|
||||
await apiClient.revokeDeviceToken(
|
||||
deviceUuid.value,
|
||||
selectedToken.value.id,
|
||||
accountStore.isAuthenticated ? null : revokePassword.value,
|
||||
accountStore.isAuthenticated ? accountStore.token : null
|
||||
)
|
||||
showRevokeDialog.value = false
|
||||
selectedToken.value = null
|
||||
revokePassword.value = ''
|
||||
await loadTokens()
|
||||
toast.success('撤销成功')
|
||||
} catch (error) {
|
||||
@ -226,47 +193,27 @@ const copyToClipboard = async (text, id) => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateUuid = () => {
|
||||
showRegisterDialog.value = false
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
const updateUuid = (newUuid = null) => {
|
||||
if (newUuid) {
|
||||
deviceUuid.value = newUuid
|
||||
} else {
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
}
|
||||
|
||||
// 记录到历史
|
||||
if (deviceUuid.value) {
|
||||
deviceStore.addDeviceToHistory({ uuid: deviceUuid.value, name: deviceInfo.value?.name || deviceInfo.value?.deviceName })
|
||||
deviceStore.addDeviceToHistory({
|
||||
uuid: deviceUuid.value,
|
||||
name: deviceInfo.value?.name || deviceInfo.value?.deviceName
|
||||
})
|
||||
}
|
||||
|
||||
loadDeviceInfo()
|
||||
loadDeviceAccount()
|
||||
loadTokens()
|
||||
}
|
||||
|
||||
const setPassword = async () => {
|
||||
if (!newPassword.value) return
|
||||
|
||||
try {
|
||||
const data = {
|
||||
newPassword: newPassword.value,
|
||||
}
|
||||
|
||||
if (hasPassword.value && !accountStore.isAuthenticated) {
|
||||
data.currentPassword = currentPassword.value
|
||||
}
|
||||
|
||||
await apiClient.setDevicePassword(
|
||||
deviceUuid.value,
|
||||
data,
|
||||
accountStore.isAuthenticated ? accountStore.token : null
|
||||
)
|
||||
|
||||
// 重新加载设备信息以更新hasPassword状态
|
||||
await loadDeviceInfo()
|
||||
|
||||
showPasswordDialog.value = false
|
||||
newPassword.value = ''
|
||||
currentPassword.value = ''
|
||||
toast.success('密码设置成功')
|
||||
} catch (error) {
|
||||
toast.error('设置密码失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
@ -369,6 +316,12 @@ const handleDeviceNameUpdated = async (newName) => {
|
||||
await loadDeviceInfo()
|
||||
}
|
||||
|
||||
// 设备注册成功回调
|
||||
const handleDeviceRegistered = () => {
|
||||
showRegisterDialog.value = false
|
||||
updateUuid()
|
||||
}
|
||||
|
||||
// 更新 namespace 成功回调
|
||||
const handleNamespaceUpdated = async (newNamespace) => {
|
||||
if (deviceInfo.value) {
|
||||
@ -393,10 +346,7 @@ onMounted(async () => {
|
||||
// 加载设备账户信息
|
||||
await loadDeviceAccount()
|
||||
|
||||
// 如果有密码但密码提示不存在,单独加载密码提示
|
||||
if (hasPassword.value && !passwordHint.value) {
|
||||
await loadPasswordHint()
|
||||
}
|
||||
|
||||
|
||||
// 加载tokens
|
||||
await loadTokens()
|
||||
@ -410,7 +360,7 @@ onMounted(async () => {
|
||||
<div class="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-10">
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<!--<div class="rounded-lg bg-gradient-to-br from-primary to-primary/80 p-2.5 shadow-lg">
|
||||
<Shield class="h-6 w-6 text-primary-foreground" />
|
||||
</div>-->
|
||||
@ -420,17 +370,16 @@ onMounted(async () => {
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">文档形键值数据库</p>
|
||||
</div>
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-8 w-px bg-border"></div>
|
||||
<!-- Vercel风格设备切换器 -->
|
||||
<DeviceSwitcher
|
||||
:device-info="deviceInfo"
|
||||
:device-uuid="deviceUuid"
|
||||
@device-changed="updateUuid"
|
||||
/>
|
||||
</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">
|
||||
@ -475,10 +424,7 @@ onMounted(async () => {
|
||||
<Layers class="h-4 w-4" />
|
||||
设备管理
|
||||
</DropdownItem>
|
||||
<DropdownItem @click="$router.push('/password-manager')">
|
||||
<Settings class="h-4 w-4" />
|
||||
高级设置
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem @click="$router.push('/auto-auth-management')">
|
||||
<Shield class="h-4 w-4" />
|
||||
自动授权配置
|
||||
@ -534,6 +480,7 @@ onMounted(async () => {
|
||||
您的命名空间当前使用设备 UUID,建议修改为更有意义的名称(如班级名、房间号等),方便自动授权时识别。
|
||||
</p>
|
||||
<Button
|
||||
v-if="accountStore.isAuthenticated"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="showEditNamespaceDialog = true"
|
||||
@ -560,20 +507,21 @@ onMounted(async () => {
|
||||
<CardTitle class="text-lg">
|
||||
{{ deviceInfo?.name || '设备' }}
|
||||
</CardTitle>
|
||||
<Button
|
||||
v-if="accountStore.isAuthenticated"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="showEditNameDialog = true"
|
||||
class="h-6 w-6 p-0"
|
||||
>
|
||||
<Edit class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>设备命名空间标识符</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="px-3 py-1"
|
||||
:class="hasPassword ? 'border-green-500 text-green-700 dark:text-green-400' : 'border-yellow-500 text-yellow-700 dark:text-yellow-400'"
|
||||
>
|
||||
<Lock v-if="hasPassword" class="h-3 w-3 mr-1.5" />
|
||||
<AlertCircle v-else class="h-3 w-3 mr-1.5" />
|
||||
{{ hasPassword ? '已设密码' : '未设密码' }}
|
||||
</Badge>
|
||||
|
||||
|
||||
<!-- 设备账户绑定状态 -->
|
||||
<Badge v-if="deviceInfo?.account" variant="secondary" class="px-3 py-1">
|
||||
@ -589,14 +537,7 @@ onMounted(async () => {
|
||||
<User class="h-4 w-4 mr-2" />
|
||||
绑定到账户
|
||||
</Button>
|
||||
<Button
|
||||
@click="$router.push('/password-manager')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Settings class="h-4 w-4 mr-1" />
|
||||
高级设置
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@click="$router.push('/auto-auth-management')"
|
||||
variant="outline"
|
||||
@ -617,6 +558,7 @@ onMounted(async () => {
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<Label class="text-sm font-medium">命名空间</Label>
|
||||
<Button
|
||||
v-if="accountStore.isAuthenticated"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="showEditNamespaceDialog = true"
|
||||
@ -654,24 +596,10 @@ onMounted(async () => {
|
||||
<div class="text-2xl font-bold text-primary">{{ tokens.length }}</div>
|
||||
<div class="text-xs text-muted-foreground">令牌数</div>
|
||||
</div>
|
||||
<div class="p-3 rounded-lg bg-muted/50 text-center">
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{{ hasPassword ? '安全' : '未设置' }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">安全状态</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Password Hint (if exists) -->
|
||||
<div v-if="hasPassword && passwordHint" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<div class="flex items-start gap-2">
|
||||
<Info class="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">密码提示</p>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">{{ passwordHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -764,20 +692,9 @@ onMounted(async () => {
|
||||
placeholder="为此授权添加备注"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasPassword">
|
||||
<PasswordInput
|
||||
v-model="authPassword"
|
||||
label="设备密码"
|
||||
placeholder="输入设备密码"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
:required="!accountStore.isAuthenticated"
|
||||
/><br/>
|
||||
<p v-if="accountStore.isAuthenticated" class="text-xs text-muted-foreground mt-2">
|
||||
已登录绑定账户,无需输入密码
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="accountStore.isAuthenticated" class="text-xs text-muted-foreground mt-2">
|
||||
已登录绑定账户,无需输入密码
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showAuthorizeDialog = false">
|
||||
@ -811,32 +728,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 如果没有登录账户且设备有密码,显示密码输入框 -->
|
||||
<div v-if="!accountStore.isAuthenticated && hasPassword" class="space-y-2">
|
||||
<Label for="revoke-password">设备密码</Label>
|
||||
<PasswordInput
|
||||
id="revoke-password"
|
||||
v-model="revokePassword"
|
||||
placeholder="请输入设备密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 显示当前认证状态 -->
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<div v-if="accountStore.isAuthenticated" class="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle2 class="h-4 w-4" />
|
||||
已登录账户,无需输入密码
|
||||
</div>
|
||||
<div v-else-if="!hasPassword" class="flex items-center gap-2 text-blue-600">
|
||||
<Info class="h-4 w-4" />
|
||||
设备未设置密码
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-orange-600">
|
||||
<Lock class="h-4 w-4" />
|
||||
需要验证设备密码
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showRevokeDialog = false">
|
||||
@ -897,57 +789,7 @@ onMounted(async () => {
|
||||
</Dialog>
|
||||
|
||||
|
||||
<Dialog v-model:open="showPasswordDialog">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ hasPassword ? '修改密码' : '设置密码' }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ hasPassword ? '输入当前密码和新密码' : '为设备设置密码以增强安全性' }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div v-if="hasPassword && !accountStore.isAuthenticated">
|
||||
<PasswordInput
|
||||
v-model="currentPassword"
|
||||
label="当前密码"
|
||||
placeholder="输入当前密码"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div v-if="accountStore.isAuthenticated && hasPassword" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<div class="flex items-start gap-2">
|
||||
<Info class="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-blue-900 dark:text-blue-100">账户已登录</p>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">您已登录绑定的账户,无需输入当前密码</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="newPassword"
|
||||
label="新密码"
|
||||
placeholder="输入新密码"
|
||||
:show-hint="false"
|
||||
:show-strength="true"
|
||||
:min-length="8"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showPasswordDialog = false">
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="setPassword">
|
||||
确认
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 登录弹框 -->
|
||||
@ -962,7 +804,7 @@ onMounted(async () => {
|
||||
/> <!-- 设备注册弹框 -->
|
||||
<DeviceRegisterDialog
|
||||
v-model="showRegisterDialog"
|
||||
@confirm="updateUuid"
|
||||
@confirm="handleDeviceRegistered"
|
||||
:required="deviceRequired"
|
||||
/>
|
||||
|
||||
@ -971,7 +813,6 @@ onMounted(async () => {
|
||||
v-model="showEditNameDialog"
|
||||
:device-uuid="deviceUuid"
|
||||
:current-name="deviceInfo?.deviceName || ''"
|
||||
:has-password="hasPassword"
|
||||
@success="handleDeviceNameUpdated"
|
||||
/>
|
||||
|
||||
|
||||
@ -1,275 +1,41 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiClient } from '@/lib/api'
|
||||
import { deviceStore } from '@/lib/deviceStore'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import PasswordInput from '@/components/PasswordInput.vue'
|
||||
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
|
||||
import {
|
||||
Shield,
|
||||
Key,
|
||||
Trash2,
|
||||
Edit,
|
||||
Info,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ArrowLeft,
|
||||
Lock,
|
||||
Unlock,
|
||||
Eye,
|
||||
EyeOff,
|
||||
HelpCircle,
|
||||
RefreshCw,
|
||||
Smartphone,
|
||||
Copy
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
Shield,
|
||||
Key
|
||||
} from 'lucide-vue-next'
|
||||
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
|
||||
import EditDeviceNameDialog from '@/components/EditDeviceNameDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const deviceUuid = ref('')
|
||||
const deviceInfo = ref(null)
|
||||
|
||||
// 计算属性获取是否有密码
|
||||
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
|
||||
const passwordHint = ref('')
|
||||
|
||||
// Dialog states
|
||||
const showChangePasswordDialog = ref(false)
|
||||
const showDeletePasswordDialog = ref(false)
|
||||
const showHintDialog = ref(false)
|
||||
const showResetDeviceDialog = ref(false)
|
||||
const showRegisterDialog = ref(false)
|
||||
const showEditNameDialog = ref(false)
|
||||
const deviceRequired = ref(false)
|
||||
|
||||
// Form data
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const deleteConfirmPassword = ref('')
|
||||
const newHint = ref('')
|
||||
const hintPassword = ref('')
|
||||
|
||||
// UI states
|
||||
const isLoading = ref(false)
|
||||
const successMessage = ref('')
|
||||
const errorMessage = ref('')
|
||||
const copied = ref(null)
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (text, type) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = type
|
||||
setTimeout(() => {
|
||||
copied.value = null
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设备信息
|
||||
const loadDeviceInfo = async () => {
|
||||
try {
|
||||
const info = await apiClient.getDeviceInfo(deviceUuid.value)
|
||||
deviceInfo.value = info
|
||||
if (info.passwordHint) {
|
||||
passwordHint.value = info.passwordHint
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to load device info:', error)
|
||||
deviceInfo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 加载密码提示
|
||||
const loadPasswordHint = async () => {
|
||||
try {
|
||||
const data = await apiClient.getPasswordHint(deviceUuid.value)
|
||||
if (data.hint) {
|
||||
passwordHint.value = data.hint
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to load password hint')
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const changePassword = async () => {
|
||||
if (!newPassword.value || !currentPassword.value) {
|
||||
errorMessage.value = '请填写所有必填字段'
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
errorMessage.value = '新密码与确认密码不一致'
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value === currentPassword.value) {
|
||||
errorMessage.value = '新密码不能与当前密码相同'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await apiClient.setDevicePassword(deviceUuid.value, {
|
||||
currentPassword: currentPassword.value,
|
||||
newPassword: newPassword.value
|
||||
})
|
||||
|
||||
successMessage.value = '密码修改成功!'
|
||||
showChangePasswordDialog.value = false
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '密码修改失败'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除密码
|
||||
const deletePassword = async () => {
|
||||
if (!deleteConfirmPassword.value) {
|
||||
errorMessage.value = '请输入当前密码'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await apiClient.deleteDevicePassword(deviceUuid.value, deleteConfirmPassword.value)
|
||||
|
||||
// 重新加载设备信息
|
||||
await loadDeviceInfo()
|
||||
|
||||
successMessage.value = '密码已删除!'
|
||||
showDeletePasswordDialog.value = false
|
||||
deleteConfirmPassword.value = ''
|
||||
passwordHint.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '密码删除失败'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置密码提示
|
||||
const setPasswordHint = async () => {
|
||||
if (!hintPassword.value) {
|
||||
errorMessage.value = '请输入当前密码'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const result = await apiClient.setPasswordHint(deviceUuid.value, newHint.value, hintPassword.value)
|
||||
|
||||
passwordHint.value = newHint.value
|
||||
successMessage.value = '密码提示已更新!'
|
||||
showHintDialog.value = false
|
||||
newHint.value = ''
|
||||
hintPassword.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '设置密码提示失败'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置新密码(无密码时)
|
||||
const setNewPassword = async () => {
|
||||
if (!newPassword.value) {
|
||||
errorMessage.value = '请输入新密码'
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
errorMessage.value = '密码与确认密码不一致'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await apiClient.setDevicePassword(deviceUuid.value, {
|
||||
newPassword: newPassword.value
|
||||
})
|
||||
|
||||
// 重新加载设备信息
|
||||
await loadDeviceInfo()
|
||||
|
||||
successMessage.value = '密码设置成功!'
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '密码设置失败'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回首页
|
||||
const goBack = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 设备重置成功后的回调
|
||||
const handleDeviceReset = () => {
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
loadDeviceInfo()
|
||||
successMessage.value = '设备已重置!'
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 更新设备UUID回调
|
||||
const updateUuid = () => {
|
||||
showRegisterDialog.value = false
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
loadDeviceInfo()
|
||||
}
|
||||
|
||||
// 更新设备名称成功回调
|
||||
const handleDeviceNameUpdated = async () => {
|
||||
await loadDeviceInfo()
|
||||
successMessage.value = '设备名称已更新!'
|
||||
setTimeout(() => {
|
||||
successMessage.value = ''
|
||||
}, 3000)
|
||||
// 设备名称更新成功回调
|
||||
const handleDeviceNameUpdated = () => {
|
||||
// 刷新页面或显示成功消息
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -277,20 +43,13 @@ onMounted(async () => {
|
||||
const existingUuid = deviceStore.getDeviceUuid()
|
||||
if (!existingUuid) {
|
||||
deviceRequired.value = true
|
||||
// 如果没有设备UUID,显示设备管理弹框
|
||||
showRegisterDialog.value = true
|
||||
} else {
|
||||
deviceUuid.value = existingUuid
|
||||
|
||||
// 加载设备信息
|
||||
await loadDeviceInfo()
|
||||
|
||||
// 如果有密码但密码提示不存在,单独加载密码提示
|
||||
if (hasPassword.value && !passwordHint.value) {
|
||||
await loadPasswordHint()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -406,83 +165,7 @@ onMounted(async () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Password Status Card -->
|
||||
<Card class="mb-6 border-2">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-lg bg-primary/10 p-2">
|
||||
<Shield class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>密码状态</CardTitle>
|
||||
<CardDescription>当前设备的密码保护状态</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge :variant="hasPassword ? 'default' : 'secondary'" class="text-sm">
|
||||
<Lock v-if="hasPassword" class="h-3 w-3 mr-1" />
|
||||
<Unlock v-else class="h-3 w-3 mr-1" />
|
||||
{{ hasPassword ? '已设置密码' : '未设置密码' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<!-- Password Hint -->
|
||||
<div v-if="hasPassword && passwordHint" class="p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<div class="flex items-start gap-2">
|
||||
<HelpCircle class="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">密码提示</p>
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">{{ passwordHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-3 pt-2">
|
||||
<Button
|
||||
v-if="!hasPassword"
|
||||
@click="showChangePasswordDialog = true"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
<Key class="h-4 w-4 mr-2" />
|
||||
设置密码
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="hasPassword"
|
||||
@click="showChangePasswordDialog = true"
|
||||
variant="outline"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
<Edit class="h-4 w-4 mr-2" />
|
||||
修改密码
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="hasPassword"
|
||||
@click="showHintDialog = true"
|
||||
variant="outline"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
<Info class="h-4 w-4 mr-2" />
|
||||
{{ passwordHint ? '修改提示' : '设置提示' }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="hasPassword"
|
||||
@click="showDeletePasswordDialog = true"
|
||||
variant="destructive"
|
||||
class="flex-1 sm:flex-none"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 mr-2" />
|
||||
删除密码
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 设备管理部分 -->
|
||||
<h2 class="text-xl font-semibold mt-12 mb-4">设备管理</h2>
|
||||
@ -515,174 +198,11 @@ onMounted(async () => {
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Change/Set Password Dialog -->
|
||||
<Dialog v-model:open="showChangePasswordDialog">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ hasPassword ? '修改密码' : '设置密码' }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ hasPassword ? '请输入当前密码和新密码' : '为您的设备设置一个安全的密码' }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<!-- Current Password (only when changing) -->
|
||||
<div v-if="hasPassword">
|
||||
<PasswordInput
|
||||
v-model="currentPassword"
|
||||
label="当前密码"
|
||||
placeholder="输入当前密码"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- New Password -->
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="newPassword"
|
||||
label="新密码"
|
||||
placeholder="输入新密码"
|
||||
:show-hint="false"
|
||||
:show-strength="true"
|
||||
:min-length="8"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="confirmPassword"
|
||||
label="确认新密码"
|
||||
placeholder="再次输入新密码"
|
||||
:show-hint="false"
|
||||
:show-strength="false"
|
||||
:confirm-password="newPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showChangePasswordDialog = false" :disabled="isLoading">
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
@click="hasPassword ? changePassword() : setNewPassword()"
|
||||
:disabled="isLoading || !newPassword || newPassword !== confirmPassword || (hasPassword && !currentPassword)"
|
||||
>
|
||||
{{ isLoading ? '处理中...' : (hasPassword ? '修改密码' : '设置密码') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Password Dialog -->
|
||||
<Dialog v-model:open="showDeletePasswordDialog">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>删除密码</DialogTitle>
|
||||
<DialogDescription>
|
||||
删除密码后,您的设备将不再受密码保护。此操作无法撤销。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="p-4 rounded-lg bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-900">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400 mt-0.5" />
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<p class="font-medium mb-1">警告</p>
|
||||
<p>删除密码后,任何拥有您设备 UUID 的人都可以管理您的授权应用。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="deleteConfirmPassword"
|
||||
label="输入当前密码以确认"
|
||||
placeholder="输入当前密码"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeletePasswordDialog = false" :disabled="isLoading">
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@click="deletePassword"
|
||||
:disabled="isLoading || !deleteConfirmPassword"
|
||||
>
|
||||
{{ isLoading ? '删除中...' : '确认删除' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Set/Update Hint Dialog -->
|
||||
<Dialog v-model:open="showHintDialog">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ passwordHint ? '修改密码提示' : '设置密码提示' }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
密码提示可以帮助您在忘记密码时回忆起密码
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
<div v-if="passwordHint" class="p-3 rounded-lg bg-muted">
|
||||
<Label class="text-xs text-muted-foreground">当前提示</Label>
|
||||
<p class="text-sm mt-1">{{ passwordHint }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="new-hint">新的密码提示</Label>
|
||||
<Input
|
||||
id="new-hint"
|
||||
v-model="newHint"
|
||||
placeholder="例如:我的宠物名字加生日"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
提示不应包含密码本身,而是能帮助您回忆密码的信息
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<PasswordInput
|
||||
v-model="hintPassword"
|
||||
label="当前密码"
|
||||
placeholder="输入当前密码以确认"
|
||||
:device-uuid="deviceUuid"
|
||||
:show-hint="true"
|
||||
:show-strength="false"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showHintDialog = false" :disabled="isLoading">
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
@click="setPasswordHint"
|
||||
:disabled="isLoading || !hintPassword"
|
||||
>
|
||||
{{ isLoading ? '保存中...' : '保存提示' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- 设备重置弹框 -->
|
||||
<DeviceRegisterDialog
|
||||
@ -703,7 +223,6 @@ onMounted(async () => {
|
||||
v-model="showEditNameDialog"
|
||||
:device-uuid="deviceUuid"
|
||||
:current-name="deviceInfo?.deviceName || ''"
|
||||
:has-password="hasPassword"
|
||||
@success="handleDeviceNameUpdated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user