feat: 新增用车需求服务及线索管理全功能
- 新增后台用车服务配置模块,支持维护服务简介、优势与使用流程 - 新增需求线索管理页面,支持筛选、查看与更新用车线索状态 - 新增小程序用车需求页面,优化登录路径与回跳逻辑 - 优化车型卡片跳转与用车需求提交功能 - 新增服务端API与数据处理逻辑,完善权限校验 - 更新全站配置与API文档,新增相关测试用例
This commit is contained in:
@@ -70,6 +70,28 @@ export type VehicleOption = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type VehicleServiceSection = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type VehicleServiceStep = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type VehicleServiceConfig = {
|
||||
id: string;
|
||||
introTitle: string;
|
||||
intro: string;
|
||||
serviceSections: VehicleServiceSection[];
|
||||
advantages: string[];
|
||||
processSteps: VehicleServiceStep[];
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type SiteConfig = {
|
||||
heroSlides: Array<{ id: string; title: string; kicker: string | null; image: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>;
|
||||
destinationHero: DestinationHero[];
|
||||
@@ -77,6 +99,7 @@ export type SiteConfig = {
|
||||
demandFeatureCards: DemandFeatureCard[];
|
||||
demandForm: DemandForm[];
|
||||
vehicleOptions: VehicleOption[];
|
||||
vehicleService: VehicleServiceConfig[];
|
||||
};
|
||||
|
||||
export type SiteModule =
|
||||
@@ -85,7 +108,8 @@ export type SiteModule =
|
||||
| "demandHero"
|
||||
| "demandFeatureCards"
|
||||
| "demandForm"
|
||||
| "vehicleOptions";
|
||||
| "vehicleOptions"
|
||||
| "vehicleService";
|
||||
|
||||
export type WanfaRoute = {
|
||||
id: string;
|
||||
@@ -275,6 +299,11 @@ export type SiteItemPatch = {
|
||||
notePlaceholder?: string | null;
|
||||
submitLabel?: string;
|
||||
chips?: string[];
|
||||
introTitle?: string;
|
||||
intro?: string;
|
||||
serviceSections?: VehicleServiceSection[];
|
||||
advantages?: string[];
|
||||
processSteps?: VehicleServiceStep[];
|
||||
targetType?: string | null;
|
||||
targetValue?: string | null;
|
||||
isActive?: boolean;
|
||||
@@ -430,6 +459,60 @@ export async function getSiteConfig() {
|
||||
return request<SiteConfig>("/api/admin/site-config");
|
||||
}
|
||||
|
||||
export type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid";
|
||||
export type LeadType = "general" | "vehicle";
|
||||
|
||||
export type VehicleDemand = {
|
||||
serviceType: "charter" | "transfer";
|
||||
charterDuration?: "halfDay" | "fullDay" | null;
|
||||
travelDate: string;
|
||||
pickupTime?: string | null;
|
||||
pickupLocation: string;
|
||||
dropoffLocation: string;
|
||||
peopleCount: number;
|
||||
luggageCount?: number | null;
|
||||
vehicleOptionId?: string | null;
|
||||
vehicleOptionTitle?: string | null;
|
||||
specialRequirements?: string | null;
|
||||
};
|
||||
|
||||
export type Lead = {
|
||||
id: string;
|
||||
leadType: LeadType;
|
||||
contactName?: string | null;
|
||||
customerId?: string | null;
|
||||
destination?: string | null;
|
||||
phone: string;
|
||||
travelDate?: string | null;
|
||||
peopleCount?: number | null;
|
||||
budgetMin?: number | null;
|
||||
budgetMax?: number | null;
|
||||
note?: string | null;
|
||||
sourcePage?: string | null;
|
||||
vehicleDemand?: VehicleDemand | null;
|
||||
status: LeadStatus;
|
||||
assignedUser?: { id: string; name: string } | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export async function getLeads(params: { leadType?: LeadType; status?: LeadStatus; keyword?: string; take?: number } = {}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.leadType) query.set("leadType", params.leadType);
|
||||
if (params.status) query.set("status", params.status);
|
||||
if (params.keyword?.trim()) query.set("keyword", params.keyword.trim());
|
||||
if (params.take) query.set("take", String(params.take));
|
||||
const suffix = query.toString() ? `?${query.toString()}` : "";
|
||||
return request<{ items: Lead[] }>(`/api/admin/leads${suffix}`);
|
||||
}
|
||||
|
||||
export async function updateLeadStatus(leadId: string, status: LeadStatus) {
|
||||
return request<Lead>(`/api/admin/leads/${leadId}/status`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWanfaCategories() {
|
||||
return request<{ categories: WanfaCategory[] }>("/api/admin/wanfa/categories");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Headset, Home, List, LogOut } from "lucide-react";
|
||||
import { ClipboardList, Headset, Home, List, LogOut } from "lucide-react";
|
||||
|
||||
import { clearToken } from "@/api";
|
||||
import { ToastStack } from "@/components/admin/ToastStack";
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { ConciergePage } from "@/pages/structure/ConciergePage";
|
||||
import { HomePage } from "@/pages/structure/HomePage";
|
||||
import { WanfaPage } from "@/pages/structure/WanfaPage";
|
||||
import { LeadsPage } from "@/pages/leads/LeadsPage";
|
||||
import type { Tab, Toast } from "@/types/admin";
|
||||
|
||||
const sidebarGroups: {
|
||||
@@ -21,6 +22,7 @@ const sidebarGroups: {
|
||||
{ id: "home", label: "首页", icon: Home, description: "" },
|
||||
{ id: "play", label: "玩法", icon: List, description: "" },
|
||||
{ id: "butler", label: "管家", icon: Headset, description: "" },
|
||||
{ id: "leads", label: "需求线索", icon: ClipboardList, description: "" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -118,6 +120,8 @@ export function AdminShell({ onLogout }: { onLogout: () => void }) {
|
||||
<WanfaPage key="wanfa-page" notify={notify} />
|
||||
) : activeTab === "butler" ? (
|
||||
<ConciergePage key="concierge-page" notify={notify} />
|
||||
) : activeTab === "leads" ? (
|
||||
<LeadsPage key="leads-page" notify={notify} />
|
||||
) : null}
|
||||
</section>
|
||||
<ToastStack
|
||||
|
||||
@@ -7,6 +7,7 @@ export function siteItemPrimaryKey(moduleId: SiteModule): keyof SiteItemPatch {
|
||||
if (moduleId === "demandForm") return "submitLabel";
|
||||
if (moduleId === "demandHero" || moduleId === "demandFeatureCards") return "title";
|
||||
if (moduleId === "vehicleOptions") return "title";
|
||||
if (moduleId === "vehicleService") return "introTitle";
|
||||
return "title";
|
||||
}
|
||||
|
||||
@@ -39,6 +40,16 @@ export function createEmptySiteItemDraft(moduleId: SiteModule, sortOrder: number
|
||||
if (moduleId === "vehicleOptions") {
|
||||
return { title: "", description: "", image: "", isActive: true, sortOrder };
|
||||
}
|
||||
if (moduleId === "vehicleService") {
|
||||
return {
|
||||
introTitle: "万趣用车",
|
||||
intro: "旅游出行专属用车服务,管家协助确认合适车型与行程安排。",
|
||||
serviceSections: [],
|
||||
advantages: [],
|
||||
processSteps: [],
|
||||
isActive: true,
|
||||
};
|
||||
}
|
||||
return { title: "", isActive: false, sortOrder };
|
||||
}
|
||||
|
||||
@@ -104,6 +115,20 @@ export function compactSiteItemPayload(moduleId: SiteModule, draft: SiteItemPatc
|
||||
payload.targetType = undefined;
|
||||
payload.targetValue = undefined;
|
||||
}
|
||||
if (moduleId === "vehicleService") {
|
||||
payload.introTitle = draft.introTitle?.trim();
|
||||
payload.intro = draft.intro?.trim() || "";
|
||||
payload.serviceSections = (draft.serviceSections ?? [])
|
||||
.map((item) => ({ title: item.title.trim(), description: item.description.trim() }))
|
||||
.filter((item) => item.title || item.description);
|
||||
payload.advantages = (draft.advantages ?? []).map((item) => item.trim()).filter(Boolean);
|
||||
payload.processSteps = (draft.processSteps ?? [])
|
||||
.map((item) => ({ title: item.title.trim(), description: item.description.trim() }))
|
||||
.filter((item) => item.title || item.description);
|
||||
payload.title = undefined;
|
||||
payload.image = undefined;
|
||||
payload.sortOrder = undefined;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -125,6 +150,7 @@ export function readImageAsDataUrl(file: File) {
|
||||
export function itemName(item: EditableSiteItem) {
|
||||
if ("title" in item) return item.title;
|
||||
if ("submitLabel" in item) return "需求表单配置";
|
||||
if ("introTitle" in item) return item.introTitle;
|
||||
return "配置项";
|
||||
}
|
||||
|
||||
@@ -135,6 +161,7 @@ export function itemMeta(item: EditableSiteItem): string {
|
||||
const description = typeof item.description === "string" ? item.description : "";
|
||||
return description;
|
||||
}
|
||||
if ("intro" in item) return item.intro || "用车需求页服务说明";
|
||||
if (!("title" in item)) return "配置项";
|
||||
return "";
|
||||
}
|
||||
@@ -153,5 +180,5 @@ export function moduleItems(config: SiteConfig, moduleId: SiteModule): EditableS
|
||||
}
|
||||
|
||||
export function moduleEditable(moduleId: ModuleId): moduleId is SiteModule {
|
||||
return moduleId === "heroSlides" || moduleId === "destinationHero" || moduleId === "demandHero" || moduleId === "demandFeatureCards" || moduleId === "demandForm" || moduleId === "vehicleOptions";
|
||||
return moduleId === "heroSlides" || moduleId === "destinationHero" || moduleId === "demandHero" || moduleId === "demandFeatureCards" || moduleId === "demandForm" || moduleId === "vehicleOptions" || moduleId === "vehicleService";
|
||||
}
|
||||
|
||||
157
WonderQ-Admin-UI/src/pages/leads/LeadsPage.tsx
Normal file
157
WonderQ-Admin-UI/src/pages/leads/LeadsPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
import { getLeads, updateLeadStatus } from "@/api";
|
||||
import type { Lead, LeadStatus } from "@/api";
|
||||
import { EmptyState } from "@/components/admin/EmptyState";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NativeSelect } from "@/components/ui/select";
|
||||
import type { Notify } from "@/types/admin";
|
||||
|
||||
const statusLabels: Record<LeadStatus, string> = {
|
||||
new: "待处理",
|
||||
assigned: "已分配",
|
||||
contacted: "已联系",
|
||||
planning: "方案沟通",
|
||||
won: "已成交",
|
||||
invalid: "无效",
|
||||
};
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
if (!value) return "未填写";
|
||||
return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium" }).format(new Date(value));
|
||||
}
|
||||
|
||||
function leadTitle(lead: Lead) {
|
||||
return lead.vehicleDemand?.vehicleOptionTitle || lead.destination || "用车需求";
|
||||
}
|
||||
|
||||
export function LeadsPage({ notify }: { notify: Notify }) {
|
||||
const [items, setItems] = useState<Lead[]>([]);
|
||||
const [selectedId, setSelectedId] = useState("");
|
||||
const [status, setStatus] = useState<LeadStatus | "">("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [message, setMessage] = useState("");
|
||||
const [savingId, setSavingId] = useState("");
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getLeads({ leadType: "vehicle", status: status || undefined, keyword });
|
||||
setItems(result.items);
|
||||
setSelectedId((current) => result.items.some((item) => item.id === current) ? current : result.items[0]?.id || "");
|
||||
setMessage("");
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : "用车线索加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [status]);
|
||||
|
||||
const selected = items.find((item) => item.id === selectedId) || null;
|
||||
|
||||
const saveStatus = async (lead: Lead, nextStatus: LeadStatus) => {
|
||||
setSavingId(lead.id);
|
||||
try {
|
||||
const saved = await updateLeadStatus(lead.id, nextStatus);
|
||||
setItems((current) => current.map((item) => item.id === saved.id ? saved : item));
|
||||
notify({ tone: "success", title: "线索状态已更新", message: `${leadTitle(lead)} 已标记为${statusLabels[nextStatus]}。` });
|
||||
} catch (error) {
|
||||
notify({ tone: "danger", title: "状态更新失败", message: error instanceof Error ? error.message : "请稍后重试" });
|
||||
} finally {
|
||||
setSavingId("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="work-area grid min-h-0 gap-4 p-4 md:grid-cols-[minmax(0,1fr)_minmax(320px,0.8fr)]">
|
||||
<Card className="min-h-0 overflow-hidden">
|
||||
<CardHeader className="border-b border-slate-100 p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-xl">用车需求</CardTitle>
|
||||
<CardDescription>查看已登录用户提交的出行信息,并通过线索状态跟进报价。</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={() => void load()} disabled={loading} aria-label="刷新用车线索">
|
||||
<RefreshCw size={16} className={loading ? "animate-spin" : ""} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-2 pt-3 sm:grid-cols-[160px_minmax(0,1fr)_auto]">
|
||||
<NativeSelect value={status} onChange={(event) => setStatus(event.target.value as LeadStatus | "")} aria-label="线索状态">
|
||||
<option value="">全部状态</option>
|
||||
{Object.entries(statusLabels).map(([value, label]) => <option value={value} key={value}>{label}</option>)}
|
||||
</NativeSelect>
|
||||
<Input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索地点、手机号或车型" aria-label="搜索用车线索" />
|
||||
<Button variant="outline" onClick={() => void load()}>查询</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 overflow-auto p-3">
|
||||
{message ? <p className="rounded-lg bg-rose-50 px-3 py-2 text-sm text-rose-700">{message}</p> : null}
|
||||
{loading ? <EmptyState text="正在读取用车线索" /> : !items.length ? <EmptyState text="暂无符合条件的用车需求" /> : (
|
||||
<div className="grid gap-2">
|
||||
{items.map((lead) => (
|
||||
<button type="button" key={lead.id} onClick={() => setSelectedId(lead.id)} className={`grid gap-2 rounded-xl border p-4 text-left transition ${selectedId === lead.id ? "border-teal-500 bg-teal-50/60" : "border-slate-200 bg-white hover:border-slate-300"}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-slate-950">{lead.contactName || "未填写联系人"}</p>
|
||||
<p className="truncate text-sm text-slate-500">{leadTitle(lead)} · {lead.phone}</p>
|
||||
</div>
|
||||
<Badge variant={lead.status === "invalid" ? "muted" : lead.status === "won" ? "success" : "default"}>{statusLabels[lead.status]}</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">{formatDate(lead.vehicleDemand?.travelDate || lead.travelDate)} · {lead.vehicleDemand?.peopleCount || lead.peopleCount || "未填写"} 人 · {lead.vehicleDemand?.pickupLocation || "未填写上车点"} → {lead.vehicleDemand?.dropoffLocation || "未填写目的地"}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="min-h-0 overflow-auto">
|
||||
{selected ? (
|
||||
<>
|
||||
<CardHeader className="border-b border-slate-100 p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-xl">需求详情</CardTitle>
|
||||
<CardDescription>提交于 {formatDate(selected.createdAt)}</CardDescription>
|
||||
</div>
|
||||
<Badge variant="success">用车</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 p-5 text-sm">
|
||||
<dl className="grid gap-3 sm:grid-cols-2">
|
||||
<div><dt className="text-slate-500">联系人</dt><dd className="mt-1 font-medium">{selected.contactName || "未填写"}</dd></div>
|
||||
<div><dt className="text-slate-500">联系方式</dt><dd className="mt-1 font-medium">{selected.phone}</dd></div>
|
||||
<div><dt className="text-slate-500">出行日期</dt><dd className="mt-1 font-medium">{formatDate(selected.vehicleDemand?.travelDate || selected.travelDate)}</dd></div>
|
||||
<div><dt className="text-slate-500">出行人数</dt><dd className="mt-1 font-medium">{selected.vehicleDemand?.peopleCount || selected.peopleCount || "未填写"} 人</dd></div>
|
||||
</dl>
|
||||
<div className="rounded-xl bg-slate-50 p-4">
|
||||
<p className="font-semibold text-slate-900">行程信息</p>
|
||||
<p className="mt-2 leading-6 text-slate-600">{selected.vehicleDemand?.pickupLocation || "未填写上车点"} → {selected.vehicleDemand?.dropoffLocation || "未填写目的地"}</p>
|
||||
<p className="mt-1 text-slate-600">车型:{selected.vehicleDemand?.vehicleOptionTitle || "未指定"} · 行李:{selected.vehicleDemand?.luggageCount ?? "未填写"} 件</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900">备注</p>
|
||||
<p className="mt-2 whitespace-pre-wrap leading-6 text-slate-600">{selected.vehicleDemand?.specialRequirements || selected.note || "暂无备注"}</p>
|
||||
</div>
|
||||
<label className="grid gap-2 font-medium text-slate-700">
|
||||
跟进状态
|
||||
<NativeSelect value={selected.status} disabled={savingId === selected.id} onChange={(event) => void saveStatus(selected, event.target.value as LeadStatus)}>
|
||||
{Object.entries(statusLabels).map(([value, label]) => <option value={value} key={value}>{label}</option>)}
|
||||
</NativeSelect>
|
||||
</label>
|
||||
</CardContent>
|
||||
</>
|
||||
) : <EmptyState text="请选择一条用车需求查看详情" />}
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function HomePage({ notify }: { notify: Notify }) {
|
||||
notify={notify}
|
||||
embedded
|
||||
hideRail
|
||||
moduleIdOverride={moduleId as "heroSlides" | "vehicleOptions"}
|
||||
moduleIdOverride={moduleId as "heroSlides" | "vehicleOptions" | "vehicleService"}
|
||||
/>
|
||||
) : (
|
||||
<HomeContentPage
|
||||
|
||||
@@ -67,6 +67,11 @@ const pageSpecs: {
|
||||
label: "万趣用车",
|
||||
hint: "维护首页用车卡片标题、描述、封面图、启用状态和排序。",
|
||||
},
|
||||
{
|
||||
id: "vehicleService",
|
||||
label: "用车服务",
|
||||
hint: "维护用车需求页的服务说明、优势和提交流程。",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -118,9 +123,10 @@ export function StructurePage({
|
||||
? moduleItems(config, moduleId)
|
||||
: [];
|
||||
const isCardContentModule = moduleId === "vehicleOptions";
|
||||
const isVehicleServiceModule = moduleId === "vehicleService";
|
||||
const isDemandFormModule = moduleId === "demandForm";
|
||||
const canReorderSiteItems = moduleEditable(moduleId) && !isDemandFormModule;
|
||||
const canCreateSiteItem = moduleEditable(moduleId) && (!isDemandFormModule || editableItems.length === 0);
|
||||
const canReorderSiteItems = moduleEditable(moduleId) && !isDemandFormModule && !isVehicleServiceModule;
|
||||
const canCreateSiteItem = moduleEditable(moduleId) && ((!isDemandFormModule && !isVehicleServiceModule) || editableItems.length === 0);
|
||||
const isCreatingSiteItem = selectedId === NEW_SITE_ITEM_ID;
|
||||
const selectedItem = isCreatingSiteItem
|
||||
? undefined
|
||||
@@ -147,6 +153,11 @@ export function StructurePage({
|
||||
notePlaceholder: "notePlaceholder" in item ? item.notePlaceholder || "" : undefined,
|
||||
submitLabel: "submitLabel" in item ? item.submitLabel : undefined,
|
||||
chips: "chips" in item && Array.isArray(item.chips) ? item.chips : undefined,
|
||||
introTitle: "introTitle" in item ? item.introTitle : undefined,
|
||||
intro: "intro" in item ? item.intro : undefined,
|
||||
serviceSections: "serviceSections" in item && Array.isArray(item.serviceSections) ? item.serviceSections : undefined,
|
||||
advantages: "advantages" in item && Array.isArray(item.advantages) ? item.advantages : undefined,
|
||||
processSteps: "processSteps" in item && Array.isArray(item.processSteps) ? item.processSteps : undefined,
|
||||
isActive: "isActive" in item ? item.isActive : undefined,
|
||||
sortOrder: itemSortOrder(item),
|
||||
});
|
||||
@@ -543,6 +554,7 @@ function SiteItemEditor({
|
||||
if (moduleId === "demandFeatureCards") return "卡片标题";
|
||||
if (moduleId === "demandForm") return "提交按钮文案";
|
||||
if (moduleId === "vehicleOptions") return "用车标题";
|
||||
if (moduleId === "vehicleService") return "服务标题";
|
||||
return "配置标题";
|
||||
})();
|
||||
const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`;
|
||||
@@ -554,7 +566,8 @@ function SiteItemEditor({
|
||||
isDemandHeroModule ||
|
||||
isDemandFeatureCardModule ||
|
||||
isDemandFormModule;
|
||||
const isTextOnlyModule = isDemandTextModule;
|
||||
const isVehicleServiceModule = moduleId === "vehicleService";
|
||||
const isTextOnlyModule = isDemandTextModule || isVehicleServiceModule;
|
||||
const isCardContentModule = moduleId === "vehicleOptions";
|
||||
const editorHint = isCreating
|
||||
? `创建后会进入「${moduleLabel}」的数据列表`
|
||||
@@ -578,7 +591,64 @@ function SiteItemEditor({
|
||||
</header>
|
||||
<div className="admin-disclosure-stack">
|
||||
<AdminDisclosure title="展示内容">
|
||||
{isDemandFormModule ? (
|
||||
{isVehicleServiceModule ? (
|
||||
<>
|
||||
<label>
|
||||
服务标题
|
||||
<Input
|
||||
value={draft.introTitle ?? ""}
|
||||
onChange={(event) => onDraft({ ...draft, introTitle: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
板块简介
|
||||
<Textarea
|
||||
value={draft.intro ?? ""}
|
||||
onChange={(event) => onDraft({ ...draft, intro: event.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
服务说明(每行一项,格式:标题 | 说明)
|
||||
<Textarea
|
||||
value={(draft.serviceSections ?? []).map((item) => `${item.title} | ${item.description}`).join("\n")}
|
||||
onChange={(event) => onDraft({
|
||||
...draft,
|
||||
serviceSections: event.target.value.split(/\r?\n/).map((line) => {
|
||||
const [title = "", ...description] = line.split("|");
|
||||
return { title: title.trim(), description: description.join("|").trim() };
|
||||
}).filter((item) => item.title || item.description),
|
||||
})}
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
服务优势(每行一项)
|
||||
<Textarea
|
||||
value={(draft.advantages ?? []).join("\n")}
|
||||
onChange={(event) => onDraft({
|
||||
...draft,
|
||||
advantages: event.target.value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean),
|
||||
})}
|
||||
rows={5}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
使用流程(每行一项,格式:步骤 | 说明)
|
||||
<Textarea
|
||||
value={(draft.processSteps ?? []).map((item) => `${item.title} | ${item.description}`).join("\n")}
|
||||
onChange={(event) => onDraft({
|
||||
...draft,
|
||||
processSteps: event.target.value.split(/\r?\n/).map((line) => {
|
||||
const [title = "", ...description] = line.split("|");
|
||||
return { title: title.trim(), description: description.join("|").trim() };
|
||||
}).filter((item) => item.title || item.description),
|
||||
})}
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
) : isDemandFormModule ? (
|
||||
<>
|
||||
<div className="form-grid compact-grid">
|
||||
<label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SiteConfig, SiteModule } from "@/api";
|
||||
|
||||
export type Tab = "home" | "play" | "butler";
|
||||
export type Tab = "home" | "play" | "butler" | "leads";
|
||||
export type PageId = "home" | "play";
|
||||
export type ModuleId = SiteModule;
|
||||
|
||||
@@ -19,4 +19,5 @@ export type EditableSiteItem =
|
||||
| SiteConfig["demandHero"][number]
|
||||
| SiteConfig["demandFeatureCards"][number]
|
||||
| SiteConfig["demandForm"][number]
|
||||
| SiteConfig["vehicleOptions"][number];
|
||||
| SiteConfig["vehicleOptions"][number]
|
||||
| SiteConfig["vehicleService"][number];
|
||||
|
||||
Reference in New Issue
Block a user