Files
WonderQ-Project/WonderQ-Admin-UI-Vue/src/lib/wanfa.ts
duanshuwen feec6ab80f feat(wanfa): 新增路线详情参考价格配置及展示功能
- 新增数据库迁移脚本,为详情表添加四个价格相关字段
- 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则
- 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位
- 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明
- 更新小程序端详情页面,支持展示配置的参考价格信息
- 补充相关测试用例,更新API文档与类型定义
2026-08-27 22:02:51 +08:00

101 lines
3.5 KiB
TypeScript

import type { DetailCreate, DetailPriceUnit, WanfaRouteCreate } from "@/types";
export function createEmptyWanfaRoute(): WanfaRouteCreate {
return { title: "", subtitle: "", image: "", routeCount: 0, demandKeyword: "" };
}
export function compactWanfaRoute(draft: WanfaRouteCreate): WanfaRouteCreate {
return {
title: draft.title.trim(),
subtitle: draft.subtitle.trim(),
image: draft.image.trim(),
routeCount: Number.isFinite(Number(draft.routeCount)) ? Math.max(0, Number(draft.routeCount)) : 0,
demandKeyword: draft.demandKeyword.trim(),
};
}
export function createEmptyDetail(): DetailCreate {
return {
key: "",
eyebrow: "玩法路线",
duration: "",
title: "",
subtitle: "",
intro: "",
highlights: [],
included: [],
excluded: [],
notes: [],
gallery: [],
priceStartingValue: null,
priceUnit: null,
pricePeopleRange: "",
priceDescription: "",
conciergeAdvisorId: null,
isActive: true,
};
}
export function compactDetail(draft: DetailCreate): DetailCreate {
return {
key: draft.key.trim(),
eyebrow: draft.eyebrow.trim(),
duration: draft.duration.trim(),
title: draft.title.trim(),
subtitle: draft.subtitle.trim(),
intro: draft.intro.trim(),
highlights: compactList(draft.highlights),
included: compactList(draft.included),
excluded: compactList(draft.excluded),
notes: compactList(draft.notes),
gallery: compactList(draft.gallery),
priceStartingValue: normalizePriceStartingValue(draft.priceStartingValue),
priceUnit: normalizePriceUnit(draft.priceUnit),
pricePeopleRange: draft.pricePeopleRange?.trim() || null,
priceDescription: draft.priceDescription?.trim() || null,
conciergeAdvisorId: draft.conciergeAdvisorId?.trim() || null,
isActive: draft.isActive ?? true,
...(draft.sortOrder === undefined ? {} : { sortOrder: draft.sortOrder }),
};
}
export function validateWanfaWrite(route: WanfaRouteCreate, detail: DetailCreate) {
if (!route.title || !route.subtitle || !route.image || !route.demandKeyword) {
return "请完整填写路线标题、副标题、封面和需求关键词。";
}
if (!isHttpUrl(route.image)) return "路线封面必须是有效的 HTTP(S) URL。";
if (!Number.isInteger(route.routeCount) || route.routeCount < 0) {
return "路线数量必须是大于等于 0 的整数。";
}
if (!detail.eyebrow || !detail.duration || !detail.title || !detail.subtitle || !detail.intro) {
return "请完整填写详情眉标、时长、标题、副标题和详情介绍。";
}
if (detail.gallery.some((image) => !isHttpUrl(image))) return "详情图片必须是有效的 HTTP(S) URL。";
if (detail.priceStartingValue !== null && detail.priceStartingValue !== undefined && !detail.priceUnit) {
return "配置价格起始值时请选择价格单位。";
}
return null;
}
function normalizePriceStartingValue(value: number | null | undefined) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return null;
return Math.max(0, Number(Number(value).toFixed(2)));
}
function normalizePriceUnit(value: DetailPriceUnit | null | undefined): DetailPriceUnit | null {
return value === "person" || value === "day" || value === "group" ? value : null;
}
function compactList(items: string[]) {
return items.map((item) => item.trim()).filter(Boolean);
}
function isHttpUrl(value: string) {
try {
const url = new URL(value);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}