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
+39 -115
View File
@@ -31,145 +31,69 @@ const cats = computed(() =>
const active = computed(() => cats.value.find((g) => g.cat.id === hoverId.value) ?? null);
const rootClass = computed(() =>
props.pinned
? "relative z-[700] flex h-hero w-[240px]"
: "absolute left-0 top-10 z-[700] flex w-[960px]",
);
const railClass = computed(() =>
props.pinned
? "box-border h-hero min-h-[450px] w-[240px] list-none bg-[rgba(30,30,30,0.92)] py-2"
: "min-h-[434px] w-[240px] list-none bg-[rgba(30,30,30,0.92)] py-2",
);
const subClass = computed(() =>
props.pinned
? "absolute left-[240px] top-0 box-border h-hero min-h-[450px] w-[700px] overflow-auto bg-surface px-[30px] py-5 shadow-lg"
: "min-h-[434px] flex-1 bg-surface px-[30px] py-5 shadow-lg",
);
defineExpose({ open });
</script>
<template>
<div class="catmenu" :class="{ pinned: props.pinned }" @mouseleave="hoverId = null">
<ul class="lvl1">
<div :class="rootClass" @mouseleave="hoverId = null">
<ul :class="railClass">
<li
v-for="g in cats"
:key="g.cat.id"
:class="{ on: hoverId === g.cat.id }"
class="cursor-pointer px-[15px] py-[10px]"
:class="hoverId === g.cat.id ? 'bg-surface' : ''"
@mouseenter="hoverId = g.cat.id"
>
<NuxtLink :to="`/search?category=${g.cat.id}`" class="c1">{{ pick(g.cat.name, locale) }}</NuxtLink>
<span class="c2s">
<NuxtLink
:to="`/search?category=${g.cat.id}`"
class="block text-sm no-underline"
:class="hoverId === g.cat.id ? 'text-primary' : 'text-white'"
>{{ pick(g.cat.name, locale) }}</NuxtLink>
<span class="mt-0.5 block">
<NuxtLink
v-for="cc in g.children.slice(0, 3)"
:key="cc.cat.id"
:to="`/search?category=${cc.cat.id}`"
class="c2"
class="mr-2 text-[12px] no-underline"
:class="hoverId === g.cat.id ? 'text-primary' : 'text-[#bbb] hover:text-white'"
>{{ pick(cc.cat.name, locale) }}</NuxtLink>
</span>
</li>
</ul>
<div v-if="active" class="sub">
<div v-for="cc in active.children" :key="cc.cat.id" class="sub-row">
<NuxtLink :to="`/search?category=${cc.cat.id}`" class="sub-title">{{ pick(cc.cat.name, locale) }}</NuxtLink>
<div class="sub-items">
<div v-if="active" :class="subClass">
<div v-for="cc in active.children" :key="cc.cat.id" class="flex border-b border-border py-3 last:border-b-0">
<NuxtLink
:to="`/search?category=${cc.cat.id}`"
class="w-[120px] shrink-0 text-[14px] font-semibold text-text no-underline"
>{{ pick(cc.cat.name, locale) }}</NuxtLink>
<div>
<NuxtLink
v-for="gc in cc.grandchildren"
:key="gc.id"
:to="`/search?category=${gc.id}`"
class="mb-2 mr-[18px] inline-block text-[13px] text-muted no-underline transition-colors hover:text-primary"
>{{ pick(gc.name, locale) }}</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.catmenu {
position: absolute;
top: 40px;
left: 0;
z-index: 700;
display: flex;
width: 960px;
}
/* pinned: in-flow left rail of the home hero row, flyout overlays the carousel */
.catmenu.pinned {
position: relative;
top: auto;
left: auto;
flex: none;
width: 240px;
height: 450px;
}
.lvl1 {
width: 240px;
margin: 0;
padding: 8px 0;
list-style: none;
background: rgba(30, 30, 30, 0.92);
min-height: 434px;
}
.lvl1 li {
padding: 10px 15px;
cursor: pointer;
}
.lvl1 li.on {
background: #fff;
}
.lvl1 li.on .c1,
.lvl1 li.on .c2 {
color: var(--mall-red);
}
.c1 {
display: block;
color: #fff;
font-size: 14px;
text-decoration: none;
}
.c2s {
display: block;
margin-top: 2px;
}
.c2 {
color: #bbb;
font-size: 12px;
margin-right: 8px;
text-decoration: none;
}
.c2:hover {
color: #fff;
}
.sub {
flex: 1;
background: #fff;
box-shadow: var(--mall-shadow);
padding: 20px 30px;
min-height: 434px;
}
.sub-row {
display: flex;
border-bottom: 1px solid var(--mall-line);
padding: 12px 0;
}
.sub-row:last-child {
border-bottom: 0;
}
.sub-title {
width: 120px;
font-weight: 600;
color: var(--mall-ink);
text-decoration: none;
}
.sub-items a {
display: inline-block;
margin: 0 18px 8px 0;
color: var(--mall-muted);
font-size: 13px;
text-decoration: none;
}
.sub-items a:hover {
color: var(--mall-red);
}
/* pinned overrides: 240x450 rail, flyout opens to the right over the carousel */
.catmenu.pinned .lvl1 {
box-sizing: border-box;
height: 450px;
min-height: 450px;
}
.catmenu.pinned .sub {
position: absolute;
top: 0;
left: 240px;
box-sizing: border-box;
width: 700px;
height: 450px;
min-height: 450px;
overflow: auto;
}
</style>