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
+122 -23
View File
@@ -17,8 +17,7 @@ const queryValue = (value: unknown): string => {
return typeof value === "string" ? value : "";
};
const parseSort = (raw: string): SortType =>
raw === "price" || raw === "sales" ? raw : "default";
const parseSort = (raw: string): SortType => (raw === "price" || raw === "sales" ? raw : "default");
const state = computed(() => {
const sort = parseSort(queryValue(route.query.sort));
@@ -95,9 +94,12 @@ const chooseBrand = (id?: string): void => {
};
const chooseSort = (sort: SortType): void => {
const order: OrderType = state.value.sort === sort && sort !== "default"
? state.value.order === "asc" ? "desc" : "asc"
: "asc";
const order: OrderType =
state.value.sort === sort && sort !== "default"
? state.value.order === "asc"
? "desc"
: "asc"
: "asc";
void replaceQuery({ sort, order, page: "1" });
};
@@ -138,34 +140,131 @@ const sortOptions = computed(() => [
</script>
<template>
<div class="min-h-screen bg-bg pb-12 font-sans text-text">
<div class="bg-bg text-text min-h-screen pb-12 font-sans">
<UiBreadcrumb :items="breadcrumbItems" />
<main class="mx-auto w-full max-w-mall px-4">
<VCard :padded="false" class="border border-border bg-surface px-5">
<div class="flex min-h-12 items-start gap-4 border-b border-border py-3 last:border-b-0">
<strong class="w-[90px] shrink-0 text-[13px] font-semibold leading-7 text-text">{{ t("search.category") }}</strong>
<main class="max-w-mall mx-auto w-full px-4">
<VCard :padded="false" class="border-border bg-surface border px-5">
<div class="border-border flex min-h-12 items-start gap-4 border-b py-3 last:border-b-0">
<strong class="text-text w-[90px] shrink-0 text-[13px] leading-7 font-semibold">{{
t("search.category")
}}</strong>
<div class="flex flex-wrap gap-x-2 gap-y-1">
<button type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="!state.category ? 'bg-primary text-white' : ''" @click="chooseCategory()">{{ t("search.all") }}</button>
<button v-for="category in topLevelCategories" :key="category.id" type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="category.id === state.category ? 'bg-primary text-white' : ''" @click="chooseCategory(category.id)">{{ pick(category.name, locale) }}</button>
<template v-if="selectedPath.length"><button v-for="category in selectedPath.slice(1)" :key="`path-${category.id}`" type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="category.id === state.category ? 'bg-primary text-white' : ''" @click="chooseCategory(category.id)">{{ pick(category.name, locale) }}</button></template>
<button v-for="category in categoryChildren" :key="`child-${category.id}`" type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="category.id === state.category ? 'bg-primary text-white' : ''" @click="chooseCategory(category.id)">{{ pick(category.name, locale) }}</button>
<button
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="!state.category ? 'bg-primary text-white' : ''"
@click="chooseCategory()"
>
{{ t("search.all") }}
</button>
<button
v-for="category in topLevelCategories"
:key="category.id"
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="category.id === state.category ? 'bg-primary text-white' : ''"
@click="chooseCategory(category.id)"
>
{{ pick(category.name, locale) }}
</button>
<template v-if="selectedPath.length"
><button
v-for="category in selectedPath.slice(1)"
:key="`path-${category.id}`"
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="category.id === state.category ? 'bg-primary text-white' : ''"
@click="chooseCategory(category.id)"
>
{{ pick(category.name, locale) }}
</button></template
>
<button
v-for="category in categoryChildren"
:key="`child-${category.id}`"
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="category.id === state.category ? 'bg-primary text-white' : ''"
@click="chooseCategory(category.id)"
>
{{ pick(category.name, locale) }}
</button>
</div>
</div>
<div v-if="brands.length" class="flex min-h-12 items-start gap-4 border-b border-border py-3">
<strong class="w-[90px] shrink-0 text-[13px] font-semibold leading-7 text-text">{{ t("search.brand") }}</strong>
<div class="flex flex-wrap gap-x-2 gap-y-1"><button type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="!state.brand ? 'bg-primary text-white' : ''" @click="chooseBrand()">{{ t("search.all") }}</button><button v-for="brand in brands" :key="brand.id" type="button" class="rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="brand.id === state.brand ? 'bg-primary text-white' : ''" @click="chooseBrand(brand.id)">{{ pick(brand.name, locale) }}</button></div>
<div
v-if="brands.length"
class="border-border flex min-h-12 items-start gap-4 border-b py-3"
>
<strong class="text-text w-[90px] shrink-0 text-[13px] leading-7 font-semibold">{{
t("search.brand")
}}</strong>
<div class="flex flex-wrap gap-x-2 gap-y-1">
<button
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="!state.brand ? 'bg-primary text-white' : ''"
@click="chooseBrand()"
>
{{ t("search.all") }}</button
><button
v-for="brand in brands"
:key="brand.id"
type="button"
class="text-muted hover:bg-primary rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="brand.id === state.brand ? 'bg-primary text-white' : ''"
@click="chooseBrand(brand.id)"
>
{{ pick(brand.name, locale) }}
</button>
</div>
</div>
<div class="flex min-h-12 items-start gap-4 py-3">
<strong class="w-[90px] shrink-0 text-[13px] font-semibold leading-7 text-text">{{ t("search.sort") }}</strong>
<div class="flex flex-wrap gap-x-2 gap-y-1"><button v-for="option in sortOptions" :key="option.key" type="button" class="min-w-[60px] rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 text-muted hover:bg-primary hover:text-white" :class="state.sort === option.key ? 'bg-primary text-white' : ''" :aria-label="option.key === state.sort ? (state.order === 'asc' ? t('search.ascending') : t('search.descending')) : undefined" @click="chooseSort(option.key)">{{ option.label }} <span v-if="state.sort === option.key && option.key !== 'default'" aria-hidden="true">{{ state.order === "asc" ? "↑" : "↓" }}</span></button></div>
<strong class="text-text w-[90px] shrink-0 text-[13px] leading-7 font-semibold">{{
t("search.sort")
}}</strong>
<div class="flex flex-wrap gap-x-2 gap-y-1">
<button
v-for="option in sortOptions"
:key="option.key"
type="button"
class="text-muted hover:bg-primary min-w-[60px] rounded-md border-0 bg-transparent px-2.5 py-0 text-[13px] leading-7 hover:text-white"
:class="state.sort === option.key ? 'bg-primary text-white' : ''"
:aria-label="
option.key === state.sort
? state.order === 'asc'
? t('search.ascending')
: t('search.descending')
: undefined
"
@click="chooseSort(option.key)"
>
{{ option.label }}
<span
v-if="state.sort === option.key && option.key !== 'default'"
aria-hidden="true"
>{{ state.order === "asc" ? "↑" : "↓" }}</span
>
</button>
</div>
</div>
</VCard>
<div class="px-0.5 pb-3 pt-[22px] text-[13px] text-muted">{{ t("search.resultCount", { n: result.total }) }}</div>
<div v-if="result.items.length" class="grid gap-4 [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]"><UiProductCard v-for="product in result.items" :key="product.id" :product="product" /></div>
<div class="text-muted px-0.5 pt-[22px] pb-3 text-[13px]">
{{ t("search.resultCount", { n: result.total }) }}
</div>
<div
v-if="result.items.length"
class="grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] gap-4"
>
<UiProductCard v-for="product in result.items" :key="product.id" :product="product" />
</div>
<UiEmptyState v-else :text="t('search.empty')" />
<UiPagination :page="result.page" :total="result.total" :per-page="result.per_page" @change="(page) => replaceQuery({ page: String(page) })" />
<UiPagination
:page="result.page"
:total="result.total"
:per-page="result.per_page"
@change="(page) => replaceQuery({ page: String(page) })"
/>
</main>
</div>
</template>