refactor: 重构页面框架,配置全局底部导航栏并优化资源
移除废弃的AppShell、BottomNav、PlayTopBar组件,统一所有页面的根布局结构。在pages.json中配置全局小程序底部导航栏,新增配套的tabbar图标资源。将本地静态图片替换为远程CDN资源,重构资源加载工具函数。更新小程序后台appid,调整全局样式与部分页面的布局样式。拆分我的页面为未登录访客态和已登录会员态,新增对应业务组件。重构登录页、首页、玩法页等多个页面的代码,移除对旧组件的依赖。
This commit is contained in:
1 parent
16caad1c40
commit
988f7c3f4c
31 files changed
+372
-325
No files matched your search
@@ -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: "擅长领域:洞穴探险 / 山地徒步" },
|
||||
|
||||
@@ -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: "峡谷溯溪",
|
||||
},
|
||||
];
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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]!;
|
||||
|
||||
Reference in new issue
Block a user