diff --git a/README.md b/README.md index 1641272..70e8fc1 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ ## 项目组成 -| 子项目 | 定位 | 技术栈 | 默认地址 | -| --- | --- | --- | --- | -| `WonderQ-MiniAPP` | H5 与微信小程序前台 | uni-app、Vue 3、TypeScript、Tailwind CSS | `http://localhost:5173` | -| `WonderQ-Admin-UI` | 运营管理后台前端 | Vite、React、TypeScript、Tailwind CSS 4 | `http://localhost:5602` | -| `WonderQ-Admin` | Public API 与 Admin API | Python、FastAPI、SQLAlchemy 2、Alembic、PostgreSQL、JWT、Pydantic | `http://localhost:4000` | +| 子项目 | 定位 | 技术栈 | 默认地址 | +| ------------------ | ----------------------- | ----------------------------------------------------------------- | ----------------------- | +| `WonderQ-MiniAPP` | H5 与微信小程序前台 | uni-app、Vue 3、TypeScript、Tailwind CSS | `http://localhost:5173` | +| `WonderQ-Admin-UI` | 运营管理后台前端 | Vite、React、TypeScript、Tailwind CSS 4 | `http://localhost:5602` | +| `WonderQ-Admin` | Public API 与 Admin API | Python、FastAPI、SQLAlchemy 2、Alembic、PostgreSQL、JWT、Pydantic | `http://localhost:4000` | 项目采用页面驱动开发:前台页面定义用户流程,管理后台维护运营内容,后端负责鉴权、接口、数据持久化和发布能力。 @@ -32,7 +32,6 @@ WonderQ-Project/ - `pages/play/index`:玩法 - `pages/concierge/index`:管家 - `pages/detail/index`:线路详情 -- `pages/login/index`:登录 - `pages/mine/index`:我的 前台可复用组件位于 `WonderQ-MiniAPP/src/components/`,页面专属组件按页面放在对应的 `components/` 目录中。 diff --git a/WonderQ-MiniAPP/src/pages.json b/WonderQ-MiniAPP/src/pages.json index f2c9cce..ac67a94 100644 --- a/WonderQ-MiniAPP/src/pages.json +++ b/WonderQ-MiniAPP/src/pages.json @@ -26,13 +26,6 @@ "navigationStyle": "custom" } }, - { - "path": "pages/login/index", - "style": { - "navigationBarTitleText": "手机号登录", - "navigationStyle": "custom" - } - }, { "path": "pages/mine/index", "style": { diff --git a/WonderQ-MiniAPP/src/pages/concierge/components/ConciergeContactSheet.vue b/WonderQ-MiniAPP/src/pages/concierge/components/ConciergeContactSheet.vue index 840d1c4..34cc9e2 100644 --- a/WonderQ-MiniAPP/src/pages/concierge/components/ConciergeContactSheet.vue +++ b/WonderQ-MiniAPP/src/pages/concierge/components/ConciergeContactSheet.vue @@ -74,6 +74,7 @@ const props = defineProps<{ const emit = defineEmits<{ close: []; + opened: []; }>(); const popupRef = ref(null); @@ -89,6 +90,7 @@ watch( ); function onPopupChange(event: { show: boolean }) { - if (!event.show) emit("close"); + if (event.show) emit("opened"); + else emit("close"); } diff --git a/WonderQ-MiniAPP/src/pages/concierge/index.vue b/WonderQ-MiniAPP/src/pages/concierge/index.vue index a3ad70d..92324b8 100644 --- a/WonderQ-MiniAPP/src/pages/concierge/index.vue +++ b/WonderQ-MiniAPP/src/pages/concierge/index.vue @@ -1,20 +1,25 @@ diff --git a/WonderQ-MiniAPP/src/pages/detail/components/DetailActionBar.vue b/WonderQ-MiniAPP/src/pages/detail/components/DetailActionBar.vue new file mode 100644 index 0000000..2ab1a6f --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/DetailActionBar.vue @@ -0,0 +1,45 @@ + + + diff --git a/WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue b/WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue new file mode 100644 index 0000000..e927fa1 --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue @@ -0,0 +1,29 @@ + + + diff --git a/WonderQ-MiniAPP/src/pages/detail/components/DetailInfoCard.vue b/WonderQ-MiniAPP/src/pages/detail/components/DetailInfoCard.vue new file mode 100644 index 0000000..ebea96e --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/DetailInfoCard.vue @@ -0,0 +1,56 @@ + + + diff --git a/WonderQ-MiniAPP/src/pages/detail/components/DetailMediaGallery.vue b/WonderQ-MiniAPP/src/pages/detail/components/DetailMediaGallery.vue new file mode 100644 index 0000000..ca62ce3 --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/DetailMediaGallery.vue @@ -0,0 +1,16 @@ + + + diff --git a/WonderQ-MiniAPP/src/pages/detail/components/DetailOverviewCard.vue b/WonderQ-MiniAPP/src/pages/detail/components/DetailOverviewCard.vue new file mode 100644 index 0000000..70b20b5 --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/DetailOverviewCard.vue @@ -0,0 +1,49 @@ + + + diff --git a/WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts b/WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts new file mode 100644 index 0000000..33539a0 --- /dev/null +++ b/WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts @@ -0,0 +1,78 @@ +import { stripTitle } from "@/lib/data"; +import type { Product } from "@/lib/types"; + +export type DetailPresentation = { + eyebrow: string; + duration: string; + title: string; + subtitle: string; + intro: string; + highlights: string[]; + included: string[]; + excluded: string[]; + notes: string[]; + gallery: string[]; +}; + +const fallbackGallery = [ + "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg", + "https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg", + "https://p2.cri.cn/M00/89/21/rBABC2aHnWeACxj5AAAAAAAAAAA159.1000x566.jpg", + "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient", +]; + +function getDuration(title: string) { + return title.match(/\d+天\d+晚/)?.[0] ?? "5天4晚"; +} + +function getTextBlocks(product: Product) { + return (product.detailSections ?? []) + .flatMap((section) => section.blocks) + .filter((block): block is { type: "text"; text: string } => block.type === "text") + .map((block) => block.text.trim()) + .filter(Boolean); +} + +export function createDetailPresentation(product: Product): DetailPresentation { + const title = stripTitle(product.title); + const combinedText = `${product.title} ${product.summary ?? ""}`; + const isCaveRoute = combinedText.includes("洞") || combinedText.includes("探险"); + const detailTexts = getTextBlocks(product); + const apiImages = (product.images ?? []) + .slice() + .sort((left, right) => left.sortOrder - right.sortOrder) + .map((image) => image.url.trim()) + .filter(Boolean); + + return { + eyebrow: product.tags[0] || "玩法推荐", + duration: getDuration(product.title), + title: isCaveRoute ? "地心探险大环线" : title, + subtitle: product.subtitle || `${product.destinationName || "贵州"}·小包团路线`, + intro: + detailTexts[0] || + product.summary || + "沿着贵州山地的自然纹理深入探索,把核心景观、在地体验和轻户外节奏安排在一条线路里。", + highlights: isCaveRoute + ? [ + "深入喀斯特洞穴与地下河,安排专业向导陪同", + "小团出行,按同行人的体力和兴趣灵活调整", + "山野咖啡与自然景观穿插,留出松弛的停留时间", + "行程前由服务管家确认天气、装备和接送细节", + ] + : [ + "核心景观串联,减少无效往返和重复换乘", + "小团出行,按同行人的节奏灵活调整", + "在地体验与舒适住宿合理衔接", + "行程前由服务管家确认天气、装备和接送细节", + ], + included: ["行程内用车与接送服务", "列明景点门票和体验项目", "服务管家行前确认与途中跟进", "行程内住宿及方案中注明的服务"], + excluded: ["往返大交通及个人消费", "未列明餐食和自选体验", "因个人原因产生的额外费用"], + notes: [ + "贵州多山多雨,请准备防滑鞋、轻便雨具和薄外套。", + "溶洞、漂流、徒步等体验会根据天气和同行人体力适当调整。", + "页面价格为参考起价,最终方案以出行日期、人数和资源确认结果为准。", + ], + gallery: Array.from(new Set([product.image, ...apiImages, ...fallbackGallery])).slice(0, 6), + }; +} diff --git a/WonderQ-MiniAPP/src/pages/detail/index.vue b/WonderQ-MiniAPP/src/pages/detail/index.vue index e344511..c36f47f 100644 --- a/WonderQ-MiniAPP/src/pages/detail/index.vue +++ b/WonderQ-MiniAPP/src/pages/detail/index.vue @@ -1,99 +1,43 @@ diff --git a/WonderQ-MiniAPP/src/pages/home/components/HomeExperienceCard.vue b/WonderQ-MiniAPP/src/pages/home/components/HomeExperienceCard.vue index a24fd40..af4500f 100644 --- a/WonderQ-MiniAPP/src/pages/home/components/HomeExperienceCard.vue +++ b/WonderQ-MiniAPP/src/pages/home/components/HomeExperienceCard.vue @@ -7,7 +7,7 @@ - {{ item.badge }} + {{ item.badge }} {{ item.category }} {{ item.title }} @@ -18,7 +18,7 @@ - + diff --git a/WonderQ-MiniAPP/src/pages/home/components/HomeVehicleGrid.vue b/WonderQ-MiniAPP/src/pages/home/components/HomeVehicleGrid.vue index 6dc1b9a..7cbd2be 100644 --- a/WonderQ-MiniAPP/src/pages/home/components/HomeVehicleGrid.vue +++ b/WonderQ-MiniAPP/src/pages/home/components/HomeVehicleGrid.vue @@ -9,7 +9,7 @@ diff --git a/WonderQ-MiniAPP/src/pages/home/components/HomeWildArchives.vue b/WonderQ-MiniAPP/src/pages/home/components/HomeWildArchives.vue index 567cb39..0b92197 100644 --- a/WonderQ-MiniAPP/src/pages/home/components/HomeWildArchives.vue +++ b/WonderQ-MiniAPP/src/pages/home/components/HomeWildArchives.vue @@ -9,25 +9,20 @@ - + - - {{ item.title }} + + {{ + item.title }} + class="mx-[8.5%] mt-[17px] flex h-11 w-[83%] items-center justify-center gap-1 rounded-[50px] border border-[#eeeae4] bg-white p-0 text-[13px] leading-[42px] text-[#7f7770] tap-feedback active:bg-[#fbf5ec] active:text-[#6e6258]" + hover-class="bg-[#fbf5ec] text-[#6e6258]" aria-label="查看更多" @click="$emit('more')"> 查看更多 diff --git a/WonderQ-MiniAPP/src/pages/home/components/homeTeamBuildingData.ts b/WonderQ-MiniAPP/src/pages/home/components/homeTeamBuildingData.ts index dfedeff..69fa243 100644 --- a/WonderQ-MiniAPP/src/pages/home/components/homeTeamBuildingData.ts +++ b/WonderQ-MiniAPP/src/pages/home/components/homeTeamBuildingData.ts @@ -11,24 +11,24 @@ export const homeTeamBuildingMocks: HomeTeamBuilding[] = [ { id: "wild-challenge", tag: "户外挑战", - title: "山野挑战,共同完成一次极境任务", - description: "洞穴、瀑降与协作任务组合,适合 10-30 人团队。", + title: "山野挑战,共创极境", + description: "洞穴、瀑降与协作,适合 10-30 人。", image: "https://dimg04.c-ctrip.com/images/1mh0412000njfr1ot9453_W_640_10000.jpg?proc=autoorient", searchKeyword: "户外团建", }, { id: "canyon-teamwork", tag: "团队协作", - title: "峡谷溯溪,把默契带进自然", - description: "轻户外水线和分组任务,兼顾参与感与安全保障。", + title: "峡谷溯溪,默契同行", + description: "溯溪与分组协作,兼顾参与感与安全。", image: "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient", searchKeyword: "峡谷团建", }, { id: "village-gathering", tag: "人文聚会", - title: "苗寨共聚,换一种方式认识彼此", - description: "夜游、长桌宴与在地文化体验,适合企业团建收尾。", + title: "苗寨共聚,认识彼此", + description: "夜游、长桌宴与文化体验,适合团建收尾。", image: "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg", searchKeyword: "贵州团建", }, diff --git a/WonderQ-MiniAPP/src/pages/login/index.vue b/WonderQ-MiniAPP/src/pages/login/index.vue deleted file mode 100644 index 188f389..0000000 --- a/WonderQ-MiniAPP/src/pages/login/index.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - diff --git a/WonderQ-MiniAPP/src/pages/mine/components/MineGuest.vue b/WonderQ-MiniAPP/src/pages/mine/components/MineGuest.vue index 0a85349..707fb3d 100644 --- a/WonderQ-MiniAPP/src/pages/mine/components/MineGuest.vue +++ b/WonderQ-MiniAPP/src/pages/mine/components/MineGuest.vue @@ -5,7 +5,7 @@ src="https://images.unsplash.com/photo-1774623703220-b3b5e6d3cedf?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=1800" mode="aspectFill" /> - + WONDERQ MEMBER SPACE @@ -19,7 +19,8 @@ - + @@ -32,18 +33,59 @@ · 收藏 + + + + + diff --git a/WonderQ-MiniAPP/src/pages/mine/index.vue b/WonderQ-MiniAPP/src/pages/mine/index.vue index 4d9796b..31987eb 100644 --- a/WonderQ-MiniAPP/src/pages/mine/index.vue +++ b/WonderQ-MiniAPP/src/pages/mine/index.vue @@ -16,7 +16,7 @@ diff --git a/WonderQ-MiniAPP/src/pages/play/components/PlayCategorySidebar.vue b/WonderQ-MiniAPP/src/pages/play/components/PlayCategorySidebar.vue index 4a3c7d6..bc5d706 100644 --- a/WonderQ-MiniAPP/src/pages/play/components/PlayCategorySidebar.vue +++ b/WonderQ-MiniAPP/src/pages/play/components/PlayCategorySidebar.vue @@ -1,15 +1,13 @@ diff --git a/WonderQ-MiniAPP/tests/auth.test.ts b/WonderQ-MiniAPP/tests/auth.test.ts deleted file mode 100644 index 9b61065..0000000 --- a/WonderQ-MiniAPP/tests/auth.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - clearAuthSession, - getAuthSession, - isLoggedIn, - maskPhone, - requireLogin, - saveAuthSession, -} from "@/lib/auth"; - -function installUniStorageMock() { - const storage = new Map(); - const navigateTo = vi.fn(); - (globalThis as unknown as { uni: unknown }).uni = { - getStorageSync: (key: string) => storage.get(key), - setStorageSync: (key: string, value: unknown) => storage.set(key, value), - removeStorageSync: (key: string) => storage.delete(key), - navigateTo, - }; - return { navigateTo, storage }; -} - -describe("miniapp auth helpers", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("stores and clears the customer auth session", () => { - installUniStorageMock(); - - saveAuthSession({ - token: "customer-token", - customer: { id: "customer-test", phoneMasked: "100****0000" }, - }); - - expect(isLoggedIn()).toBe(true); - expect(getAuthSession()).toEqual({ - token: "customer-token", - customer: { id: "customer-test", phoneMasked: "100****0000" }, - }); - - clearAuthSession(); - - expect(isLoggedIn()).toBe(false); - expect(getAuthSession()).toBeNull(); - }); - - it("redirects anonymous users to login with an encoded target", () => { - const { navigateTo } = installUniStorageMock(); - - const allowed = requireLogin("/pages/mine/index"); - - expect(allowed).toBe(false); - expect(navigateTo).toHaveBeenCalledWith({ - url: "/pages/login/index?redirect=%2Fpages%2Fmine%2Findex", - }); - }); - - it("masks phone numbers before rendering them on the client", () => { - expect(maskPhone("10000000000")).toBe("100****0000"); - }); -});