wip(freight): migration 0017 + model/column-list groundwork for add-freight-templates

This commit is contained in:
Chengdong Zhang
2026-09-23 17:39:45 +08:00
parent a83e857bdf
commit 5b426486ac
105 changed files with 6193 additions and 1566 deletions
+128 -25
View File
@@ -59,56 +59,159 @@ const counts = computed(() => ({
}));
const statusLinks = computed(() => [
{ key: "pendingPayment", label: t("user.pendingPayment"), count: counts.value.pendingPayment, to: "/user/orders?status=pending_payment" },
{ key: "pendingShipment", label: t("user.pendingShipment"), count: counts.value.pendingShipment, to: "/user/orders?status=paid" },
{ key: "pendingReceipt", label: t("user.pendingReceipt"), count: counts.value.pendingReceipt, to: "/user/orders?status=shipped" },
{ key: "completed", label: t("user.completed"), count: counts.value.completed, to: "/user/orders?status=completed" },
{ key: "afterSale", label: t("user.afterSale"), count: counts.value.afterSale, to: "/user/orders?status=after-sale" },
{
key: "pendingPayment",
label: t("user.pendingPayment"),
count: counts.value.pendingPayment,
to: "/user/orders?status=pending_payment",
},
{
key: "pendingShipment",
label: t("user.pendingShipment"),
count: counts.value.pendingShipment,
to: "/user/orders?status=paid",
},
{
key: "pendingReceipt",
label: t("user.pendingReceipt"),
count: counts.value.pendingReceipt,
to: "/user/orders?status=shipped",
},
{
key: "completed",
label: t("user.completed"),
count: counts.value.completed,
to: "/user/orders?status=completed",
},
{
key: "afterSale",
label: t("user.afterSale"),
count: counts.value.afterSale,
to: "/user/orders?status=after-sale",
},
]);
</script>
<template>
<div class="space-y-4">
<VCard class="pb-0">
<h1 class="mb-4 text-xl font-bold text-text">{{ t("user.dashboard") }}</h1>
<h1 class="text-text mb-4 text-xl font-bold">{{ t("user.dashboard") }}</h1>
<div class="mb-5 grid gap-3 sm:grid-cols-3">
<div class="border border-border bg-primary-soft p-[18px]"><span class="block text-xs text-muted">{{ t("user.statsBalance") }}</span><strong class="mt-2.5 block text-xl text-primary"><PriceText v-if="stats" :amount-minor="stats.balance_minor" :currency="stats.currency" /><template v-else>{{ t("common.loading") }}</template></strong></div>
<div class="border border-border bg-primary-soft p-[18px]"><span class="block text-xs text-muted">{{ t("user.statsPoints") }}</span><strong class="mt-2.5 block text-xl text-primary">{{ stats ? stats.points : t("common.loading") }}</strong></div>
<div class="border border-border bg-primary-soft p-[18px]"><span class="block text-xs text-muted">{{ t("user.statsFrozen") }}</span><strong class="mt-2.5 block text-xl text-primary"><PriceText v-if="stats" :amount-minor="stats.frozen_minor" :currency="stats.currency" /><template v-else>{{ t("common.loading") }}</template></strong></div>
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsBalance") }}</span
><strong class="text-primary mt-2.5 block text-xl"
><PriceText
v-if="stats"
:amount-minor="stats.balance_minor"
:currency="stats.currency"
/><template v-else>{{ t("common.loading") }}</template></strong
>
</div>
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsPoints") }}</span
><strong class="text-primary mt-2.5 block text-xl">{{
stats ? stats.points : t("common.loading")
}}</strong>
</div>
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsFrozen") }}</span
><strong class="text-primary mt-2.5 block text-xl"
><PriceText
v-if="stats"
:amount-minor="stats.frozen_minor"
:currency="stats.currency"
/><template v-else>{{ t("common.loading") }}</template></strong
>
</div>
</div>
<div class="grid grid-cols-3 border-t border-border sm:grid-cols-5">
<NuxtLink v-for="item in statusLinks" :key="item.key" :to="item.to" class="flex flex-col items-center gap-2 px-2 py-4 text-xs text-muted no-underline hover:text-primary sm:border-l sm:border-border first:sm:border-l-0"><span>{{ item.label }}</span><b class="text-lg font-semibold text-primary">{{ item.count }}</b></NuxtLink>
<div class="border-border grid grid-cols-3 border-t sm:grid-cols-5">
<NuxtLink
v-for="item in statusLinks"
:key="item.key"
:to="item.to"
class="text-muted hover:text-primary sm:border-border flex flex-col items-center gap-2 px-2 py-4 text-xs no-underline sm:border-l first:sm:border-l-0"
><span>{{ item.label }}</span
><b class="text-primary text-lg font-semibold">{{ item.count }}</b></NuxtLink
>
</div>
</VCard>
<VCard>
<h2 class="mb-4 flex items-center justify-between text-lg font-semibold text-text">{{ t("user.recentOrders") }} <NuxtLink class="text-sm font-normal text-primary no-underline" to="/user/orders">{{ t("user.viewAll") }} ›</NuxtLink></h2>
<div v-if="loading" class="py-5 text-muted">{{ t("common.loading") }}</div>
<h2 class="text-text mb-4 flex items-center justify-between text-lg font-semibold">
{{ t("user.recentOrders") }}
<NuxtLink class="text-primary text-sm font-normal no-underline" to="/user/orders"
>{{ t("user.viewAll") }} ›</NuxtLink
>
</h2>
<div v-if="loading" class="text-muted py-5">{{ t("common.loading") }}</div>
<UiEmptyState v-else-if="orders.length === 0" :text="t('user.noOrders')" />
<div v-else class="space-y-3">
<article v-for="order in orders.slice(0, 3)" :key="order.id" class="border border-border">
<header class="flex items-center justify-between gap-3 bg-bg px-3 py-2.5 text-xs text-muted"><span>{{ t("user.orderNo") }} {{ order.order_no }}</span><StatusBadge :status="order.status" kind="order" /></header>
<article v-for="order in orders.slice(0, 3)" :key="order.id" class="border-border border">
<header
class="bg-bg text-muted flex items-center justify-between gap-3 px-3 py-2.5 text-xs"
>
<span>{{ t("user.orderNo") }} {{ order.order_no }}</span
><StatusBadge :status="order.status" kind="order" />
</header>
<div class="flex items-center gap-4 p-3 max-sm:flex-wrap">
<div class="flex min-w-[140px] gap-1"><img v-for="item in order.items" :key="item.id" class="h-11 w-11 border border-border object-contain" :src="item.image || '/mock/product-1.svg'" :alt="pick(item.product_name, locale)" /></div>
<div class="flex-1 text-xs text-muted">{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</div>
<div class="text-right text-xs"><span class="mb-1 block text-muted">{{ t("user.orderTotal") }}</span><PriceText class="font-semibold text-primary" :amount-minor="order.total_minor" :currency="order.currency" /></div>
<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 class="flex min-w-[140px] gap-1">
<img
v-for="item in order.items"
:key="item.id"
class="border-border h-11 w-11 border object-contain"
:src="item.image || '/mock/product-1.svg'"
:alt="pick(item.product_name, locale)"
/>
</div>
<div class="text-muted flex-1 text-xs">
{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}
</div>
<div class="text-right text-xs">
<span class="text-muted mb-1 block">{{ t("user.orderTotal") }}</span
><PriceText
class="text-primary font-semibold"
:amount-minor="order.total_minor"
:currency="order.currency"
/>
</div>
<NuxtLink
class="border-border bg-surface text-text inline-flex items-center rounded-md border px-2.5 py-1 text-[13px] font-medium no-underline"
:to="`/user/orders/${order.id}`"
>{{ t("user.viewDetails") }}</NuxtLink
>
</div>
</article>
</div>
</VCard>
<VCard>
<h2 class="mb-4 text-lg font-semibold text-text">{{ t("user.favoriteProducts") }} ({{ favoriteProductTotal }})</h2>
<h2 class="text-text mb-4 text-lg font-semibold">
{{ t("user.favoriteProducts") }} ({{ favoriteProductTotal }})
</h2>
<UiEmptyState v-if="favoriteProducts.length === 0" :text="t('user.noFavorites')" />
<div v-else class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<NuxtLink v-for="item in favoriteProducts" :key="item.id" :to="`/goods/${item.product.slug}`" class="block border border-border p-2.5 text-text no-underline hover:border-primary">
<img class="mb-2 block h-[120px] w-full object-contain" :src="item.product.image || '/mock/product-1.svg'" :alt="pick(item.product.name, locale)" />
<span class="block h-[34px] overflow-hidden text-xs leading-[17px]">{{ pick(item.product.name, locale) }}</span>
<PriceText v-if="item.product.price_minor !== null && item.product.currency" class="mt-2 block text-sm font-semibold text-primary" :amount-minor="item.product.price_minor" :currency="item.product.currency" />
<NuxtLink
v-for="item in favoriteProducts"
:key="item.id"
:to="`/goods/${item.product.slug}`"
class="border-border text-text hover:border-primary block border p-2.5 no-underline"
>
<img
class="mb-2 block h-[120px] w-full object-contain"
:src="item.product.image || '/mock/product-1.svg'"
:alt="pick(item.product.name, locale)"
/>
<span class="block h-[34px] overflow-hidden text-xs leading-[17px]">{{
pick(item.product.name, locale)
}}</span>
<PriceText
v-if="item.product.price_minor !== null && item.product.currency"
class="text-primary mt-2 block text-sm font-semibold"
:amount-minor="item.product.price_minor"
:currency="item.product.currency"
/>
</NuxtLink>
</div>
</VCard>
</div>
</template>