- Mock adapter implementing @vmall/shared ApiClient (localStorage-persisted cart/orders/invoices), runtime switch via mockApi flag (default on) - Fixed bilingual mock catalog: 3-level categories, 24 products w/ SKUs, stores, brands, banners, floors, seckill/collective/integral, comments, coupons, addresses, seeded orders/shipments/invoices - B2B2C mall shell: top bar, logo/search/cart header, dark nav + category mega-menu, value-prop footer, back-to-top; 1200px grid, #ca151e theme - UI primitives replacing element-plus: carousel, pagination, breadcrumb, qty stepper, rating stars, modal, tabs, step bar, product card - Pages: home floors, search (filters/sort/paging), goods detail (SKU picker, store rail, review tabs), cart -> checkout -> pay -> success, auth pages, user center (dashboard/orders/addresses/favorites/coupons/ invoices), stores, seckill, collective, integral - i18n split into per-domain locale modules (en+zh) - OpenSpec change mall-pc-storefront-replica archived; all specs green
145 lines
3.2 KiB
Vue
145 lines
3.2 KiB
Vue
<script setup lang="ts">
|
||
import { t as pick } from "@vmall/shared";
|
||
import { MOCK_BANNERS, MOCK_PROMOS, MOCK_QUICK_LINKS, homeFloors } from "~/mock/data";
|
||
|
||
const { locale, t } = useI18n();
|
||
const floors = computed(() => homeFloors().filter((f) => f.products.length > 0));
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<div class="w1200 hero">
|
||
<UiCarousel :images="MOCK_BANNERS" :height="450" />
|
||
</div>
|
||
|
||
<div class="w1200 strip">
|
||
<ul class="quick">
|
||
<li v-for="q in MOCK_QUICK_LINKS" :key="q.url + pick(q.label, 'en')">
|
||
<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 MOCK_PROMOS" :key="p.image" :to="p.url">
|
||
<img :src="p.image" :alt="t('home.hotPromo')" loading="lazy" />
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-bg">
|
||
<section v-for="floor in floors" :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>
|
||
</header>
|
||
<div class="floor-body">
|
||
<NuxtLink :to="floor.advUrl" class="floor-adv">
|
||
<img :src="floor.advImage" :alt="pick(floor.name, locale)" loading="lazy" />
|
||
</NuxtLink>
|
||
<div class="floor-grid">
|
||
<UiProductCard v-for="p in floor.products" :key="p.id" :product="p" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.hero {
|
||
margin-top: 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;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 1px;
|
||
background: var(--mall-line);
|
||
}
|
||
.floor-grid :deep(.card) {
|
||
border: 0;
|
||
}
|
||
</style>
|