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
+103 -27
View File
@@ -107,7 +107,12 @@ function validate(kind: ContentKind): boolean {
for (const [index, row] of list.entries()) {
if (kind === "quick-links") {
const link = row as QuickLinkRow;
if (!link.label.en.trim() || !link.label.zh.trim() || !link.url.trim() || !link.glyph.trim()) {
if (
!link.label.en.trim() ||
!link.label.zh.trim() ||
!link.url.trim() ||
!link.glyph.trim()
) {
errors[index] = t("admin.contentQuickLinkInvalid");
}
} else if (kind === "floor-adverts") {
@@ -150,7 +155,6 @@ async function saveKind(kind: ContentKind): Promise<void> {
}
}
onMounted(() => {
void loadContent();
});
@@ -158,14 +162,23 @@ onMounted(() => {
<template>
<VPage :title="$t('nav.content')">
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
<p v-else-if="loadError" class="my-2 text-sm text-danger" role="alert">{{ loadError }}</p>
<p v-if="loading" class="text-muted text-sm">{{ $t("common.loading") }}</p>
<p v-else-if="loadError" class="text-danger my-2 text-sm" role="alert">{{ loadError }}</p>
<template v-else>
<VCard v-for="kind in (['banners', 'promos'] as ContentKind[])" :key="kind" class="mb-5">
<VCard v-for="kind in ['banners', 'promos'] as ContentKind[]" :key="kind" class="mb-5">
<h2 class="mb-4 text-base font-semibold">{{ $t(kindLabelKey[kind]) }}</h2>
<div v-for="(row, index) in rowsOf(kind)" :key="index" class="mb-3 rounded-md border border-border p-3">
<div
v-for="(row, index) in rowsOf(kind)"
:key="index"
class="border-border mb-3 rounded-md border p-3"
>
<div class="grid items-end gap-3 md:grid-cols-[auto_1fr_1fr_auto_auto]">
<img v-if="row.image" :src="row.image" :alt="$t(kindLabelKey[kind])" class="h-12 w-20 rounded object-cover" />
<img
v-if="row.image"
:src="row.image"
:alt="$t(kindLabelKey[kind])"
class="h-12 w-20 rounded object-cover"
/>
<VField :label="$t('admin.contentImage')" :error="rowErrors[kind][index]">
<VInput v-model="row.image" />
</VField>
@@ -173,12 +186,17 @@ onMounted(() => {
<VInput v-model="row.url" />
</VField>
<label class="mb-3.5 flex items-center gap-2 text-sm">
<input v-model="row.active" type="checkbox" class="h-4 w-4 accent-primary" />
<input v-model="row.active" type="checkbox" class="accent-primary h-4 w-4" />
{{ $t("admin.enabled") }}
</label>
<div class="mb-3.5 flex gap-1">
<VBtn size="sm" :disabled="index === 0" @click="moveRow(kind, index, -1)">↑</VBtn>
<VBtn size="sm" :disabled="index === rowsOf(kind).length - 1" @click="moveRow(kind, index, 1)">↓</VBtn>
<VBtn
size="sm"
:disabled="index === rowsOf(kind).length - 1"
@click="moveRow(kind, index, 1)"
>↓</VBtn
>
<VBtn size="sm" @click="removeRow(kind, index)">{{ $t("common.delete") }}</VBtn>
</div>
</div>
@@ -188,7 +206,12 @@ onMounted(() => {
<VBtn variant="primary" :disabled="saving === kind" @click="saveKind(kind)">
{{ saving === kind ? $t("common.loading") : $t("common.save") }}
</VBtn>
<p v-if="feedback[kind]" role="status" class="text-sm" :class="feedback[kind]!.ok ? 'text-success' : 'text-danger'">
<p
v-if="feedback[kind]"
role="status"
class="text-sm"
:class="feedback[kind]!.ok ? 'text-success' : 'text-danger'"
>
{{ feedback[kind]!.message }}
</p>
</div>
@@ -196,9 +219,17 @@ onMounted(() => {
<VCard class="mb-5">
<h2 class="mb-4 text-base font-semibold">{{ $t("admin.contentQuickLinks") }}</h2>
<div v-for="(row, index) in quickLinks" :key="index" class="mb-3 rounded-md border border-border p-3">
<div
v-for="(row, index) in quickLinks"
:key="index"
class="border-border mb-3 rounded-md border p-3"
>
<div class="grid items-end gap-3 md:grid-cols-[auto_1fr_1fr_1fr_1fr_auto_auto]">
<svg viewBox="0 0 24 24" class="mb-3.5 h-8 w-8 fill-current text-primary" aria-hidden="true">
<svg
viewBox="0 0 24 24"
class="text-primary mb-3.5 h-8 w-8 fill-current"
aria-hidden="true"
>
<path :d="row.glyph" />
</svg>
<VField :label="$t('admin.contentLabelEn')" :error="rowErrors['quick-links'][index]">
@@ -214,22 +245,40 @@ onMounted(() => {
<VInput v-model="row.glyph" placeholder="M12 2L2 22h20z" />
</VField>
<label class="mb-3.5 flex items-center gap-2 text-sm">
<input v-model="row.active" type="checkbox" class="h-4 w-4 accent-primary" />
<input v-model="row.active" type="checkbox" class="accent-primary h-4 w-4" />
{{ $t("admin.enabled") }}
</label>
<div class="mb-3.5 flex gap-1">
<VBtn size="sm" :disabled="index === 0" @click="moveRow('quick-links', index, -1)">↑</VBtn>
<VBtn size="sm" :disabled="index === quickLinks.length - 1" @click="moveRow('quick-links', index, 1)">↓</VBtn>
<VBtn size="sm" @click="removeRow('quick-links', index)">{{ $t("common.delete") }}</VBtn>
<VBtn size="sm" :disabled="index === 0" @click="moveRow('quick-links', index, -1)"
>↑</VBtn
>
<VBtn
size="sm"
:disabled="index === quickLinks.length - 1"
@click="moveRow('quick-links', index, 1)"
>↓</VBtn
>
<VBtn size="sm" @click="removeRow('quick-links', index)">{{
$t("common.delete")
}}</VBtn>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<VBtn size="sm" @click="addRow('quick-links')">{{ $t("admin.contentAddRow") }}</VBtn>
<VBtn variant="primary" :disabled="saving === 'quick-links'" @click="saveKind('quick-links')">
<VBtn
variant="primary"
:disabled="saving === 'quick-links'"
@click="saveKind('quick-links')"
>
{{ saving === "quick-links" ? $t("common.loading") : $t("common.save") }}
</VBtn>
<p v-if="feedback['quick-links']" role="status" class="text-sm" :class="feedback['quick-links']!.ok ? 'text-success' : 'text-danger'">
<p
v-if="feedback['quick-links']"
role="status"
class="text-sm"
:class="feedback['quick-links']!.ok ? 'text-success' : 'text-danger'"
>
{{ feedback["quick-links"]!.message }}
</p>
</div>
@@ -237,33 +286,60 @@ onMounted(() => {
<VCard class="mb-5">
<h2 class="mb-4 text-base font-semibold">{{ $t("admin.contentFloorAdverts") }}</h2>
<div v-for="(row, index) in floorAdverts" :key="index" class="mb-3 rounded-md border border-border p-3">
<div
v-for="(row, index) in floorAdverts"
:key="index"
class="border-border mb-3 rounded-md border p-3"
>
<div class="grid items-end gap-3 md:grid-cols-[auto_1fr_auto_auto]">
<img v-if="row.image" :src="row.image" :alt="$t('admin.contentFloorAdverts')" class="h-12 w-20 rounded object-cover" />
<img
v-if="row.image"
:src="row.image"
:alt="$t('admin.contentFloorAdverts')"
class="h-12 w-20 rounded object-cover"
/>
<VField :label="$t('admin.contentImage')" :error="rowErrors['floor-adverts'][index]">
<VInput v-model="row.image" />
</VField>
<label class="mb-3.5 flex items-center gap-2 text-sm">
<input v-model="row.active" type="checkbox" class="h-4 w-4 accent-primary" />
<input v-model="row.active" type="checkbox" class="accent-primary h-4 w-4" />
{{ $t("admin.enabled") }}
</label>
<div class="mb-3.5 flex gap-1">
<VBtn size="sm" :disabled="index === 0" @click="moveRow('floor-adverts', index, -1)">↑</VBtn>
<VBtn size="sm" :disabled="index === floorAdverts.length - 1" @click="moveRow('floor-adverts', index, 1)">↓</VBtn>
<VBtn size="sm" @click="removeRow('floor-adverts', index)">{{ $t("common.delete") }}</VBtn>
<VBtn size="sm" :disabled="index === 0" @click="moveRow('floor-adverts', index, -1)"
>↑</VBtn
>
<VBtn
size="sm"
:disabled="index === floorAdverts.length - 1"
@click="moveRow('floor-adverts', index, 1)"
>↓</VBtn
>
<VBtn size="sm" @click="removeRow('floor-adverts', index)">{{
$t("common.delete")
}}</VBtn>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<VBtn size="sm" @click="addRow('floor-adverts')">{{ $t("admin.contentAddRow") }}</VBtn>
<VBtn variant="primary" :disabled="saving === 'floor-adverts'" @click="saveKind('floor-adverts')">
<VBtn
variant="primary"
:disabled="saving === 'floor-adverts'"
@click="saveKind('floor-adverts')"
>
{{ saving === "floor-adverts" ? $t("common.loading") : $t("common.save") }}
</VBtn>
<p v-if="feedback['floor-adverts']" role="status" class="text-sm" :class="feedback['floor-adverts']!.ok ? 'text-success' : 'text-danger'">
<p
v-if="feedback['floor-adverts']"
role="status"
class="text-sm"
:class="feedback['floor-adverts']!.ok ? 'text-success' : 'text-danger'"
>
{{ feedback["floor-adverts"]!.message }}
</p>
</div>
</VCard>
</template>
</VPage>
</template>
</template>