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

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

View File

@@ -68,10 +68,6 @@ button::after {
}
}
.safe-bottom {
padding-bottom: calc(64px + env(safe-area-inset-bottom));
}
.top-safe {
padding-top: env(safe-area-inset-top);
}
@@ -119,7 +115,10 @@ button::after {
}
.tap-feedback {
transition: opacity 140ms ease, background-color 140ms ease, border-color 140ms ease;
transition:
opacity 140ms ease,
background-color 140ms ease,
border-color 140ms ease;
}
.tap-feedback:active {

View File

@@ -1,7 +0,0 @@
<template>
<view class="wq-page">
<view class="wq-phone">
<slot />
</view>
</view>
</template>

View File

@@ -1,105 +0,0 @@
<template>
<view>
<view class="fixed bottom-0 left-1/2 z-40 w-full max-w-[430px] -translate-x-1/2">
<view
class="bottom-nav-bar relative border-t border-[#e8e1d7] bg-[#fffdf9] px-2 pb-[calc(6px+env(safe-area-inset-bottom))]"
>
<view class="relative z-20 grid grid-cols-4 gap-1">
<view
v-for="item in items"
:key="item.id"
class="tap-feedback relative m-0 flex min-h-[50px] flex-col items-center justify-center gap-0.5 border-0 bg-transparent px-1 py-1 text-[11px] leading-none"
:class="visualActive === item.id ? 'text-[#2f2d2a]' : 'text-[#9a9590]'"
:aria-label="item.label"
@click="select(item)"
>
<view
class="bottom-nav-icon flex h-7 w-7 items-center justify-center rounded-[8px]"
:class="visualActive === item.id ? 'bg-[#eee9e2]' : 'bg-transparent'"
>
<uni-icons
:type="item.icon"
:size="20"
:color="visualActive === item.id ? '#2f2d2a' : '#9a9590'"
/>
</view>
<text class="mt-0.5" :class="visualActive === item.id ? 'font-semibold' : 'font-medium'">{{ item.label }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { goRoot } from "@/lib/navigation";
import { isLoggedIn, requireLogin } from "@/lib/auth";
import { ref, watch } from "vue";
type NavId = "home" | "play" | "manager" | "mine";
type NavIcon = "home" | "list" | "headphones" | "person";
const props = defineProps<{
active: NavId;
}>();
const items: Array<{ id: NavId; label: string; icon: NavIcon; url?: string }> =
[
{ id: "home", label: "首页", icon: "home", url: "/pages/home/index" },
{
id: "play",
label: "玩法",
icon: "list",
url: "/pages/play/index",
},
{ id: "manager", label: "管家", icon: "headphones", url: "/pages/concierge/index" },
{ id: "mine", label: "我的", icon: "person", url: "/pages/mine/index" },
];
const NAV_SLIDE_DELAY_MS = 150;
let navigationTimer: ReturnType<typeof setTimeout> | undefined;
const visualActive = ref<NavId>(props.active);
watch(
() => props.active,
(active) => {
visualActive.value = active;
},
);
function select(item: (typeof items)[number]) {
if (item.id === props.active) return;
if (item.id === "mine" && !isLoggedIn()) {
if (navigationTimer) {
clearTimeout(navigationTimer);
navigationTimer = undefined;
}
requireLogin("/pages/mine/index");
return;
}
visualActive.value = item.id;
if (navigationTimer) clearTimeout(navigationTimer);
navigationTimer = setTimeout(() => {
navigationTimer = undefined;
goRoot(item.url!);
}, NAV_SLIDE_DELAY_MS);
}
</script>
<style scoped>
.bottom-nav-bar {
overflow: visible;
}
.bottom-nav-icon {
transition:
background-color 180ms ease,
color 180ms ease;
}
@media (prefers-reduced-motion: reduce) {
.bottom-nav-icon {
transition: none;
}
}
</style>

View File

@@ -1,4 +1,16 @@
const guizhouAsset = (name: string) => `/assets/guizhou/${name}.jpg`;
const remoteGuizhouAssets: Record<string, string> = {
"jiaxiu-tower":
"https://dimg04.c-ctrip.com/images/1me6m12000rntioi4FFB2.jpg?proc=source%2Ftripcommunity",
"xijiang-miao-village":
"https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
wanfenglin:
"https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg",
"huangguoshu-waterfall":
"https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg",
};
const guizhouAsset = (name: string) =>
remoteGuizhouAssets[name] ?? `/assets/guizhou/${name}.jpg`;
export const heroSlides = [
{

View File

@@ -7,7 +7,7 @@
"经典人文打卡",
"小包团"
],
"image": "/assets/guizhou/huangguoshu-waterfall.jpg",
"image": "https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg",
"summary": "贵州首游代表线路,串联瀑布、绿宝石河谷、苗寨夜景和梵净山云海。",
"destinationName": "黄果树"
},
@@ -19,7 +19,7 @@
"经典人文打卡",
"贵阳安顺"
],
"image": "/assets/guizhou/jiaxiu-tower.jpg",
"image": "https://dimg04.c-ctrip.com/images/1me6m12000rntioi4FFB2.jpg?proc=source%2Ftripcommunity",
"summary": "适合周末或短假,从城市地标、古镇风味一路延展到黄果树瀑布。",
"destinationName": "贵阳"
},
@@ -43,7 +43,7 @@
"经典人文打卡",
"非遗体验"
],
"image": "/assets/guizhou/xijiang-miao-village.jpg",
"image": "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
"summary": "深入苗寨街巷、古城夜色和非遗手作,保留小团讲解与拍摄节奏。",
"destinationName": "西江苗寨"
},
@@ -67,7 +67,7 @@
"经典人文打卡",
"黔西南"
],
"image": "/assets/guizhou/wanfenglin.jpg",
"image": "https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg",
"summary": "在峰林、峡谷和布依村寨之间切换,适合摄影、亲友小团和慢旅行。",
"destinationName": "万峰林"
},
@@ -115,7 +115,7 @@
"极限山野户外野咖",
"漂流"
],
"image": "/assets/guizhou/shuichunhe-rafting.jpg",
"image": "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
"summary": "适合夏季出行,围绕河谷、森林、漂流和轻徒步设计每日节奏。",
"destinationName": "荔波小七孔"
},
@@ -127,7 +127,7 @@
"极限山野户外野咖",
"山野咖啡"
],
"image": "/assets/guizhou/wanfenglin.jpg",
"image": "https://wanfenglin-guanwang.oss-cn-chengdu.aliyuncs.com/uploads/20240718/23ea4291a9570b94a1f8df9a012c18ec.jpg",
"summary": "在峰林公路、峡谷观景和山野咖啡之间安排松弛感十足的小团体验。",
"destinationName": "万峰林"
},
@@ -163,7 +163,7 @@
"极限山野户外野咖",
"峡谷徒步"
],
"image": "/assets/guizhou/huangguoshu-waterfall.jpg",
"image": "https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg",
"summary": "把黄果树瀑布、坝陵河桥梁景观和峡谷轻徒步做成短假小包团。",
"destinationName": "黄果树"
},

