feat(hotel groups): add price, tags and publish status support
extend HotelGroup type with new fields including coverImage, priceAmount, priceUnit, tags and draft/published status. refactor admin utilities to separate hotel group and vehicle option handling, add proper payload normalization for hotel group submissions including syncing coverImage and image fields. update admin UI components to support hotel group-specific configurations, including default "起/晚" price unit, trimmed and limited tags, and publish status toggle. update admin documentation to reflect new hotel group API, split hotel groups docs from vehicle options, and document all new fields.
This commit is contained in:
@@ -108,6 +108,11 @@ export type HotelGroup = {
|
||||
title: string;
|
||||
description: string | null;
|
||||
image: string | null;
|
||||
coverImage?: string | null;
|
||||
priceAmount?: number | null;
|
||||
priceUnit?: string | null;
|
||||
tags?: string[];
|
||||
status?: "draft" | "published";
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt?: string;
|
||||
|
||||
@@ -180,7 +180,21 @@ export function createEmptySiteItemDraft(moduleId: SiteModule, sortOrder: number
|
||||
if (moduleId === "routeSections") {
|
||||
return { title: "", subtitle: "", productIds: [], isActive: true, sortOrder };
|
||||
}
|
||||
if (moduleId === "hotelGroups" || moduleId === "vehicleOptions") {
|
||||
if (moduleId === "hotelGroups") {
|
||||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
image: "",
|
||||
coverImage: "",
|
||||
priceAmount: null,
|
||||
priceUnit: "起/晚",
|
||||
tags: [],
|
||||
status: "published",
|
||||
isActive: true,
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
if (moduleId === "vehicleOptions") {
|
||||
return { title: "", description: "", image: "", isActive: true, sortOrder };
|
||||
}
|
||||
return { alt: "", image: "", isActive: false, sortOrder };
|
||||
@@ -236,7 +250,25 @@ export function compactSiteItemPayload(moduleId: SiteModule, draft: SiteItemPatc
|
||||
payload.targetType = undefined;
|
||||
payload.targetValue = undefined;
|
||||
}
|
||||
if (moduleId === "hotelGroups" || moduleId === "vehicleOptions") {
|
||||
if (moduleId === "hotelGroups") {
|
||||
const coverImage = draft.coverImage?.trim() || draft.image?.trim() || null;
|
||||
const isActive = draft.status ? draft.status === "published" : draft.isActive ?? true;
|
||||
payload.title = draft.title?.trim();
|
||||
payload.description = draft.description?.trim() || null;
|
||||
payload.image = coverImage;
|
||||
payload.coverImage = coverImage;
|
||||
payload.priceAmount = draft.priceAmount === null || draft.priceAmount === undefined ? null : Number(draft.priceAmount);
|
||||
payload.priceUnit = draft.priceUnit?.trim() || "起/晚";
|
||||
payload.tags = (draft.tags ?? []).map((tag) => tag.trim()).filter(Boolean).slice(0, 3);
|
||||
payload.status = isActive ? "published" : "draft";
|
||||
payload.isActive = isActive;
|
||||
payload.productIds = undefined;
|
||||
payload.kicker = undefined;
|
||||
payload.subtitle = undefined;
|
||||
payload.targetType = undefined;
|
||||
payload.targetValue = undefined;
|
||||
}
|
||||
if (moduleId === "vehicleOptions") {
|
||||
payload.title = draft.title?.trim();
|
||||
payload.description = draft.description?.trim() || null;
|
||||
payload.productIds = undefined;
|
||||
|
||||
@@ -276,6 +276,22 @@ export function StructurePage({
|
||||
.join(" ");
|
||||
|
||||
const setDraftFromItem = (item: EditableSiteItem) => {
|
||||
const imageValue = "image" in item ? item.image || "" : "";
|
||||
const coverImageValue =
|
||||
"coverImage" in item
|
||||
? item.coverImage || imageValue
|
||||
: moduleId === "hotelGroups"
|
||||
? imageValue
|
||||
: undefined;
|
||||
const activeStatus =
|
||||
"status" in item && (item.status === "draft" || item.status === "published")
|
||||
? item.status
|
||||
: moduleId === "hotelGroups"
|
||||
? "isActive" in item && item.isActive === false
|
||||
? "draft"
|
||||
: "published"
|
||||
: undefined;
|
||||
|
||||
setDraft({
|
||||
title: "title" in item ? item.title : undefined,
|
||||
kicker: "kicker" in item ? item.kicker || "" : undefined,
|
||||
@@ -284,16 +300,16 @@ export function StructurePage({
|
||||
region: "region" in item ? item.region || "" : undefined,
|
||||
label: "label" in item ? item.label : undefined,
|
||||
alt: "alt" in item ? item.alt : undefined,
|
||||
image: "image" in item ? item.image || "" : "",
|
||||
image: imageValue,
|
||||
description: "description" in item ? item.description || "" : undefined,
|
||||
coverImage: "coverImage" in item ? item.coverImage || "" : undefined,
|
||||
coverImage: coverImageValue,
|
||||
priceAmount: "priceAmount" in item ? item.priceAmount : undefined,
|
||||
priceUnit: "priceUnit" in item ? item.priceUnit || "起/人" : undefined,
|
||||
tags: "tags" in item ? item.tags.slice(0, 3) : undefined,
|
||||
priceUnit: "priceUnit" in item ? item.priceUnit || (moduleId === "hotelGroups" ? "起/晚" : "起/人") : moduleId === "hotelGroups" ? "起/晚" : undefined,
|
||||
tags: "tags" in item && Array.isArray(item.tags) ? item.tags.slice(0, 3) : moduleId === "hotelGroups" ? [] : undefined,
|
||||
isHot: "isHot" in item ? item.isHot : undefined,
|
||||
isActive: "isActive" in item ? item.isActive : undefined,
|
||||
isActive: "isActive" in item ? item.isActive : moduleId === "hotelGroups" ? activeStatus === "published" : undefined,
|
||||
sortOrder: itemSortOrder(item),
|
||||
status: "status" in item && (item.status === "draft" || item.status === "published") ? item.status : undefined,
|
||||
status: activeStatus,
|
||||
startsAt: "startsAt" in item ? item.startsAt || "" : undefined,
|
||||
endsAt: "endsAt" in item ? item.endsAt || "" : undefined,
|
||||
});
|
||||
@@ -943,16 +959,22 @@ function SiteItemEditor({
|
||||
const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`;
|
||||
const isImageOnlyModule = moduleId === "map";
|
||||
const isCampaignModule = moduleId === "campaigns";
|
||||
const isHotelModule = moduleId === "hotelGroups";
|
||||
const usesOfferConfig = isCampaignModule || isHotelModule;
|
||||
const isMoreServicesModule = moduleId === "ctaBanners";
|
||||
const isCardContentModule = moduleId === "hotelGroups" || moduleId === "vehicleOptions";
|
||||
const isCardContentModule = moduleId === "vehicleOptions";
|
||||
const editorHint = isImageOnlyModule
|
||||
? isCreating
|
||||
? `上传后会作为「${moduleLabel}」唯一展示图片`
|
||||
: `保存后会替换「${moduleLabel}」前台展示图片`
|
||||
: isCampaignModule
|
||||
? isCreating
|
||||
? `创建后会进入「${moduleLabel}」的活动列表`
|
||||
: `保存后会影响「${moduleLabel}」活动入口`
|
||||
: usesOfferConfig
|
||||
? isCampaignModule
|
||||
? isCreating
|
||||
? `创建后会进入「${moduleLabel}」的活动列表`
|
||||
: `保存后会影响「${moduleLabel}」活动入口`
|
||||
: isCreating
|
||||
? `创建后会进入「${moduleLabel}」的数据列表`
|
||||
: `保存后会影响「${moduleLabel}」首页卡片`
|
||||
: isMoreServicesModule
|
||||
? isCreating
|
||||
? "创建后会进入首页「更多服务」卡片列表"
|
||||
@@ -980,7 +1002,7 @@ function SiteItemEditor({
|
||||
<div className="admin-disclosure-stack">
|
||||
{!isImageOnlyModule ? (
|
||||
<AdminDisclosure title="展示内容">
|
||||
{isCampaignModule ? (
|
||||
{usesOfferConfig ? (
|
||||
<>
|
||||
<label>
|
||||
{primaryLabel}
|
||||
@@ -992,7 +1014,7 @@ function SiteItemEditor({
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
活动描述
|
||||
{isHotelModule ? "酒店描述" : "活动描述"}
|
||||
<Textarea
|
||||
value={draft.description ?? ""}
|
||||
onChange={(event) =>
|
||||
@@ -1071,7 +1093,7 @@ function SiteItemEditor({
|
||||
</div>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
{isCampaignModule ? (
|
||||
{usesOfferConfig ? (
|
||||
<AdminDisclosure title="价格信息">
|
||||
<div className="form-grid compact-grid">
|
||||
<label>
|
||||
@@ -1092,7 +1114,7 @@ function SiteItemEditor({
|
||||
<label>
|
||||
价格单位
|
||||
<Input
|
||||
value={draft.priceUnit ?? "起/人"}
|
||||
value={draft.priceUnit ?? (isHotelModule ? "起/晚" : "起/人")}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, priceUnit: event.target.value })
|
||||
}
|
||||
@@ -1101,34 +1123,40 @@ function SiteItemEditor({
|
||||
</div>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
{isCampaignModule ? (
|
||||
<AdminDisclosure title="活动标签">
|
||||
{usesOfferConfig ? (
|
||||
<AdminDisclosure title={isHotelModule ? "酒店标签" : "活动标签"}>
|
||||
<CampaignTagEditor
|
||||
tags={draft.tags ?? []}
|
||||
onChange={(tags) => onDraft({ ...draft, tags })}
|
||||
/>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : isCampaignModule ? "活动封面图" : isMoreServicesModule ? "服务卡片背景图" : "资源图片"}>
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : usesOfferConfig ? isHotelModule ? "酒店封面图" : "活动封面图" : isMoreServicesModule ? "服务卡片背景图" : "资源图片"}>
|
||||
<SingleImageUploader
|
||||
value={isCampaignModule ? draft.coverImage : draft.image}
|
||||
value={usesOfferConfig ? draft.coverImage ?? draft.image : draft.image}
|
||||
onChange={(image) =>
|
||||
onDraft(
|
||||
isCampaignModule
|
||||
? { ...draft, coverImage: image }
|
||||
: { ...draft, image },
|
||||
: isHotelModule
|
||||
? { ...draft, coverImage: image, image }
|
||||
: { ...draft, image },
|
||||
)
|
||||
}
|
||||
group={moduleId}
|
||||
/>
|
||||
</AdminDisclosure>
|
||||
<AdminDisclosure title={isImageOnlyModule || isCampaignModule ? "显示状态" : "排序与状态"}>
|
||||
{isCampaignModule ? (
|
||||
<AdminDisclosure title={isImageOnlyModule || usesOfferConfig ? "显示状态" : "排序与状态"}>
|
||||
{usesOfferConfig ? (
|
||||
<label className="switch-line">
|
||||
<Switch
|
||||
checked={draft.status === "published"}
|
||||
checked={draft.status === "published" || (isHotelModule && !draft.status && Boolean(draft.isActive))}
|
||||
onCheckedChange={(checked) =>
|
||||
onDraft({ ...draft, status: checked ? "published" : "draft" })
|
||||
onDraft(
|
||||
isHotelModule
|
||||
? { ...draft, status: checked ? "published" : "draft", isActive: checked }
|
||||
: { ...draft, status: checked ? "published" : "draft" },
|
||||
)
|
||||
}
|
||||
/>
|
||||
前台启用
|
||||
|
||||
Reference in New Issue
Block a user