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
+8 -3
View File
@@ -3,11 +3,16 @@ defineProps<{ items: { label: string; to?: string }[] }>();
</script>
<template>
<nav aria-label="breadcrumb" class="mx-auto my-5 max-w-mall px-4 text-[13px] text-muted">
<nav aria-label="breadcrumb" class="max-w-mall text-muted mx-auto my-5 px-4 text-[13px]">
<template v-for="(item, i) in items" :key="i">
<NuxtLink v-if="item.to" :to="item.to" class="no-underline transition-colors hover:text-primary">{{ item.label }}</NuxtLink>
<NuxtLink
v-if="item.to"
:to="item.to"
class="hover:text-primary no-underline transition-colors"
>{{ item.label }}</NuxtLink
>
<span v-else class="text-text">{{ item.label }}</span>
<span v-if="i < items.length - 1" class="mx-2 text-muted/60">/</span>
<span v-if="i < items.length - 1" class="text-muted/60 mx-2">/</span>
</template>
</nav>
</template>
+11 -5
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{ images: { image: string; url?: string }[]; height?: number; interval?: number }>(), {
height: 450,
interval: 4000,
});
const props = withDefaults(
defineProps<{ images: { image: string; url?: string }[]; height?: number; interval?: number }>(),
{
height: 450,
interval: 4000,
},
);
const index = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
@@ -34,7 +37,10 @@ onBeforeUnmount(() => {
class="block h-full w-full bg-cover bg-center"
:style="{ backgroundImage: `url(${img.image})` }"
/>
<div v-if="images.length > 1" class="absolute bottom-[14px] left-0 right-0 flex justify-center gap-2">
<div
v-if="images.length > 1"
class="absolute right-0 bottom-[14px] left-0 flex justify-center gap-2"
>
<button
v-for="(_, i) in images"
:key="i"
+1 -1
View File
@@ -3,7 +3,7 @@ withDefaults(defineProps<{ text?: string }>(), { text: "" });
</script>
<template>
<div class="py-[60px] text-center text-muted/70">
<div class="text-muted/70 py-[60px] text-center">
<div class="mb-2.5 text-[40px] leading-none"></div>
<p class="m-0">{{ text || $t("common.empty") }}</p>
</div>
+15 -6
View File
@@ -5,21 +5,30 @@ const emit = defineEmits<{ close: [] }>();
<template>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-[900] flex items-center justify-center bg-black/40" @click.self="emit('close')">
<div class="min-w-[420px] max-w-[720px] rounded-md bg-surface shadow-lg">
<header v-if="title" class="flex items-center justify-between border-b border-border px-5 py-3.5 text-[15px]">
<div
v-if="open"
class="fixed inset-0 z-[900] flex items-center justify-center bg-black/40"
@click.self="emit('close')"
>
<div class="bg-surface max-w-[720px] min-w-[420px] rounded-md shadow-lg">
<header
v-if="title"
class="border-border flex items-center justify-between border-b px-5 py-3.5 text-[15px]"
>
<span>{{ title }}</span>
<button
type="button"
class="border-0 bg-transparent text-[20px] text-muted transition-colors hover:text-primary"
class="text-muted hover:text-primary border-0 bg-transparent text-[20px] transition-colors"
:aria-label="$t('common.cancel')"
@click="emit('close')"
>×</button>
>
×
</button>
</header>
<div class="p-5">
<slot />
</div>
<footer v-if="$slots.footer" class="border-t border-border px-5 py-3 text-right">
<footer v-if="$slots.footer" class="border-border border-t px-5 py-3 text-right">
<slot name="footer" />
</footer>
</div>
+17 -7
View File
@@ -21,23 +21,33 @@ function go(p: number): void {
<nav v-if="pages > 1" class="my-[30px] flex justify-center gap-1.5">
<button
type="button"
class="h-[34px] min-w-[34px] rounded-sm border border-border bg-surface px-2 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-40"
class="border-border bg-surface h-[34px] min-w-[34px] rounded-sm border px-2 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-40"
:disabled="page <= 1"
@click="go(page - 1)"
>{{ $t("common.prev") }}</button>
>
{{ $t("common.prev") }}
</button>
<button
v-for="p in window_"
:key="p"
type="button"
class="h-[34px] min-w-[34px] rounded-sm border border-border px-2 text-[13px] transition-colors"
:class="p === page ? 'border-primary bg-primary text-white' : 'bg-surface hover:border-primary hover:text-primary'"
class="border-border h-[34px] min-w-[34px] rounded-sm border px-2 text-[13px] transition-colors"
:class="
p === page
? 'border-primary bg-primary text-white'
: 'bg-surface hover:border-primary hover:text-primary'
"
@click="go(p)"
>{{ p }}</button>
>
{{ p }}
</button>
<button
type="button"
class="h-[34px] min-w-[34px] rounded-sm border border-border bg-surface px-2 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-40"
class="border-border bg-surface h-[34px] min-w-[34px] rounded-sm border px-2 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-40"
:disabled="page >= pages"
@click="go(page + 1)"
>{{ $t("common.next") }}</button>
>
{{ $t("common.next") }}
</button>
</nav>
</template>
+13 -7
View File
@@ -14,9 +14,9 @@ const sold = computed(() => props.product.sold_count);
<template>
<NuxtLink
:to="`/goods/${product.slug}`"
class="group block h-full no-underline text-inherit transition-shadow duration-200 hover:-translate-y-0.5 hover:shadow-lg"
class="group block h-full text-inherit no-underline transition-shadow duration-200 hover:-translate-y-0.5 hover:shadow-lg"
>
<VCard :padded="false" class="h-full overflow-hidden border border-border bg-surface">
<VCard :padded="false" class="border-border bg-surface h-full overflow-hidden border">
<div class="flex items-center justify-center p-4">
<img
:src="product.images[0] || '/mock/product-1.svg'"
@@ -26,21 +26,27 @@ const sold = computed(() => props.product.sold_count);
/>
</div>
<div class="px-[14px] pb-[14px]">
<p class="m-0 h-[37px] overflow-hidden text-[13px] leading-[1.4] transition-colors group-hover:text-primary">
<p
class="group-hover:text-primary m-0 h-[37px] overflow-hidden text-[13px] leading-[1.4] transition-colors"
>
{{ pick(product.name, locale) }}
</p>
<p class="m-0 mb-2 mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[12px] text-muted/70">
<p
class="text-muted/70 m-0 mt-1 mb-2 overflow-hidden text-[12px] text-ellipsis whitespace-nowrap"
>
{{ pick(product.description, locale) }}
</p>
<p class="m-0">
<span v-if="sku" class="mr-2 text-[16px] font-bold text-primary">
<span v-if="sku" class="text-primary mr-2 text-[16px] font-bold">
<PriceText :amount-minor="sku.price_minor" :currency="sku.currency" />
</span>
<s v-if="sku" class="text-[12px] text-muted/60">
<s v-if="sku" class="text-muted/60 text-[12px]">
<PriceText :amount-minor="sku.price_minor + 10000" :currency="sku.currency" />
</s>
</p>
<p class="m-0 mt-1.5 text-[12px] text-muted/70">{{ $t("product.salesCount", { n: sold }) }}</p>
<p class="text-muted/70 m-0 mt-1.5 text-[12px]">
{{ $t("product.salesCount", { n: sold }) }}
</p>
</div>
</VCard>
</NuxtLink>
+10 -6
View File
@@ -9,26 +9,30 @@ function set(v: number): void {
</script>
<template>
<div class="inline-flex overflow-hidden rounded-md border border-border">
<div class="border-border inline-flex overflow-hidden rounded-md border">
<button
type="button"
class="h-8 w-[30px] border-0 bg-bg text-sm transition-colors hover:text-primary disabled:cursor-not-allowed disabled:opacity-40"
class="bg-bg hover:text-primary h-8 w-[30px] border-0 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40"
:disabled="modelValue <= 1"
@click="set(modelValue - 1)"
></button>
>
</button>
<input
:value="modelValue"
type="number"
:min="1"
:max="max"
class="h-8 w-12 [appearance:textfield] border-x border-border bg-surface text-center text-[13px] text-text outline-none focus:border-primary"
class="border-border bg-surface text-text focus:border-primary h-8 w-12 [appearance:textfield] border-x text-center text-[13px] outline-none"
@change="set(Number(($event.target as HTMLInputElement).value))"
/>
<button
type="button"
class="h-8 w-[30px] border-0 bg-bg text-sm transition-colors hover:text-primary disabled:cursor-not-allowed disabled:opacity-40"
class="bg-bg hover:text-primary h-8 w-[30px] border-0 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40"
:disabled="modelValue >= max"
@click="set(modelValue + 1)"
>+</button>
>
+
</button>
</div>
</template>
+5 -4
View File
@@ -3,17 +3,18 @@ defineProps<{ steps: string[]; active: number }>();
</script>
<template>
<ol class="mb-5 flex list-none border border-border bg-bg py-3.5">
<ol class="border-border bg-bg mb-5 flex list-none border py-3.5">
<li
v-for="(s, i) in steps"
:key="s"
class="flex-1 text-center text-[13px] text-muted/70"
:class="i === active ? 'font-semibold text-primary' : ''"
class="text-muted/70 flex-1 text-center text-[13px]"
:class="i === active ? 'text-primary font-semibold' : ''"
>
<span
class="mr-2 inline-block h-[22px] w-[22px] rounded-full text-center text-[12px] leading-[22px] text-white"
:class="i < active ? 'bg-success' : i === active ? 'bg-primary' : 'bg-[#ddd]'"
>{{ i + 1 }}</span>
>{{ i + 1 }}</span
>
<span>{{ s }}</span>
</li>
</ol>
+10 -4
View File
@@ -5,15 +5,21 @@ const emit = defineEmits<{ "update:modelValue": [key: string] }>();
<template>
<div>
<div class="flex border-b border-border">
<div class="border-border flex border-b">
<button
v-for="tab in tabs"
:key="tab.key"
type="button"
class="border-0 border-b-2 border-transparent bg-transparent px-6 py-3 text-sm text-muted transition-colors"
:class="tab.key === modelValue ? 'border-primary font-semibold text-primary' : 'hover:text-primary'"
class="text-muted border-0 border-b-2 border-transparent bg-transparent px-6 py-3 text-sm transition-colors"
:class="
tab.key === modelValue
? 'border-primary text-primary font-semibold'
: 'hover:text-primary'
"
@click="emit('update:modelValue', tab.key)"
>{{ tab.label }}</button>
>
{{ tab.label }}
</button>
</div>
<div class="py-5">
<slot :active="modelValue" />