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:
@@ -29,6 +29,7 @@ import { RouteSectionsPanel } from "@/components/admin/route-sections/RouteSecti
|
||||
import { SingleImageUploader } from "@/components/admin/SingleImageUploader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NativeSelect } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
@@ -53,6 +54,13 @@ import type {
|
||||
} from "@/types/admin";
|
||||
|
||||
const NEW_SITE_ITEM_ID = "__new_site_item__";
|
||||
const moreServicesTargetOptions = [
|
||||
{ value: "", label: "按默认顺序" },
|
||||
{ value: "cardBenefits", label: "万趣权益页" },
|
||||
{ value: "service", label: "服务管家" },
|
||||
{ value: "destinationPicker", label: "目的地页" },
|
||||
{ value: "demand", label: "提交需求" },
|
||||
] as const;
|
||||
|
||||
const pageSpecs: {
|
||||
id: PageId;
|
||||
@@ -108,11 +116,23 @@ const pageSpecs: {
|
||||
hint: "按运营任务新增精选线路分组,维护标题、副文案、启用状态和关联线路顺序。",
|
||||
frontPosition: "首页精选线路",
|
||||
},
|
||||
{
|
||||
id: "hotelGroups",
|
||||
label: "特色酒店",
|
||||
hint: "维护首页特色酒店卡片标题、描述、封面图、启用状态和排序。",
|
||||
frontPosition: "首页「特色酒店」",
|
||||
},
|
||||
{
|
||||
id: "vehicleOptions",
|
||||
label: "万趣用车",
|
||||
hint: "维护首页用车卡片标题、描述、封面图、启用状态和排序。",
|
||||
frontPosition: "首页「万趣用车」",
|
||||
},
|
||||
{
|
||||
id: "ctaBanners",
|
||||
label: "底部运营入口",
|
||||
hint: "控制权益卡、服务管家入口、目的地搜索、提交需求入口。",
|
||||
frontPosition: "首页页尾 CTA",
|
||||
label: "更多服务",
|
||||
hint: "维护更多服务卡片标题、背景图、启用状态和排序。",
|
||||
frontPosition: "首页「更多服务」",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -170,9 +190,9 @@ const pageSpecs: {
|
||||
},
|
||||
{
|
||||
id: "ctaBanners",
|
||||
label: "营销 CTA",
|
||||
hint: "页尾活动和需求入口。",
|
||||
frontPosition: "首页底部运营区",
|
||||
label: "更多服务入口",
|
||||
hint: "维护更多服务里的活动和需求入口。",
|
||||
frontPosition: "首页「更多服务」",
|
||||
},
|
||||
{
|
||||
id: "campaignProducts",
|
||||
@@ -232,6 +252,7 @@ export function StructurePage({
|
||||
: [];
|
||||
const isMapModule = moduleId === "map";
|
||||
const isCampaignModule = moduleId === "campaigns";
|
||||
const isCardContentModule = moduleId === "hotelGroups" || moduleId === "vehicleOptions";
|
||||
const canReorderSiteItems =
|
||||
moduleEditable(moduleId) && !isRouteSectionsModule && !isMapModule && !isCampaignModule;
|
||||
const canCreateSiteItem =
|
||||
@@ -602,18 +623,26 @@ export function StructurePage({
|
||||
</div>
|
||||
<h3>页面模块</h3>
|
||||
{activePage.modules.map((module, index) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={activeModule.id === module.id ? "active" : ""}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={activeModule.id === module.id}
|
||||
className={`module-rail-item${activeModule.id === module.id ? " active" : ""}`}
|
||||
key={module.id}
|
||||
onClick={() => setModuleId(module.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
setModuleId(module.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<em>{index + 1}</em>
|
||||
<span>
|
||||
<b>{module.label}</b>
|
||||
<small>{module.frontPosition}</small>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<section className="item-rail">
|
||||
@@ -722,7 +751,9 @@ export function StructurePage({
|
||||
<p className="mapped-empty">
|
||||
{isMapModule
|
||||
? "暂无地图图片,点击上传地图。"
|
||||
: "暂无配置数据,点击新增创建第一项。"}
|
||||
: moduleId === "ctaBanners"
|
||||
? "暂无更多服务卡片,点击新增创建第一项。"
|
||||
: "暂无配置数据,点击新增创建第一项。"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -898,21 +929,22 @@ function SiteItemEditor({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const primaryKey = siteItemPrimaryKey(moduleId);
|
||||
const primaryLabel =
|
||||
moduleId === "heroSlides"
|
||||
? "轮播标题"
|
||||
: moduleId === "destinations"
|
||||
? "目的地名称"
|
||||
: moduleId === "map"
|
||||
? "地图图片"
|
||||
: moduleId === "themes"
|
||||
? "主题名称"
|
||||
: moduleId === "campaigns"
|
||||
? "活动标题"
|
||||
: "入口文案";
|
||||
const primaryLabel = (() => {
|
||||
if (moduleId === "heroSlides") return "轮播标题";
|
||||
if (moduleId === "destinations") return "目的地名称";
|
||||
if (moduleId === "map") return "地图图片";
|
||||
if (moduleId === "themes") return "主题名称";
|
||||
if (moduleId === "campaigns") return "活动标题";
|
||||
if (moduleId === "hotelGroups") return "酒店标题";
|
||||
if (moduleId === "vehicleOptions") return "用车标题";
|
||||
if (moduleId === "ctaBanners") return "服务标题";
|
||||
return "入口文案";
|
||||
})();
|
||||
const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`;
|
||||
const isImageOnlyModule = moduleId === "map";
|
||||
const isCampaignModule = moduleId === "campaigns";
|
||||
const isMoreServicesModule = moduleId === "ctaBanners";
|
||||
const isCardContentModule = moduleId === "hotelGroups" || moduleId === "vehicleOptions";
|
||||
const editorHint = isImageOnlyModule
|
||||
? isCreating
|
||||
? `上传后会作为「${moduleLabel}」唯一展示图片`
|
||||
@@ -921,6 +953,10 @@ function SiteItemEditor({
|
||||
? isCreating
|
||||
? `创建后会进入「${moduleLabel}」的活动列表`
|
||||
: `保存后会影响「${moduleLabel}」活动入口`
|
||||
: isMoreServicesModule
|
||||
? isCreating
|
||||
? "创建后会进入首页「更多服务」卡片列表"
|
||||
: "保存后会影响首页「更多服务」卡片"
|
||||
: isCreating
|
||||
? `创建后会进入「${moduleLabel}」的数据列表`
|
||||
: `保存后会影响「${moduleLabel}」前台模块`;
|
||||
@@ -988,10 +1024,53 @@ function SiteItemEditor({
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{isCardContentModule ? (
|
||||
<label>
|
||||
卡片描述
|
||||
<Textarea
|
||||
value={draft.description ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, description: event.target.value })
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
{isMoreServicesModule ? (
|
||||
<AdminDisclosure title="跳转配置">
|
||||
<div className="form-grid compact-grid">
|
||||
<label>
|
||||
跳转类型
|
||||
<NativeSelect
|
||||
value={draft.targetType ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, targetType: event.target.value })
|
||||
}
|
||||
>
|
||||
{moreServicesTargetOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</label>
|
||||
<label>
|
||||
目标参数
|
||||
<Input
|
||||
value={draft.targetValue ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, targetValue: event.target.value })
|
||||
}
|
||||
placeholder="可选,如需求页目的地"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
{isCampaignModule ? (
|
||||
<AdminDisclosure title="价格信息">
|
||||
<div className="form-grid compact-grid">
|
||||
@@ -1030,7 +1109,7 @@ function SiteItemEditor({
|
||||
/>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : isCampaignModule ? "活动封面图" : "资源图片"}>
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : isCampaignModule ? "活动封面图" : isMoreServicesModule ? "服务卡片背景图" : "资源图片"}>
|
||||
<SingleImageUploader
|
||||
value={isCampaignModule ? draft.coverImage : draft.image}
|
||||
onChange={(image) =>
|
||||
|
||||
Reference in New Issue
Block a user