Files
WonderQ-Project/WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts
duanshuwen b7a81964c9 feat: 新增路线详情关联管家顾问及相关功能
详细变更如下:
- 更新 .gitignore 文件,添加 pnpm-store 忽略规则
- 新增数据库迁移脚本,为 DetailRecord 添加可空的 conciergeAdvisorId 字段用于关联管家顾问
- 完善 Admin 后台玩法详情编辑器,支持选择关联的管家顾问并校验合法性
- 公共 API 支持返回已启用的管家顾问完整数据,不在详情表中冗余存储管家资料
- 小程序端新增详情页联系管家入口、个人页最近浏览历史功能
- 更新所有相关文档与测试用例,修复下拉选择框的 z-index 样式问题
2026-08-22 08:26:23 +08:00

101 lines
3.9 KiB
TypeScript

import { playCategories, type PlayRoute } from "../../play/components/playData";
import type { PublicDetail } from "@/lib/types";
import { normalizeConciergeAdvisor } from "../../concierge/components/conciergeTypes";
export type DetailPresentation = PublicDetail;
const sharedGallery = [
"https://images.unsplash.com/photo-1500534623283-312aade485b7?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=800",
"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=800",
];
function createRouteFallback(route: PlayRoute): DetailPresentation {
return {
key: route.id,
eyebrow: "玩法推荐",
duration: "5天4晚",
title: route.title,
subtitle: route.subtitle,
intro: `围绕${route.title}安排一段节奏舒适的贵州探索,把自然景观、在地体验和小团服务串成一条完整路线。`,
highlights: [
"核心景观串联,减少无效往返和重复换乘",
"小团出行,按同行人的节奏灵活调整",
"在地体验与舒适停留合理衔接",
"行程前确认天气、装备和接送细节",
],
included: ["行程内用车与接送服务", "方案中注明的景点门票和体验项目", "行前服务管家确认与途中跟进"],
excluded: ["往返大交通及个人消费", "未列明餐食和自选体验", "因个人原因产生的额外费用"],
notes: [
"贵州多山多雨,请准备防滑鞋、轻便雨具和薄外套。",
"户外体验会根据天气和同行人体力适当调整。",
"最终行程以出行日期、人数和资源确认结果为准。",
],
gallery: Array.from(new Set([route.image, ...sharedGallery])).slice(0, 5),
conciergeAdvisor: null,
};
}
const routeFallbacks = new Map(
playCategories.flatMap((category) => category.routes).map((route) => [route.id, createRouteFallback(route)]),
);
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object";
}
function cleanText(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : "";
}
function cleanList(value: unknown) {
return Array.isArray(value)
? value.map((item) => cleanText(item)).filter(Boolean)
: [];
}
function cleanGallery(value: unknown) {
return Array.from(
new Set(
cleanList(value).filter((item) => /^https?:\/\//i.test(item)),
),
);
}
export function findWanfaRouteDetailFallback(routeId: string) {
return routeFallbacks.get(routeId) ?? null;
}
export function normalizeDetailPresentation(
value: unknown,
fallback: DetailPresentation | null = null,
): DetailPresentation | null {
const source = isRecord(value) ? value : {};
const key = cleanText(source.key) || fallback?.key || "";
const eyebrow = cleanText(source.eyebrow) || fallback?.eyebrow || "";
const duration = cleanText(source.duration) || fallback?.duration || "";
const title = cleanText(source.title) || fallback?.title || "";
const subtitle = cleanText(source.subtitle) || fallback?.subtitle || "";
const intro = cleanText(source.intro) || fallback?.intro || "";
const gallery = cleanGallery(source.gallery);
const conciergeAdvisor = normalizeConciergeAdvisor(source.conciergeAdvisor);
if (!key || !eyebrow || !duration || !title || !subtitle || !intro) {
return fallback;
}
return {
key,
eyebrow,
duration,
title,
subtitle,
intro,
highlights: cleanList(source.highlights).length ? cleanList(source.highlights) : fallback?.highlights ?? [],
included: cleanList(source.included).length ? cleanList(source.included) : fallback?.included ?? [],
excluded: cleanList(source.excluded).length ? cleanList(source.excluded) : fallback?.excluded ?? [],
notes: cleanList(source.notes).length ? cleanList(source.notes) : fallback?.notes ?? [],
gallery: gallery.length ? gallery : fallback?.gallery ?? [],
conciergeAdvisor,
};
}