View File

@@ -176,7 +176,7 @@ const campaignDisplayDefaults = [
},
{
campaignType: "山野野咖特惠",
image: "/assets/guizhou/shuichunhe-rafting.jpg",
image: "https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
description: "溶洞、漂流、峰林骑行、山野咖啡,把轻户外和松弛感放进同一程。",
priceText: "¥1.16万起/人",
tags: ["山野野咖", "轻户外"],
@@ -184,7 +184,7 @@ const campaignDisplayDefaults = [
},
{
campaignType: "综合混搭特惠",
image: "/assets/guizhou/xijiang-miao-village.jpg",
image: "https://p6.itc.cn/q_70/images03/20200918/df728d2b79d943da869333e2ea2c92c8.jpeg",
description: "非遗村寨、自然轻探险和精品住宿同程安排,适合想兼顾打卡与松弛的同行人。",
priceText: "¥1.39万起/人",
tags: ["人文混搭", "精品住宿"],
@@ -216,7 +216,7 @@ export function normalizeApiCampaign(campaign: PublicCampaign): CampaignCard {
}
const fallbackDestinationHero: DestinationHero = {
image: "/assets/guizhou/huangguoshu-waterfall.jpg",
image: "https://news.cgtn.com/news/2023-07-02/Picturesque-landscapes-draw-tourists-to-Guizhou--1l6RJMNVStq/img/f0d32df3426e4b70bf7a8b00a3404ef9/f0d32df3426e4b70bf7a8b00a3404ef9.jpeg",
kicker: "贵州小包团目的地",
title: "山水、苗寨、古城与野咖同程安排",
};

View File

@@ -20,7 +20,7 @@
}
},
"mp-weixin": {
"appid": "wx5e79df5996572539",
"appid": "wx72c9792d304525b7",
"setting": {
"urlCheck": false,
"es6": true,

View File

@@ -10,15 +10,13 @@
{
"path": "pages/play/index",
"style": {
"navigationBarTitleText": "玩法",
"navigationStyle": "custom"
"navigationBarTitleText": "玩法"
}
},
{
"path": "pages/concierge/index",
"style": {
"navigationBarTitleText": "管家",
"navigationStyle": "custom"
"navigationBarTitleText": "管家"
}
},
{
@@ -52,5 +50,37 @@
"rpxCalcMaxDeviceWidth": 430,
"rpxCalcBaseDeviceWidth": 430,
"rpxCalcIncludeWidth": 750
},
"tabBar": {
"color": "#9a9590",
"selectedColor": "#a45a3d",
"backgroundColor": "#fffdf9",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/home/index",
"iconPath": "static/tabbar/house-normal.png",
"selectedIconPath": "static/tabbar/house-active.png",
"text": "首页"
},
{
"pagePath": "pages/play/index",
"iconPath": "static/tabbar/list-normal.png",
"selectedIconPath": "static/tabbar/list-active.png",
"text": "玩法"
},
{
"pagePath": "pages/concierge/index",
"iconPath": "static/tabbar/headphones-normal.png",
"selectedIconPath": "static/tabbar/headphones-active.png",
"text": "管家"
},
{
"pagePath": "pages/mine/index",
"iconPath": "static/tabbar/user-round-normal.png",
"selectedIconPath": "static/tabbar/user-round-active.png",
"text": "我的"
}
]
}
}

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: "擅长领域:洞穴探险 / 山地徒步" },

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";

View File

@@ -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>

View File

@@ -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: "地心探险",
},
];

View File

@@ -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: "贵州团建",
},
];

View File

@@ -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: "峡谷溯溪",
},
];

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";

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";

View File

@@ -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>

View File

@@ -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>

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>

View File

@@ -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'"

View File

@@ -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>

View File

@@ -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,
});

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]!;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB