feat(mini-app): 清理冗余组件,新增详情页分享与管家交互功能

- 移除 ProductList、ProductCard、SectionHeading、InlineLeadForm、SearchFilterBar 等无用业务组件
- 重构线路详情页操作栏,替换电话按钮逻辑,新增原生分享按钮与页面分享配置
- 新增 ConciergeContactSheet 弹窗组件,实现专属管家咨询交互
- 调整 PlayCategorySidebar 选中态样式,优化侧边栏交互逻辑
- 统一组件导入路径为绝对路径,修复路径错误问题
- 移除详情页自定义导航栏配置,优化页面显示效果
- 优化 DetailHero 组件模板排版,提升代码可读性
This commit is contained in:
duanshuwen committed 2026-08-14 22:38:34 +08:00
1 parent d5a4f7549b
commit 1d44266595
12 files changed
+82 -552

No files matched your search

@@ -1,96 +0,0 @@
<template>
<uni-popup
ref="popupRef"
type="bottom"
background-color="transparent"
mask-background-color="rgba(0,0,0,0.45)"
:is-mask-click="true"
:safe-area="false"
@change="onPopupChange"
>
<view
v-if="advisor"
class="mx-auto max-h-[88vh] w-full max-w-[430px] overflow-y-auto rounded-t-[24px] bg-white px-5 pb-[calc(20px+env(safe-area-inset-bottom))] pt-3"
>
<view class="mx-auto h-1 w-10 rounded-full bg-[#d9d3cd]" />
<view class="mt-5 flex items-center justify-between">
<view>
<text class="block text-[11px] font-semibold tracking-[0.28em] text-[#ee5b2a]">CONTACT ADVISOR</text>
<text class="mt-2 block text-[22px] font-semibold text-[#171615]">添加专属管家</text>
</view>
<view
class="tap-feedback flex h-10 w-10 items-center justify-center text-[#4d4945]"
aria-label="关闭"
@click="$emit('close')"
>
<uni-icons type="closeempty" :size="22" color="#4d4945" />
</view>
</view>
<view class="mt-5 flex items-center gap-3 rounded-[12px] bg-[#f7f4f0] p-3">
<image class="h-12 w-12 shrink-0 rounded-full bg-[#b9a99b] object-cover" :src="advisor.avatar" mode="aspectFill" />
<view class="min-w-0">
<text class="block text-[15px] font-semibold text-[#171615]">{{ advisor.name }}</text>
<text class="mt-1 block text-[10px] font-semibold tracking-[0.12em] text-[#ee5b2a]">{{ advisor.role }}</text>
</view>
</view>
<view class="mt-5 flex flex-col items-center rounded-[16px] border border-[#ece7e1] bg-[#fffdfb] px-5 py-5">
<image
class="h-[220px] w-[220px]"
:src="advisor.qrImage"
mode="aspectFit"
show-menu-by-longpress
alt="管家微信二维码"
/>
<text class="mt-4 text-center text-[13px] font-semibold text-[#252321]">长按二维码识别,添加管家好友</text>
<text class="mt-2 text-center text-[11px] leading-5 text-[#77716b]">添加后可沟通路线、预算、酒店和用车安排</text>
</view>
<view class="mt-4 grid grid-cols-2 gap-2">
<view v-for="detail in advisor.details" :key="detail.label" class="flex items-center gap-2 rounded-[10px] bg-[#f7f4f0] px-3 py-3">
<uni-icons :type="detail.icon" :size="15" color="#4d4945" />
<text class="text-[11px] text-[#4d4945]">{{ detail.label }}</text>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from "vue";
import type { ConciergeAdvisor } from "./conciergeTypes";
type UniPopupExpose = {
open: (direction?: string) => void;
close: () => void;
};
const props = defineProps<{
open: boolean;
advisor: ConciergeAdvisor | null;
}>();
const emit = defineEmits<{
close: [];
opened: [];
}>();
const popupRef = ref<UniPopupExpose | null>(null);
watch(
() => props.open,
async (open) => {
await nextTick();
if (open) popupRef.value?.open("bottom");
else popupRef.value?.close();
},
{ immediate: true },
);
function onPopupChange(event: { show: boolean }) {
if (event.show) emit("opened");
else emit("close");
}
</script>
@@ -23,7 +23,7 @@ import { onHide, onShow, onUnload } from "@dcloudio/uni-app";
import ConciergeHero from "./components/ConciergeHero.vue";
import ConciergePrinciples from "./components/ConciergePrinciples.vue";
import ConciergeAdvisor from "./components/ConciergeAdvisor.vue";
import ConciergeContactSheet from "./components/ConciergeContactSheet.vue";
import ConciergeContactSheet from "@/components/ConciergeContactSheet.vue";
import type { ConciergeAdvisor as ConciergeAdvisorData } from "./components/conciergeTypes";
const principles = [
@@ -3,7 +3,7 @@
class="flex w-full shrink-0 items-center gap-2 border-t border-[#e8e2da] bg-white/95 px-3 pb-[calc(10px+env(safe-area-inset-bottom))] pt-2">
<view class="flex w-full shrink-0 items-center gap-1">
<view class="tap-feedback m-0 flex h-12 w-12 flex-col items-center justify-center p-0 text-[#81776f]"
@click="$emit('home')">
@click="$emit('call')">
<uni-icons type="phone" :size="18" color="#81776f" />
<text class="mt-0.5 text-[9px]">电话</text>
</view>
@@ -14,6 +14,18 @@
<text class="mt-0.5 text-[9px]">管家</text>
</view>
<view class="tap-feedback relative m-0 flex h-12 w-12 flex-col items-center justify-center p-0 text-[#81776f]">
<uni-icons type="redo" :size="18" color="#81776f" />
<text class="mt-0.5 text-[9px]">分享</text>
<button
open-type="share"
plain
hover-class="none"
aria-label="分享"
class="absolute inset-0 z-10 m-0 h-full w-full border-0 bg-transparent p-0 opacity-0"
/>
</view>
<view class="ml-auto flex min-w-0 items-center gap-2">
<view class="min-w-0">
<text class="block text-[10px] text-[#9b9188]">参考起价</text>
@@ -37,8 +49,7 @@ defineProps<{
}>();
defineEmits<{
home: [];
toggleFavorite: [];
call: [];
concierge: [];
booking: [];
}>();
@@ -4,12 +4,13 @@
<view class="absolute bottom-10 left-4 right-4 z-10 text-white">
<view class="flex items-center gap-2">
<text class="flex items-center justify-center rounded-[20px] bg-[#e96635] px-2 py-1 text-[10px] font-semibold">
{{ presentation.eyebrow }}
</text>
<text
class="flex items-center justify-center rounded-[20px] bg-[#e96635] px-2 py-1 text-[10px] font-semibold">{{
presentation.eyebrow }}</text>
<text
class="flex items-center justify-center rounded-[20px] text-[10px] bg-[#333333] text-white px-2 py-1 font-semibold">{{
presentation.duration }}</text>
class="flex items-center justify-center rounded-[20px] text-[10px] bg-[#333333] text-white px-2 py-1 font-semibold">
{{ presentation.duration }}
</text>
</view>
</view>
</view>
+35 -8
View File
@@ -18,22 +18,26 @@
<DetailMediaGallery :images="presentation.gallery" />
</scroll-view>
<DetailActionBar :price="1.68" :favorite="favorite" @home="goHome" @toggle-favorite="toggleFavorite"
@concierge="goConcierge" @booking="goBooking(product)" />
<DetailActionBar :price="1.68" :favorite="favorite" @call="goCall" @concierge="goConcierge"
@booking="goBooking(product)" />
<ConciergeContactSheet :open="Boolean(selectedAdvisor)" :advisor="selectedAdvisor" @close="handleConciergeClosed" />
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { onLoad, onShareAppMessage, onShow } from "@dcloudio/uni-app";
import DetailActionBar from "./components/DetailActionBar.vue";
import DetailHero from "./components/DetailHero.vue";
import DetailInfoCard from "./components/DetailInfoCard.vue";
import DetailMediaGallery from "./components/DetailMediaGallery.vue";
import DetailOverviewCard from "./components/DetailOverviewCard.vue";
import ConciergeContactSheet from "@/components/ConciergeContactSheet.vue";
import type { ConciergeAdvisor } from "../concierge/components/conciergeTypes";
import { createDetailPresentation } from "./components/detailPresentation";
import { getDetailMeta, getProductDetailSections } from "@/lib/data";
import { goBack, goBooking, goRoot } from "@/lib/navigation";
import { getDetailMeta, getProductDetailSections, productKey } from "@/lib/data";
import { goBack, goBooking } from "@/lib/navigation";
import { findProduct, isProductFavorite, loadAppData, rememberProduct, toggleFavoriteProduct } from "@/lib/store";
import type { Product } from "@/lib/types";
@@ -48,6 +52,12 @@ onShow(() => {
void loadAppData();
});
onShareAppMessage(() => ({
title: presentation.value.title,
path: `/pages/detail/index?product=${encodeURIComponent(productKey(product.value))}`,
imageUrl: product.value.image,
}));
const meta = computed(() => getDetailMeta(product.value));
const presentation = computed(() => createDetailPresentation(product.value));
const detailSections = computed(() => getProductDetailSections(product.value, meta.value));
@@ -74,11 +84,28 @@ function toggleFavorite() {
function goHome() {
goRoot("/pages/home/index");
function goCall() {
uni.makePhoneCall({ phoneNumber: '13800000000' });
}
function goConcierge() {
goRoot("/pages/concierge/index");
selectedAdvisor.value = conciergeAdvisor;
}
function handleConciergeClosed() {
selectedAdvisor.value = null;
}
const conciergeAdvisor: ConciergeAdvisor = {
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: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
details: [
{ icon: "calendar", label: "服务经验:8年" },
{ icon: "navigate", label: "擅长领域:瀑降 / 探洞" },
],
};
const selectedAdvisor = ref<ConciergeAdvisor | null>(null);
</script>
@@ -1,23 +1,30 @@
<template>
<view class="no-scrollbar h-full min-h-0 w-[104px] shrink-0 border-r border-[#f0f0f0] bg-[#f5f5f5]">
<view v-for="category in categories" :key="category.id"
class="flex h-[62px] w-full items-center justify-center rounded-none border-0 px-2 text-center text-[13px] leading-5 text-[#171717] after:border-0"
:class="activeId === category.id ? 'border-r-2 border-[#f26424] bg-white font-semibold' : 'border-r-2 border-transparent font-medium'"
<view class="no-scrollbar h-full min-h-0 w-[104px] shrink-0 bg-[#f5f5f5]">
<view v-for="(category, index) in categories" :key="category.id"
class="relative flex h-[62px] w-full items-center justify-center rounded-none border-0 px-2 text-center text-[13px] leading-5 text-[#171717]"
:class="activeId === category.id ? 'bg-white font-semibold' : 'font-medium'"
:aria-current="activeId === category.id ? 'page' : undefined" :aria-label="category.label"
@click="$emit('select', category.id)">
<view v-if="index !== 0 && activeId === category.id"
class="absolute right-0 top-[-12px] h-3 w-3 before:block before:h-3 before:w-3 before:rounded-br-[12px] before:content-[''] before:bg-[#f5f5f5] bg-white" />
<text>{{ category.label }}</text>
<view v-if="index !== categories.length - 1 && activeId === category.id"
class="absolute right-0 bottom-[-12px] h-3 w-3 before:block before:h-3 before:w-3 before:rounded-tr-[12px] before:content-[''] before:bg-[#f5f5f5] bg-white" />
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { PlayCategory } from "./playData";
defineProps<{
const props = defineProps<{
categories: PlayCategory[];
activeId: string;
}>();
const activeIndex = computed(() => props.categories.findIndex((category) => category.id === props.activeId));
defineEmits<{
select: [id: string];
}>();