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:
@@ -1,151 +1,150 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4">
|
||||
<!-- Breadcrumb and Title -->
|
||||
<div class="space-y-5">
|
||||
<BreadcrumbHeader />
|
||||
|
||||
<!-- Search and Controls -->
|
||||
<!-- <div v-if="userStore.loading" class="loading loading-spinner loading-lg"></div> -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<input
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[120px] lg:w-[250px]"
|
||||
placeholder="Filter" value="">
|
||||
|
||||
<div class="dropdown">
|
||||
<label tabindex="0"
|
||||
class="inline-flex items-center justify-center flex gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground rounded-md px-3 text-xs h-8 border-dashed">
|
||||
<svg viewBox="0 0 15 15" width="1.2em" height="1.2em" class="mr-2 h-4 w-4">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M7.5.877a6.623 6.623 0 1 0 0 13.246A6.623 6.623 0 0 0 7.5.877M1.827 7.5a5.673 5.673 0 1 1 11.346 0a5.673 5.673 0 0 1-11.346 0M7.5 4a.5.5 0 0 1 .5.5V7h2.5a.5.5 0 1 1 0 1H8v2.5a.5.5 0 0 1-1 0V8H4.5a.5.5 0 0 1 0-1H7V4.5a.5.5 0 0 1 .5-.5"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Status
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu shadow-lg bg-base-100 rounded-none w-24 px-0">
|
||||
<li v-for="status in statusOptions" :key="status" class="px-0 mx-0">
|
||||
<a class="px-2 mx-0 hover:rounded-none">
|
||||
<input type="checkbox" :checked="selectedStatuses.some(item => item.status === status)"
|
||||
@change="toggleStatusFilter(status)" class="checkbox checkbox-xs" />
|
||||
{{ status }}
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<p class="text-sm text-base-content/60">Team members and their access.</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="dropdown dropdown-end">
|
||||
<button tabindex="0" class="btn btn-ghost btn-square btn-sm" aria-label="Batch actions">
|
||||
<Settings2Icon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
<ul tabindex="0" class="menu dropdown-content z-30 mt-2 w-36 rounded-box border border-base-300/60 bg-base-100 p-1.5 text-sm shadow-lg">
|
||||
<li>
|
||||
<button class="text-error" @click="handleBatchAction('delete')">
|
||||
<TrashIcon class="h-4 w-4" aria-hidden="true" />Delete
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="handleBatchAction('disable')">
|
||||
<BadgeXIcon class="h-4 w-4" aria-hidden="true" />Disable
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="handleBatchAction('enable')">
|
||||
<BadgeCheckIcon class="h-4 w-4" aria-hidden="true" />Enable
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center justify-end space-x-1">
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
<button class="btn btn-primary btn-sm" @click="openModal" aria-label="Create new user">
|
||||
<PlusIcon class="h-4 w-4" aria-hidden="true" />New User
|
||||
</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>
|
||||
<UserNew @closeModal="closeModal" />
|
||||
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm p-1 h-8 w-8">
|
||||
<Settings2Icon class="w-6 h-6" />
|
||||
</div>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box z-[1] w-20 text-sm">
|
||||
<li>
|
||||
<div class="btn btn-xs p-1 text-xs hover:bg-rose-100 hover:text-rose-600 transition-colors"
|
||||
@click="handleBatchAction('delete')">
|
||||
<TrashIcon class="w-4 h-4" />删除
|
||||
</div>
|
||||
</li>
|
||||
<hr class="-mx-2 my-1 border-base-content/10">
|
||||
<li>
|
||||
<div class="btn btn-xs p-1 text-xs hover:bg-rose-100 hover:text-rose-600 transition-colors"
|
||||
@click="handleBatchAction('disable')">
|
||||
<BadgeXIcon class="w-4 h-4" />禁用
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="btn btn-xs p-1 text-xs hover:bg-green-100 hover:text-green-600 transition-colors"
|
||||
@click="handleBatchAction('enable')">
|
||||
<BadgeCheckIcon class="w-4 h-4" />启用
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Status filter -->
|
||||
<details class="dropdown dropdown-end" v-if="users.length">
|
||||
<summary class="btn btn-outline btn-sm h-8 min-h-8 border-dashed">
|
||||
<ListFilterIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Status
|
||||
<span v-if="selectedStatuses.length" class="badge badge-primary badge-sm">{{ selectedStatuses.length }}</span>
|
||||
</summary>
|
||||
<ul tabindex="0" class="menu dropdown-content z-30 mt-2 w-32 rounded-box border border-base-300/60 bg-base-100 p-1.5 shadow-lg">
|
||||
<li v-for="status in statusOptions" :key="status">
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input type="checkbox" class="checkbox checkbox-xs"
|
||||
:checked="selectedStatuses.some(item => item.status === status)"
|
||||
@change="toggleStatusFilter(status)" />
|
||||
{{ status }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card bg-base-100 shadow-xs overflow-x-auto dark:bg-base-200">
|
||||
<table class="table table-sm">
|
||||
<!-- Table Header -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="selectAll" @change="toggleSelectAll" />
|
||||
</th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>Quota</th>
|
||||
<th>Used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Table Body -->
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id"
|
||||
class="hover:bg-gray-500/50 dark:hover:bg-neutral-600 transition-colors">
|
||||
<td>
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="user.selected"
|
||||
@change="toggleUserSelection(user)" />
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ user.id }}</td>
|
||||
<td class="text-xs dark:text-white">{{ user.username }}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="user.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="user.active" @change="updateStatus(user)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">
|
||||
<template v-if="user.unlimited_quota">
|
||||
<Infinity />
|
||||
</template>
|
||||
<template v-else>{{ user.quota }}</template>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ user.used_quota }}</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square " @click="viewUser(user)">
|
||||
<EyeIcon class="w-4 h-4 dark:text-white" />
|
||||
<div class="card border border-base-300/60 bg-base-100 shadow-sm">
|
||||
<div class="overflow-x-auto" v-if="users.length">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr class="text-xs uppercase tracking-wider text-base-content/50">
|
||||
<th class="pl-4">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="selectAll" @change="toggleSelectAll"
|
||||
aria-label="Select all users" />
|
||||
</th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Active</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="user in users" :key="user.id" class="border-base-300/40 hover:bg-base-200/50">
|
||||
<td class="pl-4">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="user.selected"
|
||||
@change="toggleUserSelection(user)" :aria-label="`Select user ${user.username}`" />
|
||||
</td>
|
||||
<td class="tabular-nums text-base-content/60">{{ user.id }}</td>
|
||||
<td class="max-w-40 truncate font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="placeholder avatar">
|
||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-neutral text-neutral-content">
|
||||
<span class="text-[10px] font-medium">{{ user.username?.[0]?.toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ user.username }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-success toggle-sm"
|
||||
:class="!user.active && 'toggle-error'" v-model="user.active" @change="updateStatus(user)"
|
||||
:aria-label="`Toggle user ${user.username}`" />
|
||||
</td>
|
||||
<td class="text-right tabular-nums">
|
||||
<template v-if="user.unlimited_quota">
|
||||
<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>{{ user.quota }}</template>
|
||||
</td>
|
||||
<td class="text-right tabular-nums">{{ user.used_quota }}</td>
|
||||
<td class="pr-3">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<button class="btn btn-ghost btn-xs btn-square" @click="viewUser(user)" aria-label="View user details">
|
||||
<EyeIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
<button v-if="user.role < 20" class="btn btn-ghost btn-xs btn-square text-error"
|
||||
@click="confirmDeleteUser(user)" aria-label="Delete user">
|
||||
<TrashIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除" v-if="user.role < 20">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30"
|
||||
@click="confirmDeleteUser(user)">
|
||||
<TrashIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="flex flex-col items-center gap-2 px-4 py-14 text-center">
|
||||
<UsersRoundIcon class="h-10 w-10 text-base-content/20" aria-hidden="true" />
|
||||
<h2 class="text-sm font-semibold">No users yet</h2>
|
||||
<p class="max-w-xs text-sm text-base-content/60">
|
||||
Invite teammates by creating accounts with their own quotas.
|
||||
</p>
|
||||
<button class="btn btn-primary btn-sm mt-2" @click="openModal">
|
||||
<PlusIcon class="h-4 w-4" aria-hidden="true" />Create User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
<Pagination v-if="totalItems > 0" :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" />
|
||||
|
||||
<!-- New user 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>
|
||||
<UserNew @closeModal="closeModal" />
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button aria-label="Close dialog">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +159,7 @@ import { useToast } from '@/composables/toast';
|
||||
import type { UserInfo } from '@/types';
|
||||
import {
|
||||
BadgeXIcon, BadgeCheckIcon, EyeIcon, PlusIcon, Settings2Icon,
|
||||
TrashIcon, Infinity
|
||||
TrashIcon, UsersRoundIcon, Infinity, ListFilterIcon
|
||||
} from '@lucide/vue';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -176,7 +175,6 @@ const totalItems = computed(() => userStore.totalUsers);
|
||||
// 封装公共的用户列表获取方法
|
||||
const listUsers = async (size?: number, page?: number, active?: boolean[] | boolean) => {
|
||||
currentPage.value = page || currentPage.value;
|
||||
// console.log('pagesize', pageSize.value, 'page', currentPage.value, 'active', selectedStatuses.map(status => status.value));
|
||||
await userStore.listUser(size ?? pageSize.value, page ?? currentPage.value, active ?? selectedStatuses.map(status => status.value));
|
||||
};
|
||||
|
||||
@@ -246,9 +244,6 @@ const handleBatchAction = async (action: string) => {
|
||||
if (!['enable', 'disable', 'delete'].includes(action)) {
|
||||
return setToast(`无效的操作 ${action}`, 'error');
|
||||
}
|
||||
if (selectedUsers.value.length === 0) {
|
||||
return setToast('请选择用户', 'error');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await userStore.userOption(action, selectedUsers.value.map(user => user.id));
|
||||
@@ -315,6 +310,9 @@ const deleteUser = async (user: UserInfo) => {
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref<HTMLDialogElement | null>(null);
|
||||
const openModal = () => {
|
||||
modalRef.value?.showModal();
|
||||
};
|
||||
const closeModal = async () => {
|
||||
if (modalRef.value) {
|
||||
modalRef.value.close();
|
||||
|
||||
Reference in New Issue
Block a user