feat(ui): adopt Tailwind v4 design system and archive change

- Add tailwindcss v4 + @tailwindcss/vite to mall, shop-admin, admin
- Add @vmall/shared/theme.css tokens with html[data-accent] presets
- Add @vmall/ui kit (VBtn/VBadge/VField/VInput/VCard/VPanel/VTable/VPage,
  VAccentSwatch, useAccent) as a Nuxt module
- Convert all three apps to kit + utilities; delete ui.css/mall.css and
  every <style scoped>; consoles get accent presets, mall locked to red
- Fix VCard boolean prop default (padding) and PDP/store stale
  useAsyncData keys on param navigation
- Archive adopt-tailwind-design-system; new frontend-ui capability spec
This commit is contained in:
Chengdong Zhang
2026-09-22 18:22:50 +08:00
parent 4d2ee3b0bf
commit 0d0e10b97b
101 changed files with 2833 additions and 7207 deletions
+32 -100
View File
@@ -49,110 +49,42 @@ const cards = computed(() => [
</script>
<template>
<div class="page">
<div class="page-head">
<div>
<p class="eyebrow">{{ $t("common.appName") }} · Admin</p>
<h1 class="page-title">{{ $t("admin.dashboardTitle") }}</h1>
</div>
<button class="btn sm" type="button" :disabled="loading" @click="loadDashboard">
{{ $t("admin.refresh") }}
</button>
</div>
<p v-if="loading" class="muted">{{ $t("common.loading") }}</p>
<p v-else-if="errorMessage" class="error-text" role="alert">{{ errorMessage }}</p>
<VPage :title="$t('admin.dashboardTitle')">
<template #actions>
<VBtn size="sm" :disabled="loading" @click="loadDashboard">{{ $t("admin.refresh") }}</VBtn>
</template>
<p class="mb-4 text-xs font-bold uppercase tracking-[0.08em] text-primary">{{ $t("common.appName") }} · Admin</p>
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
<p v-else-if="errorMessage" class="my-2 text-sm text-danger" role="alert">{{ errorMessage }}</p>
<template v-else>
<div class="stat-grid">
<NuxtLink v-for="card in cards" :key="card.link" :to="card.link" class="stat-card">
<span class="stat-label">{{ $t(card.label) }}</span>
<strong class="stat-value" :class="`tone-${card.tone}`">{{ card.value }}</strong>
<span class="stat-link">{{ $t("admin.open") }} </span>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<NuxtLink
v-for="card in cards"
:key="card.link"
:to="card.link"
class="flex flex-col gap-2 rounded-lg border border-border bg-surface p-5 shadow-sm transition-transform hover:-translate-y-0.5 hover:border-primary"
>
<span class="text-[13px] text-muted">{{ $t(card.label) }}</span>
<strong
class="text-[32px] leading-none"
:class="card.tone === 'green' ? 'text-success' : card.tone === 'orange' ? 'text-warning' : 'text-primary'"
>{{ card.value }}</strong>
<span class="text-xs text-primary">{{ $t("admin.open") }} </span>
</NuxtLink>
</div>
<div class="grid shortcuts">
<NuxtLink class="card shortcut" to="/users">
<strong>{{ $t("admin.userManagement") }}</strong>
<span class="muted">{{ $t("nav.users") }}</span>
</NuxtLink>
<NuxtLink class="card shortcut" to="/shops">
<strong>{{ $t("admin.shopManagement") }}</strong>
<span class="muted">{{ $t("nav.shops") }}</span>
</NuxtLink>
<NuxtLink class="card shortcut" to="/orders">
<strong>{{ $t("admin.orderManagement") }}</strong>
<span class="muted">{{ $t("nav.orders") }}</span>
</NuxtLink>
<NuxtLink class="card shortcut" to="/currencies">
<strong>{{ $t("admin.currencyManagement") }}</strong>
<span class="muted">{{ $t("nav.currencies") }}</span>
<div class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<NuxtLink v-for="shortcut in [
{ to: '/users', title: 'admin.userManagement', label: 'nav.users' },
{ to: '/shops', title: 'admin.shopManagement', label: 'nav.shops' },
{ to: '/orders', title: 'admin.orderManagement', label: 'nav.orders' },
{ to: '/currencies', title: 'admin.currencyManagement', label: 'nav.currencies' },
]" :key="shortcut.to" :to="shortcut.to" class="block no-underline hover:no-underline">
<VCard>
<strong class="block">{{ $t(shortcut.title) }}</strong>
<span class="text-sm text-muted">{{ $t(shortcut.label) }}</span>
</VCard>
</NuxtLink>
</div>
</template>
</div>
</VPage>
</template>
<style scoped>
.eyebrow {
color: var(--primary);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
margin: 0 0 6px;
text-transform: uppercase;
}
.stat-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
text-decoration: none;
transition: transform 150ms ease, border-color 150ms ease;
}
.stat-card:hover {
border-color: var(--primary);
text-decoration: none;
transform: translateY(-2px);
}
.stat-label {
color: var(--muted);
font-size: 13px;
}
.stat-value {
font-size: 32px;
line-height: 1;
}
.tone-blue { color: var(--primary); }
.tone-green { color: var(--success); }
.tone-orange { color: var(--warning); }
.tone-purple { color: #7257b8; }
.stat-link {
color: var(--primary);
font-size: 12px;
}
.shortcuts {
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 24px;
}
.shortcut {
display: flex;
flex-direction: column;
gap: 4px;
text-decoration: none;
}
.shortcut:hover { text-decoration: none; }
@media (max-width: 900px) {
.stat-grid, .shortcuts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
.stat-grid, .shortcuts { grid-template-columns: 1fr; }
}
</style>