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 {