mirror of
https://github.com/ZeroCatDev/ClassworksKVAdmin.git
synced 2025-12-08 19:03:40 +00:00
规范代码格式
This commit is contained in:
parent
008d93e76c
commit
5fd99c2121
@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {RouterView} from 'vue-router'
|
import {RouterView} from 'vue-router'
|
||||||
import {Toaster} from '@/components/ui/sonner'
|
import {Toaster} from '@/components/ui/sonner'
|
||||||
import 'vue-sonner/style.css'
|
import 'vue-sonner/style.css'
|
||||||
@ -6,7 +6,7 @@ import 'vue-sonner/style.css'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView v-slot="{ Component, route }">
|
<RouterView v-slot="{ Component, route }">
|
||||||
<Transition name="page" mode="out-in">
|
<Transition mode="out-in" name="page">
|
||||||
<component :is="Component" :key="route.fullPath"/>
|
<component :is="Component" :key="route.fullPath"/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="256" height="256"
|
||||||
|
viewBox="0 0 256 256" fill="none">
|
||||||
<g clip-path="url(#clip-path-74_1)">
|
<g clip-path="url(#clip-path-74_1)">
|
||||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||||
</path>
|
</path>
|
||||||
@ -11,7 +12,8 @@
|
|||||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A">
|
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A">
|
||||||
</path>
|
</path>
|
||||||
<g>
|
<g>
|
||||||
<path fill="#000000" d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
<path fill="#000000"
|
||||||
|
d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||||
</path>
|
</path>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
@ -196,8 +196,8 @@ fetchApp();
|
|||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<img
|
<img
|
||||||
v-if="iconUrl"
|
v-if="iconUrl"
|
||||||
:src="iconUrl"
|
|
||||||
:alt="app.name"
|
:alt="app.name"
|
||||||
|
:src="iconUrl"
|
||||||
class="w-12 h-12 rounded-lg object-cover shrink-0"
|
class="w-12 h-12 rounded-lg object-cover shrink-0"
|
||||||
@error="(e) => (e.target.style.display = 'none')"
|
@error="(e) => (e.target.style.display = 'none')"
|
||||||
/>
|
/>
|
||||||
@ -222,8 +222,8 @@ fetchApp();
|
|||||||
<div class="flex items-start gap-4 mb-4">
|
<div class="flex items-start gap-4 mb-4">
|
||||||
<img
|
<img
|
||||||
v-if="iconUrl"
|
v-if="iconUrl"
|
||||||
:src="iconUrl"
|
|
||||||
:alt="app.name"
|
:alt="app.name"
|
||||||
|
:src="iconUrl"
|
||||||
class="w-20 h-20 rounded-lg object-cover"
|
class="w-20 h-20 rounded-lg object-cover"
|
||||||
@error="(e) => (e.target.style.display = 'none')"
|
@error="(e) => (e.target.style.display = 'none')"
|
||||||
/>
|
/>
|
||||||
@ -245,8 +245,8 @@ fetchApp();
|
|||||||
<div class="text-sm text-muted-foreground">应用主页</div>
|
<div class="text-sm text-muted-foreground">应用主页</div>
|
||||||
<a
|
<a
|
||||||
:href="app.homepage_url"
|
:href="app.homepage_url"
|
||||||
target="_blank"
|
|
||||||
class="text-primary hover:underline inline-flex items-center gap-1"
|
class="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
访问
|
访问
|
||||||
<ExternalLink class="h-3 w-3"/>
|
<ExternalLink class="h-3 w-3"/>
|
||||||
@ -256,8 +256,8 @@ fetchApp();
|
|||||||
<div class="text-sm text-muted-foreground">服务条款</div>
|
<div class="text-sm text-muted-foreground">服务条款</div>
|
||||||
<a
|
<a
|
||||||
:href="app.terms_url"
|
:href="app.terms_url"
|
||||||
target="_blank"
|
|
||||||
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
<ExternalLink class="h-3 w-3"/>
|
<ExternalLink class="h-3 w-3"/>
|
||||||
@ -267,8 +267,8 @@ fetchApp();
|
|||||||
<div class="text-sm text-muted-foreground">隐私政策</div>
|
<div class="text-sm text-muted-foreground">隐私政策</div>
|
||||||
<a
|
<a
|
||||||
:href="app.privacy_url"
|
:href="app.privacy_url"
|
||||||
target="_blank"
|
|
||||||
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
<ExternalLink class="h-3 w-3"/>
|
<ExternalLink class="h-3 w-3"/>
|
||||||
|
|||||||
@ -160,10 +160,10 @@ const saveConfig = async () => {
|
|||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="text"
|
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
:placeholder="isEditMode ? '留空表示无密码授权' : '留空表示无密码授权'"
|
:placeholder="isEditMode ? '留空表示无密码授权' : '留空表示无密码授权'"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ isEditMode ? '留空表示设为无密码' : '设备使用此密码可以自动获取访问授权' }}
|
{{ isEditMode ? '留空表示设为无密码' : '设备使用此密码可以自动获取访问授权' }}
|
||||||
@ -199,8 +199,8 @@ const saveConfig = async () => {
|
|||||||
v-model="formData.isReadOnly"
|
v-model="formData.isReadOnly"
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
for="isReadOnly"
|
|
||||||
class="text-sm font-normal cursor-pointer"
|
class="text-sm font-normal cursor-pointer"
|
||||||
|
for="isReadOnly"
|
||||||
>
|
>
|
||||||
只读权限(仅允许读取数据,不能修改)
|
只读权限(仅允许读取数据,不能修改)
|
||||||
</Label>
|
</Label>
|
||||||
@ -219,17 +219,17 @@ const saveConfig = async () => {
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="closeDialog"
|
@click="closeDialog"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
@click="saveConfig"
|
@click="saveConfig"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
{{ isEditMode ? '保存' : '创建' }}
|
{{ isEditMode ? '保存' : '创建' }}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ const handleAuth = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="modelValue" @update:open="(val) => closable && emit('update:modelValue', val)">
|
<Dialog :open="modelValue" @update:open="(val) => closable && emit('update:modelValue', val)">
|
||||||
<DialogContent class="sm:max-w-[500px]" :closable="closable">
|
<DialogContent :closable="closable" class="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ title }}</DialogTitle>
|
<DialogTitle>{{ title }}</DialogTitle>
|
||||||
<DialogDescription>{{ description }}</DialogDescription>
|
<DialogDescription>{{ description }}</DialogDescription>
|
||||||
@ -109,17 +109,17 @@ const handleAuth = async () => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
v-if="closable"
|
v-if="closable"
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="closeDialog"
|
@click="closeDialog"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
@click="handleAuth"
|
@click="handleAuth"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
确认
|
确认
|
||||||
|
|||||||
@ -271,7 +271,8 @@ const loadHistoryDevices = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<!-- 必需模式的提示 -->
|
<!-- 必需模式的提示 -->
|
||||||
<div v-if="props.required" class="mt-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900">
|
<div v-if="props.required"
|
||||||
|
class="mt-4 p-3 rounded-lg bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<AlertTriangle class="h-5 w-5 text-amber-600 dark:text-amber-400 mt-0.5"/>
|
<AlertTriangle class="h-5 w-5 text-amber-600 dark:text-amber-400 mt-0.5"/>
|
||||||
<div>
|
<div>
|
||||||
@ -300,7 +301,7 @@ const loadHistoryDevices = () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- 加载设备选项卡 -->
|
<!-- 加载设备选项卡 -->
|
||||||
<TabsContent value="load" class="space-y-4 mt-4">
|
<TabsContent class="space-y-4 mt-4" value="load">
|
||||||
<!-- 账户设备区域 -->
|
<!-- 账户设备区域 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div v-if="!accountStore.isAuthenticated" class="text-center py-6">
|
<div v-if="!accountStore.isAuthenticated" class="text-center py-6">
|
||||||
@ -343,8 +344,8 @@ const loadHistoryDevices = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
@click.stop="loadDevice(device)"
|
@click.stop="loadDevice(device)"
|
||||||
>
|
>
|
||||||
加载
|
加载
|
||||||
@ -364,11 +365,11 @@ const loadHistoryDevices = () => {
|
|||||||
<Input
|
<Input
|
||||||
id="manualUuid"
|
id="manualUuid"
|
||||||
v-model="manualUuid"
|
v-model="manualUuid"
|
||||||
placeholder="输入设备 UUID 直接加载"
|
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
placeholder="输入设备 UUID 直接加载"
|
||||||
@keyup.enter="loadByUuid"
|
@keyup.enter="loadByUuid"
|
||||||
/>
|
/>
|
||||||
<Button @click="loadByUuid" :disabled="!manualUuid.trim()">
|
<Button :disabled="!manualUuid.trim()" @click="loadByUuid">
|
||||||
加载
|
加载
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -377,7 +378,7 @@ const loadHistoryDevices = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 注册设备选项卡 -->
|
<!-- 注册设备选项卡 -->
|
||||||
<TabsContent value="register" class="space-y-4 mt-4">
|
<TabsContent class="space-y-4 mt-4" value="register">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- UUID输入 -->
|
<!-- UUID输入 -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@ -386,14 +387,14 @@ const loadHistoryDevices = () => {
|
|||||||
<Input
|
<Input
|
||||||
id="registerUuid"
|
id="registerUuid"
|
||||||
v-model="newUuid"
|
v-model="newUuid"
|
||||||
placeholder="自动生成或手动输入UUID"
|
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
placeholder="自动生成或手动输入UUID"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="generateRandomUuid"
|
|
||||||
title="生成随机UUID"
|
title="生成随机UUID"
|
||||||
|
variant="outline"
|
||||||
|
@click="generateRandomUuid"
|
||||||
>
|
>
|
||||||
<Shuffle class="h-4 w-4"/>
|
<Shuffle class="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -422,8 +423,8 @@ const loadHistoryDevices = () => {
|
|||||||
/>
|
/>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label
|
<label
|
||||||
for="bindToAccount"
|
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||||
|
for="bindToAccount"
|
||||||
>
|
>
|
||||||
绑定到账户
|
绑定到账户
|
||||||
</label>
|
</label>
|
||||||
@ -439,14 +440,14 @@ const loadHistoryDevices = () => {
|
|||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
@click="handleClose"
|
|
||||||
:disabled="props.required"
|
:disabled="props.required"
|
||||||
:title="props.required ? '必须先注册设备' : '取消'"
|
:title="props.required ? '必须先注册设备' : '取消'"
|
||||||
|
variant="outline"
|
||||||
|
@click="handleClose"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="registerDevice" :disabled="!newUuid.trim() || !deviceName.trim()">
|
<Button :disabled="!newUuid.trim() || !deviceName.trim()" @click="registerDevice">
|
||||||
<Plus class="h-4 w-4 mr-2"/>
|
<Plus class="h-4 w-4 mr-2"/>
|
||||||
注册设备
|
注册设备
|
||||||
</Button>
|
</Button>
|
||||||
@ -454,7 +455,7 @@ const loadHistoryDevices = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- 历史设备选项卡 -->
|
<!-- 历史设备选项卡 -->
|
||||||
<TabsContent value="history" class="space-y-4 mt-4">
|
<TabsContent class="space-y-4 mt-4" value="history">
|
||||||
<div v-if="historyDevices.length === 0" class="text-center py-8 text-muted-foreground">
|
<div v-if="historyDevices.length === 0" class="text-center py-8 text-muted-foreground">
|
||||||
暂无历史设备
|
暂无历史设备
|
||||||
</div>
|
</div>
|
||||||
@ -478,8 +479,8 @@ const loadHistoryDevices = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
@click.stop="loadDevice(device)"
|
@click.stop="loadDevice(device)"
|
||||||
>
|
>
|
||||||
加载
|
加载
|
||||||
|
|||||||
@ -189,8 +189,8 @@ onMounted(() => {
|
|||||||
<DropdownMenu v-model:open="showDropdown">
|
<DropdownMenu v-model:open="showDropdown">
|
||||||
<template #trigger="{ toggle, open }">
|
<template #trigger="{ toggle, open }">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
class="h-8 px-3 max-w-[300px] justify-start font-normal hover:bg-accent/50 border border-border"
|
class="h-8 px-3 max-w-[300px] justify-start font-normal hover:bg-accent/50 border border-border"
|
||||||
|
variant="ghost"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
@ -202,12 +202,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 ml-auto">
|
<div class="flex items-center gap-1 ml-auto">
|
||||||
<Badge v-if="!currentDevice.isOwned" variant="secondary" class="h-4 px-1 text-[10px]">
|
<Badge v-if="!currentDevice.isOwned" class="h-4 px-1 text-[10px]" variant="secondary">
|
||||||
未绑定
|
未绑定
|
||||||
</Badge>
|
</Badge>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
class="h-3 w-3 text-muted-foreground flex-shrink-0 transition-transform duration-200"
|
|
||||||
:class="{ 'rotate-180': open }"
|
:class="{ 'rotate-180': open }"
|
||||||
|
class="h-3 w-3 text-muted-foreground flex-shrink-0 transition-transform duration-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -222,8 +222,8 @@ onMounted(() => {
|
|||||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"/>
|
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"/>
|
||||||
<Input
|
<Input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索设备..."
|
|
||||||
class="pl-9 h-8"
|
class="pl-9 h-8"
|
||||||
|
placeholder="搜索设备..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -265,8 +265,8 @@ onMounted(() => {
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="device in filteredAccountDevices"
|
v-for="device in filteredAccountDevices"
|
||||||
:key="device.uuid"
|
:key="device.uuid"
|
||||||
@click="switchToDevice(device)"
|
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
|
@click="switchToDevice(device)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
<Monitor class="h-4 w-4 text-muted-foreground"/>
|
<Monitor class="h-4 w-4 text-muted-foreground"/>
|
||||||
@ -296,8 +296,8 @@ onMounted(() => {
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="device in filteredHistoryDevices.slice(0, 5)"
|
v-for="device in filteredHistoryDevices.slice(0, 5)"
|
||||||
:key="device.uuid"
|
:key="device.uuid"
|
||||||
@click="switchToDevice(device)"
|
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
|
@click="switchToDevice(device)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
<Monitor class="h-4 w-4 text-muted-foreground"/>
|
<Monitor class="h-4 w-4 text-muted-foreground"/>
|
||||||
@ -320,30 +320,31 @@ onMounted(() => {
|
|||||||
<div class="p-2 border-t bg-muted/20 space-y-1">
|
<div class="p-2 border-t bg-muted/20 space-y-1">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-if="!accountStore.isAuthenticated"
|
v-if="!accountStore.isAuthenticated"
|
||||||
@click="showLoginDialog = true"
|
|
||||||
class="cursor-pointer text-primary"
|
class="cursor-pointer text-primary"
|
||||||
|
@click="showLoginDialog = true"
|
||||||
>
|
>
|
||||||
<User class="h-4 w-4"/>
|
<User class="h-4 w-4"/>
|
||||||
登录账户
|
登录账户
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@click="showManualInputDialog = true"
|
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
|
@click="showManualInputDialog = true"
|
||||||
>
|
>
|
||||||
<Settings class="h-4 w-4"/>
|
<Settings class="h-4 w-4"/>
|
||||||
手动输入UUID
|
手动输入UUID
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
@click="showRegisterDialog = true"
|
|
||||||
class="cursor-pointer text-primary"
|
class="cursor-pointer text-primary"
|
||||||
|
@click="showRegisterDialog = true"
|
||||||
>
|
>
|
||||||
<Plus class="h-4 w-4"/>
|
<Plus class="h-4 w-4"/>
|
||||||
注册新设备
|
注册新设备
|
||||||
</DropdownItem> <DropdownItem
|
</DropdownItem>
|
||||||
@click="showRegisterDialog = true"
|
<DropdownItem
|
||||||
class="cursor-pointer text-primary"
|
class="cursor-pointer text-primary"
|
||||||
|
@click="showRegisterDialog = true"
|
||||||
>
|
>
|
||||||
<Plus class="h-4 w-4"/>
|
<Plus class="h-4 w-4"/>
|
||||||
高级选项
|
高级选项
|
||||||
@ -374,7 +375,7 @@ onMounted(() => {
|
|||||||
<Button variant="outline" @click="showManualInputDialog = false">
|
<Button variant="outline" @click="showManualInputDialog = false">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleManualInput" :disabled="!manualUuid.trim()">
|
<Button :disabled="!manualUuid.trim()" @click="handleManualInput">
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -44,7 +44,6 @@ const isOpen = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updateDeviceName = async () => {
|
const updateDeviceName = async () => {
|
||||||
if (!deviceName.value.trim()) {
|
if (!deviceName.value.trim()) {
|
||||||
toast.error('请输入设备名称')
|
toast.error('请输入设备名称')
|
||||||
@ -100,10 +99,10 @@ const updateDeviceName = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="isOpen = false" :disabled="isSubmitting">
|
<Button :disabled="isSubmitting" variant="outline" @click="isOpen = false">
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="updateDeviceName" :disabled="isSubmitting || !deviceName.trim()">
|
<Button :disabled="isSubmitting || !deviceName.trim()" @click="updateDeviceName">
|
||||||
{{ isSubmitting ? '更新中...' : '确认' }}
|
{{ isSubmitting ? '更新中...' : '确认' }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -97,10 +97,10 @@ const saveNamespace = async () => {
|
|||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="namespace"
|
id="namespace"
|
||||||
type="text"
|
|
||||||
v-model="namespace"
|
v-model="namespace"
|
||||||
placeholder="例如: class-2024-grade1"
|
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
placeholder="例如: class-2024-grade1"
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
命名空间用于自动授权接口,必须全局唯一
|
命名空间用于自动授权接口,必须全局唯一
|
||||||
@ -120,17 +120,17 @@ const saveNamespace = async () => {
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="closeDialog"
|
@click="closeDialog"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
:disabled="isLoading"
|
||||||
type="button"
|
type="button"
|
||||||
@click="saveNamespace"
|
@click="saveNamespace"
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="isLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
保存
|
保存
|
||||||
|
|||||||
@ -78,7 +78,8 @@ const navigateTo = (path) => {
|
|||||||
<div :class="[feature.iconBg, 'p-3 rounded-lg']">
|
<div :class="[feature.iconBg, 'p-3 rounded-lg']">
|
||||||
<component :is="feature.icon" :class="[feature.iconColor, 'h-6 w-6']"/>
|
<component :is="feature.icon" :class="[feature.iconColor, 'h-6 w-6']"/>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRight class="h-5 w-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
<ArrowRight
|
||||||
|
class="h-5 w-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all"/>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle class="text-lg">{{ feature.title }}</CardTitle>
|
<CardTitle class="text-lg">{{ feature.title }}</CardTitle>
|
||||||
<CardDescription class="text-xs line-clamp-2">
|
<CardDescription class="text-xs line-clamp-2">
|
||||||
@ -86,7 +87,7 @@ const navigateTo = (path) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Button variant="ghost" size="sm" class="w-full group-hover:bg-primary/10">
|
<Button class="w-full group-hover:bg-primary/10" size="sm" variant="ghost">
|
||||||
前往
|
前往
|
||||||
<ArrowRight class="ml-2 h-3 w-3"/>
|
<ArrowRight class="ml-2 h-3 w-3"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import {ref} from 'vue'
|
import {ref} from 'vue'
|
||||||
|
|
||||||
defineProps<{ msg: string }>()
|
defineProps<{ msg: string }>()
|
||||||
|
|||||||
@ -19,11 +19,11 @@
|
|||||||
<button
|
<button
|
||||||
v-for="provider in providers"
|
v-for="provider in providers"
|
||||||
:key="provider.id"
|
:key="provider.id"
|
||||||
@click="handleLogin(provider)"
|
|
||||||
class="w-full flex items-center gap-3 px-4 py-3 border rounded-lg transition-colors hover:bg-accent"
|
|
||||||
:style="{
|
:style="{
|
||||||
borderColor: (provider.color || '#666')
|
borderColor: (provider.color || '#666')
|
||||||
}"
|
}"
|
||||||
|
class="w-full flex items-center gap-3 px-4 py-3 border rounded-lg transition-colors hover:bg-accent"
|
||||||
|
@click="handleLogin(provider)"
|
||||||
>
|
>
|
||||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-muted">
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-muted">
|
||||||
<component :is="getProviderIcon(provider.icon)" class="w-6 h-6"/>
|
<component :is="getProviderIcon(provider.icon)" class="w-6 h-6"/>
|
||||||
|
|||||||
@ -81,7 +81,6 @@ const effectiveDeviceUuid = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 验证状态
|
// 验证状态
|
||||||
const validationState = computed(() => {
|
const validationState = computed(() => {
|
||||||
const errors = []
|
const errors = []
|
||||||
@ -136,7 +135,6 @@ const loadPasswordHint = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理输入变化
|
// 处理输入变化
|
||||||
const handleInput = (event) => {
|
const handleInput = (event) => {
|
||||||
localValue.value = event.target.value
|
localValue.value = event.target.value
|
||||||
@ -172,9 +170,9 @@ onMounted(() => {
|
|||||||
<!-- 密码提示按钮 -->
|
<!-- 密码提示按钮 -->
|
||||||
<button
|
<button
|
||||||
v-if="showHint && passwordHint"
|
v-if="showHint && passwordHint"
|
||||||
|
class="group relative"
|
||||||
type="button"
|
type="button"
|
||||||
@click="showHintPopup = !showHintPopup"
|
@click="showHintPopup = !showHintPopup"
|
||||||
class="group relative"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary transition-colors">
|
<div class="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary transition-colors">
|
||||||
<HelpCircle class="h-3.5 w-3.5"/>
|
<HelpCircle class="h-3.5 w-3.5"/>
|
||||||
@ -204,14 +202,14 @@ onMounted(() => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
:id="id"
|
:id="id"
|
||||||
type="text"
|
|
||||||
:value="localValue"
|
|
||||||
@input="handleInput"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="{
|
:class="{
|
||||||
'border-red-500': !validationState.isValid && localValue
|
'border-red-500': !validationState.isValid && localValue
|
||||||
}"
|
}"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:value="localValue"
|
||||||
|
type="text"
|
||||||
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 可见性切换按钮(已移除) -->
|
<!-- 可见性切换按钮(已移除) -->
|
||||||
@ -228,7 +226,6 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 错误信息 -->
|
<!-- 错误信息 -->
|
||||||
<div v-if="!validationState.isValid && localValue" class="space-y-1">
|
<div v-if="!validationState.isValid && localValue" class="space-y-1">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -40,7 +40,9 @@ const relText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
timer = setInterval(() => { now.value = Date.now() }, Math.max(5_000, props.refreshMs))
|
timer = setInterval(() => {
|
||||||
|
now.value = Date.now()
|
||||||
|
}, Math.max(5_000, props.refreshMs))
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -49,7 +51,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
watch(() => props.refreshMs, (v) => {
|
watch(() => props.refreshMs, (v) => {
|
||||||
if (timer) clearInterval(timer)
|
if (timer) clearInterval(timer)
|
||||||
timer = setInterval(() => { now.value = Date.now() }, Math.max(5_000, v || 60_000))
|
timer = setInterval(() => {
|
||||||
|
now.value = Date.now()
|
||||||
|
}, Math.max(5_000, v || 60_000))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed} from 'vue'
|
import {computed} from 'vue'
|
||||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, TableEmpty } from '@/components/ui/table'
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableEmpty
|
||||||
|
} from '@/components/ui/table'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {Badge} from '@/components/ui/badge'
|
import {Badge} from '@/components/ui/badge'
|
||||||
import {Copy, CheckCircle2, Key, Clock, Trash2} from 'lucide-vue-next'
|
import {Copy, CheckCircle2, Key, Clock, Trash2} from 'lucide-vue-next'
|
||||||
@ -63,7 +72,7 @@ const formatDate = (dateString) => new Date(dateString).toLocaleString('zh-CN')
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-else-if="rows.length === 0">
|
<TableRow v-else-if="rows.length === 0">
|
||||||
<TableCell :colspan="colCount">
|
<TableCell :colspan="colCount">
|
||||||
<TableEmpty icon="package" description="暂无数据" />
|
<TableEmpty description="暂无数据" icon="package"/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<!-- 非紧凑模式:完整列集 -->
|
<!-- 非紧凑模式:完整列集 -->
|
||||||
@ -71,7 +80,7 @@ const formatDate = (dateString) => new Date(dateString).toLocaleString('zh-CN')
|
|||||||
<TableRow v-for="item in rows" :key="item.id">
|
<TableRow v-for="item in rows" :key="item.id">
|
||||||
<TableCell v-if="props.showAppColumn">
|
<TableCell v-if="props.showAppColumn">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="secondary" class="shrink-0">{{ item.appId }}</Badge>
|
<Badge class="shrink-0" variant="secondary">{{ item.appId }}</Badge>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="font-medium truncate">{{ item.appName || `应用 ${item.appId}` }}</div>
|
<div class="font-medium truncate">{{ item.appName || `应用 ${item.appId}` }}</div>
|
||||||
<div class="text-xs text-muted-foreground truncate">ID: {{ item.appId }}</div>
|
<div class="text-xs text-muted-foreground truncate">ID: {{ item.appId }}</div>
|
||||||
@ -83,11 +92,11 @@ const formatDate = (dateString) => new Date(dateString).toLocaleString('zh-CN')
|
|||||||
<Key class="h-3.5 w-3.5 text-muted-foreground"/>
|
<Key class="h-3.5 w-3.5 text-muted-foreground"/>
|
||||||
<code class="text-xs font-mono truncate">{{ item.token }}</code>
|
<code class="text-xs font-mono truncate">{{ item.token }}</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 w-7 ml-auto"
|
|
||||||
@click="emit('copy', item)"
|
|
||||||
:title="props.copiedId === item.token ? '已复制' : '复制令牌'"
|
:title="props.copiedId === item.token ? '已复制' : '复制令牌'"
|
||||||
|
class="h-7 w-7 ml-auto"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="emit('copy', item)"
|
||||||
>
|
>
|
||||||
<CheckCircle2 v-if="props.copiedId === item.token" class="h-3.5 w-3.5 text-green-500"/>
|
<CheckCircle2 v-if="props.copiedId === item.token" class="h-3.5 w-3.5 text-green-500"/>
|
||||||
<Copy v-else class="h-3.5 w-3.5"/>
|
<Copy v-else class="h-3.5 w-3.5"/>
|
||||||
@ -107,12 +116,13 @@ const formatDate = (dateString) => new Date(dateString).toLocaleString('zh-CN')
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right">
|
<TableCell class="text-right">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
class="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
@click="emit('revoke', item)"
|
@click="emit('revoke', item)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3.5 w-3.5 mr-1" /> 撤销
|
<Trash2 class="h-3.5 w-3.5 mr-1"/>
|
||||||
|
撤销
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -143,5 +153,10 @@ const formatDate = (dateString) => new Date(dateString).toLocaleString('zh-CN')
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.truncate { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.truncate {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -45,9 +45,17 @@ export function useOAuthCallback() {
|
|||||||
router.replace({query: {}})
|
router.replace({query: {}})
|
||||||
|
|
||||||
// 触发storage事件,通知其他窗口
|
// 触发storage事件,通知其他窗口
|
||||||
window.dispatchEvent(new StorageEvent('storage', { key: 'auth_token', newValue: access_token, url: window.location.href }))
|
window.dispatchEvent(new StorageEvent('storage', {
|
||||||
|
key: 'auth_token',
|
||||||
|
newValue: access_token,
|
||||||
|
url: window.location.href
|
||||||
|
}))
|
||||||
if (refresh_token) {
|
if (refresh_token) {
|
||||||
window.dispatchEvent(new StorageEvent('storage', { key: 'auth_refresh_token', newValue: refresh_token, url: window.location.href }))
|
window.dispatchEvent(new StorageEvent('storage', {
|
||||||
|
key: 'auth_refresh_token',
|
||||||
|
newValue: refresh_token,
|
||||||
|
url: window.location.href
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是在新窗口中打开的OAuth回调,自动关闭窗口
|
// 如果是在新窗口中打开的OAuth回调,自动关闭窗口
|
||||||
|
|||||||
@ -154,9 +154,6 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 设备授权相关 API
|
// 设备授权相关 API
|
||||||
async bindDeviceCode(deviceCode, token) {
|
async bindDeviceCode(deviceCode, token) {
|
||||||
return this.fetch('/auth/device/bind', {
|
return this.fetch('/auth/device/bind', {
|
||||||
@ -215,7 +212,6 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 账户相关 API(Authorization 由 axios 拦截器统一注入)
|
// 账户相关 API(Authorization 由 axios 拦截器统一注入)
|
||||||
async getOAuthProviders() {
|
async getOAuthProviders() {
|
||||||
return this.fetch('/accounts/oauth/providers')
|
return this.fetch('/accounts/oauth/providers')
|
||||||
@ -304,7 +300,6 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 设备注册 API
|
// 设备注册 API
|
||||||
async registerDevice(uuid, deviceName, token = null) {
|
async registerDevice(uuid, deviceName, token = null) {
|
||||||
return this.authenticatedFetch('/devices', {
|
return this.authenticatedFetch('/devices', {
|
||||||
@ -314,7 +309,6 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 兼容性方法 - 保持旧的API调用方式
|
// 兼容性方法 - 保持旧的API调用方式
|
||||||
async getTokens(deviceUuid, options = {}) {
|
async getTokens(deviceUuid, options = {}) {
|
||||||
return this.getDeviceTokens(deviceUuid, options);
|
return this.getDeviceTokens(deviceUuid, options);
|
||||||
|
|||||||
@ -21,11 +21,13 @@ let authHandlers = {
|
|||||||
// 可选:返回刷新令牌
|
// 可选:返回刷新令牌
|
||||||
getRefreshToken: () => null,
|
getRefreshToken: () => null,
|
||||||
// 可选:仅更新访问令牌
|
// 可选:仅更新访问令牌
|
||||||
setAccessToken: (_t) => {},
|
setAccessToken: (_t) => {
|
||||||
|
},
|
||||||
// 可选:执行刷新动作,返回新的访问令牌
|
// 可选:执行刷新动作,返回新的访问令牌
|
||||||
refreshAccessToken: null,
|
refreshAccessToken: null,
|
||||||
// 可选:当刷新失败时回调(例如触发登出)
|
// 可选:当刷新失败时回调(例如触发登出)
|
||||||
onAuthFailure: () => {},
|
onAuthFailure: () => {
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对外方法:由外部(如 Pinia store)注入实际的处理函数
|
// 对外方法:由外部(如 Pinia store)注入实际的处理函数
|
||||||
@ -112,15 +114,20 @@ async function ensureDeviceRegistered(uuid, authHeader) {
|
|||||||
const p = axiosInstance.post(
|
const p = axiosInstance.post(
|
||||||
'/devices',
|
'/devices',
|
||||||
{uuid, deviceName},
|
{uuid, deviceName},
|
||||||
{ headers, // 避免递归触发注册重试
|
{
|
||||||
|
headers, // 避免递归触发注册重试
|
||||||
skipDeviceRegistrationRetry: true,
|
skipDeviceRegistrationRetry: true,
|
||||||
__isRegistrationRequest: true }
|
__isRegistrationRequest: true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
registrationLocks.set(uuid, p)
|
registrationLocks.set(uuid, p)
|
||||||
try {
|
try {
|
||||||
await p
|
await p
|
||||||
// 保存UUID到本地存储,确保后续可用
|
// 保存UUID到本地存储,确保后续可用
|
||||||
try { deviceStore.setDeviceUuid(uuid) } catch {}
|
try {
|
||||||
|
deviceStore.setDeviceUuid(uuid)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false
|
||||||
@ -142,7 +149,8 @@ axiosInstance.interceptors.response.use(
|
|||||||
if (newToken && authHandlers?.setAccessToken) {
|
if (newToken && authHandlers?.setAccessToken) {
|
||||||
authHandlers.setAccessToken(newToken)
|
authHandlers.setAccessToken(newToken)
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
@ -159,7 +167,10 @@ axiosInstance.interceptors.response.use(
|
|||||||
// 若没有刷新能力或没有刷新令牌,则直接走失败逻辑
|
// 若没有刷新能力或没有刷新令牌,则直接走失败逻辑
|
||||||
if (!authHandlers?.refreshAccessToken || !authHandlers?.getRefreshToken || !authHandlers.getRefreshToken()) {
|
if (!authHandlers?.refreshAccessToken || !authHandlers?.getRefreshToken || !authHandlers.getRefreshToken()) {
|
||||||
// 无法刷新,触发认证失败回调并退出
|
// 无法刷新,触发认证失败回调并退出
|
||||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error('NO_REFRESH_TOKEN')) } catch {}
|
try {
|
||||||
|
authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error('NO_REFRESH_TOKEN'))
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
throw new Error('NO_REFRESH_TOKEN')
|
throw new Error('NO_REFRESH_TOKEN')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +179,10 @@ axiosInstance.interceptors.response.use(
|
|||||||
refreshingPromise = authHandlers.refreshAccessToken()
|
refreshingPromise = authHandlers.refreshAccessToken()
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
// 刷新失败,触发失败处理
|
// 刷新失败,触发失败处理
|
||||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(e) } catch {}
|
try {
|
||||||
|
authHandlers?.onAuthFailure && authHandlers.onAuthFailure(e)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -183,14 +197,20 @@ axiosInstance.interceptors.response.use(
|
|||||||
return await axiosInstance.request(config)
|
return await axiosInstance.request(config)
|
||||||
} catch (refreshErr) {
|
} catch (refreshErr) {
|
||||||
// 刷新失败,触发认证失败并返回原始错误信息
|
// 刷新失败,触发认证失败并返回原始错误信息
|
||||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(refreshErr) } catch {}
|
try {
|
||||||
|
authHandlers?.onAuthFailure && authHandlers.onAuthFailure(refreshErr)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
return Promise.reject(new Error(message))
|
return Promise.reject(new Error(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 明确的权限问题同样触发登出(例如服务端使用 403 表示 Token 无效或权限已失效)
|
// 明确的权限问题同样触发登出(例如服务端使用 403 表示 Token 无效或权限已失效)
|
||||||
if (status === 403 && resp?.data?.code === 'AUTH_JWT_EXPIRED') {
|
if (status === 403 && resp?.data?.code === 'AUTH_JWT_EXPIRED') {
|
||||||
try { authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error(message || 'FORBIDDEN')) } catch {}
|
try {
|
||||||
|
authHandlers?.onAuthFailure && authHandlers.onAuthFailure(new Error(message || 'FORBIDDEN'))
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
return Promise.reject(new Error(message || 'FORBIDDEN'))
|
return Promise.reject(new Error(message || 'FORBIDDEN'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +224,8 @@ axiosInstance.interceptors.response.use(
|
|||||||
try {
|
try {
|
||||||
const body = typeof config.data === 'string' ? JSON.parse(config.data) : config.data
|
const body = typeof config.data === 'string' ? JSON.parse(config.data) : config.data
|
||||||
if (body && typeof body === 'object' && body.uuid) uuid = body.uuid
|
if (body && typeof body === 'object' && body.uuid) uuid = body.uuid
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可能需要账户授权头
|
// 可能需要账户授权头
|
||||||
|
|||||||
@ -61,7 +61,7 @@ onMounted(() => {
|
|||||||
返回首页
|
返回首页
|
||||||
</Button>
|
</Button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<Button variant="outline" size="lg" @click="goBack">
|
<Button size="lg" variant="outline" @click="goBack">
|
||||||
<ArrowLeft class="size-4"/>
|
<ArrowLeft class="size-4"/>
|
||||||
返回上一页
|
返回上一页
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -295,7 +295,6 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 当前设备UUID显示 -->
|
<!-- 当前设备UUID显示 -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<code class="text-xs font-mono bg-muted px-3 py-2 rounded flex-1 truncate">
|
<code class="text-xs font-mono bg-muted px-3 py-2 rounded flex-1 truncate">
|
||||||
@ -311,10 +310,10 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="accountStore.isAuthenticated && !showDeviceList" class="flex items-center gap-2">
|
<div v-else-if="accountStore.isAuthenticated && !showDeviceList" class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@click="bindCurrentDevice"
|
class="text-xs"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="text-xs"
|
@click="bindCurrentDevice"
|
||||||
>
|
>
|
||||||
<Plus class="h-3 w-3 mr-1"/>
|
<Plus class="h-3 w-3 mr-1"/>
|
||||||
绑定到我的账户
|
绑定到我的账户
|
||||||
@ -337,8 +336,8 @@ onMounted(async () => {
|
|||||||
<Input
|
<Input
|
||||||
id="device-code"
|
id="device-code"
|
||||||
v-model="inputDeviceCode"
|
v-model="inputDeviceCode"
|
||||||
placeholder="例如:1234-ABCD"
|
|
||||||
class="font-mono"
|
class="font-mono"
|
||||||
|
placeholder="例如:1234-ABCD"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -368,17 +367,17 @@ onMounted(async () => {
|
|||||||
<!-- 授权按钮 -->
|
<!-- 授权按钮 -->
|
||||||
<div class="space-y-3 pt-2">
|
<div class="space-y-3 pt-2">
|
||||||
<Button
|
<Button
|
||||||
@click="handleSubmit"
|
:disabled="(isDeviceCodeMode && !currentDeviceCode)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
:disabled="(isDeviceCodeMode && !currentDeviceCode)"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
<Key class="mr-2 h-4 w-4"/>
|
<Key class="mr-2 h-4 w-4"/>
|
||||||
确认授权
|
确认授权
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- 返回首页 -->
|
<!-- 返回首页 -->
|
||||||
<Button @click="goHome" variant="ghost" class="w-full">
|
<Button class="w-full" variant="ghost" @click="goHome">
|
||||||
返回管理页面
|
返回管理页面
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -414,7 +413,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button @click="goHome" class="w-full" size="lg">
|
<Button class="w-full" size="lg" @click="goHome">
|
||||||
返回管理页面
|
返回管理页面
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -432,17 +431,18 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Button @click="retry" class="w-full" size="lg">
|
<Button class="w-full" size="lg" @click="retry">
|
||||||
重试
|
重试
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="goHome" variant="ghost" class="w-full">
|
<Button class="w-full" variant="ghost" @click="goHome">
|
||||||
返回管理页面
|
返回管理页面
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示信息 -->
|
<!-- 提示信息 -->
|
||||||
<div v-if="step === 'input'" class="rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 p-4">
|
<div v-if="step === 'input'"
|
||||||
|
class="rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 p-4">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<AlertCircle class="h-5 w-5 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5"/>
|
<AlertCircle class="h-5 w-5 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5"/>
|
||||||
<div class="space-y-1.5 text-sm">
|
<div class="space-y-1.5 text-sm">
|
||||||
@ -467,8 +467,8 @@ onMounted(async () => {
|
|||||||
<!-- 设备注册弹框 -->
|
<!-- 设备注册弹框 -->
|
||||||
<DeviceRegisterDialog
|
<DeviceRegisterDialog
|
||||||
v-model="showRegisterDialog"
|
v-model="showRegisterDialog"
|
||||||
@confirm="updateUuid"
|
|
||||||
:required="deviceRequired"
|
:required="deviceRequired"
|
||||||
|
@confirm="updateUuid"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -242,8 +242,8 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="h-5 w-5"/>
|
<ArrowLeft class="h-5 w-5"/>
|
||||||
@ -259,10 +259,10 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="isAuthenticated"
|
v-if="isAuthenticated"
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
@click="loadConfigs"
|
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click="loadConfigs"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -313,11 +313,11 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<code class="text-xs bg-muted px-2 py-1 rounded">{{ deviceInfo.namespace }}</code>
|
<code class="text-xs bg-muted px-2 py-1 rounded">{{ deviceInfo.namespace }}</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@click="editNamespace"
|
size="icon"
|
||||||
title="编辑命名空间"
|
title="编辑命名空间"
|
||||||
|
variant="ghost"
|
||||||
|
@click="editNamespace"
|
||||||
>
|
>
|
||||||
<Edit class="h-3 w-3"/>
|
<Edit class="h-3 w-3"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -358,7 +358,7 @@ onMounted(async () => {
|
|||||||
<Shield class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
<Shield class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
||||||
<p class="text-lg font-medium text-muted-foreground mb-2">暂无自动授权配置</p>
|
<p class="text-lg font-medium text-muted-foreground mb-2">暂无自动授权配置</p>
|
||||||
<p class="text-sm text-muted-foreground mb-4">创建配置以允许设备自动授权访问</p>
|
<p class="text-sm text-muted-foreground mb-4">创建配置以允许设备自动授权访问</p>
|
||||||
<Button @click="createConfig" variant="outline">
|
<Button variant="outline" @click="createConfig">
|
||||||
<Plus class="h-4 w-4 mr-2"/>
|
<Plus class="h-4 w-4 mr-2"/>
|
||||||
创建第一个配置
|
创建第一个配置
|
||||||
</Button>
|
</Button>
|
||||||
@ -402,7 +402,7 @@ onMounted(async () => {
|
|||||||
<div v-if="config.password || config.isLegacyHash" class="rounded-lg border bg-muted/50 p-3 space-y-2">
|
<div v-if="config.password || config.isLegacyHash" class="rounded-lg border bg-muted/50 p-3 space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs font-medium text-muted-foreground">授权密码</span>
|
<span class="text-xs font-medium text-muted-foreground">授权密码</span>
|
||||||
<Badge v-if="config.isLegacyHash" variant="secondary" class="text-xs">
|
<Badge v-if="config.isLegacyHash" class="text-xs" variant="secondary">
|
||||||
旧格式
|
旧格式
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@ -411,9 +411,9 @@ onMounted(async () => {
|
|||||||
{{ config.password }}
|
{{ config.password }}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7"
|
class="h-7 w-7"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
@click="copyPassword(config.password)"
|
@click="copyPassword(config.password)"
|
||||||
>
|
>
|
||||||
<Copy class="h-3 w-3"/>
|
<Copy class="h-3 w-3"/>
|
||||||
@ -433,19 +433,19 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="editConfig(config)"
|
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="editConfig(config)"
|
||||||
>
|
>
|
||||||
<Edit class="h-3 w-3 mr-1"/>
|
<Edit class="h-3 w-3 mr-1"/>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
@click="confirmDelete(config)"
|
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
@click="confirmDelete(config)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3 w-3 mr-1"/>
|
<Trash2 class="h-3 w-3 mr-1"/>
|
||||||
删除
|
删除
|
||||||
@ -467,9 +467,9 @@ onMounted(async () => {
|
|||||||
<AutoAuthConfigDialog
|
<AutoAuthConfigDialog
|
||||||
v-if="isAuthenticated"
|
v-if="isAuthenticated"
|
||||||
v-model="showConfigDialog"
|
v-model="showConfigDialog"
|
||||||
:device-uuid="deviceUuid"
|
|
||||||
:account-token="accountStore.token"
|
:account-token="accountStore.token"
|
||||||
:config="editingConfig"
|
:config="editingConfig"
|
||||||
|
:device-uuid="deviceUuid"
|
||||||
@success="handleConfigSaved"
|
@success="handleConfigSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -477,9 +477,9 @@ onMounted(async () => {
|
|||||||
<EditNamespaceDialog
|
<EditNamespaceDialog
|
||||||
v-if="isAuthenticated && deviceInfo"
|
v-if="isAuthenticated && deviceInfo"
|
||||||
v-model="showNamespaceDialog"
|
v-model="showNamespaceDialog"
|
||||||
:device-uuid="deviceUuid"
|
|
||||||
:current-namespace="deviceInfo.namespace"
|
|
||||||
:account-token="accountStore.token"
|
:account-token="accountStore.token"
|
||||||
|
:current-namespace="deviceInfo.namespace"
|
||||||
|
:device-uuid="deviceUuid"
|
||||||
@success="handleNamespaceUpdated"
|
@success="handleNamespaceUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -495,7 +495,8 @@ onMounted(async () => {
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
<AlertDialogAction @click="deleteConfig" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
<AlertDialogAction class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
@click="deleteConfig">
|
||||||
确认删除
|
确认删除
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
@ -213,8 +213,8 @@ const goBack = () => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="h-5 w-5"/>
|
<ArrowLeft class="h-5 w-5"/>
|
||||||
@ -233,7 +233,7 @@ const goBack = () => {
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
||||||
<Tabs default-value="token" class="w-full">
|
<Tabs class="w-full" default-value="token">
|
||||||
<TabsList class="grid w-full grid-cols-3">
|
<TabsList class="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="token">
|
<TabsTrigger value="token">
|
||||||
<Key class="h-4 w-4 mr-2"/>
|
<Key class="h-4 w-4 mr-2"/>
|
||||||
@ -250,7 +250,7 @@ const goBack = () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<!-- Tab 1: 获取 Token -->
|
<!-- Tab 1: 获取 Token -->
|
||||||
<TabsContent value="token" class="space-y-4">
|
<TabsContent class="space-y-4" value="token">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>通过 Namespace 获取 Token</CardTitle>
|
<CardTitle>通过 Namespace 获取 Token</CardTitle>
|
||||||
@ -273,17 +273,17 @@ const goBack = () => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
:type="tab1ShowPassword ? 'text' : 'password'"
|
|
||||||
v-model="tab1Form.password"
|
v-model="tab1Form.password"
|
||||||
|
:type="tab1ShowPassword ? 'text' : 'password'"
|
||||||
placeholder="留空表示无密码授权"
|
placeholder="留空表示无密码授权"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
||||||
|
size="icon"
|
||||||
|
tabindex="-1"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
|
||||||
class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
|
||||||
@click="tab1ShowPassword = !tab1ShowPassword"
|
@click="tab1ShowPassword = !tab1ShowPassword"
|
||||||
tabindex="-1"
|
|
||||||
>
|
>
|
||||||
<Eye v-if="!tab1ShowPassword" class="h-4 w-4 text-muted-foreground"/>
|
<Eye v-if="!tab1ShowPassword" class="h-4 w-4 text-muted-foreground"/>
|
||||||
<EyeOff v-else class="h-4 w-4 text-muted-foreground"/>
|
<EyeOff v-else class="h-4 w-4 text-muted-foreground"/>
|
||||||
@ -301,9 +301,9 @@ const goBack = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@click="testGetToken"
|
|
||||||
:disabled="tab1Loading"
|
:disabled="tab1Loading"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
@click="testGetToken"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="tab1Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="tab1Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
<Play v-else class="mr-2 h-4 w-4"/>
|
<Play v-else class="mr-2 h-4 w-4"/>
|
||||||
@ -330,7 +330,7 @@ const goBack = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- Tab 2: 设置学生名称 -->
|
<!-- Tab 2: 设置学生名称 -->
|
||||||
<TabsContent value="student" class="space-y-4">
|
<TabsContent class="space-y-4" value="student">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>设置学生名称</CardTitle>
|
<CardTitle>设置学生名称</CardTitle>
|
||||||
@ -361,9 +361,9 @@ const goBack = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@click="testSetStudentName"
|
|
||||||
:disabled="tab2Loading"
|
:disabled="tab2Loading"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
@click="testSetStudentName"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="tab2Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="tab2Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
<Play v-else class="mr-2 h-4 w-4"/>
|
<Play v-else class="mr-2 h-4 w-4"/>
|
||||||
@ -390,7 +390,7 @@ const goBack = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<!-- Tab 3: KV 操作 -->
|
<!-- Tab 3: KV 操作 -->
|
||||||
<TabsContent value="kv" class="space-y-4">
|
<TabsContent class="space-y-4" value="kv">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>KV 存储操作测试</CardTitle>
|
<CardTitle>KV 存储操作测试</CardTitle>
|
||||||
@ -436,15 +436,15 @@ const goBack = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
id="value"
|
id="value"
|
||||||
v-model="tab3Form.value"
|
v-model="tab3Form.value"
|
||||||
placeholder='例如: {"message": "Hello World"}'
|
|
||||||
class="flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
class="flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
placeholder='例如: {"message": "Hello World"}'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@click="testKVOperation"
|
|
||||||
:disabled="tab3Loading"
|
:disabled="tab3Loading"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
@click="testKVOperation"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="tab3Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="tab3Loading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
<Play v-else class="mr-2 h-4 w-4"/>
|
<Play v-else class="mr-2 h-4 w-4"/>
|
||||||
|
|||||||
@ -105,8 +105,8 @@ onMounted(() => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
@click="router.push('/')"
|
@click="router.push('/')"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="h-5 w-5"/>
|
<ArrowLeft class="h-5 w-5"/>
|
||||||
@ -117,15 +117,15 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="secondary" class="px-3 py-1">
|
<Badge class="px-3 py-1" variant="secondary">
|
||||||
<User class="h-3 w-3 mr-1.5"/>
|
<User class="h-3 w-3 mr-1.5"/>
|
||||||
{{ accountStore.userName }}
|
{{ accountStore.userName }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
@click="loadDevices"
|
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click="loadDevices"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -148,7 +148,7 @@ onMounted(() => {
|
|||||||
<Smartphone class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
<Smartphone class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
||||||
<p class="text-lg font-medium text-muted-foreground mb-2">暂无绑定设备</p>
|
<p class="text-lg font-medium text-muted-foreground mb-2">暂无绑定设备</p>
|
||||||
<p class="text-sm text-muted-foreground mb-4">您可以在主页面注册并绑定新设备</p>
|
<p class="text-sm text-muted-foreground mb-4">您可以在主页面注册并绑定新设备</p>
|
||||||
<Button @click="router.push('/')" variant="outline">
|
<Button variant="outline" @click="router.push('/')">
|
||||||
返回主页
|
返回主页
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -188,10 +188,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="editDeviceName(device)"
|
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="editDeviceName(device)"
|
||||||
>
|
>
|
||||||
<Edit class="h-3 w-3 mr-1"/>
|
<Edit class="h-3 w-3 mr-1"/>
|
||||||
重命名
|
重命名
|
||||||
@ -200,10 +200,10 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
@click="confirmUnbind(device)"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
@click="confirmUnbind(device)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3 w-3 mr-1"/>
|
<Trash2 class="h-3 w-3 mr-1"/>
|
||||||
解绑设备
|
解绑设备
|
||||||
@ -218,14 +218,13 @@ onMounted(() => {
|
|||||||
<EditDeviceNameDialog
|
<EditDeviceNameDialog
|
||||||
v-if="currentDevice"
|
v-if="currentDevice"
|
||||||
v-model="showEditNameDialog"
|
v-model="showEditNameDialog"
|
||||||
:device-uuid="currentDevice.uuid"
|
|
||||||
:current-name="currentDevice.name || ''"
|
:current-name="currentDevice.name || ''"
|
||||||
|
:device-uuid="currentDevice.uuid"
|
||||||
:has-password="false"
|
:has-password="false"
|
||||||
@success="handleDeviceNameUpdated"
|
@success="handleDeviceNameUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 解绑确认对话框 -->
|
<!-- 解绑确认对话框 -->
|
||||||
<AlertDialog v-model:open="showDeleteDialog">
|
<AlertDialog v-model:open="showDeleteDialog">
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|||||||
@ -10,7 +10,27 @@ import { Input } from '@/components/ui/input'
|
|||||||
import {Label} from '@/components/ui/label'
|
import {Label} from '@/components/ui/label'
|
||||||
import {Badge} from '@/components/ui/badge'
|
import {Badge} from '@/components/ui/badge'
|
||||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
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, Edit } 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 DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue'
|
||||||
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
|
import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue'
|
||||||
import AppCard from '@/components/AppCard.vue'
|
import AppCard from '@/components/AppCard.vue'
|
||||||
@ -55,7 +75,6 @@ const authNote = ref('')
|
|||||||
const {handleOAuthCallback} = useOAuthCallback()
|
const {handleOAuthCallback} = useOAuthCallback()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 检查 namespace 是否等于 UUID(需要提示用户修改)
|
// 检查 namespace 是否等于 UUID(需要提示用户修改)
|
||||||
const namespaceEqualsUuid = computed(() => {
|
const namespaceEqualsUuid = computed(() => {
|
||||||
return deviceInfo.value && deviceInfo.value.namespace === deviceInfo.value.uuid
|
return deviceInfo.value && deviceInfo.value.namespace === deviceInfo.value.uuid
|
||||||
@ -92,7 +111,6 @@ const loadDeviceInfo = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const loadTokens = async () => {
|
const loadTokens = async () => {
|
||||||
if (!deviceUuid.value) return
|
if (!deviceUuid.value) return
|
||||||
|
|
||||||
@ -214,7 +232,6 @@ const updateUuid = (newUuid = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
return new Date(dateString).toLocaleString('zh-CN')
|
return new Date(dateString).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
@ -347,7 +364,6 @@ onMounted(async () => {
|
|||||||
await loadDeviceAccount()
|
await loadDeviceAccount()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 加载tokens
|
// 加载tokens
|
||||||
await loadTokens()
|
await loadTokens()
|
||||||
}
|
}
|
||||||
@ -385,37 +401,37 @@ onMounted(async () => {
|
|||||||
<DropdownMenu v-model:open="showUserMenu" class="z-50">
|
<DropdownMenu v-model:open="showUserMenu" class="z-50">
|
||||||
<template #trigger="{ toggle, open }">
|
<template #trigger="{ toggle, open }">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-2"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="accountStore.userAvatar"
|
v-if="accountStore.userAvatar"
|
||||||
:src="accountStore.userAvatar"
|
|
||||||
:alt="accountStore.userName"
|
:alt="accountStore.userName"
|
||||||
|
:src="accountStore.userAvatar"
|
||||||
class="w-5 h-5 rounded-full"
|
class="w-5 h-5 rounded-full"
|
||||||
>
|
>
|
||||||
<User v-else class="h-4 w-4"/>
|
<User v-else class="h-4 w-4"/>
|
||||||
<span class="hidden sm:inline">{{ accountStore.userName }}</span>
|
<span class="hidden sm:inline">{{ accountStore.userName }}</span>
|
||||||
<span v-if="accountStore.profile?.providerInfo"
|
<span v-if="accountStore.profile?.providerInfo"
|
||||||
class="hidden md:inline px-1.5 py-0.5 rounded text-[10px]"
|
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: (accountStore.profile.providerInfo.color || '#999') + '22',
|
backgroundColor: (accountStore.profile.providerInfo.color || '#999') + '22',
|
||||||
color: accountStore.profile.providerInfo.color || 'inherit',
|
color: accountStore.profile.providerInfo.color || 'inherit',
|
||||||
border: `1px solid ${(accountStore.profile.providerInfo.color || '#999')}55`
|
border: `1px solid ${(accountStore.profile.providerInfo.color || '#999')}55`
|
||||||
}"
|
}"
|
||||||
|
class="hidden md:inline px-1.5 py-0.5 rounded text-[10px]"
|
||||||
>
|
>
|
||||||
{{ accountStore.profile.providerInfo.displayName }}
|
{{ accountStore.profile.providerInfo.displayName }}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown class="h-3.5 w-3.5" :class="{ 'rotate-180': open }" />
|
<ChevronDown :class="{ 'rotate-180': open }" class="h-3.5 w-3.5"/>
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<DropdownItem :href="accountStore.profile.providerInfo.website" target="_blank" :style="{
|
<DropdownItem :href="accountStore.profile.providerInfo.website" :style="{
|
||||||
backgroundColor: (accountStore.profile.providerInfo.color || '#999') + '22',
|
backgroundColor: (accountStore.profile.providerInfo.color || '#999') + '22',
|
||||||
color: accountStore.profile.providerInfo.color || 'inherit',
|
color: accountStore.profile.providerInfo.color || 'inherit',
|
||||||
border: `1px solid ${(accountStore.profile.providerInfo.color || '#999')}55`
|
border: `1px solid ${(accountStore.profile.providerInfo.color || '#999')}55`
|
||||||
}">
|
}" target="_blank">
|
||||||
<Layers class="h-4 w-4"/>
|
<Layers class="h-4 w-4"/>
|
||||||
账户渠道:{{ accountStore.profile.providerInfo.displayName }}
|
账户渠道:{{ accountStore.profile.providerInfo.displayName }}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@ -433,7 +449,8 @@ onMounted(async () => {
|
|||||||
<TestTube2 class="h-4 w-4"/>
|
<TestTube2 class="h-4 w-4"/>
|
||||||
API 测试工具
|
API 测试工具
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem @click="handleLogout" class="text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300">
|
<DropdownItem class="text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
@click="handleLogout">
|
||||||
<LogOut class="h-4 w-4"/>
|
<LogOut class="h-4 w-4"/>
|
||||||
退出登录
|
退出登录
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@ -441,8 +458,8 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
@click="showLoginDialog = true"
|
@click="showLoginDialog = true"
|
||||||
>
|
>
|
||||||
<User class="h-4 w-4 mr-2"/>
|
<User class="h-4 w-4 mr-2"/>
|
||||||
@ -450,13 +467,11 @@ onMounted(async () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
@click="loadTokens"
|
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click="loadTokens"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
<RefreshCw :class="{ 'animate-spin': isLoading }" class="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -468,7 +483,8 @@ onMounted(async () => {
|
|||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
||||||
<!-- Namespace 提示卡片 - 如果 namespace 等于 UUID -->
|
<!-- Namespace 提示卡片 - 如果 namespace 等于 UUID -->
|
||||||
<Card v-if="namespaceEqualsUuid" class="mb-6 border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-950/20">
|
<Card v-if="namespaceEqualsUuid"
|
||||||
|
class="mb-6 border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-950/20">
|
||||||
<CardContent class="py-4">
|
<CardContent class="py-4">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<AlertCircle class="h-5 w-5 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0"/>
|
<AlertCircle class="h-5 w-5 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0"/>
|
||||||
@ -481,10 +497,10 @@ onMounted(async () => {
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
v-if="accountStore.isAuthenticated"
|
v-if="accountStore.isAuthenticated"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="showEditNamespaceDialog = true"
|
|
||||||
class="bg-yellow-100 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700 hover:bg-yellow-200 dark:hover:bg-yellow-900/50"
|
class="bg-yellow-100 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700 hover:bg-yellow-200 dark:hover:bg-yellow-900/50"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="showEditNamespaceDialog = true"
|
||||||
>
|
>
|
||||||
<Settings class="h-3 w-3 mr-2"/>
|
<Settings class="h-3 w-3 mr-2"/>
|
||||||
立即修改
|
立即修改
|
||||||
@ -509,10 +525,10 @@ onMounted(async () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
v-if="accountStore.isAuthenticated"
|
v-if="accountStore.isAuthenticated"
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
@click="showEditNameDialog = true"
|
|
||||||
class="h-6 w-6 p-0"
|
class="h-6 w-6 p-0"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="showEditNameDialog = true"
|
||||||
>
|
>
|
||||||
<Edit class="h-3 w-3"/>
|
<Edit class="h-3 w-3"/>
|
||||||
</Button>
|
</Button>
|
||||||
@ -524,14 +540,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
|
|
||||||
<!-- 设备账户绑定状态 -->
|
<!-- 设备账户绑定状态 -->
|
||||||
<Badge v-if="deviceInfo?.account" variant="secondary" class="px-3 py-1">
|
<Badge v-if="deviceInfo?.account" class="px-3 py-1" variant="secondary">
|
||||||
<User class="h-3 w-3 mr-1.5"/>
|
<User class="h-3 w-3 mr-1.5"/>
|
||||||
{{ deviceInfo.account.name }}
|
{{ deviceInfo.account.name }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="accountStore.isAuthenticated"
|
v-else-if="accountStore.isAuthenticated"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
@click="bindCurrentDevice"
|
@click="bindCurrentDevice"
|
||||||
>
|
>
|
||||||
<User class="h-4 w-4 mr-2"/>
|
<User class="h-4 w-4 mr-2"/>
|
||||||
@ -539,9 +555,9 @@ onMounted(async () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@click="$router.push('/auto-auth-management')"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
@click="$router.push('/auto-auth-management')"
|
||||||
>
|
>
|
||||||
<Shield class="h-4 w-4 mr-1"/>
|
<Shield class="h-4 w-4 mr-1"/>
|
||||||
自动授权
|
自动授权
|
||||||
@ -553,16 +569,17 @@ onMounted(async () => {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Namespace Display (主要显示) -->
|
<!-- Namespace Display (主要显示) -->
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/10 rounded-lg blur-xl group-hover:blur-2xl transition-all duration-300 opacity-50" />
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/10 rounded-lg blur-xl group-hover:blur-2xl transition-all duration-300 opacity-50"/>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<Label class="text-sm font-medium">命名空间</Label>
|
<Label class="text-sm font-medium">命名空间</Label>
|
||||||
<Button
|
<Button
|
||||||
v-if="accountStore.isAuthenticated"
|
v-if="accountStore.isAuthenticated"
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
@click="showEditNamespaceDialog = true"
|
|
||||||
class="h-7"
|
class="h-7"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="showEditNamespaceDialog = true"
|
||||||
>
|
>
|
||||||
<Settings class="h-3 w-3 mr-1"/>
|
<Settings class="h-3 w-3 mr-1"/>
|
||||||
编辑
|
编辑
|
||||||
@ -573,11 +590,11 @@ onMounted(async () => {
|
|||||||
{{ deviceInfo?.namespace || deviceUuid }}
|
{{ deviceInfo?.namespace || deviceUuid }}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
class="h-8 w-8"
|
||||||
@click="copyToClipboard(deviceInfo?.namespace || deviceUuid, 'namespace')"
|
size="icon"
|
||||||
title="复制命名空间"
|
title="复制命名空间"
|
||||||
|
variant="ghost"
|
||||||
|
@click="copyToClipboard(deviceInfo?.namespace || deviceUuid, 'namespace')"
|
||||||
>
|
>
|
||||||
<CheckCircle2 v-if="copied === 'namespace'" class="h-4 w-4 text-green-500 animate-in zoom-in-50"/>
|
<CheckCircle2 v-if="copied === 'namespace'" class="h-4 w-4 text-green-500 animate-in zoom-in-50"/>
|
||||||
<Copy v-else class="h-4 w-4"/>
|
<Copy v-else class="h-4 w-4"/>
|
||||||
@ -607,7 +624,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="text-xl font-semibold">已授权应用</h2>
|
<h2 class="text-xl font-semibold">已授权应用</h2>
|
||||||
<Button @click="showAuthorizeDialog = true" class="gap-2">
|
<Button class="gap-2" @click="showAuthorizeDialog = true">
|
||||||
<Plus class="h-4 w-4"/>
|
<Plus class="h-4 w-4"/>
|
||||||
授权新应用
|
授权新应用
|
||||||
</Button>
|
</Button>
|
||||||
@ -625,7 +642,7 @@ onMounted(async () => {
|
|||||||
<Package class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
<Package class="h-16 w-16 text-muted-foreground/50 mb-4"/>
|
||||||
<p class="text-lg font-medium text-muted-foreground mb-2">暂无授权应用</p>
|
<p class="text-lg font-medium text-muted-foreground mb-2">暂无授权应用</p>
|
||||||
<p class="text-sm text-muted-foreground mb-4">点击上方按钮授权您的第一个应用</p>
|
<p class="text-sm text-muted-foreground mb-4">点击上方按钮授权您的第一个应用</p>
|
||||||
<Button @click="showAuthorizeDialog = true" variant="outline">
|
<Button variant="outline" @click="showAuthorizeDialog = true">
|
||||||
<Plus class="h-4 w-4 mr-2"/>
|
<Plus class="h-4 w-4 mr-2"/>
|
||||||
授权应用
|
授权应用
|
||||||
</Button>
|
</Button>
|
||||||
@ -640,6 +657,7 @@ onMounted(async () => {
|
|||||||
<AppCard :app-id="group.appId"/>
|
<AppCard :app-id="group.appId"/>
|
||||||
|
|
||||||
<TokenList
|
<TokenList
|
||||||
|
:copied-id="copied"
|
||||||
:items="group.tokens.map(t => ({
|
:items="group.tokens.map(t => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
token: t.token,
|
token: t.token,
|
||||||
@ -649,13 +667,12 @@ onMounted(async () => {
|
|||||||
installedAt: t.installedAt,
|
installedAt: t.installedAt,
|
||||||
}))"
|
}))"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:copied-id="copied"
|
|
||||||
:show-app-column="false"
|
:show-app-column="false"
|
||||||
compact
|
compact
|
||||||
sort-by-time
|
sort-by-time
|
||||||
@copy="(item) => copyToClipboard(item.token, item.token)"
|
@copy="(item) => copyToClipboard(item.token, item.token)"
|
||||||
@revoke="confirmRevoke"
|
|
||||||
@open="(item) => { selectedToken = item; showTokenDialog = true }"
|
@open="(item) => { selectedToken = item; showTokenDialog = true }"
|
||||||
|
@revoke="confirmRevoke"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -767,9 +784,9 @@ onMounted(async () => {
|
|||||||
<span class="font-medium">令牌:</span>
|
<span class="font-medium">令牌:</span>
|
||||||
<code class="text-xs font-mono break-all">{{ selectedToken.token.slice(0, 8) }}...</code>
|
<code class="text-xs font-mono break-all">{{ selectedToken.token.slice(0, 8) }}...</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 w-7 ml-auto"
|
class="h-7 w-7 ml-auto"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
@click="copyToClipboard(selectedToken.token, selectedToken.token)"
|
@click="copyToClipboard(selectedToken.token, selectedToken.token)"
|
||||||
>
|
>
|
||||||
<CheckCircle2 v-if="copied === selectedToken.token" class="h-3.5 w-3.5 text-green-500"/>
|
<CheckCircle2 v-if="copied === selectedToken.token" class="h-3.5 w-3.5 text-green-500"/>
|
||||||
@ -783,13 +800,14 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="showTokenDialog = false">关闭</Button>
|
<Button variant="outline" @click="showTokenDialog = false">关闭</Button>
|
||||||
<Button variant="destructive" @click="() => { showTokenDialog = false; confirmRevoke(selectedToken) }">撤销</Button>
|
<Button variant="destructive" @click="() => { showTokenDialog = false; confirmRevoke(selectedToken) }">
|
||||||
|
撤销
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录弹框 -->
|
<!-- 登录弹框 -->
|
||||||
@ -804,15 +822,15 @@ onMounted(async () => {
|
|||||||
/> <!-- 设备注册弹框 -->
|
/> <!-- 设备注册弹框 -->
|
||||||
<DeviceRegisterDialog
|
<DeviceRegisterDialog
|
||||||
v-model="showRegisterDialog"
|
v-model="showRegisterDialog"
|
||||||
@confirm="handleDeviceRegistered"
|
|
||||||
:required="deviceRequired"
|
:required="deviceRequired"
|
||||||
|
@confirm="handleDeviceRegistered"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 设备名称编辑弹框 -->
|
<!-- 设备名称编辑弹框 -->
|
||||||
<EditDeviceNameDialog
|
<EditDeviceNameDialog
|
||||||
v-model="showEditNameDialog"
|
v-model="showEditNameDialog"
|
||||||
:device-uuid="deviceUuid"
|
|
||||||
:current-name="deviceInfo?.deviceName || ''"
|
:current-name="deviceInfo?.deviceName || ''"
|
||||||
|
:device-uuid="deviceUuid"
|
||||||
@success="handleDeviceNameUpdated"
|
@success="handleDeviceNameUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -820,9 +838,9 @@ onMounted(async () => {
|
|||||||
<EditNamespaceDialog
|
<EditNamespaceDialog
|
||||||
v-if="accountStore.isAuthenticated && deviceInfo"
|
v-if="accountStore.isAuthenticated && deviceInfo"
|
||||||
v-model="showEditNamespaceDialog"
|
v-model="showEditNamespaceDialog"
|
||||||
:device-uuid="deviceUuid"
|
|
||||||
:current-namespace="deviceInfo.namespace"
|
|
||||||
:account-token="accountStore.token"
|
:account-token="accountStore.token"
|
||||||
|
:current-namespace="deviceInfo.namespace"
|
||||||
|
:device-uuid="deviceUuid"
|
||||||
@success="handleNamespaceUpdated"
|
@success="handleNamespaceUpdated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,8 +11,33 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||||||
import { Table, TableBody, TableCaption, TableCell, TableEmpty, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import {
|
||||||
import { Download, Upload, Trash2, Plus, Loader2, Search, RefreshCw, Copy, Edit, Check, X, Key, ShieldCheck, Database } from 'lucide-vue-next'
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableEmpty,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
Trash2,
|
||||||
|
Plus,
|
||||||
|
Loader2,
|
||||||
|
Search,
|
||||||
|
RefreshCw,
|
||||||
|
Copy,
|
||||||
|
Edit,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Key,
|
||||||
|
ShieldCheck,
|
||||||
|
Database
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
// Token 与自动授权
|
// Token 与自动授权
|
||||||
const token = ref(localStorage.getItem('kv_token') || '')
|
const token = ref(localStorage.getItem('kv_token') || '')
|
||||||
@ -310,10 +335,17 @@ const previewValue = (v) => {
|
|||||||
if (v === undefined) return '点击查看'
|
if (v === undefined) return '点击查看'
|
||||||
try {
|
try {
|
||||||
return typeof v === 'string' ? v : JSON.stringify(v, null, 2)
|
return typeof v === 'string' ? v : JSON.stringify(v, null, 2)
|
||||||
} catch { return String(v) }
|
} catch {
|
||||||
|
return String(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const copy = async (text) => {
|
const copy = async (text) => {
|
||||||
try { await navigator.clipboard.writeText(text); toast.success('已复制') } catch { toast.error('复制失败') }
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast.success('已复制')
|
||||||
|
} catch {
|
||||||
|
toast.error('复制失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -337,7 +369,8 @@ const copy = async (text) => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<ShieldCheck class="h-5 w-5" /> 自动授权 / Token
|
<ShieldCheck class="h-5 w-5"/>
|
||||||
|
自动授权 / Token
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>通过命名空间快速获取 Token,或手动填写 Token</CardDescription>
|
<CardDescription>通过命名空间快速获取 Token,或手动填写 Token</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -349,16 +382,16 @@ const copy = async (text) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="pwd">授权密码(可选)</Label>
|
<Label for="pwd">授权密码(可选)</Label>
|
||||||
<Input id="pwd" type="password" v-model="autoAuth.password" placeholder="留空表示无密码" />
|
<Input id="pwd" v-model="autoAuth.password" placeholder="留空表示无密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="appid">App ID</Label>
|
<Label for="appid">App ID</Label>
|
||||||
<Input id="appid" disabled v-model="autoAuth.appId" placeholder="应用标识符" />
|
<Input id="appid" v-model="autoAuth.appId" disabled placeholder="应用标识符"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Button @click="acquireToken" :disabled="autoAuthLoading">
|
<Button :disabled="autoAuthLoading" @click="acquireToken">
|
||||||
<Loader2 v-if="autoAuthLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="autoAuthLoading" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
<Key v-else class="mr-2 h-4 w-4"/>
|
<Key v-else class="mr-2 h-4 w-4"/>
|
||||||
自动授权获取 Token
|
自动授权获取 Token
|
||||||
@ -366,7 +399,7 @@ const copy = async (text) => {
|
|||||||
<div class="flex-1"/>
|
<div class="flex-1"/>
|
||||||
<div class="flex items-center gap-2 min-w-[280px]">
|
<div class="flex items-center gap-2 min-w-[280px]">
|
||||||
<Input v-model="token" placeholder="或手动粘贴 Token"/>
|
<Input v-model="token" placeholder="或手动粘贴 Token"/>
|
||||||
<Button variant="outline" @click="clearToken" :disabled="!isTokenSet">清除</Button>
|
<Button :disabled="!isTokenSet" variant="outline" @click="clearToken">清除</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -379,30 +412,38 @@ const copy = async (text) => {
|
|||||||
<div class="flex items-center gap-2 md:w-[700px] w-full flex-wrap">
|
<div class="flex items-center gap-2 md:w-[700px] w-full flex-wrap">
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground"/>
|
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground"/>
|
||||||
<Input class="pl-8" v-model="searchText" placeholder="本地过滤关键字" />
|
<Input v-model="searchText" class="pl-8" placeholder="本地过滤关键字"/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" @click="loadKeys" :disabled="!isTokenSet">
|
<Button :disabled="!isTokenSet" variant="outline" @click="loadKeys">
|
||||||
<RefreshCw class="h-4 w-4 mr-2" /> 刷新
|
<RefreshCw class="h-4 w-4 mr-2"/>
|
||||||
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex items-center gap-2 min-w-[300px]">
|
<div class="flex items-center gap-2 min-w-[300px]">
|
||||||
<Input v-model="specificKey" placeholder="输入完整键名(加载单项)"/>
|
<Input v-model="specificKey" placeholder="输入完整键名(加载单项)"/>
|
||||||
<Button variant="outline" @click="loadSpecificKey" :disabled="!isTokenSet || !specificKey.trim()">加载项</Button>
|
<Button :disabled="!isTokenSet || !specificKey.trim()" variant="outline" @click="loadSpecificKey">
|
||||||
|
加载项
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:ml-auto flex items-center gap-2">
|
<div class="md:ml-auto flex items-center gap-2">
|
||||||
<Button variant="outline" @click="startImport" :disabled="!isTokenSet">
|
<Button :disabled="!isTokenSet" variant="outline" @click="startImport">
|
||||||
<Upload class="h-4 w-4 mr-2" /> 导入
|
<Upload class="h-4 w-4 mr-2"/>
|
||||||
|
导入
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" @click="exportAll" :disabled="!isTokenSet || keys.length===0">
|
<Button :disabled="!isTokenSet || keys.length===0" variant="outline" @click="exportAll">
|
||||||
<Download class="h-4 w-4 mr-2" /> 导出
|
<Download class="h-4 w-4 mr-2"/>
|
||||||
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
<Separator orientation="vertical" class="h-6" />
|
<Separator class="h-6" orientation="vertical"/>
|
||||||
<Button variant="secondary" @click="openCreate" :disabled="!isTokenSet">
|
<Button :disabled="!isTokenSet" variant="secondary" @click="openCreate">
|
||||||
<Plus class="h-4 w-4 mr-2" /> 新建
|
<Plus class="h-4 w-4 mr-2"/>
|
||||||
|
新建
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" @click="bulkDelete" :disabled="!isTokenSet || selected.size===0 || bulkDeleting">
|
<Button :disabled="!isTokenSet || selected.size===0 || bulkDeleting" variant="destructive"
|
||||||
|
@click="bulkDelete">
|
||||||
<Loader2 v-if="bulkDeleting" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="bulkDeleting" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
<Trash2 v-else class="h-4 w-4 mr-2" /> 删除所选
|
<Trash2 v-else class="h-4 w-4 mr-2"/>
|
||||||
|
删除所选
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -425,7 +466,9 @@ const copy = async (text) => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead class="w-10">
|
<TableHead class="w-10">
|
||||||
<Checkbox :checked="selected.size>0 && selected.size===pagedKeys.length" :indeterminate="selected.size>0 && selected.size<pagedKeys.length" @update:checked="val => { if(val){ pagedKeys.forEach(k=>selected.add(k)) } else { pagedKeys.forEach(k=>selected.delete(k)) } }" />
|
<Checkbox :checked="selected.size>0 && selected.size===pagedKeys.length"
|
||||||
|
:indeterminate="selected.size>0 && selected.size<pagedKeys.length"
|
||||||
|
@update:checked="val => { if(val){ pagedKeys.forEach(k=>selected.add(k)) } else { pagedKeys.forEach(k=>selected.delete(k)) } }"/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="min-w-[260px]">键名</TableHead>
|
<TableHead class="min-w-[260px]">键名</TableHead>
|
||||||
<TableHead>值预览</TableHead>
|
<TableHead>值预览</TableHead>
|
||||||
@ -435,32 +478,42 @@ const copy = async (text) => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow v-for="k in pagedKeys" :key="k">
|
<TableRow v-for="k in pagedKeys" :key="k">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Checkbox :checked="selected.has(k)" @update:checked="val => { val ? selected.add(k) : selected.delete(k) }" />
|
<Checkbox :checked="selected.has(k)"
|
||||||
|
@update:checked="val => { val ? selected.add(k) : selected.delete(k) }"/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
class="font-mono text-sm break-all cursor-pointer hover:underline"
|
class="font-mono text-sm break-all cursor-pointer hover:underline"
|
||||||
@click="openEdit(k)"
|
|
||||||
title="点击查看/编辑"
|
title="点击查看/编辑"
|
||||||
|
@click="openEdit(k)"
|
||||||
>
|
>
|
||||||
{{ k }}
|
{{ k }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div
|
<div
|
||||||
class="text-xs whitespace-pre-wrap max-h-40 overflow-auto rounded-md bg-muted p-2 cursor-pointer hover:bg-muted/70 transition-colors"
|
class="text-xs whitespace-pre-wrap max-h-40 overflow-auto rounded-md bg-muted p-2 cursor-pointer hover:bg-muted/70 transition-colors"
|
||||||
@click="openEdit(k)"
|
|
||||||
title="点击查看/编辑"
|
title="点击查看/编辑"
|
||||||
|
@click="openEdit(k)"
|
||||||
>
|
>
|
||||||
{{ previewValue(values[k]) }}
|
{{ previewValue(values[k]) }}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="space-x-1 whitespace-nowrap">
|
<TableCell class="space-x-1 whitespace-nowrap">
|
||||||
<Button size="sm" variant="outline" @click="copy(k)"><Copy class="h-3.5 w-3.5 mr-1" />复制键</Button>
|
<Button size="sm" variant="outline" @click="copy(k)">
|
||||||
<Button size="sm" variant="outline" @click="openEdit(k)"><Edit class="h-3.5 w-3.5 mr-1" />编辑</Button>
|
<Copy class="h-3.5 w-3.5 mr-1"/>
|
||||||
<Button size="sm" variant="destructive" @click="deleteKey(k)"><Trash2 class="h-3.5 w-3.5 mr-1" />删除</Button>
|
复制键
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" @click="openEdit(k)">
|
||||||
|
<Edit class="h-3.5 w-3.5 mr-1"/>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive" @click="deleteKey(k)">
|
||||||
|
<Trash2 class="h-3.5 w-3.5 mr-1"/>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-if="!pagedKeys.length">
|
<TableRow v-if="!pagedKeys.length">
|
||||||
<TableCell colspan="4" class="text-center text-muted-foreground py-10">暂无数据</TableCell>
|
<TableCell class="text-center text-muted-foreground py-10" colspan="4">暂无数据</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@ -469,7 +522,8 @@ const copy = async (text) => {
|
|||||||
<div class="flex items-center justify-between mt-4 text-sm">
|
<div class="flex items-center justify-between mt-4 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>每页</span>
|
<span>每页</span>
|
||||||
<select v-model.number="pageSize" class="h-9 w-[80px] rounded-md border border-input bg-background px-2">
|
<select v-model.number="pageSize"
|
||||||
|
class="h-9 w-[80px] rounded-md border border-input bg-background px-2">
|
||||||
<option :value="10">10</option>
|
<option :value="10">10</option>
|
||||||
<option :value="20">20</option>
|
<option :value="20">20</option>
|
||||||
<option :value="50">50</option>
|
<option :value="50">50</option>
|
||||||
@ -477,9 +531,11 @@ const copy = async (text) => {
|
|||||||
<span class="text-muted-foreground">共 {{ totalPages }} 页</span>
|
<span class="text-muted-foreground">共 {{ totalPages }} 页</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button variant="outline" :disabled="page===1" @click="page=Math.max(1,page-1)">上一页</Button>
|
<Button :disabled="page===1" variant="outline" @click="page=Math.max(1,page-1)">上一页</Button>
|
||||||
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
<span>第 {{ page }} / {{ totalPages }} 页</span>
|
||||||
<Button variant="outline" :disabled="page===totalPages" @click="page=Math.min(totalPages,page+1)">下一页</Button>
|
<Button :disabled="page===totalPages" variant="outline" @click="page=Math.min(totalPages,page+1)">
|
||||||
|
下一页
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -501,13 +557,14 @@ const copy = async (text) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label for="kv-val">值(JSON 或文本)</Label>
|
<Label for="kv-val">值(JSON 或文本)</Label>
|
||||||
<textarea id="kv-val" v-model="formValue" rows="10" class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono"></textarea>
|
<textarea id="kv-val" v-model="formValue" class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono"
|
||||||
|
rows="10"></textarea>
|
||||||
<p v-if="formError" class="text-sm text-red-500">{{ formError }}</p>
|
<p v-if="formError" class="text-sm text-red-500">{{ formError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="editOpen=false" :disabled="saving">取消</Button>
|
<Button :disabled="saving" variant="outline" @click="editOpen=false">取消</Button>
|
||||||
<Button @click="saveKeyValue" :disabled="saving">
|
<Button :disabled="saving" @click="saveKeyValue">
|
||||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
@ -523,12 +580,14 @@ const copy = async (text) => {
|
|||||||
<DialogDescription>JSON 对象的每个键会写入为一个 KV 项</DialogDescription>
|
<DialogDescription>JSON 对象的每个键会写入为一个 KV 项</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="space-y-3 py-2">
|
<div class="space-y-3 py-2">
|
||||||
<textarea v-model="importJson" rows="12" class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono" placeholder='{"key":"value"}'></textarea>
|
<textarea v-model="importJson" class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-mono"
|
||||||
|
placeholder='{"key":"value"}'
|
||||||
|
rows="12"></textarea>
|
||||||
<p v-if="importError" class="text-sm text-red-500">{{ importError }}</p>
|
<p v-if="importError" class="text-sm text-red-500">{{ importError }}</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="importOpen=false" :disabled="importing">取消</Button>
|
<Button :disabled="importing" variant="outline" @click="importOpen=false">取消</Button>
|
||||||
<Button @click="doImport" :disabled="importing">
|
<Button :disabled="importing" @click="doImport">
|
||||||
<Loader2 v-if="importing" class="mr-2 h-4 w-4 animate-spin"/>
|
<Loader2 v-if="importing" class="mr-2 h-4 w-4 animate-spin"/>
|
||||||
开始导入
|
开始导入
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -59,7 +59,7 @@ onMounted(async () => {
|
|||||||
<div class="container mx-auto px-6 py-4">
|
<div class="container mx-auto px-6 py-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="icon" @click="goBack">
|
<Button size="icon" variant="ghost" @click="goBack">
|
||||||
<ArrowLeft class="h-5 w-5"/>
|
<ArrowLeft class="h-5 w-5"/>
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
@ -75,14 +75,16 @@ onMounted(async () => {
|
|||||||
<div class="container mx-auto px-6 py-8 max-w-4xl">
|
<div class="container mx-auto px-6 py-8 max-w-4xl">
|
||||||
<!-- Success/Error Messages -->
|
<!-- Success/Error Messages -->
|
||||||
<div v-if="successMessage" class="mb-6">
|
<div v-if="successMessage" class="mb-6">
|
||||||
<div class="flex items-center gap-2 p-4 rounded-lg bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900">
|
<div
|
||||||
|
class="flex items-center gap-2 p-4 rounded-lg bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900">
|
||||||
<CheckCircle2 class="h-5 w-5 text-green-600 dark:text-green-400"/>
|
<CheckCircle2 class="h-5 w-5 text-green-600 dark:text-green-400"/>
|
||||||
<span class="text-green-800 dark:text-green-200">{{ successMessage }}</span>
|
<span class="text-green-800 dark:text-green-200">{{ successMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="mb-6">
|
<div v-if="errorMessage" class="mb-6">
|
||||||
<div class="flex items-center gap-2 p-4 rounded-lg bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900">
|
<div
|
||||||
|
class="flex items-center gap-2 p-4 rounded-lg bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900">
|
||||||
<AlertTriangle class="h-5 w-5 text-red-600 dark:text-red-400"/>
|
<AlertTriangle class="h-5 w-5 text-red-600 dark:text-red-400"/>
|
||||||
<span class="text-red-800 dark:text-red-200">{{ errorMessage }}</span>
|
<span class="text-red-800 dark:text-red-200">{{ errorMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -110,10 +112,10 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<Label class="text-sm font-medium">设备名称</Label>
|
<Label class="text-sm font-medium">设备名称</Label>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
@click="showEditNameDialog = true"
|
|
||||||
class="h-7"
|
class="h-7"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="showEditNameDialog = true"
|
||||||
>
|
>
|
||||||
<Edit class="h-3 w-3 mr-1"/>
|
<Edit class="h-3 w-3 mr-1"/>
|
||||||
编辑
|
编辑
|
||||||
@ -130,11 +132,11 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
||||||
<code class="flex-1 text-sm font-mono break-all">{{ deviceUuid }}</code>
|
<code class="flex-1 text-sm font-mono break-all">{{ deviceUuid }}</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8 flex-shrink-0"
|
class="h-8 w-8 flex-shrink-0"
|
||||||
@click="copyToClipboard(deviceUuid, 'uuid')"
|
size="icon"
|
||||||
title="复制 UUID"
|
title="复制 UUID"
|
||||||
|
variant="ghost"
|
||||||
|
@click="copyToClipboard(deviceUuid, 'uuid')"
|
||||||
>
|
>
|
||||||
<CheckCircle2 v-if="copied === 'uuid'" class="h-4 w-4 text-green-500"/>
|
<CheckCircle2 v-if="copied === 'uuid'" class="h-4 w-4 text-green-500"/>
|
||||||
<Copy v-else class="h-4 w-4"/>
|
<Copy v-else class="h-4 w-4"/>
|
||||||
@ -149,11 +151,11 @@ onMounted(async () => {
|
|||||||
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
<div class="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
||||||
<code class="flex-1 text-sm font-mono break-all">{{ deviceInfo?.namespace || deviceUuid }}</code>
|
<code class="flex-1 text-sm font-mono break-all">{{ deviceInfo?.namespace || deviceUuid }}</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8 flex-shrink-0"
|
class="h-8 w-8 flex-shrink-0"
|
||||||
@click="copyToClipboard(deviceInfo?.namespace || deviceUuid, 'namespace')"
|
size="icon"
|
||||||
title="复制命名空间"
|
title="复制命名空间"
|
||||||
|
variant="ghost"
|
||||||
|
@click="copyToClipboard(deviceInfo?.namespace || deviceUuid, 'namespace')"
|
||||||
>
|
>
|
||||||
<CheckCircle2 v-if="copied === 'namespace'" class="h-4 w-4 text-green-500"/>
|
<CheckCircle2 v-if="copied === 'namespace'" class="h-4 w-4 text-green-500"/>
|
||||||
<Copy v-else class="h-4 w-4"/>
|
<Copy v-else class="h-4 w-4"/>
|
||||||
@ -166,7 +168,6 @@ onMounted(async () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 设备管理部分 -->
|
<!-- 设备管理部分 -->
|
||||||
<h2 class="text-xl font-semibold mt-12 mb-4">设备管理</h2>
|
<h2 class="text-xl font-semibold mt-12 mb-4">设备管理</h2>
|
||||||
|
|
||||||
@ -185,8 +186,8 @@ onMounted(async () => {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
|
||||||
class="w-full flex items-center justify-center gap-2"
|
class="w-full flex items-center justify-center gap-2"
|
||||||
|
variant="destructive"
|
||||||
@click="showResetDeviceDialog = true"
|
@click="showResetDeviceDialog = true"
|
||||||
>
|
>
|
||||||
<RefreshCw class="h-4 w-4"/>
|
<RefreshCw class="h-4 w-4"/>
|
||||||
@ -199,11 +200,6 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 设备重置弹框 -->
|
<!-- 设备重置弹框 -->
|
||||||
<DeviceRegisterDialog
|
<DeviceRegisterDialog
|
||||||
v-model="showResetDeviceDialog"
|
v-model="showResetDeviceDialog"
|
||||||
@ -214,15 +210,15 @@ onMounted(async () => {
|
|||||||
<!-- 必需注册弹框 -->
|
<!-- 必需注册弹框 -->
|
||||||
<DeviceRegisterDialog
|
<DeviceRegisterDialog
|
||||||
v-model="showRegisterDialog"
|
v-model="showRegisterDialog"
|
||||||
@confirm="updateUuid"
|
|
||||||
:required="deviceRequired"
|
:required="deviceRequired"
|
||||||
|
@confirm="updateUuid"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 设备名称编辑弹框 -->
|
<!-- 设备名称编辑弹框 -->
|
||||||
<EditDeviceNameDialog
|
<EditDeviceNameDialog
|
||||||
v-model="showEditNameDialog"
|
v-model="showEditNameDialog"
|
||||||
:device-uuid="deviceUuid"
|
|
||||||
:current-name="deviceInfo?.deviceName || ''"
|
:current-name="deviceInfo?.deviceName || ''"
|
||||||
|
:device-uuid="deviceUuid"
|
||||||
@success="handleDeviceNameUpdated"
|
@success="handleDeviceNameUpdated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -55,7 +55,8 @@ export const useAccountStore = defineStore('account', () => {
|
|||||||
let delay = expMs - now - lead
|
let delay = expMs - now - lead
|
||||||
if (delay < 5000) delay = 5000
|
if (delay < 5000) delay = 5000
|
||||||
proactiveTimer = setTimeout(() => {
|
proactiveTimer = setTimeout(() => {
|
||||||
refreshNow().catch(() => {})
|
refreshNow().catch(() => {
|
||||||
|
})
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@ -118,10 +119,12 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
/* Reserve space for the vertical scrollbar to avoid layout shift/flicker */
|
/* Reserve space for the vertical scrollbar to avoid layout shift/flicker */
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
@ -134,11 +137,13 @@
|
|||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter-from,
|
.page-enter-from,
|
||||||
.page-leave-to {
|
.page-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter-to,
|
.page-enter-to,
|
||||||
.page-leave-from {
|
.page-leave-from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user