mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-10-22 12:03: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 deviceName = ref('')
|
||||||
const bindToAccount = ref(false)
|
const bindToAccount = ref(false)
|
||||||
const accountDevices = ref([])
|
const accountDevices = ref([])
|
||||||
|
const historyDevices = ref([])
|
||||||
|
const manualUuid = ref('')
|
||||||
const loadingDevices = ref(false)
|
const loadingDevices = ref(false)
|
||||||
const activeTab = ref('load') // 'load' 或 'register'
|
const activeTab = ref('load') // 'load' | 'history' | 'register'
|
||||||
const showLoginDialog = ref(false) // 登录对话框状态
|
const showLoginDialog = ref(false) // 登录对话框状态
|
||||||
|
|
||||||
const isOpen = computed({
|
const isOpen = computed({
|
||||||
@ -47,6 +49,9 @@ watch(isOpen, (newVal) => {
|
|||||||
if (newVal && accountStore.isAuthenticated && activeTab.value === 'load') {
|
if (newVal && accountStore.isAuthenticated && activeTab.value === 'load') {
|
||||||
loadAccountDevices()
|
loadAccountDevices()
|
||||||
}
|
}
|
||||||
|
if (newVal && activeTab.value === 'history') {
|
||||||
|
loadHistoryDevices()
|
||||||
|
}
|
||||||
// 切换到注册选项卡时,自动生成UUID
|
// 切换到注册选项卡时,自动生成UUID
|
||||||
if (newVal && activeTab.value === 'register' && !newUuid.value) {
|
if (newVal && activeTab.value === 'register' && !newUuid.value) {
|
||||||
generateRandomUuid()
|
generateRandomUuid()
|
||||||
@ -58,6 +63,9 @@ watch(activeTab, (newVal) => {
|
|||||||
if (newVal === 'load' && accountStore.isAuthenticated && isOpen.value) {
|
if (newVal === 'load' && accountStore.isAuthenticated && isOpen.value) {
|
||||||
loadAccountDevices()
|
loadAccountDevices()
|
||||||
}
|
}
|
||||||
|
if (newVal === 'history' && isOpen.value) {
|
||||||
|
loadHistoryDevices()
|
||||||
|
}
|
||||||
if (newVal === 'register' && !newUuid.value) {
|
if (newVal === 'register' && !newUuid.value) {
|
||||||
generateRandomUuid()
|
generateRandomUuid()
|
||||||
}
|
}
|
||||||
@ -121,12 +129,35 @@ const loadAccountDevices = async () => {
|
|||||||
// 加载选中的设备
|
// 加载选中的设备
|
||||||
const loadDevice = (device) => {
|
const loadDevice = (device) => {
|
||||||
deviceStore.setDeviceUuid(device.uuid)
|
deviceStore.setDeviceUuid(device.uuid)
|
||||||
|
// 写入历史
|
||||||
|
deviceStore.addDeviceToHistory({ uuid: device.uuid, name: device.name })
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
emit('confirm')
|
emit('confirm')
|
||||||
resetForm()
|
resetForm()
|
||||||
toast.success(`已切换到设备: ${device.name || device.uuid}`)
|
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 () => {
|
const registerDevice = async () => {
|
||||||
if (!newUuid.value.trim()) {
|
if (!newUuid.value.trim()) {
|
||||||
@ -142,6 +173,8 @@ const registerDevice = async () => {
|
|||||||
try {
|
try {
|
||||||
// 1. 保存UUID到本地
|
// 1. 保存UUID到本地
|
||||||
deviceStore.setDeviceUuid(newUuid.value.trim())
|
deviceStore.setDeviceUuid(newUuid.value.trim())
|
||||||
|
// 写入历史
|
||||||
|
deviceStore.addDeviceToHistory({ uuid: newUuid.value.trim(), name: deviceName.value.trim() })
|
||||||
|
|
||||||
// 2. 调用设备注册接口(会自动在云端创建设备)
|
// 2. 调用设备注册接口(会自动在云端创建设备)
|
||||||
await apiClient.registerDevice(
|
await apiClient.registerDevice(
|
||||||
@ -181,6 +214,7 @@ const resetForm = () => {
|
|||||||
bindToAccount.value = accountStore.isAuthenticated
|
bindToAccount.value = accountStore.isAuthenticated
|
||||||
accountDevices.value = []
|
accountDevices.value = []
|
||||||
activeTab.value = 'load'
|
activeTab.value = 'load'
|
||||||
|
manualUuid.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理弹框关闭
|
// 处理弹框关闭
|
||||||
@ -211,6 +245,11 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('keydown', handleKeydown, true)
|
document.removeEventListener('keydown', handleKeydown, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 加载本地历史设备
|
||||||
|
const loadHistoryDevices = () => {
|
||||||
|
historyDevices.value = deviceStore.getDeviceHistory()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -239,11 +278,14 @@ onUnmounted(() => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs v-model="activeTab" class="w-full">
|
<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">
|
<TabsTrigger value="load">
|
||||||
<Download class="h-4 w-4 mr-2" />
|
<Download class="h-4 w-4 mr-2" />
|
||||||
加载设备
|
加载设备
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="history">
|
||||||
|
历史记录
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="register">
|
<TabsTrigger value="register">
|
||||||
<Plus class="h-4 w-4 mr-2" />
|
<Plus class="h-4 w-4 mr-2" />
|
||||||
注册设备
|
注册设备
|
||||||
@ -252,54 +294,79 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 加载设备选项卡 -->
|
<!-- 加载设备选项卡 -->
|
||||||
<TabsContent value="load" class="space-y-4 mt-4">
|
<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">
|
||||||
<Button variant="outline" @click="handleOpenLogin">
|
<div v-if="!accountStore.isAuthenticated" class="text-center py-6">
|
||||||
登录账户
|
<p class="text-muted-foreground mb-3">登录后可查看您账户绑定的设备</p>
|
||||||
</Button>
|
<Button variant="outline" @click="handleOpenLogin">
|
||||||
</div>
|
登录账户
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="loadingDevices" class="text-center py-8">
|
<div v-else>
|
||||||
<p class="text-muted-foreground">加载中...</p>
|
<div v-if="loadingDevices" class="text-center py-6">
|
||||||
</div>
|
<p class="text-muted-foreground">加载中...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="accountDevices.length === 0" class="text-center py-8">
|
<div v-else-if="accountDevices.length === 0" class="text-center py-6">
|
||||||
<p class="text-muted-foreground mb-4">您的账户暂未绑定任何设备</p>
|
<p class="text-muted-foreground mb-3">您的账户暂未绑定任何设备</p>
|
||||||
<Button variant="outline" @click="activeTab = 'register'">
|
<Button variant="outline" @click="activeTab = 'register'">
|
||||||
<Plus class="h-4 w-4 mr-2" />
|
<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>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 注册设备选项卡 -->
|
<!-- 注册设备选项卡 -->
|
||||||
@ -328,7 +395,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 设备名称输入 -->
|
<!-- 设备名称输入 -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="deviceName">设备名称</Label>
|
<Label for="deviceName">* 设备名称</Label>
|
||||||
<Input
|
<Input
|
||||||
id="deviceName"
|
id="deviceName"
|
||||||
v-model="deviceName"
|
v-model="deviceName"
|
||||||
@ -361,16 +428,6 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
@ -388,6 +445,42 @@ onUnmounted(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -13,6 +13,7 @@ export const deviceStore = {
|
|||||||
STORAGE_KEY: 'device_uuid',
|
STORAGE_KEY: 'device_uuid',
|
||||||
BACKUP_KEY: 'device_uuid_backup',
|
BACKUP_KEY: 'device_uuid_backup',
|
||||||
SESSION_KEY: 'device_uuid_session',
|
SESSION_KEY: 'device_uuid_session',
|
||||||
|
HISTORY_KEY: 'device_history', // 本地历史设备记录
|
||||||
|
|
||||||
// 获取当前设备 UUID(从多个存储位置尝试读取)
|
// 获取当前设备 UUID(从多个存储位置尝试读取)
|
||||||
getDeviceUuid() {
|
getDeviceUuid() {
|
||||||
@ -189,3 +190,64 @@ export const deviceStore = {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
deviceStore.tryRestoreFromIndexedDB()
|
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 = () => {
|
const updateUuid = () => {
|
||||||
showRegisterDialog.value = false
|
showRegisterDialog.value = false
|
||||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||||
|
// 记录到历史
|
||||||
|
if (deviceUuid.value) {
|
||||||
|
deviceStore.addDeviceToHistory({ uuid: deviceUuid.value, name: deviceInfo.value?.name || deviceInfo.value?.deviceName })
|
||||||
|
}
|
||||||
loadDeviceInfo()
|
loadDeviceInfo()
|
||||||
loadDeviceAccount()
|
loadDeviceAccount()
|
||||||
loadTokens()
|
loadTokens()
|
||||||
@ -400,6 +404,15 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 切换设备按钮 -->
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="showRegisterDialog = true"
|
||||||
|
title="切换设备"
|
||||||
|
>
|
||||||
|
切换设备
|
||||||
|
</Button>
|
||||||
<!-- 账户状态 -->
|
<!-- 账户状态 -->
|
||||||
<template v-if="accountStore.isAuthenticated">
|
<template v-if="accountStore.isAuthenticated">
|
||||||
<DropdownMenu v-model:open="showUserMenu" class="z-50">
|
<DropdownMenu v-model:open="showUserMenu" class="z-50">
|
||||||
|
@ -398,22 +398,11 @@ onMounted(async () => {
|
|||||||
重置设备
|
重置设备
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
重置或换新设备标识。此操作无法撤销,您将失去当前设备的所有授权。
|
更换新的设备标识。
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="space-y-4">
|
<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
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user