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
+20 -136
View File
@@ -102,154 +102,38 @@ onMounted(() => {
</script>
<template>
<section class="mpanel orders-panel">
<h1 class="mpanel-title">{{ t("user.ordersTitle") }}</h1>
<VCard class="min-h-[560px]">
<h1 class="mb-4 text-xl font-bold text-text">{{ t("user.ordersTitle") }}</h1>
<UiTabs :model-value="activeFilter" :tabs="tabs" @update:model-value="changeFilter">
<template #default>
<div v-if="loading" class="muted">{{ t("common.loading") }}</div>
<div v-if="loading" class="py-[30px] text-muted">{{ t("common.loading") }}</div>
<UiEmptyState v-else-if="filteredOrders.length === 0" :text="t('user.noOrders')" />
<div v-else class="order-list">
<article v-for="order in filteredOrders" :key="order.id" class="order-card">
<header class="order-header">
<div>
<strong>{{ shopName(order.shop_id) }}</strong>
<span>{{ t("user.orderNo") }} {{ order.order_no }}</span>
<span>{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</span>
</div>
<div v-else class="space-y-3.5">
<VCard v-for="order in filteredOrders" :key="order.id" :padded="false" class="overflow-hidden">
<header class="flex items-center justify-between gap-2.5 border-b border-border bg-bg px-3.5 py-3 text-xs text-muted max-sm:items-start">
<div class="flex min-w-0 items-center gap-3.5 max-sm:flex-col max-sm:items-start max-sm:gap-1"><strong class="text-[13px] font-semibold text-text">{{ shopName(order.shop_id) }}</strong><span>{{ t("user.orderNo") }} {{ order.order_no }}</span><span>{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</span></div>
<StatusBadge :status="order.status" kind="order" />
</header>
<div class="order-items">
<div v-for="item in order.items" :key="item.id" class="order-item">
<img :src="item.image || '/mock/product-1.svg'" :alt="pick(item.product_name, locale)" />
<div class="item-name">
<span>{{ pick(item.product_name, locale) }}</span>
<small>{{ item.sku_code }} × {{ item.qty }}</small>
</div>
<PriceText :amount-minor="item.unit_price_minor" :currency="order.currency" />
<div class="px-3.5 py-1">
<div v-for="item in order.items" :key="item.id" class="flex items-center gap-3 border-b border-border py-2.5 last:border-b-0">
<img class="h-[54px] w-[54px] border border-border object-contain" :src="item.image || '/mock/product-1.svg'" :alt="pick(item.product_name, locale)" />
<div class="flex min-w-0 flex-1 flex-col gap-1 text-[13px]"><span class="truncate">{{ pick(item.product_name, locale) }}</span><small class="text-[11px] text-muted">{{ item.sku_code }} × {{ item.qty }}</small></div>
<PriceText class="text-[13px] text-muted" :amount-minor="item.unit_price_minor" :currency="order.currency" />
</div>
</div>
<footer class="order-footer">
<span>{{ t("user.orderTotal") }} <strong><PriceText :amount-minor="order.total_minor" :currency="order.currency" /></strong></span>
<div class="actions">
<button v-if="order.status === 'pending_payment'" class="mbtn red" type="button" :disabled="workingId === order.id" @click="pay(order)">{{ t("user.payNow") }}</button>
<button v-if="order.status === 'pending_payment'" class="mbtn" type="button" :disabled="workingId === order.id" @click="cancel(order)">{{ t("user.cancelOrder") }}</button>
<NuxtLink class="mbtn" :to="`/user/orders/${order.id}`">{{ t("user.viewDetails") }}</NuxtLink>
<footer class="flex items-center justify-between gap-3 border-t border-border px-3.5 py-3 text-xs text-muted max-sm:flex-col max-sm:items-start">
<span>{{ t("user.orderTotal") }} <strong class="text-primary"><PriceText :amount-minor="order.total_minor" :currency="order.currency" /></strong></span>
<div class="flex gap-2">
<VBtn v-if="order.status === 'pending_payment'" variant="primary" size="sm" type="button" :disabled="workingId === order.id" @click="pay(order)">{{ t("user.payNow") }}</VBtn>
<VBtn v-if="order.status === 'pending_payment'" size="sm" type="button" :disabled="workingId === order.id" @click="cancel(order)">{{ t("user.cancelOrder") }}</VBtn>
<NuxtLink class="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[13px] font-medium text-text no-underline" :to="`/user/orders/${order.id}`">{{ t("user.viewDetails") }}</NuxtLink>
</div>
</footer>
</article>
</VCard>
</div>
<UiPagination :page="page" :total="total" :per-page="perPage" @change="loadOrders" />
</template>
</UiTabs>
</section>
</VCard>
</template>
<style scoped>
.orders-panel {
min-height: 560px;
}
.muted {
padding: 30px 0;
color: var(--mall-faint);
}
.order-card {
margin-bottom: 14px;
border: 1px solid var(--mall-line);
}
.order-card:last-child {
margin-bottom: 0;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: #fafafa;
border-bottom: 1px solid var(--mall-line);
}
.order-header > div {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
color: var(--mall-faint);
font-size: 12px;
}
.order-header strong {
color: var(--mall-ink);
font-size: 13px;
font-weight: 600;
}
.order-items {
padding: 4px 14px;
}
.order-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
}
.order-item + .order-item {
border-top: 1px solid var(--mall-line);
}
.order-item img {
width: 54px;
height: 54px;
border: 1px solid var(--mall-line);
object-fit: contain;
}
.item-name {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px;
min-width: 0;
font-size: 13px;
}
.item-name span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-name small {
color: var(--mall-faint);
font-size: 11px;
}
.order-item :deep(.price) {
color: var(--mall-muted);
font-size: 13px;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-top: 1px solid var(--mall-line);
color: var(--mall-muted);
font-size: 12px;
}
.order-footer strong :deep(.price) {
color: var(--mall-red);
font-size: 16px;
}
.actions {
display: flex;
gap: 8px;
}
.actions .mbtn {
padding: 5px 11px;
}
@media (max-width: 760px) {
.order-header > div {
align-items: flex-start;
flex-direction: column;
gap: 3px;
}
.order-footer {
align-items: flex-start;
flex-direction: column;
}
}
</style>