refactor: 重构页面框架,配置全局底部导航栏并优化资源
移除废弃的AppShell、BottomNav、PlayTopBar组件,统一所有页面的根布局结构。在pages.json中配置全局小程序底部导航栏,新增配套的tabbar图标资源。将本地静态图片替换为远程CDN资源,重构资源加载工具函数。更新小程序后台appid,调整全局样式与部分页面的布局样式。拆分我的页面为未登录访客态和已登录会员态,新增对应业务组件。重构登录页、首页、玩法页等多个页面的代码,移除对旧组件的依赖。
@@ -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 {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<view class="wq-page">
|
||||
<view class="wq-phone">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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": "黄果树"
|
||||
},
|
||||
|
||||
@@ -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: "山水、苗寨、古城与野咖同程安排",
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
},
|
||||
"mp-weixin": {
|
||||
"appid": "wx5e79df5996572539",
|
||||
"appid": "wx72c9792d304525b7",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
|
||||
@@ -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": "我的"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
49
WonderQ-MiniAPP/src/pages/mine/components/MineGuest.vue
Normal 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>
|
||||
108
WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue
Normal 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>
|
||||
@@ -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]!;
|
||||
|
||||
BIN
WonderQ-MiniAPP/src/static/tabbar/headphones-active.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
WonderQ-MiniAPP/src/static/tabbar/headphones-normal.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
WonderQ-MiniAPP/src/static/tabbar/house-active.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
WonderQ-MiniAPP/src/static/tabbar/house-normal.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
WonderQ-MiniAPP/src/static/tabbar/list-active.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
WonderQ-MiniAPP/src/static/tabbar/list-normal.png
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
WonderQ-MiniAPP/src/static/tabbar/user-round-active.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
WonderQ-MiniAPP/src/static/tabbar/user-round-normal.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |