From 473ffc2f5097110ca861a236cc00118a3fba61f6 Mon Sep 17 00:00:00 2001 From: SunWuyuan Date: Wed, 8 Oct 2025 14:10:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=A4=E7=89=8C?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=89=E6=97=B6=E9=97=B4=E6=8E=92=E5=BA=8F=E5=92=8C=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=9B=E4=BC=98=E5=8C=96=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=BA=94=E7=94=A8=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RelativeTime.vue | 62 +++++++++++++ src/components/TokenList.vue | 147 +++++++++++++++++++++++++++++ src/pages/index.vue | 160 ++++++++++++++++++-------------- src/style.css | 6 ++ 4 files changed, 304 insertions(+), 71 deletions(-) create mode 100644 src/components/RelativeTime.vue create mode 100644 src/components/TokenList.vue diff --git a/src/components/RelativeTime.vue b/src/components/RelativeTime.vue new file mode 100644 index 0000000..36c0fdc --- /dev/null +++ b/src/components/RelativeTime.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/TokenList.vue b/src/components/TokenList.vue new file mode 100644 index 0000000..7e4857d --- /dev/null +++ b/src/components/TokenList.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/pages/index.vue b/src/pages/index.vue index c65f05f..78c284a 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -14,6 +14,7 @@ import { Plus, Trash2, Key, Shield, RefreshCw, Copy, CheckCircle2, Settings, Pac import DropdownMenu from '@/components/ui/dropdown-menu/DropdownMenu.vue' import DropdownItem from '@/components/ui/dropdown-menu/DropdownItem.vue' import AppCard from '@/components/AppCard.vue' +import TokenList from '@/components/TokenList.vue' import PasswordInput from '@/components/PasswordInput.vue' import LoginDialog from '@/components/LoginDialog.vue' import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue' @@ -39,6 +40,7 @@ const showEditNameDialog = ref(false) const showUserMenu = ref(false) const deviceRequired = ref(false) // 标记是否必须注册设备 const selectedToken = ref(null) +const showTokenDialog = ref(false) // Form data const appIdToAuthorize = ref('') @@ -56,19 +58,22 @@ const { handleOAuthCallback } = useOAuthCallback() // 使用计算属性来获取是否有密码 const hasPassword = computed(() => deviceInfo.value?.hasPassword || false) -// Group tokens by appId -const groupedByApp = computed(() => { +// 为 TokenList 扁平化数据并附带 appName +const flatTokenList = computed(() => { + return tokens.value.map(t => ({ + ...t, + appName: appInfoCache.value[t.appId]?.name || null, + })) +}) + +// 按应用分组以用于“应用卡片 + 下方小列表”布局 +const groupedTokens = computed(() => { const groups = {} - tokens.value.forEach(token => { - const appId = token.appId - if (!groups[appId]) { - groups[appId] = { - appId: appId, - tokens: [] - } - } - groups[appId].tokens.push(token) - }) + for (const t of tokens.value) { + const id = t.appId + if (!groups[id]) groups[id] = { appId: id, tokens: [] } + groups[id].tokens.push(t) + } return Object.values(groups) }) @@ -581,7 +586,7 @@ onMounted(async () => {
-
{{ groupedByApp.length }}
+
{{ new Set(tokens.map(t => t.appId)).size }}
应用数
@@ -612,7 +617,7 @@ onMounted(async () => {
-

已授权应用

+

已授权应用

- +

暂无授权应用

@@ -637,67 +642,33 @@ onMounted(async () => {
- -
- - - -
-
-
- - - {{ token.token }} - - -
+ -
- {{ token.note }} -
+ -
-
- - {{ formatDate(token.installedAt) }} -
- -
-
- -
-
- -
@@ -759,7 +730,7 @@ onMounted(async () => { 撤销授权 - 确定要撤销此令牌的授权吗?此操作无法撤销。{{selectedToken}} + 确定要撤销此令牌的授权吗?此操作无法撤销。
@@ -812,6 +783,53 @@ onMounted(async () => { + + + + + 令牌详情 + + 查看并对该令牌执行操作 + + +
+
+ 备注: + {{ selectedToken.note || '—' }} +
+
+ 应用名称: + {{ selectedToken.appName }} +
+
+ 应用ID: + {{ selectedToken.appId }} +
+
+ 令牌: + {{ selectedToken.token.slice(0, 8) }}... + +
+
+ + {{ formatDate(selectedToken.installedAt) }} +
+
+ + + + +
+
+ diff --git a/src/style.css b/src/style.css index cc27a7f..ac7b088 100644 --- a/src/style.css +++ b/src/style.css @@ -118,6 +118,10 @@ * { @apply border-border outline-ring/50; } + html { + /* Reserve space for the vertical scrollbar to avoid layout shift/flicker */ + scrollbar-gutter: stable; + } body { @apply bg-background text-foreground; } @@ -127,6 +131,8 @@ .page-enter-active, .page-leave-active { transition: opacity 200ms ease, transform 200ms ease; + will-change: opacity; + } .page-enter-from, .page-leave-to {