feat: 新增路线详情关联管家顾问及相关功能
详细变更如下: - 更新 .gitignore 文件,添加 pnpm-store 忽略规则 - 新增数据库迁移脚本,为 DetailRecord 添加可空的 conciergeAdvisorId 字段用于关联管家顾问 - 完善 Admin 后台玩法详情编辑器,支持选择关联的管家顾问并校验合法性 - 公共 API 支持返回已启用的管家顾问完整数据,不在详情表中冗余存储管家资料 - 小程序端新增详情页联系管家入口、个人页最近浏览历史功能 - 更新所有相关文档与测试用例,修复下拉选择框的 z-index 样式问题
This commit is contained in:
1 parent
e7b8c5bf43
commit
b7a81964c9
34 files changed
+739
-63
No files matched your search
@@ -1,7 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeConciergeAdvisors } from "@/pages/concierge/components/conciergeTypes";
|
||||
import { normalizeConciergeAdvisor, normalizeConciergeAdvisors } from "@/pages/concierge/components/conciergeTypes";
|
||||
|
||||
describe("normalizeConciergeAdvisors", () => {
|
||||
it("normalizes a single valid advisor and rejects incomplete data", () => {
|
||||
expect(
|
||||
normalizeConciergeAdvisor({
|
||||
avatar: " https://example.test/amanda.jpg ",
|
||||
name: " Amanda ",
|
||||
role: " ADVISOR ",
|
||||
details: [{ icon: " calendar ", label: " 服务经验:8年 " }],
|
||||
qrImage: " https://example.test/amanda-qr.png ",
|
||||
}),
|
||||
).toEqual({
|
||||
avatar: "https://example.test/amanda.jpg",
|
||||
name: "Amanda",
|
||||
role: "ADVISOR",
|
||||
details: [{ icon: "calendar", label: "服务经验:8年" }],
|
||||
qrImage: "https://example.test/amanda-qr.png",
|
||||
});
|
||||
expect(normalizeConciergeAdvisor({ name: "缺少头像" })).toBeNull();
|
||||
});
|
||||
|
||||
it("normalizes public advisor data and filters incomplete records", () => {
|
||||
expect(
|
||||
normalizeConciergeAdvisors({
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("wanfa route detail presentation", () => {
|
||||
excluded: [" 往返大交通 "],
|
||||
notes: [" 请准备雨具。 "],
|
||||
gallery: [" https://example.test/cover.jpg "],
|
||||
conciergeAdvisor: null,
|
||||
}),
|
||||
).toEqual({
|
||||
key: "family-water",
|
||||
@@ -33,6 +34,40 @@ describe("wanfa route detail presentation", () => {
|
||||
excluded: ["往返大交通"],
|
||||
notes: ["请准备雨具。"],
|
||||
gallery: ["https://example.test/cover.jpg"],
|
||||
conciergeAdvisor: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes a valid concierge advisor without inventing fallback data", () => {
|
||||
expect(
|
||||
normalizeDetailPresentation({
|
||||
key: "family-water",
|
||||
eyebrow: "玩法推荐",
|
||||
duration: "5天4晚",
|
||||
title: "亲子玩水",
|
||||
subtitle: "贵州路线",
|
||||
intro: "介绍",
|
||||
highlights: [],
|
||||
included: [],
|
||||
excluded: [],
|
||||
notes: [],
|
||||
gallery: ["https://example.test/cover.jpg"],
|
||||
conciergeAdvisor: {
|
||||
avatar: " https://example.test/amanda.jpg ",
|
||||
name: " Amanda ",
|
||||
role: " SENIOR TRAVEL ADVISOR ",
|
||||
details: [{ icon: " calendar ", label: " 服务经验:8年 " }],
|
||||
qrImage: " https://example.test/amanda-qr.png ",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
conciergeAdvisor: {
|
||||
avatar: "https://example.test/amanda.jpg",
|
||||
name: "Amanda",
|
||||
role: "SENIOR TRAVEL ADVISOR",
|
||||
details: [{ icon: "calendar", label: "服务经验:8年" }],
|
||||
qrImage: "https://example.test/amanda-qr.png",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
normalizeRecentlyViewed,
|
||||
upsertRecentlyViewed,
|
||||
type RecentlyViewedItem,
|
||||
} from "@/lib/recentlyViewed";
|
||||
|
||||
const baseItem: RecentlyViewedItem = {
|
||||
type: "wanfa-route",
|
||||
id: "route-1",
|
||||
title: "亲子玩水",
|
||||
image: "https://example.test/route-1.jpg",
|
||||
visitedAt: 100,
|
||||
};
|
||||
|
||||
describe("recently viewed data", () => {
|
||||
it("filters invalid records and sorts valid records by latest visit", () => {
|
||||
expect(
|
||||
normalizeRecentlyViewed([
|
||||
{ ...baseItem, visitedAt: 100 },
|
||||
{ ...baseItem, id: "route-2", visitedAt: 300 },
|
||||
{ id: "missing-title", type: "wanfa-route", visitedAt: 200 },
|
||||
"invalid",
|
||||
]),
|
||||
).toEqual([
|
||||
{ ...baseItem, id: "route-2", visitedAt: 300 },
|
||||
baseItem,
|
||||
]);
|
||||
});
|
||||
|
||||
it("updates an existing ID, puts it first, and keeps at most five records", () => {
|
||||
const existing = Array.from({ length: 5 }, (_, index) => ({
|
||||
...baseItem,
|
||||
id: `route-${index}`,
|
||||
visitedAt: index,
|
||||
}));
|
||||
|
||||
expect(
|
||||
upsertRecentlyViewed(existing, {
|
||||
...baseItem,
|
||||
id: "route-3",
|
||||
title: "更新后的路线",
|
||||
visitedAt: 10,
|
||||
}),
|
||||
).toEqual([
|
||||
{ ...baseItem, id: "route-3", title: "更新后的路线", visitedAt: 10 },
|
||||
{ ...baseItem, id: "route-4", visitedAt: 4 },
|
||||
{ ...baseItem, id: "route-2", visitedAt: 2 },
|
||||
{ ...baseItem, id: "route-1", visitedAt: 1 },
|
||||
{ ...baseItem, id: "route-0", visitedAt: 0 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user