feat(admin): add hotel groups and vehicle options site modules
Add support for two new standalone site content modules in the admin dashboard: hotel groups and vehicle options. This commit includes: - Added TypeScript types for HotelGroup and VehicleOption, extended SiteConfig and SiteModule enum - Updated admin utilities (primary key lookup, empty drafts, payload compaction) for the new modules - Added management UI panels and editors in the structure page for configuring the new modules - Adjusted module rail styling and interactive states for consistent UI across the admin - Updated ctaBanners type definition to include missing sortOrder, createdAt, and updatedAt fields - Updated API and admin documentation to cover the new modules' CRUD endpoints and field contracts
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { MutableRefObject, useEffect, useState } from "react";
|
||||
|
||||
import type { ProductInput } from "@/api";
|
||||
import type { Product, ProductInput } from "@/api";
|
||||
import { AdminDisclosure } from "@/components/admin/AdminDisclosure";
|
||||
import { SingleImageUploader } from "@/components/admin/SingleImageUploader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -21,32 +21,57 @@ type QuickRouteDraft = {
|
||||
sortWeight: number;
|
||||
};
|
||||
|
||||
const emptyDraft: QuickRouteDraft = {
|
||||
title: "",
|
||||
summary: "",
|
||||
destinationName: "",
|
||||
priceAmount: null,
|
||||
priceUnit: "起/人",
|
||||
tags: ["", ""],
|
||||
coverImage: "",
|
||||
published: true,
|
||||
sortWeight: 0,
|
||||
};
|
||||
function createEmptyDraft(): QuickRouteDraft {
|
||||
return {
|
||||
title: "",
|
||||
summary: "",
|
||||
destinationName: "",
|
||||
priceAmount: null,
|
||||
priceUnit: "起/人",
|
||||
tags: ["", ""],
|
||||
coverImage: "",
|
||||
published: true,
|
||||
sortWeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function createDraftFromProduct(product?: Product | null): QuickRouteDraft {
|
||||
if (!product) return createEmptyDraft();
|
||||
|
||||
const tags = product.tags.length ? product.tags.slice(0, 3) : ["", ""];
|
||||
|
||||
return {
|
||||
title: product.title,
|
||||
summary: product.summary ?? "",
|
||||
destinationName: product.subtitle ?? product.destination?.name ?? "",
|
||||
priceAmount: product.priceAmount ?? null,
|
||||
priceUnit: product.priceUnit || "起/人",
|
||||
tags: tags.length === 1 ? [...tags, ""] : tags,
|
||||
coverImage: product.coverImage ?? "",
|
||||
published: product.status === "published",
|
||||
sortWeight: product.sortWeight ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function RouteProductQuickCreateForm({
|
||||
product,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onSubmitRef,
|
||||
}: {
|
||||
product?: Product | null;
|
||||
onCreate: (input: ProductInput) => Promise<void>;
|
||||
onUpdate: (productId: string, input: ProductInput) => Promise<void>;
|
||||
onSubmitRef: MutableRefObject<(() => Promise<void>) | null>;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<QuickRouteDraft>(emptyDraft);
|
||||
const [draft, setDraft] = useState<QuickRouteDraft>(() => createDraftFromProduct(product));
|
||||
const editing = Boolean(product);
|
||||
|
||||
const submit = async () => {
|
||||
const title = draft.title.trim();
|
||||
if (!title) return;
|
||||
|
||||
await onCreate({
|
||||
const input: ProductInput = {
|
||||
title,
|
||||
subtitle: draft.destinationName.trim(),
|
||||
destinationId: null,
|
||||
@@ -55,20 +80,31 @@ export function RouteProductQuickCreateForm({
|
||||
tags: draft.tags.map((tag) => tag.trim()).filter(Boolean).slice(0, 3),
|
||||
coverImage: draft.coverImage.trim() || null,
|
||||
summary: draft.summary.trim(),
|
||||
images: [],
|
||||
detailSections: [],
|
||||
images: product?.images ?? [],
|
||||
detailSections: product?.detailSections ?? [],
|
||||
status: draft.published ? "published" : "draft",
|
||||
sortWeight: draft.sortWeight,
|
||||
});
|
||||
setDraft(emptyDraft);
|
||||
};
|
||||
|
||||
if (editing && product) {
|
||||
await onUpdate(product.id, input);
|
||||
return;
|
||||
}
|
||||
|
||||
await onCreate(input);
|
||||
setDraft(createEmptyDraft());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(createDraftFromProduct(product));
|
||||
}, [product?.id, product?.updatedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
onSubmitRef.current = submit;
|
||||
return () => {
|
||||
onSubmitRef.current = null;
|
||||
};
|
||||
}, [draft, onSubmitRef]);
|
||||
}, [draft, product?.id, product?.updatedAt, onSubmitRef]);
|
||||
|
||||
return (
|
||||
<div className="admin-disclosure-stack route-product-create-form">
|
||||
@@ -153,7 +189,7 @@ export function RouteProductQuickCreateForm({
|
||||
checked={draft.published}
|
||||
onCheckedChange={(checked) => setDraft({ ...draft, published: checked })}
|
||||
/>
|
||||
创建后上架
|
||||
{editing ? "保存为上架" : "创建后上架"}
|
||||
</label>
|
||||
</AdminDisclosure>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user