feat: 新增路线详情关联管家顾问及相关功能

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

No files matched your search

@@ -28,6 +28,7 @@
price }}万</text>
</view>
<view
v-if="showConcierge"
class="tap-feedback flex items-center justify-center m-0 h-11 rounded-full bg-[#e96635] px-4 text-[13px] font-semibold text-white shadow-[0_6px_16px_rgba(233,102,53,0.22)]"
@click="$emit('concierge')">联系管家</view>
</view>
@@ -38,10 +39,13 @@
<script setup lang="ts">
import { formatWan } from "@/lib/data";
defineProps<{
withDefaults(defineProps<{
price: number;
favorite: boolean;
}>();
showConcierge?: boolean;
}>(), {
showConcierge: true,
});
defineEmits<{
call: [];
@@ -1,5 +1,6 @@
import { playCategories, type PlayRoute } from "../../play/components/playData";
import type { PublicDetail } from "@/lib/types";
import { normalizeConciergeAdvisor } from "../../concierge/components/conciergeTypes";
export type DetailPresentation = PublicDetail;
@@ -30,6 +31,7 @@ function createRouteFallback(route: PlayRoute): DetailPresentation {
"最终行程以出行日期、人数和资源确认结果为准。",
],
gallery: Array.from(new Set([route.image, ...sharedGallery])).slice(0, 5),
conciergeAdvisor: null,
};
}
@@ -75,6 +77,7 @@ export function normalizeDetailPresentation(
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;
@@ -92,5 +95,6 @@ export function normalizeDetailPresentation(
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,
};
}
+24 -13
View File
@@ -42,7 +42,13 @@
</view>
</scroll-view>
<DetailActionBar :price="1.68" :favorite="false" @call="goCall" @concierge="goConcierge" />
<DetailActionBar
:price="1.68"
:favorite="false"
:show-concierge="Boolean(presentation?.conciergeAdvisor)"
@call="goCall"
@concierge="goConcierge"
/>
<ConciergeContactSheet :open="Boolean(selectedAdvisor)" :advisor="selectedAdvisor" @close="handleConciergeClosed" />
</view>
@@ -54,6 +60,7 @@ import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
import ConciergeContactSheet from "@/components/ConciergeContactSheet.vue";
import { fetchPublicDetail } from "@/lib/api";
import { goBack } from "@/lib/navigation";
import { rememberRecentlyViewed } from "@/lib/recentlyViewed";
import { SUPPORT_PHONE } from "@/lib/types";
import type { ConciergeAdvisor } from "../concierge/components/conciergeTypes";
import DetailActionBar from "./components/DetailActionBar.vue";
@@ -74,17 +81,6 @@ const usingFallback = ref(false);
const errorMessage = ref("");
const selectedAdvisor = ref<ConciergeAdvisor | null>(null);
const conciergeAdvisor: ConciergeAdvisor = {
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
name: "Amanda",
role: "SENIOR TRAVEL ADVISOR",
qrImage: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
details: [
{ icon: "calendar", label: "服务经验:8年" },
{ icon: "navigate", label: "擅长领域:瀑降 / 探洞" },
],
};
const heroImage = computed(() => presentation.value?.gallery[0] ?? "");
const priceParagraphs = computed(() => ["最终价格会根据出行日期、人数和资源确认结果生成。"]);
@@ -125,16 +121,31 @@ async function loadDetail() {
usingFallback.value = Boolean(fallback);
errorMessage.value = cause instanceof Error ? cause.message : "路线详情接口暂时不可用。";
} finally {
rememberCurrentDetail();
loading.value = false;
}
}
function rememberCurrentDetail() {
const current = presentation.value;
if (!current || !routeId.value) return;
rememberRecentlyViewed({
type: "wanfa-route",
id: routeId.value,
title: current.title,
image: current.gallery[0] ?? "",
visitedAt: Date.now(),
});
}
function goCall() {
uni.makePhoneCall({ phoneNumber: SUPPORT_PHONE });
}
function goConcierge() {
selectedAdvisor.value = conciergeAdvisor;
const advisor = presentation.value?.conciergeAdvisor;
if (advisor) selectedAdvisor.value = advisor;
}
function handleConciergeClosed() {