refactor: 重构页面框架,配置全局底部导航栏并优化资源

移除废弃的AppShell、BottomNav、PlayTopBar组件,统一所有页面的根布局结构。在pages.json中配置全局小程序底部导航栏,新增配套的tabbar图标资源。将本地静态图片替换为远程CDN资源,重构资源加载工具函数。更新小程序后台appid,调整全局样式与部分页面的布局样式。拆分我的页面为未登录访客态和已登录会员态,新增对应业务组件。重构登录页、首页、玩法页等多个页面的代码,移除对旧组件的依赖。
This commit is contained in:
duanshuwen committed 2026-08-12 21:09:18 +08:00
1 parent 16caad1c40
commit 988f7c3f4c
31 files changed
+372 -325

No files matched your search

+16 -13
View File
@@ -1,20 +1,20 @@
<template>
<AppShell>
<scroll-view scroll-y class="min-h-screen bg-white text-[#171615] safe-bottom no-scrollbar">
<view class="wq-page">
<view class="wq-phone">
<scroll-view scroll-y class="min-h-screen bg-white text-[#171615] no-scrollbar">
<ConciergeHero />
<ConciergeAdvisor :advisors="advisors" @select="openAdvisor" />
<ConciergePrinciples :principles="principles" />
</scroll-view>
<BottomNav active="manager" />
<ConciergeContactSheet :open="Boolean(selectedAdvisor)" :advisor="selectedAdvisor" @close="selectedAdvisor = null" />
</AppShell>
<ConciergeContactSheet :open="Boolean(selectedAdvisor)" :advisor="selectedAdvisor"
@close="selectedAdvisor = null" />
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import AppShell from "@/components/AppShell.vue";
import BottomNav from "@/components/BottomNav.vue";
import ConciergeHero from "./components/ConciergeHero.vue";
import ConciergePrinciples from "./components/ConciergePrinciples.vue";
import ConciergeAdvisor from "./components/ConciergeAdvisor.vue";
@@ -41,30 +41,33 @@ const principles = [
const advisors: ConciergeAdvisorData[] = [
{
avatar: "/assets/source/asset-001-e3ccdf0858.jpg",
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: "/assets/concierge/wonderq-concierge-qr.png",
qrImage: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
details: [
{ icon: "calendar", label: "服务经验:8年" },
{ icon: "navigate", label: "擅长领域:瀑降 / 探洞" },
],
},
{
avatar: "/assets/source/asset-000-6130f5faf6.jpg",
avatar:
"https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
name: "Mia",
role: "LIFESTYLE TRAVEL ADVISOR",
qrImage: "/assets/concierge/wonderq-concierge-qr.png",
qrImage: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
details: [
{ icon: "calendar", label: "服务经验:6年" },
{ icon: "navigate", label: "擅长领域:溯溪 / 人文探索" },
],
},
{
avatar: "/assets/guizhou/karst-cave-explorer.jpg",
avatar:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
name: "Leo",
role: "WILD TRAVEL ADVISOR",
qrImage: "/assets/concierge/wonderq-concierge-qr.png",
qrImage: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
details: [
{ icon: "calendar", label: "服务经验:9年" },
{ icon: "navigate", label: "擅长领域:洞穴探险 / 山地徒步" },
+27 -22
View File
@@ -1,14 +1,19 @@
<template>
<AppShell>
<view class="safe-bottom min-h-screen bg-transparent">
<view class="wq-page">
<view class="wq-phone">
<view class="min-h-screen bg-transparent">
<scroll-view scroll-y class="h-screen pb-[118px]">
<view class="relative h-[430px] text-white">
<image class="absolute inset-0 h-full w-full" :src="product.image" mode="aspectFill" />
<view class="hero-scrim absolute inset-0" />
<view class="top-safe absolute left-0 right-0 top-0 z-10 flex items-center justify-between px-4 pt-2">
<button class="tap-feedback m-0 h-11 w-11 rounded-full bg-black/30 p-0 text-[28px] leading-none text-white backdrop-blur" @click="goBack()">‹</button>
<button
class="tap-feedback m-0 h-11 w-11 rounded-full bg-black/30 p-0 text-[28px] leading-none text-white backdrop-blur"
@click="goBack()">‹</button>
<view class="flex gap-2">
<button class="tap-feedback m-0 h-11 min-w-[48px] rounded-full bg-black/30 px-3 text-[12px] font-semibold text-white backdrop-blur" @click="toggleFavorite">{{ favorite ? "已藏" : "收藏" }}</button>
<button
class="tap-feedback m-0 h-11 min-w-[48px] rounded-full bg-black/30 px-3 text-[12px] font-semibold text-white backdrop-blur"
@click="toggleFavorite">{{ favorite ? "已藏" : "收藏" }}</button>
</view>
</view>
<view class="absolute bottom-8 left-0 right-0 px-5">
@@ -18,15 +23,13 @@
</view>
</view>
<view class="sticky top-0 z-20 border-b border-[#245c45]/10 bg-[#fffaf2]/95 px-3 py-3 shadow-[0_8px_20px_rgba(31,44,37,0.05)] backdrop-blur">
<view
class="sticky top-0 z-20 border-b border-[#245c45]/10 bg-[#fffaf2]/95 px-3 py-3 shadow-[0_8px_20px_rgba(31,44,37,0.05)] backdrop-blur">
<scroll-view scroll-x class="no-scrollbar whitespace-nowrap">
<button
v-for="section in detailSections"
:key="section.key"
<button v-for="section in detailSections" :key="section.key"
class="tap-feedback m-0 mr-2 min-h-[40px] rounded-full border px-4 py-2 text-[13px]"
:class="activeTab === section.key ? 'border-[#245c45] bg-[#245c45] text-white shadow-[0_8px_16px_rgba(36,92,69,0.16)]' : 'border-[#245c45]/10 bg-white/90 text-[#526256]'"
@click="activeTab = section.key"
>
@click="activeTab = section.key">
{{ section.label }}
</button>
</scroll-view>
@@ -35,7 +38,9 @@
<view class="px-4 py-4">
<view class="elevated-card rounded-[24px] p-4">
<view class="flex flex-wrap gap-2">
<text v-for="tag in product.tags" :key="tag" class="rounded-full border border-[#245c45]/10 bg-[#edf4ee] px-3 py-1 text-[11px] font-semibold text-[#245c45]">{{ tag }}</text>
<text v-for="tag in product.tags" :key="tag"
class="rounded-full border border-[#245c45]/10 bg-[#edf4ee] px-3 py-1 text-[11px] font-semibold text-[#245c45]">{{
tag }}</text>
</view>
<view class="mt-4 flex items-end justify-between">
<text class="text-[12px] text-[#718073]">参考起价</text>
@@ -45,18 +50,16 @@
</view>
<view class="px-4">
<view
v-for="section in visibleSections"
:key="section.key"
class="elevated-card mb-4 rounded-[24px] p-4"
>
<view v-for="section in visibleSections" :key="section.key" class="elevated-card mb-4 rounded-[24px] p-4">
<text class="block text-[18px] font-bold text-[#1f2c25]">{{ section.title || section.label }}</text>
<view class="mt-3 space-y-3">
<template v-for="(block, index) in section.blocks" :key="`${section.key}-${index}`">
<text v-if="block.type === 'text'" class="block text-[13px] leading-6 text-[#526256]">{{ block.text }}</text>
<text v-if="block.type === 'text'" class="block text-[13px] leading-6 text-[#526256]">{{ block.text
}}</text>
<view v-else class="overflow-hidden rounded-[20px] border border-[#245c45]/10">
<image class="h-[210px] w-full" :src="block.url" mode="aspectFill" />
<text v-if="block.alt" class="block bg-[#f6f7f2] px-3 py-2 text-[11px] text-[#718073]">{{ block.alt }}</text>
<text v-if="block.alt" class="block bg-[#f6f7f2] px-3 py-2 text-[11px] text-[#718073]">{{ block.alt
}}</text>
</view>
</template>
</view>
@@ -69,17 +72,19 @@
</view>
</scroll-view>
<view class="fixed bottom-0 left-1/2 z-40 flex w-full max-w-[430px] -translate-x-1/2 items-center justify-end gap-3 border-t border-[#245c45]/10 bg-[#fffaf2]/95 px-4 pb-[calc(12px+env(safe-area-inset-bottom))] pt-3 shadow-[0_-14px_32px_rgba(31,44,37,0.1)] backdrop-blur">
<button class="primary-action tap-feedback m-0 w-full text-[15px] font-bold" @click="goBooking(product)">预订咨询</button>
<view
class="fixed bottom-0 left-1/2 z-40 flex w-full max-w-[430px] -translate-x-1/2 items-center justify-end gap-3 border-t border-[#245c45]/10 bg-[#fffaf2]/95 px-4 pb-[calc(12px+env(safe-area-inset-bottom))] pt-3 shadow-[0_-14px_32px_rgba(31,44,37,0.1)] backdrop-blur">
<button class="primary-action tap-feedback m-0 w-full text-[15px] font-bold"
@click="goBooking(product)">预订咨询</button>
</view>
</view>
</AppShell>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import AppShell from "@/components/AppShell.vue";
import ProductList from "@/components/ProductList.vue";
import SectionHeading from "@/components/SectionHeading.vue";
import { formatWan, getDetailMeta, getProductDetailSections, productKey, stripTitle } from "@/lib/data";
@@ -1,29 +1,29 @@
<template>
<view class="bg-[#fffdf9] pb-7 pt-8">
<view class="mx-[8.5%] mb-[18px] flex items-baseline justify-between gap-[18px]">
<view class="bg-[#f7f3ed] pb-8 pt-9">
<view class="mx-6 mb-5 flex items-end justify-between gap-4">
<view class="flex min-w-0 items-baseline gap-[10px]">
<text class="font-serif text-[20px] font-semibold leading-7 tracking-[0.04em] text-[#18212a]">团队共创</text>
<text class="text-[14px] leading-[22px] text-[#8b8781]">团建旅行</text>
<text class="font-serif text-[22px] font-semibold leading-7 tracking-[0.03em] text-[#202522]">团队共创</text>
<text class="text-[13px] leading-5 text-[#8d857c]">团建旅行</text>
</view>
<text class="flex-none font-sans text-[9px] leading-[18px] tracking-[0.2em] text-[#a3988e]">TEAM BUILDING</text>
<text class="flex-none font-sans text-[9px] leading-4 tracking-[0.18em] text-[#a3988e]">TEAM BUILDING</text>
</view>
<view class="mx-[8.5%] grid gap-3">
<view class="mx-6 grid gap-3">
<view
v-for="item in items"
:key="item.id"
class="flex min-h-[112px] overflow-hidden rounded-[4px] border border-[#e8e1d7] bg-white tap-feedback active:border-[#d3b890] active:bg-[#fffaf2]"
class="flex min-h-[120px] overflow-hidden rounded-[12px] border border-[#e6ded3] bg-[#fffdfa] tap-feedback active:border-[#b97b5f] active:bg-[#fffaf4]"
:aria-label="`${item.title},${item.description}`"
@click="$emit('select', item)"
>
<image class="block h-[112px] w-[128px] shrink-0 bg-[#e8e5df] min-[431px]:w-[142px]" :src="item.image" mode="aspectFill" />
<view class="min-w-0 flex-1 p-[12px_12px_11px]">
<image class="block h-[120px] w-[116px] shrink-0 bg-[#e8e5df] min-[431px]:w-[132px]" :src="item.image" mode="aspectFill" />
<view class="min-w-0 flex-1 px-4 py-3">
<view class="flex items-center justify-between gap-2">
<text class="text-[10px] leading-[18px] tracking-[0.04em] text-[#9a744b]">{{ item.tag }}</text>
<uni-icons type="right" :size="16" color="#b5aaa0" />
<text class="text-[10px] font-medium leading-4 tracking-[0.05em] text-[#a45a3d]">{{ item.tag }}</text>
<uni-icons type="right" :size="15" color="#a79b90" />
</view>
<text class="mt-[5px] text-clamp-2 font-serif text-[15px] font-semibold leading-[21px] text-[#1c252d]">{{ item.title }}</text>
<text class="mt-1 text-clamp-2 text-[11px] leading-[17px] text-[#7a7d78]">{{ item.description }}</text>
<text class="mt-1 text-clamp-2 font-serif text-[16px] font-semibold leading-[21px] text-[#202522]">{{ item.title }}</text>
<text class="mt-1 text-clamp-2 text-[11px] leading-4 text-[#7c817b]">{{ item.description }}</text>
</view>
</view>
</view>
@@ -15,7 +15,7 @@ export const homeExperienceMocks: HomeExperience[] = [
category: "瀑降体验",
title: "悬崖瀑降",
englishTitle: "WATERFALL DESCENT",
image: "/assets/guizhou/tiankeng-pool.jpg",
image: "https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg",
searchKeyword: "悬崖瀑降",
},
{
@@ -24,7 +24,7 @@ export const homeExperienceMocks: HomeExperience[] = [
category: "地心探险",
title: "森林&探洞",
englishTitle: "CAVE EXPLORATION",
image: "/assets/guizhou/zhijin-cave-pool.jpg",
image: "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
searchKeyword: "地心探险",
},
];
@@ -13,7 +13,7 @@ export const homeTeamBuildingMocks: HomeTeamBuilding[] = [
tag: "户外挑战",
title: "山野挑战,共同完成一次极境任务",
description: "洞穴、瀑降与协作任务组合,适合 10-30 人团队。",
image: "/assets/guizhou/tiankeng-sunbeam-group.jpg",
image: "https://dimg04.c-ctrip.com/images/1mh0412000njfr1ot9453_W_640_10000.jpg?proc=autoorient",
searchKeyword: "户外团建",
},
{
@@ -21,7 +21,7 @@ export const homeTeamBuildingMocks: HomeTeamBuilding[] = [
tag: "团队协作",
title: "峡谷溯溪,把默契带进自然",
description: "轻户外水线和分组任务,兼顾参与感与安全保障。",
image: "/assets/guizhou/shuichunhe-rafting.jpg",
image: "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
searchKeyword: "峡谷团建",
},
{
@@ -29,7 +29,7 @@ export const homeTeamBuildingMocks: HomeTeamBuilding[] = [
tag: "人文聚会",
title: "苗寨共聚,换一种方式认识彼此",
description: "夜游、长桌宴与在地文化体验,适合企业团建收尾。",
image: "/assets/guizhou/xijiang-miao-village.jpg",
image: "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
searchKeyword: "贵州团建",
},
];
@@ -9,25 +9,25 @@ export const homeWildArchiveMocks: HomeWildArchive[] = [
{
id: "hundred-meter-descent",
title: "百米自降",
image: "/assets/guizhou/tiankeng-pool.jpg",
image: "https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg",
searchKeyword: "悬崖瀑降",
},
{
id: "shilong-cave",
title: "石龙洞",
image: "/assets/guizhou/zhijin-cave-pool.jpg",
image: "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
searchKeyword: "地心探险",
},
{
id: "cliff-current",
title: "绝壁迎流",
image: "/assets/guizhou/gorge-paddle.jpg",
image: "https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg",
searchKeyword: "峡谷探险",
},
{
id: "canyon-streaming",
title: "峡谷溯溪",
image: "/assets/guizhou/shuichunhe-rafting.jpg",
image: "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
searchKeyword: "峡谷溯溪",
},
];
+11 -13
View File
@@ -1,22 +1,20 @@
<template>
<AppShell>
<scroll-view id="home-scroll" scroll-y class="h-screen safe-bottom no-scrollbar">
<HomeHeroCarousel :slides="appState.data.heroSlides" @select="openHero" />
<HomeExperienceList :items="homeExperienceMocks" @select="openExperience" />
<HomeVehicleGrid :vehicle-options="vehicleOptions" @select="goDemand('贵州')" />
<HomeTeamBuilding :items="homeTeamBuildingMocks" @select="openTeamBuilding" />
<HomeWildArchives :items="homeWildArchiveMocks" @select="openWildArchive" @more="openWildArchives" />
</scroll-view>
<BottomNav active="home" />
</AppShell>
<view class="wq-page">
<view class="wq-phone">
<scroll-view scroll-y class="h-screen no-scrollbar" :show-scrollbar="false">
<HomeHeroCarousel :slides="appState.data.heroSlides" @select="openHero" />
<HomeExperienceList :items="homeExperienceMocks" @select="openExperience" />
<HomeVehicleGrid :vehicle-options="vehicleOptions" @select="goDemand('贵州')" />
<HomeTeamBuilding :items="homeTeamBuildingMocks" @select="openTeamBuilding" />
<HomeWildArchives :items="homeWildArchiveMocks" @select="openWildArchive" @more="openWildArchives" />
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { onShow } from "@dcloudio/uni-app";
import AppShell from "@/components/AppShell.vue";
import BottomNav from "@/components/BottomNav.vue";
import HomeHeroCarousel from "./components/HomeHeroCarousel.vue";
import HomeExperienceList from "./components/HomeExperienceList.vue";
import HomeTeamBuilding from "./components/HomeTeamBuilding.vue";
+4 -3
View File
@@ -1,5 +1,6 @@
<template>
<AppShell>
<view class="wq-page">
<view class="wq-phone">
<view class="safe-bottom min-h-screen bg-transparent">
<TopHeader title="手机号登录" subtitle="用于查看我的需求" @back="goRoot('/pages/home/index')" />
@@ -40,13 +41,13 @@
</view>
</view>
</view>
</AppShell>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppShell from "@/components/AppShell.vue";
import TopHeader from "@/components/TopHeader.vue";
import { loginWithPhoneCode } from "@/lib/auth";
import { goRoot } from "@/lib/navigation";
@@ -0,0 +1,49 @@
<template>
<view class="flex h-screen flex-col overflow-hidden bg-[#f5f2ed]">
<view class="relative h-[52%] min-h-[300px] overflow-hidden rounded-b-[24px] bg-[#211e1c]">
<image class="absolute inset-0 h-full w-full opacity-75"
src="https://images.unsplash.com/photo-1774623703220-b3b5e6d3cedf?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=1800"
mode="aspectFill" />
<view class="absolute inset-0 bg-[#171513]/45" />
<view class="absolute left-6 top-[calc(16px+env(safe-area-inset-top))]">
<text class="block text-[11px] font-semibold tracking-[0.28em] text-white/90">WONDERQ</text>
<text class="mt-1 block text-[10px] tracking-[0.22em] text-white/55">MEMBER SPACE</text>
</view>
<view class="absolute bottom-6 left-6 right-6 flex items-end justify-between">
<view>
<text class="block text-[18px] font-medium tracking-[0.04em] text-white">把旅程交给懂你的人</text>
<text class="mt-2 block text-[10px] tracking-[0.2em] text-white/60">A QUIETER WAY TO TRAVEL</text>
</view>
<view class="flex h-9 w-9 items-center justify-center rounded-full border border-white/30 bg-black/15">
<uni-icons type="arrowright" :size="18" color="#fffaf5" />
</view>
</view>
</view>
<view class="flex min-h-0 flex-1 flex-col items-center px-6 pb-[calc(12px+env(safe-area-inset-bottom))] pt-6 text-center">
<view class="flex h-12 w-12 items-center justify-center rounded-[14px] border border-[#ded6cd] bg-white/80">
<uni-icons type="person" :size="30" color="#6d665f" />
</view>
<text class="mt-4 block text-[25px] font-light leading-tight tracking-[0.02em] text-[#24211f]">探索你的专属空间</text>
<text class="mt-2 block text-[13px] leading-5 tracking-[0.03em] text-[#857d75]">登录后查看行程、管理订单</text>
<view class="mt-4 flex items-center gap-3 text-[11px] tracking-[0.08em] text-[#9b9188]">
<text>行程</text>
<text class="text-[#c8beb5]">·</text>
<text>订单</text>
<text class="text-[#c8beb5]">·</text>
<text>收藏</text>
</view>
<button
class="tap-feedback m-0 mt-5 flex min-h-[48px] w-full max-w-[280px] items-center justify-center gap-2 rounded-[14px] border-0 bg-[#a45a3d] px-6 text-[16px] font-semibold tracking-[0.05em] text-white shadow-none"
@click="$emit('login')">
<uni-icons type="chat" :size="19" color="#fffaf5" />
<text>微信授权登录</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
defineEmits<{
login: [];
}>();
</script>
@@ -0,0 +1,108 @@
<template>
<view class="min-h-screen bg-transparent">
<TopHeader title="我的" subtitle="需求、收藏和最近浏览" search @back="$emit('backHome')" @search="$emit('search', $event ?? '贵州')" />
<view class="px-4 pt-4">
<view class="brand-panel rounded-[26px] p-5 text-white">
<view class="relative z-10 flex items-center justify-between">
<view>
<text class="block text-[12px] font-semibold uppercase tracking-[0.12em] text-[#f6dfb7]">万趣旅行</text>
<text class="mt-1 block text-[24px] font-bold">旅行需求中心</text>
<text class="mt-2 block text-[12px] leading-5 text-white/80">{{ customerLine }}</text>
</view>
<view
class="flex h-16 w-16 items-center justify-center rounded-full border border-white/15 bg-white/15 text-[22px] font-bold shadow-[inset_0_1px_rgba(255,255,255,0.24)]"
>
我
</view>
</view>
</view>
</view>
<view class="grid grid-cols-3 gap-2 px-4 py-4">
<button
v-for="item in tabs"
:key="item.id"
class="tap-feedback m-0 min-h-[76px] rounded-[20px] border px-3 py-4 text-center"
:class="tab === item.id ? 'border-[#245c45] bg-[#245c45] text-white shadow-[0_10px_20px_rgba(36,92,69,0.16)]' : 'border-[#245c45]/10 bg-white/90 text-[#718073]'"
@click="tab = item.id"
>
<text class="block text-[18px] font-bold">{{ item.count }}</text>
<text class="mt-1 block text-[11px]">{{ item.label }}</text>
</button>
</view>
<view class="px-4 pb-6">
<view v-if="tab === 'requests'" class="space-y-3">
<view v-if="latestBooking" class="elevated-card rounded-[22px] p-4">
<view class="mb-3 flex items-center justify-between">
<text class="font-bold text-[#1f2c25]">{{ latestBooking.status }}</text>
<text class="text-[11px] text-[#718073]">{{ latestBooking.time }}</text>
</view>
<button class="m-0 flex w-full gap-3 bg-transparent p-0 text-left" @click="$emit('goDetail', latestBooking.product)">
<image class="h-[86px] w-[104px] rounded-[16px]" :src="latestBooking.product.image" mode="aspectFill" />
<view class="min-w-0 flex-1">
<text class="text-clamp-2 text-[14px] font-bold text-[#1f2c25]">{{ stripTitle(latestBooking.product.title) }}</text>
<text class="mt-2 block text-[11px] text-[#718073]">{{ latestBooking.product.tags.slice(0, 3).join(" / ") }}</text>
</view>
</button>
</view>
<EmptyState v-else title="暂无提交记录" action="去提交需求" @action="$emit('goDemand')" />
</view>
<view v-if="tab === 'favorites'" class="space-y-3">
<MineProductRow
v-for="product in favoriteProducts"
:key="productKey(product)"
:product="product"
primary="查看详情"
secondary="取消收藏"
@primary="$emit('goDetail', product)"
@secondary="$emit('toggleFavorite', product)"
/>
<EmptyState v-if="!favoriteProducts.length" title="暂无收藏线路" action="去首页看看" @action="$emit('goHome')" />
</view>
<view v-if="tab === 'history'" class="space-y-3">
<MineProductRow
v-for="product in browsingHistory"
:key="productKey(product)"
:product="product"
primary="再次查看"
@primary="$emit('goDetail', product)"
/>
<EmptyState v-if="!browsingHistory.length" title="暂无浏览记录" action="去找线路" @action="$emit('goSearch', $event ?? '贵州')" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import EmptyState from "@/components/EmptyState.vue";
import MineProductRow from "@/components/MineProductRow.vue";
import TopHeader from "@/components/TopHeader.vue";
import { productKey, stripTitle } from "@/lib/data";
import type { BookingRecord, Product } from "@/lib/types";
type MineTab = "requests" | "favorites" | "history";
defineProps<{
customerLine: string;
tabs: { id: MineTab; label: string; count: number }[];
latestBooking: BookingRecord | null;
favoriteProducts: Product[];
browsingHistory: Product[];
}>();
defineEmits<{
backHome: [];
search: [keyword: string];
goDetail: [product: Product];
toggleFavorite: [product: Product];
goDemand: [];
goHome: [];
goSearch: [keyword: string];
}>();
const tab = ref<MineTab>("requests");
</script>
+24 -86
View File
@@ -1,98 +1,32 @@
<template>
<AppShell>
<view class="safe-bottom min-h-screen bg-transparent">
<TopHeader title="我的" subtitle="需求、收藏和最近浏览" search @back="goRoot('/pages/home/index')" @search="goSearch('贵州')" />
<view class="px-4 pt-4">
<view class="brand-panel rounded-[26px] p-5 text-white">
<view class="relative z-10 flex items-center justify-between">
<view>
<text class="block text-[12px] font-semibold uppercase tracking-[0.12em] text-[#f6dfb7]">万趣旅行</text>
<text class="mt-1 block text-[24px] font-bold">旅行需求中心</text>
<text class="mt-2 block text-[12px] leading-5 text-white/80">{{ customerLine }}</text>
</view>
<view class="flex h-16 w-16 items-center justify-center rounded-full border border-white/15 bg-white/15 text-[22px] font-bold shadow-[inset_0_1px_rgba(255,255,255,0.24)]">我</view>
</view>
</view>
</view>
<view class="grid grid-cols-3 gap-2 px-4 py-4">
<button
v-for="item in tabs"
:key="item.id"
class="tap-feedback m-0 min-h-[76px] rounded-[20px] border px-3 py-4 text-center"
:class="tab === item.id ? 'border-[#245c45] bg-[#245c45] text-white shadow-[0_10px_20px_rgba(36,92,69,0.16)]' : 'border-[#245c45]/10 bg-white/90 text-[#718073]'"
@click="tab = item.id"
>
<text class="block text-[18px] font-bold">{{ item.count }}</text>
<text class="mt-1 block text-[11px]">{{ item.label }}</text>
</button>
</view>
<view class="px-4 pb-6">
<view v-if="tab === 'requests'" class="space-y-3">
<view v-if="appState.latestBooking" class="elevated-card rounded-[22px] p-4">
<view class="mb-3 flex items-center justify-between">
<text class="font-bold text-[#1f2c25]">{{ appState.latestBooking.status }}</text>
<text class="text-[11px] text-[#718073]">{{ appState.latestBooking.time }}</text>
</view>
<button class="m-0 flex w-full gap-3 bg-transparent p-0 text-left" @click="goDetail(appState.latestBooking.product)">
<image class="h-[86px] w-[104px] rounded-[16px]" :src="appState.latestBooking.product.image" mode="aspectFill" />
<view class="min-w-0 flex-1">
<text class="text-clamp-2 text-[14px] font-bold text-[#1f2c25]">{{ stripTitle(appState.latestBooking.product.title) }}</text>
<text class="mt-2 block text-[11px] text-[#718073]">{{ appState.latestBooking.product.tags.slice(0, 3).join(" / ") }}</text>
</view>
</button>
</view>
<EmptyState v-else title="暂无提交记录" action="去提交需求" @action="go('/pages/demand/index')" />
</view>
<view v-if="tab === 'favorites'" class="space-y-3">
<MineProductRow
v-for="product in appState.favoriteProducts"
:key="productKey(product)"
:product="product"
primary="查看详情"
secondary="取消收藏"
@primary="goDetail(product)"
@secondary="toggleFavoriteProduct(product)"
/>
<EmptyState v-if="!appState.favoriteProducts.length" title="暂无收藏线路" action="去首页看看" @action="goRoot('/pages/home/index')" />
</view>
<view v-if="tab === 'history'" class="space-y-3">
<MineProductRow
v-for="product in appState.browsingHistory"
:key="productKey(product)"
:product="product"
primary="再次查看"
@primary="goDetail(product)"
/>
<EmptyState v-if="!appState.browsingHistory.length" title="暂无浏览记录" action="去找线路" @action="goSearch('贵州')" />
</view>
</view>
<view class="wq-page h-screen overflow-hidden">
<view class="wq-phone h-screen overflow-hidden">
<MineGuest v-if="!session" @login="goLogin" />
<scroll-view v-else scroll-y class="h-full no-scrollbar">
<MineMember :customer-line="customerLine" :tabs="tabs" :latest-booking="appState.latestBooking"
:favorite-products="appState.favoriteProducts" :browsing-history="appState.browsingHistory"
@back-home="goRoot('/pages/home/index')" @search="goSearch" @go-detail="goDetail"
@toggle-favorite="toggleFavoriteProduct" @go-demand="go('/pages/demand/index')"
@go-home="goRoot('/pages/home/index')" @go-search="goSearch" />
</scroll-view>
</view>
<BottomNav active="mine" />
</AppShell>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import AppShell from "@/components/AppShell.vue";
import BottomNav from "@/components/BottomNav.vue";
import EmptyState from "@/components/EmptyState.vue";
import MineProductRow from "@/components/MineProductRow.vue";
import TopHeader from "@/components/TopHeader.vue";
import { getAuthSession, redirectToLogin } from "@/lib/auth";
import { productKey, stripTitle } from "@/lib/data";
import { getAuthSession } from "@/lib/auth";
import { go, goDetail, goRoot, goSearch } from "@/lib/navigation";
import { appState, loadAppData, loadPersistedCollections, toggleFavoriteProduct } from "@/lib/store";
import type { Product } from "@/lib/types";
import type { AuthSession } from "@/lib/types";
import MineGuest from "./components/MineGuest.vue";
import MineMember from "./components/MineMember.vue";
type MineTab = "requests" | "favorites" | "history";
const tab = ref<MineTab>("requests");
const customerLine = ref("已登录,可查看当前设备的需求、收藏和浏览记录。");
const session = ref<AuthSession | null>(null);
const customerLine = ref("可查看当前设备的需求、收藏和浏览记录。");
const tabs = computed(() => [
{ id: "requests" as MineTab, label: "需求", count: appState.latestBooking ? 1 : 0 },
{ id: "favorites" as MineTab, label: "收藏", count: appState.favoriteProducts.length },
@@ -100,11 +34,15 @@ const tabs = computed(() => [
]);
onShow(() => {
if (!redirectToLogin("/pages/mine/index")) return;
const session = getAuthSession();
customerLine.value = session ? `已绑定手机号 ${session.customer.phoneMasked}` : "已登录,可查看当前设备的需求、收藏和浏览记录。";
session.value = getAuthSession();
customerLine.value = session.value
? `已绑定手机号 ${session.value.customer.phoneMasked}`
: "可查看当前设备的需求、收藏和浏览记录。";
loadPersistedCollections();
void loadAppData();
});
function goLogin() {
go("/pages/login/index", { redirect: "/pages/mine/index" });
}
</script>
@@ -1,6 +1,6 @@
<template>
<scroll-view scroll-y class="no-scrollbar h-full min-h-0 w-[104px] shrink-0 border-r border-[#f0f0f0] bg-white">
<view class="pb-[96px] pt-[12px]">
<view class="pb-[96px]">
<button v-for="category in categories" :key="category.id"
class="tap-feedback m-0 flex h-[62px] w-full items-center justify-center rounded-none border-0 bg-white px-2 text-center text-[13px] leading-5 text-[#171717] after:border-0"
:class="activeId === category.id ? 'border-l-2 border-[#f26424] bg-[#f5f5f5] font-semibold' : 'border-l-2 border-transparent font-medium'"
@@ -1,14 +0,0 @@
<template>
<view class="flex shrink-0 border-b border-[#eeeeee] bg-white pt-[env(safe-area-inset-top)]">
<view class="w-[104px] shrink-0" aria-hidden="true" />
<view class="flex h-12 min-w-0 flex-1 items-center pl-[20px] pr-[88px]">
<text class="truncate text-[17px] font-semibold leading-6 text-[#151515]">{{ title }}</text>
</view>
</view>
</template>
<script setup lang="ts">
defineProps<{
title: string;
}>();
</script>
@@ -13,6 +13,39 @@ export type PlayCategory = {
routes: PlayRoute[];
};
const routeImageByAsset: Record<string, string> = {
"libo-xiaoqikong": "https://p0.51img.ca/i/669740a019b94.jpeg",
"xijiang-miao-village":
"https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
"wumeng-grassland":
"https://p2.cri.cn/M00/89/21/rBABC2aHnWeACxj5AAAAAAAAAAA159.1000x566.jpg",
"miao-costume":
"https://i.pinimg.com/736x/d5/86/5d/d5865d0b2765dfb6713787aede09f086.jpg",
wanfenglin:
"https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg",
"jiabang-terrace":
"https://news.cgtn.com/news/2023-08-28/Spectacular-rice-terraces-in-Guizhou-1mDHGG4vtHq/img/9ff1dc49c6304b35bdb83bc1f7f8df93/9ff1dc49c6304b35bdb83bc1f7f8df93.jpeg",
"mountain-resort":
"https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&fm=jpg&q=80&w=1200&h=800",
"jianhe-hot-spring": "https://www.gz007.net/images/upload/jianhewenquan2.jpg",
"libo-water-forest": "https://p0.51img.ca/i/669740a019b94.jpeg",
"gorge-paddle":
"https://q9.itc.cn/q_70/images03/20250810/b62f5afc191a4947a66e6b32721b0235.jpeg",
"shuichunhe-rafting":
"https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
"miao-dragon-boat":
"https://news.cgtn.com/news/2023-07-13/Miao-people-celebrate-Canoe-Dragon-Boat-Festival-1lpanidzxu0/img/61b7100caa8a441aacb3780cfa3eeb69/61b7100caa8a441aacb3780cfa3eeb69.jpeg",
"huangguoshu-waterfall":
"https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg",
"zhenyuan-ancient-town":
"https://news.cgtn.com/news/2023-06-23/Zhenyuan-ancient-town-a-must-see-in-Guizhou-1kRXUSXOP1m/img/6aead33b391640589740a7672c66a685/6aead33b391640589740a7672c66a685.jpeg",
"zhijin-cave": "https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
};
const resolveRouteImage = (image: string) =>
routeImageByAsset[image] ??
(image.startsWith("http") ? image : `/assets/guizhou/${image}.jpg`);
const route = (
id: string,
title: string,
@@ -24,7 +57,7 @@ const route = (
id,
title,
subtitle,
image: `/public/assets/guizhou/${image}.jpg`,
image: resolveRouteImage(image),
routeCount,
searchKeyword,
});
+14 -17
View File
@@ -1,33 +1,30 @@
<template>
<AppShell>
<view class="flex h-screen max-h-screen flex-col overflow-hidden bg-white">
<PlayTopBar :title="activeCategory.label" />
<view class="flex min-h-0 flex-1">
<PlayCategorySidebar :categories="playCategories" :active-id="activeCategoryId" @select="selectCategory" />
<view class="wq-page">
<view class="wq-phone">
<view class="flex h-screen max-h-screen flex-col overflow-hidden bg-white">
<view class="flex min-h-0 flex-1">
<PlayCategorySidebar :categories="playCategories" :active-id="activeCategoryId" @select="selectCategory" />
<scroll-view scroll-y class="no-scrollbar h-full min-h-0 min-w-0 flex-1 bg-white pl-[20px] pr-0 pt-[16px]">
<view class="min-h-full bg-white pb-[calc(96px+env(safe-area-inset-bottom))]">
<view class="pb-10">
<view v-for="route in activeCategory.routes" :key="route.id" class="pb-[40px] last:pb-0">
<PlayRouteCard :route="route" @select="selectRoute" />
<scroll-view scroll-y class="no-scrollbar h-full min-h-0 min-w-0 flex-1 bg-white">
<view class="min-h-full bg-white pb-[calc(96px+env(safe-area-inset-bottom))] px-[20px] pt-[16px]">
<view class="pb-10">
<view v-for="route in activeCategory.routes" :key="route.id" class="pb-[40px] last:pb-0">
<PlayRouteCard :route="route" @select="selectRoute" />
</view>
</view>
</view>
</view>
</scroll-view>
</scroll-view>
</view>
</view>
</view>
<BottomNav active="play" />
</AppShell>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import AppShell from "@/components/AppShell.vue";
import BottomNav from "@/components/BottomNav.vue";
import { goSearch } from "@/lib/navigation";
import PlayCategorySidebar from "./components/PlayCategorySidebar.vue";
import PlayRouteCard from "./components/PlayRouteCard.vue";
import PlayTopBar from "./components/PlayTopBar.vue";
import { playCategories, type PlayCategory, type PlayRoute } from "./components/playData";
const defaultCategory = playCategories[0]!;