refactor(home-api): 统一首页API响应格式并迁移配套代码

完成首页数据模型的统一重构,具体变更如下:
1.  重构公共首页API接口,移除`playRecommendations`字段,将玩法推荐数据统一放入`experiences`字段
2.  更新前后端类型定义、序列化逻辑与前端页面组件,适配新的API响应结构
3.  为微信登录相关接口添加`trust_env=True`配置,支持企业代理环境并新增`socksio`依赖
4.  新增图片画廊上传组件,重构SingleImageUploader组件支持自定义宽高比
5.  重构Toast与Select组件的实现与样式,统一后台UI设计系统
6.  移除个人中心页面不必要的返回事件与顶部标题组件,优化详情卡片布局
7.  新增微信接口单元测试,更新官方文档与测试用例适配变更
8.  删除过期的文档图片资源
This commit is contained in:
duanshuwen committed 2026-08-20 21:18:27 +08:00
1 parent d3a246a873
commit 6296392e80
27 files changed
+1055 -218

No files matched your search

+3 -7
View File
@@ -1,7 +1,6 @@
import { reactive } from "vue";
import { createPublicLead, fetchPublicContent, fetchPublicHome } from "./api";
import { fallbackData, getSubmitErrorMessage, normalizeSiteConfig } from "./data";
import { homeExperienceFallback, normalizeHomeExperiences } from "@/pages/home/components/homeExperienceData";
import { homeTeamBuildingFallback, normalizeHomeTeamBuildings } from "@/pages/home/components/homeTeamBuildingData";
import { homeWildArchiveFallback, normalizeHomeWildArchives } from "@/pages/home/components/homeWildArchivesData";
import { normalizeHomeWanfaRecommendations } from "@/pages/home/components/homeWanfaRecommendationData";
@@ -16,10 +15,9 @@ export const appState = reactive({
});
export const homeContentState = reactive({
experiences: homeExperienceFallback.map((item) => ({ ...item })),
experiences: [] as HomeWanfaRecommendation[],
teamBuildings: homeTeamBuildingFallback.map((item) => ({ ...item })),
wildArchives: homeWildArchiveFallback.map((item) => ({ ...item })),
playRecommendations: [] as HomeWanfaRecommendation[],
loading: false,
loaded: false,
loadError: "",
@@ -48,16 +46,14 @@ export async function loadHomeContent() {
homeContentState.loadError = "";
try {
const home = await fetchPublicHome();
homeContentState.experiences = normalizeHomeExperiences(home.experiences);
homeContentState.experiences = normalizeHomeWanfaRecommendations(home.experiences);
homeContentState.teamBuildings = normalizeHomeTeamBuildings(home.teamBuildings);
homeContentState.wildArchives = normalizeHomeWildArchives(home.wildArchives);
homeContentState.playRecommendations = normalizeHomeWanfaRecommendations(home.playRecommendations);
homeContentState.loaded = true;
} catch (error) {
homeContentState.experiences = homeExperienceFallback.map((item) => ({ ...item }));
homeContentState.experiences = [];
homeContentState.teamBuildings = homeTeamBuildingFallback.map((item) => ({ ...item }));
homeContentState.wildArchives = homeWildArchiveFallback.map((item) => ({ ...item }));
homeContentState.playRecommendations = [];
homeContentState.loadError = getSubmitErrorMessage(error);
homeContentState.loaded = true;
} finally {
+1 -2
View File
@@ -95,10 +95,9 @@ export type PublicHomeWanfaRecommendation = {
};
export type PublicHomeResponse = {
experiences: PublicHomeExperience[];
experiences: PublicHomeWanfaRecommendation[];
teamBuildings: PublicHomeTeamBuilding[];
wildArchives: PublicHomeWildArchive[];
playRecommendations: PublicHomeWanfaRecommendation[];
};
export type PublicHomeWildArchivesResponse = {
@@ -1,14 +1,6 @@
<template>
<view class="relative z-10 -mt-5 mx-3 rounded-[14px] bg-white px-4 py-5 shadow-[0_8px_24px_rgba(63,48,37,0.08)]">
<view class="flex flex-wrap items-center gap-2">
<text class="rounded-full bg-[#eaf6f1] px-2.5 py-1 text-[10px] font-semibold text-[#16806d]">
{{ presentation.eyebrow }}
</text>
<text class="rounded-full bg-[#f3f0eb] px-2.5 py-1 text-[10px] font-medium text-[#746c64]">
{{ presentation.duration }}
</text>
</view>
<text class="mt-3 block text-[24px] font-bold leading-8 text-[#24211f]">{{ presentation.title }}</text>
<text class="block text-[24px] font-bold leading-8 text-[#24211f]">{{ presentation.title }}</text>
<text class="mt-2 block text-[13px] leading-6 text-[#756d65]">{{ presentation.subtitle }}</text>
<view class="mt-4 grid grid-cols-2 gap-2 border-t border-[#f0ebe5] pt-3">
<view class="rounded-[8px] bg-[#f8f6f2] px-3 py-2.5">
@@ -1,30 +1,27 @@
<template>
<view v-if="items.length" class="bg-[#fffdf9] px-[8.5%] pb-8 pt-7">
<view class="mb-[14px] flex items-end justify-between gap-4">
<view>
<text class="block font-serif text-[21px] font-semibold leading-7 tracking-[0.04em] text-[#18212a]">玩法推荐</text>
<text class="mt-1 block text-[12px] leading-[18px] text-[#7a7d78]">按主题挑选适合你的旅行方式</text>
</view>
<text class="shrink-0 font-sans text-[9px] leading-4 tracking-[0.18em] text-[#a3988e]">WAYS TO GO</text>
</view>
<view class="grid grid-cols-2 gap-3">
<view
v-for="item in items"
:key="item.id"
class="min-w-0 overflow-hidden rounded-[12px] border border-[#e8e1d7] bg-white tap-feedback active:border-[#b97b5f] active:bg-[#fffaf4]"
:aria-label="`${item.label},${item.routes.length} 条路线`"
@click="$emit('select', item)"
>
<image class="block h-[112px] w-full bg-[#ebe8e2] min-[431px]:h-[126px]" :src="item.routes[0].image" mode="aspectFill" />
<view class="p-[11px_10px_13px]">
<view class="flex min-h-[22px] items-center justify-between gap-1.5">
<text class="min-w-0 flex-1 font-serif text-[15px] font-semibold leading-[22px] text-[#1c252d]">{{ item.label }}</text>
<uni-icons type="right" :size="16" color="#b5aaa0" />
<view v-if="items.length" class="bg-[#fffdf9] px-[8.5%] pb-[26px]">
<view
v-for="item in items"
:key="item.id"
class="w-full bg-[#fffdf9] pt-[18px] tap-feedback min-[431px]:pt-[22px]"
:aria-label="`${item.label},${item.routes.length} 条路线`"
@click="$emit('select', item)"
>
<view class="flex items-start justify-between gap-4 pr-[18px] min-[431px]:pr-[22px]">
<view class="min-w-0 flex-1">
<view class="flex min-h-5 items-center gap-[10px]">
<text class="inline-flex min-h-5 items-center rounded-[12px] border border-[#eadfce] bg-[#f8f1e8] px-[6px] text-[10px] leading-[18px] tracking-[0.06em] text-[#98744b]">玩法推荐</text>
<text class="min-w-0 truncate text-[13px] font-semibold leading-5 text-[#283b4d]">{{ item.label }} · {{ item.routes.length }} 条路线</text>
</view>
<text class="mt-1.5 block text-[11px] leading-[17px] text-[#7a7d78]">{{ item.routes.length }} 条路线可选</text>
<text class="mt-[10px] block whitespace-pre-line font-serif text-[25px] font-medium leading-8 tracking-[0.04em] text-[#18212a]">{{ item.routes[0].title }}</text>
<text class="mt-0.5 block font-sans text-[11px] font-medium leading-[18px] tracking-[0.08em] text-[#a7a0a0]">{{ item.routes[0].subtitle }}</text>
</view>
<view class="mt-8 flex h-10 w-6 shrink-0 items-center justify-center" aria-hidden="true">
<uni-icons type="right" :size="24" color="#c8c8c8" />
</view>
</view>
<image class="mt-[17px] block h-[174px] w-full rounded-[12px] bg-[#eae7e1] min-[431px]:h-[190px]" :src="item.routes[0].image" mode="aspectFill" />
</view>
</view>
</template>
+8 -5
View File
@@ -3,7 +3,7 @@
<view class="wq-phone">
<scroll-view scroll-y class="h-screen no-scrollbar" :show-scrollbar="false">
<HomeHeroCarousel :slides="appState.data.heroSlides" />
<HomeExperienceList :items="homeContentState.experiences" @select="openExperience" />
<HomeWanfaRecommendations :items="homeContentState.experiences" @select="openWanfaRecommendation" />
<HomeVehicleGrid :vehicle-options="vehicleOptions" @select="openVehicle" />
<HomeTeamBuilding :items="homeContentState.teamBuildings" @select="openTeamBuilding" />
<HomeWildArchives :items="homeContentState.wildArchives" @select="openWildArchive" @more="openWildArchives" />
@@ -16,13 +16,13 @@
import { computed } from "vue";
import { onShow } from "@dcloudio/uni-app";
import HomeHeroCarousel from "./components/HomeHeroCarousel.vue";
import HomeExperienceList from "./components/HomeExperienceList.vue";
import HomeWanfaRecommendations from "./components/HomeWanfaRecommendations.vue";
import HomeTeamBuilding from "./components/HomeTeamBuilding.vue";
import HomeWildArchives from "./components/HomeWildArchives.vue";
import HomeVehicleGrid from "./components/HomeVehicleGrid.vue";
import type { HomeExperience } from "./components/homeExperienceData";
import type { HomeTeamBuilding as HomeTeamBuildingItem } from "./components/homeTeamBuildingData";
import type { HomeWildArchive } from "./components/homeWildArchivesData";
import type { HomeWanfaRecommendation } from "./components/homeWanfaRecommendationData";
import {
goTeamBuildingDetail,
goWanfaRouteDetail,
@@ -38,8 +38,11 @@ onShow(() => {
void loadHomeContent();
});
function openExperience(item: HomeExperience) {
goWanfaRouteDetail(item.id);
function openWanfaRecommendation(item: HomeWanfaRecommendation) {
const route = item.routes[0];
if (route) {
goWanfaRouteDetail(route.id);
}
}
function openVehicle(item: any) {
@@ -1,6 +1,5 @@
<template>
<view class="min-h-screen bg-transparent">
<TopHeader title="我的" subtitle="需求与服务管家" @back="$emit('backHome')" />
<view class="px-4 pt-4">
<view class="brand-panel rounded-[26px] p-5 text-white">
<text class="block text-[12px] font-semibold uppercase tracking-[0.12em] text-[#f6dfb7]">万趣旅行</text>
@@ -8,21 +7,9 @@
<text class="mt-2 block text-[12px] leading-5 text-white/80">{{ customerLine }}</text>
</view>
</view>
<view class="px-4 py-4">
<view class="elevated-card rounded-[22px] p-5">
<text class="block text-[17px] font-bold text-[#1f2c25]">已登录</text>
<text class="mt-2 block text-[13px] leading-6 text-[#718073]">你的手机号已完成绑定,后续可通过服务管家沟通出行日期、人数与偏好。</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import TopHeader from "@/components/TopHeader.vue";
defineProps<{ customerLine: string }>();
defineEmits<{
backHome: [];
}>();
</script>
+1 -2
View File
@@ -3,7 +3,7 @@
<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" @back-home="goRoot('/pages/home/index')" />
<MineMember :customer-line="customerLine" />
</scroll-view>
</view>
</view>
@@ -13,7 +13,6 @@
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getAuthSession, loginWithPhoneCode } from "@/lib/auth";
import { goRoot } from "@/lib/navigation";
import { loadAppData } from "@/lib/store";
import type { AuthSession } from "@/lib/types";
import MineGuest from "./components/MineGuest.vue";