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:
@@ -73,240 +73,62 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mpanel order-detail-panel">
|
||||
<h1 class="mpanel-title">{{ t("user.orderDetail") }}</h1>
|
||||
<div v-if="loading" class="muted">{{ t("common.loading") }}</div>
|
||||
<VCard class="min-h-[560px]">
|
||||
<h1 class="mb-4 text-xl font-bold text-text">{{ t("user.orderDetail") }}</h1>
|
||||
<div v-if="loading" class="py-5 text-muted">{{ t("common.loading") }}</div>
|
||||
<UiEmptyState v-else-if="!order" :text="t('user.noOrders')" />
|
||||
<div v-else>
|
||||
<header class="summary">
|
||||
<div>
|
||||
<strong>{{ t("user.orderNo") }} {{ order.order_no }}</strong>
|
||||
<p>{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</p>
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<header class="flex items-center justify-between border border-border bg-bg p-3.5 max-sm:flex-col max-sm:items-start max-sm:gap-2.5">
|
||||
<div><strong class="text-[15px] text-text">{{ t("user.orderNo") }} {{ order.order_no }}</strong><p class="mt-1 text-xs text-muted">{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</p></div>
|
||||
<StatusBadge :status="order.status" kind="order" />
|
||||
</header>
|
||||
|
||||
<article class="mpanel block">
|
||||
<h2 class="block-title">{{ t("user.shippingAddress") }}</h2>
|
||||
<p>{{ order.shipping_address.recipient }} · {{ order.shipping_address.phone }}</p>
|
||||
<p>
|
||||
{{ order.shipping_address.country }} {{ order.shipping_address.region }} {{ order.shipping_address.city }}
|
||||
{{ order.shipping_address.line1 }} {{ order.shipping_address.postal_code }}
|
||||
</p>
|
||||
</article>
|
||||
<VCard :padded="false" class="p-5">
|
||||
<h2 class="mb-3.5 text-[15px] font-semibold text-text">{{ t("user.shippingAddress") }}</h2>
|
||||
<p class="m-0 text-sm text-text">{{ order.shipping_address.recipient }} · {{ order.shipping_address.phone }}</p>
|
||||
<p class="mt-1 text-sm text-text">{{ order.shipping_address.country }} {{ order.shipping_address.region }} {{ order.shipping_address.city }} {{ order.shipping_address.line1 }} {{ order.shipping_address.postal_code }}</p>
|
||||
</VCard>
|
||||
|
||||
<article class="mpanel block">
|
||||
<h2 class="block-title">{{ t("user.orderItems") }}</h2>
|
||||
<table class="mtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t("user.product") }}</th>
|
||||
<th>{{ t("common.price") }}</th>
|
||||
<th>{{ t("common.qty") }}</th>
|
||||
<th>{{ t("common.total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<VCard :padded="false" class="p-5">
|
||||
<h2 class="mb-3.5 text-[15px] font-semibold text-text">{{ t("user.orderItems") }}</h2>
|
||||
<VTable>
|
||||
<thead><tr><th>{{ t("user.product") }}</th><th>{{ t("common.price") }}</th><th>{{ t("common.qty") }}</th><th>{{ t("common.total") }}</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in order.items" :key="item.id">
|
||||
<td class="prod-col">
|
||||
<img :src="item.image || '/mock/product-1.svg'" :alt="pick(item.product_name, locale)" />
|
||||
<span>{{ pick(item.product_name, locale) }}</span>
|
||||
<span v-if="item.flash_sale_item_id" class="activity-tag">{{ t("marketing.flashTag") }}</span>
|
||||
</td>
|
||||
<td><div class="flex items-center gap-2.5"><img class="h-11 w-11 border border-border object-contain" :src="item.image || '/mock/product-1.svg'" :alt="pick(item.product_name, locale)" /><span>{{ pick(item.product_name, locale) }}</span><VBadge v-if="item.flash_sale_item_id" tone="red">{{ t("marketing.flashTag") }}</VBadge></div></td>
|
||||
<td><PriceText :amount-minor="item.unit_price_minor" :currency="order.currency" /></td>
|
||||
<td>{{ item.qty }}</td>
|
||||
<td><PriceText :amount-minor="item.unit_price_minor * item.qty" :currency="order.currency" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="order.discount_minor > 0" class="order-discount">
|
||||
{{ t("checkout.couponDiscount") }}:
|
||||
<PriceText :amount-minor="order.discount_minor" :currency="order.currency" />
|
||||
</p>
|
||||
<p class="order-total">{{ t("user.total") }}: <PriceText :amount-minor="order.total_minor" :currency="order.currency" /></p>
|
||||
</article>
|
||||
</VTable>
|
||||
<p v-if="order.discount_minor > 0" class="mt-4 text-right text-[13px] text-primary">{{ t("checkout.couponDiscount") }}: <PriceText :amount-minor="order.discount_minor" :currency="order.currency" /></p>
|
||||
<p class="mt-4 text-right text-[13px] text-muted">{{ t("user.total") }}: <PriceText class="text-lg font-semibold text-primary" :amount-minor="order.total_minor" :currency="order.currency" /></p>
|
||||
</VCard>
|
||||
|
||||
<article class="mpanel block">
|
||||
<h2 class="block-title">{{ t("user.shipments") }}</h2>
|
||||
<VCard :padded="false" class="p-5">
|
||||
<h2 class="mb-3.5 text-[15px] font-semibold text-text">{{ t("user.shipments") }}</h2>
|
||||
<UiEmptyState v-if="orderShipments.length === 0" :text="t('user.noShipments')" />
|
||||
<div v-else>
|
||||
<div v-for="shipment in orderShipments" :key="shipment.id" class="shipment-row">
|
||||
<div>
|
||||
<strong>{{ t("user.shipmentNo") }} {{ shipment.shipment_no }}</strong>
|
||||
<p>{{ t("user.carrier") }}: {{ shipment.carrier }}</p>
|
||||
<p>{{ t("user.trackingNo") }}: {{ shipment.tracking_no }}</p>
|
||||
</div>
|
||||
<div class="shipment-right">
|
||||
<StatusBadge :status="shipment.status" kind="shipment" />
|
||||
<button
|
||||
v-if="shipment.status === 'shipped'"
|
||||
class="mbtn red"
|
||||
type="button"
|
||||
:disabled="confirmingShipmentId === shipment.id"
|
||||
@click="confirmDelivery(shipment)"
|
||||
>
|
||||
{{ t("user.confirmDelivery") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="space-y-2.5">
|
||||
<div v-for="shipment in orderShipments" :key="shipment.id" class="flex items-center justify-between border border-border p-3 max-sm:flex-col max-sm:items-start max-sm:gap-2.5">
|
||||
<div><strong class="text-sm text-text">{{ t("user.shipmentNo") }} {{ shipment.shipment_no }}</strong><p class="mt-1 text-xs text-muted">{{ t("user.carrier") }}: {{ shipment.carrier }}</p><p class="mt-1 text-xs text-muted">{{ t("user.trackingNo") }}: {{ shipment.tracking_no }}</p></div>
|
||||
<div class="flex items-center gap-2"><StatusBadge :status="shipment.status" kind="shipment" /><VBtn v-if="shipment.status === 'shipped'" variant="primary" size="sm" type="button" :disabled="confirmingShipmentId === shipment.id" @click="confirmDelivery(shipment)">{{ t("user.confirmDelivery") }}</VBtn></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</VCard>
|
||||
|
||||
<article class="mpanel block">
|
||||
<h2 class="block-title">{{ t("user.invoiceRequest") }}</h2>
|
||||
<div v-if="invoiceSuccess" class="invoice-success">{{ t("user.invoiceSuccess") }}</div>
|
||||
<form v-else class="invoice-form" @submit.prevent="submitInvoice">
|
||||
<label>
|
||||
<span>{{ t("user.invoiceTitle") }}</span>
|
||||
<input v-model.trim="invoiceTitle" class="minput" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t("user.taxNo") }}</span>
|
||||
<input v-model.trim="invoiceTaxNo" class="minput" type="text" />
|
||||
</label>
|
||||
<fieldset>
|
||||
<legend>{{ t("user.invoiceKind") }}</legend>
|
||||
<label><input v-model="invoiceKind" type="radio" value="personal" /> {{ t("user.personal") }}</label>
|
||||
<label><input v-model="invoiceKind" type="radio" value="company" /> {{ t("user.company") }}</label>
|
||||
</fieldset>
|
||||
<p v-if="invoiceErrorKey" class="error">{{ t(invoiceErrorKey) }}</p>
|
||||
<button class="mbtn red" type="submit" :disabled="invoiceWorking">{{ t("user.submitInvoice") }}</button>
|
||||
<VCard :padded="false" class="p-5">
|
||||
<h2 class="mb-3.5 text-[15px] font-semibold text-text">{{ t("user.invoiceRequest") }}</h2>
|
||||
<div v-if="invoiceSuccess" class="text-sm text-success">{{ t("user.invoiceSuccess") }}</div>
|
||||
<form v-else class="grid max-w-[520px] gap-3" @submit.prevent="submitInvoice">
|
||||
<VField :label="t('user.invoiceTitle')"><VInput v-model="invoiceTitle" /></VField>
|
||||
<VField :label="t('user.taxNo')"><VInput v-model="invoiceTaxNo" /></VField>
|
||||
<fieldset class="flex items-center gap-4 border-0 p-0"><legend class="mb-1 block text-[13px] text-muted">{{ t("user.invoiceKind") }}</legend><label class="text-[13px] text-text"><input v-model="invoiceKind" class="mr-1 accent-primary" type="radio" value="personal" /> {{ t("user.personal") }}</label><label class="text-[13px] text-text"><input v-model="invoiceKind" class="mr-1 accent-primary" type="radio" value="company" /> {{ t("user.company") }}</label></fieldset>
|
||||
<p v-if="invoiceErrorKey" class="m-0 text-xs text-danger">{{ t(invoiceErrorKey) }}</p>
|
||||
<VBtn class="w-fit" variant="primary" type="submit" :disabled="invoiceWorking">{{ t("user.submitInvoice") }}</VBtn>
|
||||
</form>
|
||||
</article>
|
||||
</VCard>
|
||||
</div>
|
||||
</section>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-panel {
|
||||
min-height: 560px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--mall-faint);
|
||||
padding: 20px 0;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--mall-line);
|
||||
background: #fafafa;
|
||||
}
|
||||
.summary strong {
|
||||
color: var(--mall-ink);
|
||||
font-size: 15px;
|
||||
}
|
||||
.summary p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--mall-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
.block {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
border: 1px solid var(--mall-line);
|
||||
}
|
||||
.block-title {
|
||||
margin: 0 0 14px;
|
||||
color: var(--mall-ink);
|
||||
font-size: 15px;
|
||||
}
|
||||
.prod-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.prod-col img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid var(--mall-line);
|
||||
object-fit: contain;
|
||||
}
|
||||
.order-discount {
|
||||
margin: 16px 0 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: var(--mall-red);
|
||||
}
|
||||
.activity-tag {
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--mall-red);
|
||||
color: var(--mall-red);
|
||||
font-size: 11px;
|
||||
}
|
||||
.order-total {
|
||||
margin: 16px 0 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: var(--mall-muted);
|
||||
}
|
||||
.order-total :deep(.price) {
|
||||
color: var(--mall-red);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.shipment-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--mall-line);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.shipment-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.shipment-row p {
|
||||
margin: 5px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--mall-muted);
|
||||
}
|
||||
.shipment-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.invoice-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
max-width: 520px;
|
||||
}
|
||||
.invoice-form label > span,
|
||||
.invoice-form legend {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--mall-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.invoice-form fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.invoice-form fieldset label {
|
||||
font-size: 13px;
|
||||
color: var(--mall-ink);
|
||||
}
|
||||
.error {
|
||||
margin: 0;
|
||||
color: var(--mall-red);
|
||||
font-size: 12px;
|
||||
}
|
||||
.invoice-success {
|
||||
color: var(--mall-green);
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.summary,
|
||||
.shipment-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user