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:
+49
-131
@@ -29,6 +29,9 @@ const banners = computed(() =>
|
||||
);
|
||||
const quickLinks = computed(() => content.value.quick_links);
|
||||
const promos = computed(() => content.value.promos);
|
||||
const heroQuickLinks = computed(() => quickLinks.value.slice(0, 6));
|
||||
const sidePromos = computed(() => promos.value.slice(0, 2));
|
||||
const stripPromos = computed(() => (promos.value.length > 2 ? promos.value.slice(2, 5) : promos.value.slice(0, 3)));
|
||||
|
||||
// Floor structure comes from the category tree and floor products from the
|
||||
// catalog API; advert art is an ordered pool assigned to floors in turn.
|
||||
@@ -59,40 +62,59 @@ const visibleFloors = computed(() => floors.value ?? []);
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="w1200 hero">
|
||||
<ShellCategoryMenu pinned />
|
||||
<div class="hero-slider">
|
||||
<UiCarousel :images="banners" :height="450" />
|
||||
<section class="mx-auto max-w-mall px-4">
|
||||
<div class="grid h-hero grid-cols-[240px_minmax(0,1fr)_220px] bg-surface">
|
||||
<ShellCategoryMenu pinned />
|
||||
<div class="min-w-0">
|
||||
<UiCarousel :images="banners" :height="450" />
|
||||
</div>
|
||||
<aside class="flex h-full flex-col border-l border-border bg-surface">
|
||||
<ul class="grid list-none grid-cols-2 gap-px bg-border p-px">
|
||||
<li v-for="q in heroQuickLinks" :key="q.id" class="bg-surface text-center">
|
||||
<NuxtLink :to="q.url" class="block px-1 py-3 text-[12px] text-muted no-underline transition-colors hover:text-primary">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" class="mx-auto mb-1.5">
|
||||
<path :d="q.glyph" />
|
||||
</svg>
|
||||
<span>{{ pick(q.label, locale) }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2 flex-1 space-y-2 px-2 pb-2">
|
||||
<NuxtLink
|
||||
v-for="p in sidePromos"
|
||||
:key="p.id"
|
||||
:to="p.url"
|
||||
class="block overflow-hidden"
|
||||
>
|
||||
<img :src="p.image" :alt="t('home.hotPromo')" loading="lazy" class="block h-[106px] w-full object-cover" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="w1200 strip">
|
||||
<ul class="quick">
|
||||
<li v-for="q in quickLinks" :key="q.id">
|
||||
<NuxtLink :to="q.url">
|
||||
<svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor"><path :d="q.glyph" /></svg>
|
||||
<span>{{ pick(q.label, locale) }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="promos">
|
||||
<NuxtLink v-for="p in promos" :key="p.id" :to="p.url">
|
||||
<img :src="p.image" :alt="t('home.hotPromo')" loading="lazy" />
|
||||
<section v-if="stripPromos.length > 0" class="mx-auto mb-5 mt-5 max-w-mall px-4">
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<NuxtLink v-for="p in stripPromos" :key="p.id" :to="p.url" class="block overflow-hidden">
|
||||
<img :src="p.image" :alt="t('home.hotPromo')" loading="lazy" class="block h-40 w-full object-cover" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-bg">
|
||||
<section v-for="floor in visibleFloors" :key="floor.categoryId" class="w1200 floor">
|
||||
<header class="floor-head">
|
||||
<h2>{{ pick(floor.name, locale) }}</h2>
|
||||
<NuxtLink :to="`/search?category=${floor.categoryId}`" class="more">{{ t("home.viewMore") }} ›</NuxtLink>
|
||||
<div class="bg-bg py-8">
|
||||
<section v-for="floor in visibleFloors" :key="floor.categoryId" class="mx-auto mb-[30px] max-w-mall px-4 last:mb-0">
|
||||
<header class="flex items-end justify-between border-b-2 border-primary">
|
||||
<h2 class="m-0 py-[10px] text-[18px] leading-none">{{ pick(floor.name, locale) }}</h2>
|
||||
<NuxtLink
|
||||
:to="`/search?category=${floor.categoryId}`"
|
||||
class="pb-2 text-[12px] text-muted no-underline transition-colors hover:text-primary"
|
||||
>{{ t("home.viewMore") }} ›</NuxtLink>
|
||||
</header>
|
||||
<div class="floor-body">
|
||||
<NuxtLink v-if="floor.advImage" :to="floor.advUrl" class="floor-adv">
|
||||
<img :src="floor.advImage" :alt="pick(floor.name, locale)" loading="lazy" />
|
||||
<div class="flex bg-surface">
|
||||
<NuxtLink v-if="floor.advImage" :to="floor.advUrl" class="shrink-0 border-r border-border">
|
||||
<img :src="floor.advImage" :alt="pick(floor.name, locale)" loading="lazy" class="block h-[614px] w-[234px] object-cover" />
|
||||
</NuxtLink>
|
||||
<div class="floor-grid">
|
||||
<div class="grid min-w-0 flex-1 grid-cols-4 gap-px bg-border p-px">
|
||||
<UiProductCard v-for="p in floor.products" :key="p.id" :product="p" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,107 +122,3 @@ const visibleFloors = computed(() => floors.value ?? []);
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero {
|
||||
display: flex;
|
||||
height: 450px;
|
||||
margin-top: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.hero-slider {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.strip {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.quick {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1px;
|
||||
background: var(--mall-line);
|
||||
border: 1px solid var(--mall-line);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 240px;
|
||||
}
|
||||
.quick li {
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.quick a {
|
||||
display: block;
|
||||
padding: 14px 0;
|
||||
color: var(--mall-muted);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.quick a:hover {
|
||||
color: var(--mall-red);
|
||||
}
|
||||
.quick svg {
|
||||
display: block;
|
||||
margin: 0 auto 6px;
|
||||
}
|
||||
.promos {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.promos img {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.floor {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.floor-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
border-bottom: 2px solid var(--mall-red);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.floor-head h2 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.more {
|
||||
font-size: 12px;
|
||||
color: var(--mall-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.more:hover {
|
||||
color: var(--mall-red);
|
||||
}
|
||||
.floor-body {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.floor-adv img {
|
||||
width: 234px;
|
||||
height: 614px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.floor-grid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--mall-line);
|
||||
}
|
||||
.floor-grid :deep(.card) {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user