redesign: site-wide UI/UX overhaul per web-interface-guidelines

- unified design system: focus-visible rings, reduced-motion, color-scheme,
  theme persistence, tightened emerald radius tokens (checkbox circle bug)
- redesigned all pages: Home/Login/Signup/404, dashboard shell, Overview hero
  (time-based gradient), list pages with empty states, form modals, detail pages
- role-scoped navigation: console menu vs admin console with cross-switch
  entries, Chinese route titles and breadcrumbs
- theme switcher (light/dark/auto) via composables/theme, home + dashboard
- state-driven dropdowns (touch-safe logout), menu divider pointer-events fix
- btn-primary unified to black-on-white / white-on-black per theme
- update REFACTOR_PLAN.md (increments 2-6)
This commit is contained in:
Sakurasan
2026-08-30 01:35:10 +08:00
parent ac0a6808ec
commit 1e07c5903f
26 changed files with 2189 additions and 2323 deletions
+76 -75
View File
@@ -1,105 +1,103 @@
<template>
<div class="min-h-screen bg-base-100 p-4">
<!-- Breadcrumb and Title -->
<div class="space-y-5">
<BreadcrumbHeader />
<div class="flex flex-wrap gap-2 mb-4 justify-end items-center">
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
<PlusIcon class="w-3.5 h-3.5" />New
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="text-sm text-base-content/60">Tokens authenticate OpenAI-compatible clients with your team.</p>
<button class="btn btn-primary btn-sm" @click="openModal" aria-label="Create new token">
<PlusIcon class="h-4 w-4" aria-hidden="true" />New Token
</button>
<dialog id="myModal" class="modal" ref="modalRef">
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
</form>
<TokenNew @closeModal="closeModal" />
</div>
<form method="dialog" class="modal-backdrop">
<button>关闭</button>
</form>
</dialog>
</div>
<!-- Table -->
<div class="card card-bordered bg-base-100 shadow-sm mt-6">
<div class="card bg-base-100 shadow-xs overflow-x-auto dark:bg-base-200" v-if="user">
<table class="table table-sm w-full">
<div v-if="user" class="card border border-base-300/60 bg-base-100 shadow-sm">
<div class="overflow-x-auto" v-if="user.tokens && user.tokens.length">
<table class="table table-sm">
<thead>
<tr class="text-xs text-base-content/70 uppercase bg-base-200">
<th class="px-2 py-3">Token</th>
<th class="px-2 py-3">Status</th>
<!-- <th class="px-2 py-3">Key</th> -->
<th class="px-2 py-3">Expired</th>
<th class="px-2 py-3">Quota</th>
<th class="px-2 py-3">Used</th>
<th class="text-right px-2 py-3"></th>
<tr class="text-xs uppercase tracking-wider text-base-content/50">
<th class="pl-4">Token</th>
<th>Status</th>
<th>Expired</th>
<th class="text-right">Quota</th>
<th class="text-right">Used</th>
<th class="pr-4 text-right"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
<tr v-for="token in user.tokens" :key="token.id" class="hover">
<td class="font-mono text-xs px-2 py-3">{{ token.name }}</td>
<tr v-for="token in user.tokens" :key="token.id" class="border-base-300/40 hover:bg-base-200/50">
<td class="pl-4 font-medium">{{ token.name }}</td>
<td>
<input type="checkbox" class="toggle toggle-xs"
:class="token.active ? 'toggle-success' : 'toggle-error'" v-model="token.active"
@change="updateStatus(token)" />
<input type="checkbox" class="toggle toggle-success toggle-sm"
:class="!token.active && 'toggle-error'" v-model="token.active"
@change="updateStatus(token)" :aria-label="`Toggle token ${token.name}`" />
</td>
<!-- <td class="font-mono text-xs px-2 py-3">{{ token.key }}</td> -->
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at ?? 0) }}</td>
<td class="px-2 py-3">
<td class="tabular-nums text-base-content/70">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at ?? 0) }}</td>
<td class="text-right tabular-nums">
<template v-if="token.unlimited_quota">
<Infinity />
<Infinity class="inline h-4 w-4 text-base-content/60" aria-label="Unlimited quota" />
<span class="sr-only">Unlimited</span>
</template>
<template v-else>{{ token.quota }}</template>
</td>
<td class="px-2 py-3">{{ token.used_quota }}</td>
<td class="text-right px-1 py-3">
<div class="flex items-center gap-2.5">
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open pt-1" data-tip="预览">
<button class="btn btn-ghost btn-xs btn-square" onclick="" @click="viewToken(token)">
<EyeIcon class="w-5 h-5 dark:text-white" />
</button>
</div>
<div class="md:tooltip" data-tip="clean used">
<button class="btn btn-ghost btn-xs btn-square text-sky-300 mt-1" @click="cleanUsedToken(token)"
aria-label="Revoke token">
<Eraser class="w-5 h-5" />
</button>
</div>
<button v-if="token.name !== 'default'"
class="btn btn-ghost btn-xs btn-square text-error items-center" @click="confirmRevokeToken(token)"
aria-label="Revoke token">
<TrashIcon class="w-5 h-5" />
<td class="text-right tabular-nums">{{ token.used_quota }}</td>
<td class="pr-3 text-right">
<div class="flex items-center justify-end gap-1">
<button class="btn btn-ghost btn-xs btn-square" @click="viewToken(token)" aria-label="Preview token key">
<EyeIcon class="h-4 w-4" aria-hidden="true" />
</button>
<button v-if="(token.used_quota ?? 0) > 0" class="btn btn-ghost btn-xs btn-square text-info"
@click="cleanUsedToken(token)" aria-label="Reset used quota">
<Eraser class="h-4 w-4" aria-hidden="true" />
</button>
<button v-if="token.name !== 'default'" class="btn btn-ghost btn-xs btn-square text-error"
@click="confirmRevokeToken(token)" aria-label="Revoke token">
<TrashIcon class="h-4 w-4" aria-hidden="true" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
<dialog id="myToken" class="modal" ref="tokenRef">
<div class="modal-box px-0 sm:px-8">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
</form>
<QRCodeCard :value="qrCodeValue" :size="120" />
</div>
<form method="dialog" class="modal-backdrop">
<button>关闭</button>
</form>
</dialog>
</div>
<p v-else class="text-center text-base-content/70 py-4">No tokens found</p>
<!-- Empty state -->
<div v-else class="flex flex-col items-center gap-2 px-4 py-14 text-center">
<Braces class="h-10 w-10 text-base-content/20" aria-hidden="true" />
<h2 class="text-sm font-semibold">No tokens yet</h2>
<p class="max-w-xs text-sm text-base-content/60">
Create a token to connect OpenCat, BotGem and other OpenAI-compatible clients.
</p>
<button class="btn btn-primary btn-sm mt-2" @click="openModal">
<PlusIcon class="h-4 w-4" aria-hidden="true" />Create Token
</button>
</div>
</div>
<!-- </div> -->
<!-- New token modal -->
<dialog ref="modalRef" class="modal">
<div class="modal-box max-w-3xl px-0 sm:px-6">
<form method="dialog">
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" aria-label="Close dialog">✕</button>
</form>
<TokenNew @closeModal="closeModal" />
</div>
<form method="dialog" class="modal-backdrop">
<button aria-label="Close dialog">close</button>
</form>
</dialog>
<!-- Token QR modal -->
<dialog ref="tokenRef" class="modal">
<div class="modal-box max-w-sm px-0 sm:px-6">
<form method="dialog">
<button class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" aria-label="Close dialog">✕</button>
</form>
<QRCodeCard :value="qrCodeValue" :size="120" />
</div>
<form method="dialog" class="modal-backdrop">
<button aria-label="Close dialog">close</button>
</form>
</dialog>
</div>
</template>
@@ -110,7 +108,7 @@ import QRCodeCard from '@/components/common/QRCodeCard.vue';
import TokenNew from '@/views/dashboard/TokenNew.vue';
import { useAuthStore } from '@/stores/auth';
import {
EyeIcon, PlusIcon, TrashIcon, Infinity, Eraser
EyeIcon, PlusIcon, TrashIcon, Infinity, Eraser, Braces
} from '@lucide/vue';
import { unixToDate } from '@/utils/format-date';
import { useToast } from '@/composables/toast';
@@ -203,6 +201,9 @@ const qrCodeValue = ref('');
// 关闭模态框
const modalRef = ref<HTMLDialogElement | null>(null);
const openModal = () => {
modalRef.value?.showModal();
};
const closeModal = async () => {
if (modalRef.value) {
modalRef.value.close();