- 新增客片案例全流程功能,包含前台页面、管理端配置、后端API、数据库迁移与文档 - 新增团队共创详情页面与对应接口 - 重构玩法模块:将静态playData替换为动态API获取,拆分类型定义到playTypes.ts - 优化vite构建配置与环境变量处理,调整详情页操作栏文案 - 更新全量相关文档与配图,新增客片案例、团队共创API文档 - 新增测试用例,完善数据归一化逻辑 - 调整路由配置与环境变量示例文件
130 lines
5.6 KiB
TypeScript
130 lines
5.6 KiB
TypeScript
export type HomeTeamBuilding = {
|
|
id: string;
|
|
tag: string;
|
|
title: string;
|
|
description: string;
|
|
image: string;
|
|
demandKeyword: string;
|
|
detailSubtitle: string;
|
|
detailParagraphs: string[];
|
|
};
|
|
|
|
export type HomeTeamBuildingApiItem = {
|
|
id?: string | null;
|
|
tag?: string | null;
|
|
title?: string | null;
|
|
description?: string | null;
|
|
image?: string | null;
|
|
demandKeyword?: string | null;
|
|
detailSubtitle?: string | null;
|
|
detailParagraphs?: string[] | null;
|
|
isActive?: boolean | null;
|
|
sortOrder?: number | null;
|
|
};
|
|
|
|
export type HomeTeamBuildings = HomeTeamBuildingApiItem[];
|
|
|
|
export const homeTeamBuildingMocks: HomeTeamBuilding[] = [
|
|
{
|
|
id: "wild-challenge",
|
|
tag: "户外挑战",
|
|
title: "山野挑战,共创极境",
|
|
description: "洞穴、瀑降与协作,适合 10-30 人。",
|
|
image: "https://dimg04.c-ctrip.com/images/1mh0412000njfr1ot9453_W_640_10000.jpg?proc=autoorient",
|
|
demandKeyword: "户外团建",
|
|
detailSubtitle: "越过山丘,向来处去",
|
|
detailParagraphs: [
|
|
"真正的贵州,从未被写进流水线的攻略里。洞穴、瀑降、峡谷和村寨,都是团队一起重新认识彼此的方式。",
|
|
"当所有人都在熟悉的点里寻找同一个机位,我们换一条路,去看真正的贵州。",
|
|
"我们是一群生于斯、长于斯的地坪探索者,也是对体验和美学有高偏好的本地旅行人。",
|
|
"十年的时间,我们用影像和脚步寻找贵州的小众特色目的地,把贵州在社交媒体分享给全世界。",
|
|
"我们深知这片土地的魅力,绝不仅是走马观花的打卡。它藏在高山峡谷的边缘感中,也藏在溶洞和地心暗河的回音里。",
|
|
"去喀斯特洞河里释放野性,去无名山野间寻找宁静,把未知的风险与沿途的惊艳交给最强悍的团队兜底。",
|
|
"越过山丘,向来处去,有深度,有记录,有趣。如果想解锁贵州这段独特的旅行,欢迎与野呀团队一起,解锁贵州。",
|
|
],
|
|
},
|
|
{
|
|
id: "canyon-teamwork",
|
|
tag: "团队协作",
|
|
title: "峡谷溯溪,默契同行",
|
|
description: "溯溪与分组协作,兼顾参与感与安全。",
|
|
image: "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
|
|
demandKeyword: "峡谷团建",
|
|
detailSubtitle: "在峡谷里重新认识彼此",
|
|
detailParagraphs: [
|
|
"把会议室换成溪流和峡谷,让团队在共同完成一段路的过程中自然建立默契。",
|
|
"从分组协作到沿途探索,每个人都能找到自己的节奏,也能看见同伴身上不一样的力量。",
|
|
"这是一场有挑战、有风景,也留得下故事的团队共创。",
|
|
],
|
|
},
|
|
{
|
|
id: "village-gathering",
|
|
tag: "人文聚会",
|
|
title: "苗寨共聚,认识彼此",
|
|
description: "夜游、长桌宴与文化体验,适合团建收尾。",
|
|
image: "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
|
|
demandKeyword: "贵州团建",
|
|
detailSubtitle: "在山野与人文之间共创一席",
|
|
detailParagraphs: [
|
|
"走进村寨,和当地的人打个照面,再围坐在长桌旁分享一天的见闻。团队关系,往往在这些没有安排的时刻里变得更近。",
|
|
"夜游、长桌宴与文化体验,为一次团建留下温度,也为下一次共同出发埋下期待。",
|
|
],
|
|
},
|
|
];
|
|
|
|
export const homeTeamBuildingFallback = homeTeamBuildingMocks;
|
|
|
|
export function normalizeHomeTeamBuildings(
|
|
items: HomeTeamBuildings | null | undefined,
|
|
): HomeTeamBuilding[] {
|
|
const fallback = homeTeamBuildingFallback.map((item) => ({ ...item }));
|
|
const activeItems = (items ?? [])
|
|
.map((item, index) => ({ item, index }))
|
|
.filter(({ item }) => item.isActive !== false && Boolean(normalizeText(item.title)))
|
|
.sort(
|
|
(left, right) =>
|
|
(left.item.sortOrder ?? left.index) - (right.item.sortOrder ?? right.index),
|
|
);
|
|
|
|
if (!activeItems.length) return fallback;
|
|
|
|
return activeItems.map(({ item }, index) => {
|
|
const fallbackItem = homeTeamBuildingFallback.find((candidate) => candidate.id === item.id)
|
|
?? homeTeamBuildingFallback[index]
|
|
?? homeTeamBuildingFallback[0];
|
|
const title = normalizeText(item.title) || fallbackItem.title;
|
|
return {
|
|
id: normalizeText(item.id) || fallbackItem.id,
|
|
tag: normalizeText(item.tag) || fallbackItem.tag,
|
|
title,
|
|
description: normalizeText(item.description) || fallbackItem.description,
|
|
image: normalizeText(item.image) || fallbackItem.image,
|
|
demandKeyword: normalizeText(item.demandKeyword) || title,
|
|
detailSubtitle: normalizeText(item.detailSubtitle) || fallbackItem.detailSubtitle || fallbackItem.description,
|
|
detailParagraphs: normalizeDetailParagraphs(item.detailParagraphs, fallbackItem.detailParagraphs, fallbackItem.description),
|
|
};
|
|
});
|
|
}
|
|
|
|
function normalizeDetailParagraphs(
|
|
paragraphs: string[] | null | undefined,
|
|
fallbackParagraphs: string[],
|
|
description: string,
|
|
) {
|
|
const normalized = (paragraphs ?? []).map((paragraph) => normalizeText(paragraph)).filter(Boolean);
|
|
return normalized.length ? normalized : fallbackParagraphs.length ? [...fallbackParagraphs] : [description];
|
|
}
|
|
|
|
function normalizeText(value: unknown) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
export function normalizeHomeTeamBuildingDetail(
|
|
item: HomeTeamBuildingApiItem | null | undefined,
|
|
): HomeTeamBuilding {
|
|
const fallback = homeTeamBuildingFallback.find((candidate) => candidate.id === item?.id)
|
|
?? homeTeamBuildingFallback[0];
|
|
const normalized = normalizeHomeTeamBuildings(item ? [item] : [fallback])[0];
|
|
return normalized;
|
|
}
|