chore: import WonderQ project

This commit is contained in:
duanshuwen committed 2026-08-11 19:19:11 +08:00
commit f5a9e4403a
452 files changed
+34173

No files matched your search

+129
View File
@@ -0,0 +1,129 @@
<template>
<AppShell>
<view class="safe-bottom 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>
<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="supportOpen = true">管家</button>
</view>
</view>
<view class="absolute bottom-8 left-0 right-0 px-5">
<text class="block text-[12px] font-semibold opacity-80">{{ meta.location }}</text>
<text class="mt-2 block text-[28px] font-bold leading-9">{{ primaryTitle }}</text>
<text class="mt-3 text-clamp-2 text-[13px] leading-5 opacity-85">{{ meta.summary }}</text>
</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">
<scroll-view scroll-x class="no-scrollbar whitespace-nowrap">
<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"
>
{{ section.label }}
</button>
</scroll-view>
</view>
<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>
</view>
<view class="mt-4 flex items-end justify-between">
<text class="text-[12px] text-[#718073]">参考起价</text>
<text class="text-[24px] font-bold text-[#c36f21]">¥{{ formatWan(product.price) }}万/人起</text>
</view>
</view>
</view>
<view class="px-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>
<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>
</view>
</template>
</view>
</view>
</view>
<view class="py-4">
<SectionHeading title="你可能还想看" subtitle="按当前线路为你推荐" />
<ProductList :products="recommended" @select="openProduct" />
</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 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="secondary-action tap-feedback m-0 flex-1 text-[14px] font-semibold" @click="supportOpen = true">服务管家</button>
<button class="primary-action tap-feedback m-0 flex-[1.4] text-[15px] font-bold" @click="goBooking(product)">预订咨询</button>
</view>
</view>
<SupportSheet :open="supportOpen" @close="supportOpen = false" />
</AppShell>
</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 SupportSheet from "@/components/SupportSheet.vue";
import { formatWan, getDetailMeta, getProductDetailSections, productKey, stripTitle } from "@/lib/data";
import { goBack, goBooking, goDetail } from "@/lib/navigation";
import { appState, findProduct, isProductFavorite, loadAppData, rememberProduct, toggleFavoriteProduct } from "@/lib/store";
import type { Product } from "@/lib/types";
const product = ref<Product>(findProduct());
const activeTab = ref("overview");
const supportOpen = ref(false);
onLoad((query) => {
product.value = findProduct(typeof query?.product === "string" ? decodeURIComponent(query.product) : undefined);
rememberProduct(product.value);
});
onShow(() => {
void loadAppData();
});
const meta = computed(() => getDetailMeta(product.value));
const primaryTitle = computed(() => stripTitle(product.value.title));
const detailSections = computed(() => getProductDetailSections(product.value, meta.value));
const visibleSections = computed(() => detailSections.value.filter((section) => activeTab.value === section.key || activeTab.value === "overview"));
const favorite = computed(() => isProductFavorite(product.value));
const recommended = computed(() => {
const start = Math.max(0, product.value.id % Math.max(1, appState.data.products.length - 4));
return appState.data.products.filter((item) => productKey(item) !== productKey(product.value)).slice(start, start + 4);
});
function toggleFavorite() {
const wasFavorite = favorite.value;
toggleFavoriteProduct(product.value);
uni.showToast({ title: wasFavorite ? "已取消收藏" : "已收藏", icon: "none" });
}
function openProduct(next: Product) {
product.value = next;
activeTab.value = "overview";
rememberProduct(next);
goDetail(next);
}
</script>