Compare commits
3 Commits
4e04feda5b
...
5210a7385a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5210a7385a | ||
|
|
ed3b9a26f5 | ||
|
|
967c5a4987 |
1815
WonderQ-Admin/data/wonderq-production.sql
Normal file
1815
WonderQ-Admin/data/wonderq-production.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,11 @@
|
||||
<template>
|
||||
<view class="relative w-full overflow-hidden bg-[#1f2924]">
|
||||
<swiper
|
||||
class="h-[620px]"
|
||||
circular
|
||||
autoplay
|
||||
:interval="4600"
|
||||
:current="heroIndex"
|
||||
@change="onSlideChange"
|
||||
>
|
||||
<swiper class="h-[620px]" circular autoplay :interval="4600" :current="heroIndex" @change="onSlideChange">
|
||||
<swiper-item v-for="(slide, index) in props.slides" :key="slide.id || slide.image">
|
||||
<view
|
||||
class="relative m-0 h-full w-full overflow-hidden rounded-none p-0 text-left"
|
||||
@click="$emit('select', slide.title)"
|
||||
>
|
||||
<image
|
||||
class="absolute inset-0 h-full w-full"
|
||||
:src="getSlideImage(slide, index)"
|
||||
mode="aspectFill"
|
||||
@error="onImageError(slide, index)"
|
||||
/>
|
||||
<view class="relative m-0 h-full w-full overflow-hidden rounded-none p-0 text-left"
|
||||
@click="$emit('select', slide.title)">
|
||||
<image class="absolute inset-0 h-full w-full" :src="getSlideImage(slide, index)" mode="aspectFill"
|
||||
@error="onImageError(slide, index)" />
|
||||
<view class="hero-scrim absolute inset-0 flex items-end px-6 pb-[68px]">
|
||||
<view class="min-w-0 text-white">
|
||||
<text v-if="slide.kicker" class="block text-[12px] font-medium leading-5 tracking-[0.12em] text-white/80">
|
||||
@@ -27,12 +14,6 @@
|
||||
<text v-if="slide.title" class="mt-1 block text-[25px] font-semibold leading-8 tracking-[0.04em]">
|
||||
{{ slide.title }}
|
||||
</text>
|
||||
<text
|
||||
v-if="slide.actionLabel || slide.action"
|
||||
class="mt-3 inline-flex rounded-[8px] border border-white/55 px-3 py-1 text-[12px] leading-5 text-white/95"
|
||||
>
|
||||
{{ slide.actionLabel || slide.action }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -40,12 +21,8 @@
|
||||
</swiper>
|
||||
|
||||
<view class="absolute bottom-8 left-0 right-0 flex justify-center gap-2">
|
||||
<text
|
||||
v-for="(_, index) in props.slides"
|
||||
:key="index"
|
||||
class="h-1.5 rounded-full transition-all"
|
||||
:class="heroIndex === index ? 'w-6 bg-white' : 'w-1.5 bg-white/50'"
|
||||
/>
|
||||
<text v-for="(_, index) in props.slides" :key="index" class="h-1.5 rounded-full transition-all"
|
||||
:class="heroIndex === index ? 'w-6 bg-white' : 'w-1.5 bg-white/50'" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="px-6 pb-[calc(28px+env(safe-area-inset-bottom))] pt-6">
|
||||
<view class="px-6 pb-[calc(28px+env(safe-area-inset-bottom))] pt-6">
|
||||
<view
|
||||
class="w-full rounded-[22px] border border-[#ded6cd] bg-white/95 px-4 py-4 shadow-[0_12px_30px_rgba(80,58,45,0.08)]">
|
||||
<view class="flex items-center gap-3">
|
||||
@@ -29,7 +28,8 @@
|
||||
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="flex items-center justify-between gap-2">
|
||||
<text class="truncate text-[17px] font-semibold tracking-[0.02em] text-[#24211f]">{{ customerName }}</text>
|
||||
<text class="truncate text-[17px] font-semibold tracking-[0.02em] text-[#24211f]">{{ customerName
|
||||
}}</text>
|
||||
<view class="shrink-0 rounded-full bg-[#edf5ef] px-2 py-1">
|
||||
<text class="text-[10px] font-medium tracking-[0.06em] text-[#3f7b57]">已登录</text>
|
||||
</view>
|
||||
@@ -37,49 +37,10 @@
|
||||
<text class="mt-2 block truncate text-[12px] leading-5 text-[#857d75]">{{ customerLine }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
v-if="showAvatarAction && !avatarUrl"
|
||||
class="m-0 mt-4 flex min-h-[40px] w-full items-center justify-center rounded-full border border-[#d9c7bb] bg-[#fffaf5] px-4 text-[12px] font-medium tracking-[0.04em] text-[#8c4f39]"
|
||||
@click="emit('request-avatar')">
|
||||
<uni-icons type="person" :size="16" color="#8c4f39" />
|
||||
<text class="ml-1">获取微信头像</text>
|
||||
</button>
|
||||
<text v-else-if="avatarUrl" class="mt-3 block text-[11px] leading-5 text-[#9a9189]">头像仅保存在当前设备</text>
|
||||
<text v-else class="mt-3 block text-[11px] leading-5 text-[#9a9189]">微信头像可在授权后显示</text>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="m-0 mt-5 flex min-h-[58px] w-full items-center gap-3 rounded-[16px] bg-[#a45a3d] px-4 text-left text-white shadow-[0_8px_20px_rgba(164,90,61,0.16)]"
|
||||
@click="emit('open-concierge')"
|
||||
>
|
||||
<view class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/15">
|
||||
<uni-icons type="headphones" :size="18" color="#fffaf5" />
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="block text-[14px] font-semibold">联系管家</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-white/75">获取适合你的出行建议</text>
|
||||
</view>
|
||||
<uni-icons type="right" :size="16" color="#fffaf5" />
|
||||
</button>
|
||||
|
||||
<MineRecentViews
|
||||
:items="props.recentViews"
|
||||
@select="(item) => emit('select-recent', item)"
|
||||
@explore="emit('open-play')"
|
||||
/>
|
||||
|
||||
<view class="mt-6 rounded-[16px] border border-[#e2d9d0] bg-white px-4 py-4 shadow-[0_8px_20px_rgba(80,58,45,0.04)]">
|
||||
<text class="block text-[14px] font-semibold text-[#2b2724]">账号操作</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">退出登录会清除本机最近浏览记录</text>
|
||||
<button
|
||||
class="m-0 mt-4 flex min-h-[42px] w-full items-center justify-center rounded-full border border-[#d9c7bb] bg-[#fffaf5] px-4 text-[12px] font-medium tracking-[0.04em] text-[#8c4f39]"
|
||||
@click="emit('logout')"
|
||||
>
|
||||
<uni-icons type="forward" :size="16" color="#8c4f39" />
|
||||
<text class="ml-1">退出登录</text>
|
||||
</button>
|
||||
</view>
|
||||
<MineRecentViews :items="props.recentViews" @open-vehicle="emit('open-vehicle')"
|
||||
@select="(item) => emit('select-recent', item)" @explore="emit('open-play')" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -106,11 +67,9 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"request-avatar": [];
|
||||
"open-concierge": [];
|
||||
"open-play": [];
|
||||
"open-vehicle": [];
|
||||
"select-recent": [item: RecentlyViewedItem];
|
||||
logout: [];
|
||||
}>();
|
||||
|
||||
const menuButtonTop = ref<number | null>(null);
|
||||
|
||||
@@ -2,46 +2,62 @@
|
||||
<view class="mt-6">
|
||||
<view class="mb-3 flex items-end justify-between">
|
||||
<view>
|
||||
<text class="block text-[18px] font-semibold tracking-[0.02em] text-[#24211f]">继续探索</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">最近浏览的内容</text>
|
||||
<text class="block text-[18px] font-semibold tracking-[0.02em] text-[#24211f]">我的出行记录</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">用车需求与最近浏览集中在这里</text>
|
||||
</view>
|
||||
<text v-if="items.length" class="text-[11px] text-[#b18a76]">{{ items.length }} 条记录</text>
|
||||
<text class="text-[11px] text-[#b18a76]">{{ items.length + 1 }} 项</text>
|
||||
</view>
|
||||
|
||||
<view v-if="items.length" class="overflow-hidden rounded-[16px] border border-[#e2d9d0] bg-white shadow-[0_8px_20px_rgba(80,58,45,0.05)]">
|
||||
<view class="overflow-hidden rounded-[16px] border border-[#e2d9d0] bg-white shadow-[0_8px_20px_rgba(80,58,45,0.05)]">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="`${item.type}-${item.id}`"
|
||||
class="m-0 flex w-full items-center gap-3 px-3 py-3 text-left"
|
||||
:class="index < items.length - 1 ? 'border-b border-[#eee7e0]' : ''"
|
||||
@click="emit('select', item)"
|
||||
class="m-0 flex w-full items-center gap-3 border-b border-[#eee7e0] px-3 py-3 text-left"
|
||||
@click="emit('open-vehicle')"
|
||||
>
|
||||
<view class="h-14 w-16 shrink-0 overflow-hidden rounded-[10px] bg-[#f1ece6]">
|
||||
<image v-if="item.image" class="h-full w-full" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="flex h-full w-full items-center justify-center">
|
||||
<uni-icons type="image" :size="20" color="#b8aaa0" />
|
||||
</view>
|
||||
<view class="flex h-14 w-16 shrink-0 items-center justify-center rounded-[10px] bg-[#f4e5dc]">
|
||||
<uni-icons type="car" :size="24" color="#a45a3d" />
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="block truncate text-[14px] font-medium text-[#2b2724]">{{ item.title }}</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-[#a19890]">{{ typeLabel(item.type) }}</text>
|
||||
<text class="block truncate text-[14px] font-medium text-[#2b2724]">用车需求</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-[#a19890]">填写专属出行方案</text>
|
||||
</view>
|
||||
<uni-icons type="right" :size="15" color="#b3a79e" />
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="rounded-[16px] border border-dashed border-[#d8cec5] bg-white/70 px-5 py-6 text-center">
|
||||
<view class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-[#f4eee8]">
|
||||
<uni-icons type="eye" :size="20" color="#a45a3d" />
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="`${item.type}-${item.id}`"
|
||||
class="m-0 flex w-full items-center gap-3 px-3 py-3 text-left"
|
||||
:class="index < items.length - 1 ? 'border-b border-[#eee7e0]' : ''"
|
||||
@click="emit('select', item)"
|
||||
>
|
||||
<view class="h-14 w-16 shrink-0 overflow-hidden rounded-[10px] bg-[#f1ece6]">
|
||||
<image v-if="item.image" class="h-full w-full" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="flex h-full w-full items-center justify-center">
|
||||
<uni-icons type="image" :size="20" color="#b8aaa0" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="block truncate text-[14px] font-medium text-[#2b2724]">{{ item.title }}</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-[#a19890]">{{ typeLabel(item.type) }}</text>
|
||||
</view>
|
||||
<uni-icons type="right" :size="15" color="#b3a79e" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<view v-else class="border-t border-dashed border-[#d8cec5] bg-white/70 px-5 py-6 text-center">
|
||||
<view class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-[#f4eee8]">
|
||||
<uni-icons type="eye" :size="20" color="#a45a3d" />
|
||||
</view>
|
||||
<text class="mt-3 block text-[14px] font-medium text-[#4a413a]">暂无浏览记录</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">去看看玩法,收藏下一段旅程</text>
|
||||
<button
|
||||
class="m-0 mx-auto mt-4 flex min-h-[36px] items-center justify-center rounded-full bg-[#a45a3d] px-5 text-[12px] font-medium text-white"
|
||||
@click="emit('explore')"
|
||||
>
|
||||
去看看玩法
|
||||
</button>
|
||||
</view>
|
||||
<text class="mt-3 block text-[14px] font-medium text-[#4a413a]">暂无浏览记录</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">去看看玩法,收藏下一段旅程</text>
|
||||
<button
|
||||
class="m-0 mx-auto mt-4 flex min-h-[36px] items-center justify-center rounded-full bg-[#a45a3d] px-5 text-[12px] font-medium text-white"
|
||||
@click="emit('explore')"
|
||||
>
|
||||
去看看玩法
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -54,6 +70,7 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"open-vehicle": [];
|
||||
select: [item: RecentlyViewedItem];
|
||||
explore: [];
|
||||
}>();
|
||||
|
||||
@@ -3,17 +3,9 @@
|
||||
<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"
|
||||
:customer-name="wechatProfile?.nickName || '万趣会员'"
|
||||
:avatar-url="wechatProfile?.avatarUrl"
|
||||
:show-avatar-action="isWechatMiniProgram"
|
||||
:recent-views="recentViews"
|
||||
@request-avatar="loadWechatProfile"
|
||||
@open-concierge="goConcierge"
|
||||
@open-play="goPlay"
|
||||
@select-recent="openRecent"
|
||||
@logout="logout" />
|
||||
<MineMember :customer-line="customerLine" :customer-name="wechatProfile?.nickName || '万趣会员'"
|
||||
:avatar-url="wechatProfile?.avatarUrl" :show-avatar-action="isWechatMiniProgram" :recent-views="recentViews"
|
||||
@open-play="goPlay" @open-vehicle="goVehicleDemand" @select-recent="openRecent" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -22,22 +14,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { clearAuthSession, getAuthSession, loginWithPhoneCode } from "@/lib/auth";
|
||||
import { getAuthSession, loginWithPhoneCode } from "@/lib/auth";
|
||||
import {
|
||||
clearRecentlyViewed,
|
||||
getRecentlyViewed,
|
||||
type RecentlyViewedItem,
|
||||
} from "@/lib/recentlyViewed";
|
||||
import {
|
||||
goConcierge,
|
||||
goPlay,
|
||||
goVehicleDemand,
|
||||
goTeamBuildingDetail,
|
||||
goWanfaRouteDetail,
|
||||
goWildArchiveDetail,
|
||||
} from "@/lib/navigation";
|
||||
import { loadAppData } from "@/lib/store";
|
||||
import {
|
||||
clearWechatProfile,
|
||||
getStoredWechatProfile,
|
||||
requestWechatProfile,
|
||||
saveWechatProfile,
|
||||
@@ -101,27 +91,6 @@ async function goLogin(event: WechatPhoneNumberEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWechatProfile() {
|
||||
if (isLoadingWechatProfile.value) return;
|
||||
|
||||
isLoadingWechatProfile.value = true;
|
||||
uni.showLoading({ title: "获取头像中" });
|
||||
try {
|
||||
const profile = await requestWechatProfile();
|
||||
if (!profile) {
|
||||
uni.showToast({ title: "未获取到头像,请允许授权后重试", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
saveWechatProfile(profile);
|
||||
wechatProfile.value = profile;
|
||||
uni.showToast({ title: "头像已更新", icon: "none" });
|
||||
} finally {
|
||||
isLoadingWechatProfile.value = false;
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function openRecent(item: RecentlyViewedItem) {
|
||||
if (item.type === "wanfa-route") {
|
||||
goWanfaRouteDetail(item.id);
|
||||
@@ -133,15 +102,4 @@ function openRecent(item: RecentlyViewedItem) {
|
||||
}
|
||||
goWildArchiveDetail(item.id);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearAuthSession();
|
||||
clearWechatProfile();
|
||||
clearRecentlyViewed();
|
||||
session.value = null;
|
||||
wechatProfile.value = null;
|
||||
recentViews.value = [];
|
||||
customerLine.value = "登录后管理头像与手机号,继续探索旅程。";
|
||||
uni.showToast({ title: "已退出登录", icon: "none" });
|
||||
}
|
||||
</script>
|
||||
|
||||
20
WonderQ-MiniAPP/tests/mine-records.test.ts
Normal file
20
WonderQ-MiniAPP/tests/mine-records.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import mineMemberSource from "@/pages/mine/components/MineMember.vue?raw";
|
||||
import mineRecentViewsSource from "@/pages/mine/components/MineRecentViews.vue?raw";
|
||||
import minePageSource from "@/pages/mine/index.vue?raw";
|
||||
|
||||
describe("mine travel records integration contract", () => {
|
||||
it("keeps vehicle demand and recent views in one list", () => {
|
||||
expect(mineRecentViewsSource).toContain("我的出行记录");
|
||||
expect(mineRecentViewsSource).toContain("用车需求与最近浏览集中在这里");
|
||||
expect(mineRecentViewsSource).toContain("items.length + 1");
|
||||
expect(mineRecentViewsSource).toContain("open-vehicle");
|
||||
expect(mineRecentViewsSource).toContain("暂无浏览记录");
|
||||
});
|
||||
|
||||
it("wires the vehicle demand action through MineMember to the existing page", () => {
|
||||
expect(mineMemberSource).toContain('@open-vehicle="emit(\'open-vehicle\')"');
|
||||
expect(minePageSource).toContain("goVehicleDemand");
|
||||
expect(minePageSource).toContain('@open-vehicle="goVehicleDemand"');
|
||||
});
|
||||
});
|
||||
291
docs/superpowers/plans/2026-08-27-mine-records-plan.md
Normal file
291
docs/superpowers/plans/2026-08-27-mine-records-plan.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# 我的出行记录合并 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在 MiniAPP「我的」页面中,把固定的“用车需求”入口和本地“最近浏览”列表收敛到一个“我的出行记录”模块。
|
||||
|
||||
**Architecture:** 保留 `RecentlyViewedItem` 和本地存储结构不变,在现有 `MineRecentViews.vue` 内把“用车需求”作为第一条固定操作项,再渲染最近浏览项或空态。通过 `MineMember.vue` 透传一个 `open-vehicle` 事件,由 `mine/index.vue` 调用现有 `goVehicleDemand()`,不新增 API、后端查询或数据库字段。
|
||||
|
||||
**Tech Stack:** Vue 3 `<script setup lang="ts">`、uni-app、TypeScript、TailwindCSS、Vitest。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add the failing integration contract test
|
||||
|
||||
**Files:**
|
||||
- Create: `WonderQ-MiniAPP/tests/mine-records.test.ts`
|
||||
- Read: `WonderQ-MiniAPP/src/pages/mine/components/MineRecentViews.vue`
|
||||
- Read: `WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue`
|
||||
- Read: `WonderQ-MiniAPP/src/pages/mine/index.vue`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `WonderQ-MiniAPP/tests/mine-records.test.ts` with this exact content:
|
||||
|
||||
```ts
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
function read(relativePath: string) {
|
||||
return readFileSync(resolve(process.cwd(), relativePath), "utf8");
|
||||
}
|
||||
|
||||
describe("mine travel records integration contract", () => {
|
||||
it("keeps vehicle demand and recent views in one list", () => {
|
||||
const component = read("src/pages/mine/components/MineRecentViews.vue");
|
||||
|
||||
expect(component).toContain("我的出行记录");
|
||||
expect(component).toContain("用车需求与最近浏览集中在这里");
|
||||
expect(component).toContain("items.length + 1");
|
||||
expect(component).toContain("open-vehicle");
|
||||
expect(component).toContain("暂无浏览记录");
|
||||
});
|
||||
|
||||
it("wires the vehicle demand action through MineMember to the existing page", () => {
|
||||
const member = read("src/pages/mine/components/MineMember.vue");
|
||||
const page = read("src/pages/mine/index.vue");
|
||||
|
||||
expect(member).toContain('@open-vehicle="emit(\'open-vehicle\')"');
|
||||
expect(page).toContain("goVehicleDemand");
|
||||
expect(page).toContain('@open-vehicle="goVehicleDemand"');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run from `D:\www\znkj\WonderQ-Project\WonderQ-MiniAPP`:
|
||||
|
||||
```powershell
|
||||
yarn test tests/mine-records.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the current component still uses “继续探索”, has no fixed vehicle row, and the parent chain does not expose `open-vehicle`.
|
||||
|
||||
### Task 2: Implement the unified list in MineRecentViews
|
||||
|
||||
**Files:**
|
||||
- Modify: `WonderQ-MiniAPP/src/pages/mine/components/MineRecentViews.vue`
|
||||
|
||||
- [ ] **Step 1: Replace the component template and emits with the unified-list implementation**
|
||||
|
||||
Keep the existing `RecentlyViewedItem` and `RecentlyViewedType` imports and type-label helper. The complete component should be:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="mt-6">
|
||||
<view class="mb-3 flex items-end justify-between">
|
||||
<view>
|
||||
<text class="block text-[18px] font-semibold tracking-[0.02em] text-[#24211f]">我的出行记录</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">用车需求与最近浏览集中在这里</text>
|
||||
</view>
|
||||
<text class="text-[11px] text-[#b18a76]">{{ items.length + 1 }} 项</text>
|
||||
</view>
|
||||
|
||||
<view class="overflow-hidden rounded-[16px] border border-[#e2d9d0] bg-white shadow-[0_8px_20px_rgba(80,58,45,0.05)]">
|
||||
<button
|
||||
class="m-0 flex w-full items-center gap-3 border-b border-[#eee7e0] px-3 py-3 text-left"
|
||||
@click="emit('open-vehicle')"
|
||||
>
|
||||
<view class="flex h-14 w-16 shrink-0 items-center justify-center rounded-[10px] bg-[#f4e5dc]">
|
||||
<uni-icons type="car" :size="24" color="#a45a3d" />
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="block truncate text-[14px] font-medium text-[#2b2724]">用车需求</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-[#a19890]">填写专属出行方案</text>
|
||||
</view>
|
||||
<uni-icons type="right" :size="15" color="#b3a79e" />
|
||||
</button>
|
||||
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
:key="`${item.type}-${item.id}`"
|
||||
class="m-0 flex w-full items-center gap-3 px-3 py-3 text-left"
|
||||
:class="index < items.length - 1 ? 'border-b border-[#eee7e0]' : ''"
|
||||
@click="emit('select', item)"
|
||||
>
|
||||
<view class="h-14 w-16 shrink-0 overflow-hidden rounded-[10px] bg-[#f1ece6]">
|
||||
<image v-if="item.image" class="h-full w-full" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="flex h-full w-full items-center justify-center">
|
||||
<uni-icons type="image" :size="20" color="#b8aaa0" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<text class="block truncate text-[14px] font-medium text-[#2b2724]">{{ item.title }}</text>
|
||||
<text class="mt-1 block text-[10px] leading-4 text-[#a19890]">{{ typeLabel(item.type) }}</text>
|
||||
</view>
|
||||
<uni-icons type="right" :size="15" color="#b3a79e" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<view v-else class="border-t border-dashed border-[#d8cec5] bg-white/70 px-5 py-6 text-center">
|
||||
<view class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-[#f4eee8]">
|
||||
<uni-icons type="eye" :size="20" color="#a45a3d" />
|
||||
</view>
|
||||
<text class="mt-3 block text-[14px] font-medium text-[#4a413a]">暂无浏览记录</text>
|
||||
<text class="mt-1 block text-[11px] leading-5 text-[#9a9189]">去看看玩法,收藏下一段旅程</text>
|
||||
<button
|
||||
class="m-0 mx-auto mt-4 flex min-h-[36px] items-center justify-center rounded-full bg-[#a45a3d] px-5 text-[12px] font-medium text-white"
|
||||
@click="emit('explore')"
|
||||
>
|
||||
去看看玩法
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RecentlyViewedItem, RecentlyViewedType } from "@/lib/recentlyViewed";
|
||||
|
||||
defineProps<{
|
||||
items: RecentlyViewedItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"open-vehicle": [];
|
||||
select: [item: RecentlyViewedItem];
|
||||
explore: [];
|
||||
}>();
|
||||
|
||||
function typeLabel(type: RecentlyViewedType) {
|
||||
if (type === "wanfa-route") return "玩法路线";
|
||||
if (type === "team-building") return "团队共创";
|
||||
return "客片案例";
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused test to verify the component contract passes**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
yarn test tests/mine-records.test.ts
|
||||
```
|
||||
|
||||
Expected: the first test passes; the parent wiring test remains red until Task 3.
|
||||
|
||||
### Task 3: Wire the vehicle action through the existing navigation guard
|
||||
|
||||
**Files:**
|
||||
- Modify: `WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue`
|
||||
- Modify: `WonderQ-MiniAPP/src/pages/mine/index.vue`
|
||||
- Read: `WonderQ-MiniAPP/src/lib/navigation.ts`
|
||||
|
||||
- [ ] **Step 1: Expose the event from MineMember**
|
||||
|
||||
In `MineMember.vue`, add the listener to the existing `MineRecentViews` instance:
|
||||
|
||||
```vue
|
||||
<MineRecentViews
|
||||
:items="props.recentViews"
|
||||
@open-vehicle="emit('open-vehicle')"
|
||||
@select="(item) => emit('select-recent', item)"
|
||||
@explore="emit('open-play')"
|
||||
/>
|
||||
```
|
||||
|
||||
Add the matching event to `defineEmits`:
|
||||
|
||||
```ts
|
||||
const emit = defineEmits<{
|
||||
"request-avatar": [];
|
||||
"open-concierge": [];
|
||||
"open-play": [];
|
||||
"open-vehicle": [];
|
||||
"select-recent": [item: RecentlyViewedItem];
|
||||
logout: [];
|
||||
}>();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Bind the event to goVehicleDemand in the page**
|
||||
|
||||
In `mine/index.vue`, add `@open-vehicle="goVehicleDemand"` to `MineMember`:
|
||||
|
||||
```vue
|
||||
@open-concierge="goConcierge"
|
||||
@open-play="goPlay"
|
||||
@open-vehicle="goVehicleDemand"
|
||||
@select-recent="openRecent"
|
||||
@logout="logout" />
|
||||
```
|
||||
|
||||
Add the existing navigation function to the import list without changing its implementation:
|
||||
|
||||
```ts
|
||||
import {
|
||||
goConcierge,
|
||||
goPlay,
|
||||
goVehicleDemand,
|
||||
goTeamBuildingDetail,
|
||||
goWanfaRouteDetail,
|
||||
goWildArchiveDetail,
|
||||
} from "@/lib/navigation";
|
||||
```
|
||||
|
||||
This preserves `goVehicleDemand()`'s current `requireLogin()` behavior and `/pages/vehicle-demand` target.
|
||||
|
||||
- [ ] **Step 3: Run the focused integration test**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
yarn test tests/mine-records.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS with both tests passing.
|
||||
|
||||
### Task 4: Verify the MiniAPP change and review the diff
|
||||
|
||||
**Files:**
|
||||
- Verify: `WonderQ-MiniAPP/src/pages/mine/components/MineRecentViews.vue`
|
||||
- Verify: `WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue`
|
||||
- Verify: `WonderQ-MiniAPP/src/pages/mine/index.vue`
|
||||
- Verify: `WonderQ-MiniAPP/tests/mine-records.test.ts`
|
||||
|
||||
- [ ] **Step 1: Run all MiniAPP unit tests**
|
||||
|
||||
Run from `D:\www\znkj\WonderQ-Project\WonderQ-MiniAPP`:
|
||||
|
||||
```powershell
|
||||
yarn test
|
||||
```
|
||||
|
||||
Expected: all existing tests and `mine-records.test.ts` pass.
|
||||
|
||||
- [ ] **Step 2: Run the MiniAPP typecheck and H5 build**
|
||||
|
||||
```powershell
|
||||
yarn build:h5
|
||||
```
|
||||
|
||||
Expected: `vue-tsc --noEmit` and the H5 build complete successfully.
|
||||
|
||||
- [ ] **Step 3: Run the WeChat Mini Program build**
|
||||
|
||||
```powershell
|
||||
yarn build:mp-weixin
|
||||
```
|
||||
|
||||
Expected: the Mini Program build completes and emits `dist/build/mp-weixin`.
|
||||
|
||||
- [ ] **Step 4: Inspect the final diff and confirm scope**
|
||||
|
||||
Run from `D:\www\znkj\WonderQ-Project`:
|
||||
|
||||
```powershell
|
||||
git diff -- WonderQ-MiniAPP/src/pages/mine/components/MineRecentViews.vue WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue WonderQ-MiniAPP/src/pages/mine/index.vue WonderQ-MiniAPP/tests/mine-records.test.ts
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: only the three Mine-related source files and the new focused test are part of this implementation; existing unrelated backend and home-page changes remain untouched. Confirm no new SFC `<style>` block, API path, storage key, database change, or hardcoded sensitive value was introduced.
|
||||
|
||||
- [ ] **Step 5: Commit the implementation**
|
||||
|
||||
```powershell
|
||||
git add -- WonderQ-MiniAPP/src/pages/mine/components/MineRecentViews.vue WonderQ-MiniAPP/src/pages/mine/components/MineMember.vue WonderQ-MiniAPP/src/pages/mine/index.vue WonderQ-MiniAPP/tests/mine-records.test.ts
|
||||
git commit -m "feat: merge vehicle demand and recent views"
|
||||
```
|
||||
Reference in New Issue
Block a user