feat: add customer travel records and browse history

This commit is contained in:
duanshuwen committed 2026-08-27 14:19:03 +08:00
1 parent 0139fe6382
commit 6d9a546918
79 files changed
+12362 -112

No files matched your search

+5 -3
View File
@@ -60,7 +60,7 @@ import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
import ConciergeContactSheet from "@/components/ConciergeContactSheet.vue";
import { fetchPublicDetail } from "@/lib/api";
import { goBack } from "@/lib/navigation";
import { rememberRecentlyViewed } from "@/lib/recentlyViewed";
import { rememberRecentlyViewed, syncRecentlyViewed } from "@/lib/recentlyViewed";
import { SUPPORT_PHONE } from "@/lib/types";
import type { ConciergeAdvisor } from "../concierge/components/conciergeTypes";
import DetailActionBar from "./components/DetailActionBar.vue";
@@ -130,13 +130,15 @@ function rememberCurrentDetail() {
const current = presentation.value;
if (!current || !routeId.value) return;
rememberRecentlyViewed({
const item = {
type: "wanfa-route",
id: routeId.value,
title: current.title,
image: current.gallery[0] ?? "",
visitedAt: Date.now(),
});
} as const;
rememberRecentlyViewed(item);
void syncRecentlyViewed(item);
}
function goCall() {
@@ -0,0 +1,172 @@
<template>
<z-paging v-if="isReady" ref="paging" v-model="records" class="bg-[#f5f2ed]" @query="queryList">
<template #top>
<TopHeader title="历史浏览记录" @back="goBack()" />
</template>
<view class="px-5 pb-10 pt-5">
<view class="mb-5 flex items-end justify-between">
<view>
<text class="block text-[22px] font-semibold tracking-[0.01em] text-[#24211f]">历史浏览记录</text>
<text class="mt-1 block text-[12px] leading-5 text-[#958b83]">最近看过的路线、团队玩法和客片案例</text>
</view>
<text class="text-[11px] text-[#6d8377]">{{ total }} 条</text>
</view>
<view v-if="usingLocalFallback" class="mb-4 flex items-center gap-2 rounded-[10px] bg-[#fff8ed] px-3 py-2 text-[11px] leading-5 text-[#a67842]">
<uni-icons type="info" :size="15" color="#a67842" />
<text>网络暂不可用,当前展示本地缓存</text>
</view>
<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 records"
:key="`${item.itemType}-${item.itemId}`"
class="m-0 flex w-full items-center gap-3 px-4 py-3 text-left"
:class="index < records.length - 1 ? 'border-b border-[#eee7e0]' : ''"
@click="openHistory(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>
<view class="mt-1 flex items-center gap-2">
<text class="rounded-full bg-[#edf1ec] px-2 py-0.5 text-[10px] text-[#6d8377]">{{ browseHistoryTypeLabel(item.itemType) }}</text>
<text class="truncate text-[10px] leading-4 text-[#a19890]">{{ formatDateTime(item.visitedAt) }}</text>
</view>
</view>
<uni-icons class="shrink-0" type="right" :size="15" color="#b3a79e" />
</button>
</view>
</view>
<template #empty="{ isLoadFailed }">
<view class="flex min-h-[360px] flex-col items-center justify-center px-8 text-center">
<view class="flex h-14 w-14 items-center justify-center rounded-full bg-[#edf1ec]">
<uni-icons :type="isLoadFailed ? 'refresh' : 'eye'" :size="25" color="#6d8377" />
</view>
<text class="mt-4 block text-[15px] font-semibold text-[#3b342f]">
{{ isLoadFailed ? "记录加载失败" : "暂无历史浏览记录" }}
</text>
<text class="mt-2 block text-[12px] leading-5 text-[#978d85]">
{{ isLoadFailed ? errorMessage : "去玩法页看看,找到适合你的下一段旅程。" }}
</text>
<button
v-if="isLoadFailed"
class="m-0 mt-5 h-10 rounded-full border border-[#bfd0c3] bg-white px-6 text-[12px] font-medium text-[#5f7a69]"
@click="retry"
>
重试
</button>
<button
v-else
class="m-0 mt-5 h-10 rounded-full bg-[#6d8377] px-6 text-[12px] font-medium text-white"
@click="goPlay()"
>
去看看玩法
</button>
</view>
</template>
</z-paging>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import TopHeader from "@/components/TopHeader.vue";
import { ApiRequestError, fetchCustomerBrowseHistory } from "@/lib/api";
import { clearAuthSession, getAuthSession, redirectToLogin } from "@/lib/auth";
import { browseHistoryTypeLabel, formatDateTime, localBrowseHistoryPage } from "@/lib/customerRecords";
import {
goBack,
goPlay,
goTeamBuildingDetail,
goWanfaRouteDetail,
goWildArchiveDetail,
} from "@/lib/navigation";
import type { CustomerBrowseHistoryItem } from "@/lib/types";
type PagingInstance = {
reload: () => void;
complete: (data: unknown[], success?: boolean) => Promise<unknown>;
completeByTotal: (data: unknown[], total: number, success?: boolean) => Promise<unknown>;
};
const PAGE_PATH = "/pages/mine/browse-history/index";
const paging = ref<PagingInstance | null>(null);
const records = ref<CustomerBrowseHistoryItem[]>([]);
const isReady = ref(false);
const total = ref(0);
const usingLocalFallback = ref(false);
const errorMessage = ref("请检查网络后重试");
const hasShown = ref(false);
function isUnauthorized(error: unknown) {
return error instanceof ApiRequestError && error.statusCode === 401;
}
async function queryList(pageNum: number, pageSize: number) {
const session = getAuthSession();
if (!session) {
paging.value?.complete([], false);
return;
}
try {
const result = await fetchCustomerBrowseHistory(session.token, pageNum, pageSize);
total.value = result.total;
usingLocalFallback.value = false;
errorMessage.value = "请检查网络后重试";
await paging.value?.completeByTotal(result.items, result.total);
} catch (error) {
if (isUnauthorized(error)) {
clearAuthSession();
redirectToLogin(PAGE_PATH);
await paging.value?.complete([], false);
return;
}
if (pageNum === 1) {
const localPage = localBrowseHistoryPage(pageNum, pageSize);
if (localPage.items.length) {
total.value = localPage.total;
usingLocalFallback.value = true;
await paging.value?.completeByTotal(localPage.items, localPage.total);
return;
}
}
errorMessage.value = error instanceof Error && error.message ? error.message : "记录加载失败,请重试";
await paging.value?.complete([], false);
}
}
function openHistory(item: CustomerBrowseHistoryItem) {
if (item.itemType === "wanfa-route") {
goWanfaRouteDetail(item.itemId);
return;
}
if (item.itemType === "team-building") {
goTeamBuildingDetail(item.itemId);
return;
}
goWildArchiveDetail(item.itemId);
}
function retry() {
paging.value?.reload();
}
onLoad(() => {
isReady.value = redirectToLogin(PAGE_PATH);
});
onShow(() => {
if (hasShown.value) paging.value?.reload();
hasShown.value = true;
});
</script>
@@ -54,6 +54,7 @@ const brandPositionStyle = computed(() =>
);
function syncMenuButtonPosition() {
if (typeof uni.getMenuButtonBoundingClientRect !== "function") return;
const menuButton = uni.getMenuButtonBoundingClientRect();
menuButtonTop.value = menuButton.top;
}
@@ -39,37 +39,33 @@
</view>
</view>
<MineRecentViews :items="props.recentViews" @open-vehicle="emit('open-vehicle')"
@select="(item) => emit('select-recent', item)" @explore="emit('open-play')" />
<MineRecentViews @open-vehicle-demands="emit('open-vehicle-demands')"
@open-browse-history="emit('open-browse-history')" />
</view>
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import type { RecentlyViewedItem } from "@/lib/recentlyViewed";
import MineRecentViews from "./MineRecentViews.vue";
const props = withDefaults(
withDefaults(
defineProps<{
customerLine: string;
customerName?: string;
avatarUrl?: string;
showAvatarAction?: boolean;
recentViews?: RecentlyViewedItem[];
}>(),
{
customerName: "万趣会员",
avatarUrl: "",
showAvatarAction: false,
recentViews: () => [],
},
);
const emit = defineEmits<{
"open-play": [];
"open-vehicle": [];
"select-recent": [item: RecentlyViewedItem];
"open-vehicle-demands": [];
"open-browse-history": [];
}>();
const menuButtonTop = ref<number | null>(null);
@@ -3,81 +3,46 @@
<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="mt-1 block text-[11px] leading-5 text-[#9a9189]">用车需求与历史浏览集中在这里</text>
</view>
<text class="text-[11px] text-[#b18a76]">{{ items.length + 1 }} 项</text>
<text class="text-[11px] text-[#b18a76]">2 项</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')"
@click="emit('open-vehicle-demands')"
>
<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>
<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" />
<button
class="m-0 flex w-full items-center gap-3 px-3 py-3 text-left"
@click="emit('open-browse-history')"
>
<view class="flex h-14 w-16 shrink-0 items-center justify-center rounded-[10px] bg-[#edf1ec]">
<uni-icons type="eye" :size="24" color="#6d8377" />
</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 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>
</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: [];
"open-vehicle-demands": [];
"open-browse-history": [];
}>();
function typeLabel(type: RecentlyViewedType) {
if (type === "wanfa-route") return "玩法路线";
if (type === "team-building") return "团队共创";
return "客片案例";
}
</script>
+4 -24
View File
@@ -4,8 +4,8 @@
<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"
@open-play="goPlay" @open-vehicle="goVehicleDemand" @select-recent="openRecent" />
:avatar-url="wechatProfile?.avatarUrl" :show-avatar-action="isWechatMiniProgram"
@open-vehicle-demands="goCustomerVehicleDemands" @open-browse-history="goCustomerBrowseHistory" />
</scroll-view>
</view>
</view>
@@ -16,15 +16,8 @@ import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getAuthSession, loginWithPhoneCode } from "@/lib/auth";
import {
getRecentlyViewed,
type RecentlyViewedItem,
} from "@/lib/recentlyViewed";
import {
goPlay,
goVehicleDemand,
goTeamBuildingDetail,
goWanfaRouteDetail,
goWildArchiveDetail,
goCustomerBrowseHistory,
goCustomerVehicleDemands,
} from "@/lib/navigation";
import { loadAppData } from "@/lib/store";
import {
@@ -42,11 +35,9 @@ const wechatProfile = ref(getStoredWechatProfile());
const isWechatMiniProgram = ref(false);
const isLoggingIn = ref(false);
const isLoadingWechatProfile = ref(false);
const recentViews = ref<RecentlyViewedItem[]>([]);
onShow(() => {
session.value = getAuthSession();
wechatProfile.value = getStoredWechatProfile();
recentViews.value = getRecentlyViewed();
try {
isWechatMiniProgram.value = uni.getSystemInfoSync().uniPlatform === "mp-weixin";
} catch {
@@ -91,15 +82,4 @@ async function goLogin(event: WechatPhoneNumberEvent) {
}
}
function openRecent(item: RecentlyViewedItem) {
if (item.type === "wanfa-route") {
goWanfaRouteDetail(item.id);
return;
}
if (item.type === "team-building") {
goTeamBuildingDetail(item.id);
return;
}
goWildArchiveDetail(item.id);
}
</script>
@@ -0,0 +1,190 @@
<template>
<view class="wq-page min-h-screen bg-[#f5f2ed]">
<view class="wq-phone min-h-screen">
<TopHeader title="用车提交详情" @back="goBack()" />
<scroll-view scroll-y class="h-[calc(100vh-60px)]" :show-scrollbar="false">
<view v-if="loading" class="flex min-h-[520px] items-center justify-center px-8 text-center">
<view>
<view class="mx-auto h-8 w-8 animate-pulse rounded-full bg-[#e4cabc]" />
<text class="mt-3 block text-[13px] text-[#8c857e]">正在加载用车记录…</text>
</view>
</view>
<view v-else-if="!record" class="flex min-h-[520px] flex-col items-center justify-center px-8 text-center">
<view class="flex h-14 w-14 items-center justify-center rounded-full bg-[#f4e5dc]">
<uni-icons type="car" :size="25" color="#a45a3d" />
</view>
<text class="mt-4 block text-[15px] font-semibold text-[#3b342f]">用车记录暂时不可用</text>
<text class="mt-2 block text-[12px] leading-5 text-[#978d85]">{{ errorMessage || "没有找到这条用车提交记录。" }}</text>
<button
class="m-0 mt-5 h-10 rounded-full border border-[#d9c5b8] bg-white px-6 text-[12px] font-medium text-[#a45a3d]"
@click="loadDetail"
>
重试
</button>
</view>
<view v-else class="px-5 pb-10 pt-5">
<view class="rounded-[18px] border border-[#e2d9d0] bg-white p-5 shadow-[0_8px_20px_rgba(80,58,45,0.05)]">
<view class="flex items-start justify-between gap-3">
<view class="min-w-0 flex-1">
<text class="block text-[11px] font-semibold tracking-[0.18em] text-[#a45a3d]">VEHICLE SERVICE</text>
<text class="mt-2 block truncate text-[23px] font-semibold text-[#24211f]">{{ vehicleDestination(record) }}</text>
<text class="mt-2 block text-[11px] leading-5 text-[#a19890]">提交于 {{ formatDateTime(record.createdAt) }}</text>
</view>
<text class="shrink-0 rounded-full px-2.5 py-1.5 text-[10px]" :class="statusClass(record.status)">
{{ vehicleStatusLabel(record.status) }}
</text>
</view>
</view>
<view class="mt-4 rounded-[16px] border border-[#e2d9d0] bg-white p-4 shadow-[0_8px_20px_rgba(80,58,45,0.04)]">
<text class="block text-[15px] font-semibold text-[#2b2724]">出行信息</text>
<view class="mt-4 grid grid-cols-2 gap-4">
<view>
<text class="block text-[10px] text-[#a19890]">出行日期</text>
<text class="mt-1 block text-[13px] text-[#3b342f]">{{ formatTravelDate(vehicleTravelDate(record)) }}</text>
</view>
<view>
<text class="block text-[10px] text-[#a19890]">出行人数</text>
<text class="mt-1 block text-[13px] text-[#3b342f]">{{ vehiclePeopleCount(record) ?? "待确认" }} 人</text>
</view>
<view>
<text class="block text-[10px] text-[#a19890]">服务类型</text>
<text class="mt-1 block text-[13px] text-[#3b342f]">{{ serviceTypeLabel(record.vehicleDemand?.serviceType) }}</text>
</view>
<view>
<text class="block text-[10px] text-[#a19890]">包车时长</text>
<text class="mt-1 block text-[13px] text-[#3b342f]">{{ durationLabel(record.vehicleDemand?.charterDuration) }}</text>
</view>
</view>
</view>
<view class="mt-4 rounded-[16px] border border-[#e2d9d0] bg-white p-4 shadow-[0_8px_20px_rgba(80,58,45,0.04)]">
<text class="block text-[15px] font-semibold text-[#2b2724]">行程安排</text>
<view class="mt-4 space-y-3">
<view class="flex items-start gap-3">
<view class="mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-[#a45a3d]" />
<view class="min-w-0 flex-1">
<text class="block text-[10px] text-[#a19890]">上车地点</text>
<text class="mt-1 block text-[13px] leading-5 text-[#3b342f]">{{ record.vehicleDemand?.pickupLocation || "待确认" }}</text>
</view>
</view>
<view class="ml-1 border-l border-dashed border-[#d8cec5] py-1" />
<view class="flex items-start gap-3">
<view class="mt-1 h-2.5 w-2.5 shrink-0 rounded-full bg-[#6d8377]" />
<view class="min-w-0 flex-1">
<text class="block text-[10px] text-[#a19890]">目的地 / 下车地点</text>
<text class="mt-1 block text-[13px] leading-5 text-[#3b342f]">{{ record.vehicleDemand?.dropoffLocation || record.destination || "待确认" }}</text>
</view>
</view>
</view>
</view>
<view class="mt-4 rounded-[16px] border border-[#e2d9d0] bg-white p-4 shadow-[0_8px_20px_rgba(80,58,45,0.04)]">
<text class="block text-[15px] font-semibold text-[#2b2724]">联系人信息</text>
<view class="mt-4 flex items-center justify-between border-b border-[#eee7e0] pb-3">
<text class="text-[11px] text-[#a19890]">联系人</text>
<text class="text-[13px] text-[#3b342f]">{{ record.contactName || "未填写" }}</text>
</view>
<view class="flex items-center justify-between pt-3">
<text class="text-[11px] text-[#a19890]">联系电话</text>
<text class="text-[13px] text-[#3b342f]">{{ record.phoneMasked || "***" }}</text>
</view>
</view>
<view v-if="record.vehicleDemand?.vehicleOptionTitle || record.vehicleDemand?.specialRequirements || record.note" class="mt-4 rounded-[16px] border border-[#e2d9d0] bg-white p-4 shadow-[0_8px_20px_rgba(80,58,45,0.04)]">
<text class="block text-[15px] font-semibold text-[#2b2724]">其他需求</text>
<view v-if="record.vehicleDemand?.vehicleOptionTitle" class="mt-3 flex items-center justify-between">
<text class="text-[11px] text-[#a19890]">意向车型</text>
<text class="text-[13px] text-[#3b342f]">{{ record.vehicleDemand.vehicleOptionTitle }}</text>
</view>
<text v-if="record.vehicleDemand?.specialRequirements || record.note" class="mt-3 block text-[13px] leading-6 text-[#5f5852]">
{{ record.vehicleDemand.specialRequirements || record.note }}
</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import TopHeader from "@/components/TopHeader.vue";
import { ApiRequestError, fetchCustomerVehicleDemand } from "@/lib/api";
import { clearAuthSession, getAuthSession, redirectToLogin } from "@/lib/auth";
import {
formatDateTime,
formatTravelDate,
vehicleDestination,
vehiclePeopleCount,
vehicleStatusLabel,
vehicleTravelDate,
} from "@/lib/customerRecords";
import { goBack } from "@/lib/navigation";
import type { CustomerVehicleDemandRecord } from "@/lib/types";
const record = ref<CustomerVehicleDemandRecord | null>(null);
const recordId = ref("");
const loading = ref(true);
const errorMessage = ref("");
function statusClass(status: CustomerVehicleDemandRecord["status"]) {
if (status === "won") return "bg-[#edf5ef] text-[#3f7b57]";
if (status === "invalid") return "bg-[#f2efed] text-[#8d8179]";
return "bg-[#fff1ea] text-[#a45a3d]";
}
function serviceTypeLabel(value: CustomerVehicleDemandRecord["vehicleDemand"]["serviceType"] | undefined) {
return value === "transfer" ? "接送服务" : "旅游包车";
}
function durationLabel(value: CustomerVehicleDemandRecord["vehicleDemand"]["charterDuration"] | undefined) {
if (value === "halfDay") return "半天包车";
if (value === "fullDay") return "全天包车";
return "按接送安排";
}
function isUnauthorized(error: unknown) {
return error instanceof ApiRequestError && error.statusCode === 401;
}
async function loadDetail() {
if (!recordId.value) {
errorMessage.value = "缺少记录 ID,无法读取详情。";
record.value = null;
loading.value = false;
return;
}
const session = getAuthSession();
if (!session) {
redirectToLogin(`/pages/mine/vehicle-demand-detail?id=${encodeURIComponent(recordId.value)}`);
return;
}
loading.value = true;
errorMessage.value = "";
try {
record.value = await fetchCustomerVehicleDemand(recordId.value, session.token);
} catch (error) {
record.value = null;
if (isUnauthorized(error)) {
clearAuthSession();
redirectToLogin(`/pages/mine/vehicle-demand-detail?id=${encodeURIComponent(recordId.value)}`);
} else {
errorMessage.value = error instanceof Error && error.message ? error.message : "详情加载失败,请重试";
}
} finally {
loading.value = false;
}
}
onLoad((options) => {
recordId.value = typeof options?.id === "string" ? options.id : "";
void loadDetail();
});
</script>
@@ -0,0 +1,164 @@
<template>
<z-paging v-if="isReady" ref="paging" v-model="records" class="bg-[#f5f2ed]" @query="queryList">
<template #top>
<TopHeader title="用车提交记录" @back="goBack()" />
</template>
<view class="px-5 pb-10 pt-5">
<view class="mb-5 flex items-end justify-between">
<view>
<text class="block text-[22px] font-semibold tracking-[0.01em] text-[#24211f]">用车提交记录</text>
<text class="mt-1 block text-[12px] leading-5 text-[#958b83]">查看需求进度和已提交的出行信息</text>
</view>
<text class="text-[11px] text-[#a45a3d]">{{ total }} 条</text>
</view>
<button
class="m-0 mb-4 flex h-11 w-full items-center justify-center rounded-full bg-[#a45a3d] text-[13px] font-semibold text-white shadow-[0_8px_16px_rgba(164,90,61,0.16)]"
@click="goVehicleDemand()"
>
<uni-icons type="plus" :size="16" color="#ffffff" />
<text class="ml-1">提交新的用车需求</text>
</button>
<view class="overflow-hidden rounded-[16px] border border-[#e2d9d0] bg-white shadow-[0_8px_20px_rgba(80,58,45,0.05)]">
<button
v-for="(record, index) in records"
:key="record.id"
class="m-0 flex w-full items-start gap-3 px-4 py-4 text-left"
:class="index < records.length - 1 ? 'border-b border-[#eee7e0]' : ''"
@click="openRecord(record.id)"
>
<view class="flex h-11 w-11 shrink-0 items-center justify-center rounded-[12px] bg-[#f4e5dc]">
<uni-icons type="car" :size="21" color="#a45a3d" />
</view>
<view class="min-w-0 flex-1">
<view class="flex items-center justify-between gap-2">
<text class="truncate text-[15px] font-semibold text-[#2b2724]">{{ vehicleDestination(record) }}</text>
<text class="shrink-0 rounded-full px-2 py-1 text-[10px]" :class="statusClass(record.status)">
{{ vehicleStatusLabel(record.status) }}
</text>
</view>
<view class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#817870]">
<text>{{ formatTravelDate(vehicleTravelDate(record)) }}</text>
<text>{{ vehiclePeopleCount(record) ?? "待确认" }} 人</text>
</view>
<text class="mt-2 block text-[10px] leading-4 text-[#aaa098]">{{ formatDateTime(record.createdAt) }} 提交</text>
</view>
<uni-icons class="mt-2 shrink-0" type="right" :size="15" color="#b3a79e" />
</button>
</view>
</view>
<template #empty="{ isLoadFailed }">
<view class="flex min-h-[360px] flex-col items-center justify-center px-8 text-center">
<view class="flex h-14 w-14 items-center justify-center rounded-full bg-[#f4e5dc]">
<uni-icons :type="isLoadFailed ? 'refresh' : 'car'" :size="25" color="#a45a3d" />
</view>
<text class="mt-4 block text-[15px] font-semibold text-[#3b342f]">
{{ isLoadFailed ? "记录加载失败" : "还没有用车提交记录" }}
</text>
<text class="mt-2 block text-[12px] leading-5 text-[#978d85]">
{{ isLoadFailed ? errorMessage : "提交一份出行需求,让管家为你安排旅程。" }}
</text>
<button
v-if="isLoadFailed"
class="m-0 mt-5 h-10 rounded-full border border-[#d9c5b8] bg-white px-6 text-[12px] font-medium text-[#a45a3d]"
@click="retry"
>
重试
</button>
<button
v-else
class="m-0 mt-5 h-10 rounded-full bg-[#a45a3d] px-6 text-[12px] font-medium text-white"
@click="goVehicleDemand()"
>
提交新的用车需求
</button>
</view>
</template>
</z-paging>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import TopHeader from "@/components/TopHeader.vue";
import { ApiRequestError, fetchCustomerVehicleDemands } from "@/lib/api";
import { clearAuthSession, getAuthSession, redirectToLogin } from "@/lib/auth";
import {
formatDateTime,
formatTravelDate,
vehicleDestination,
vehiclePeopleCount,
vehicleStatusLabel,
vehicleTravelDate,
} from "@/lib/customerRecords";
import { goBack, goVehicleDemand, goCustomerVehicleDemandDetail } from "@/lib/navigation";
import type { CustomerVehicleDemandRecord } from "@/lib/types";
type PagingInstance = {
reload: () => void;
complete: (data: unknown[], success?: boolean) => Promise<unknown>;
completeByTotal: (data: unknown[], total: number, success?: boolean) => Promise<unknown>;
};
const PAGE_PATH = "/pages/mine/vehicle-demands/index";
const paging = ref<PagingInstance | null>(null);
const records = ref<CustomerVehicleDemandRecord[]>([]);
const isReady = ref(false);
const total = ref(0);
const errorMessage = ref("请检查网络后重试");
const hasShown = ref(false);
function statusClass(status: CustomerVehicleDemandRecord["status"]) {
if (status === "won") return "bg-[#edf5ef] text-[#3f7b57]";
if (status === "invalid") return "bg-[#f2efed] text-[#8d8179]";
return "bg-[#fff1ea] text-[#a45a3d]";
}
function isUnauthorized(error: unknown) {
return error instanceof ApiRequestError && error.statusCode === 401;
}
async function queryList(pageNum: number, pageSize: number) {
const session = getAuthSession();
if (!session) {
paging.value?.complete([], false);
return;
}
try {
const result = await fetchCustomerVehicleDemands(session.token, pageNum, pageSize);
total.value = result.total;
errorMessage.value = "请检查网络后重试";
await paging.value?.completeByTotal(result.items, result.total);
} catch (error) {
if (isUnauthorized(error)) {
clearAuthSession();
redirectToLogin(PAGE_PATH);
await paging.value?.complete([], false);
return;
}
errorMessage.value = error instanceof Error && error.message ? error.message : "记录加载失败,请重试";
await paging.value?.complete([], false);
}
}
function openRecord(id: string) {
goCustomerVehicleDemandDetail(id);
}
function retry() {
paging.value?.reload();
}
onLoad(() => {
isReady.value = redirectToLogin(PAGE_PATH);
});
onShow(() => {
if (hasShown.value) paging.value?.reload();
hasShown.value = true;
});
</script>
@@ -42,7 +42,7 @@ import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { fetchPublicTeamBuildingDetail } from "@/lib/api";
import { goBack } from "@/lib/navigation";
import { rememberRecentlyViewed } from "@/lib/recentlyViewed";
import { rememberRecentlyViewed, syncRecentlyViewed } from "@/lib/recentlyViewed";
import {
homeTeamBuildingFallback,
normalizeHomeTeamBuildingDetail,
@@ -73,13 +73,15 @@ function rememberCurrentDetail(teamBuildingId: string) {
const current = detail.value;
if (!current || !teamBuildingId) return;
rememberRecentlyViewed({
const item = {
type: "team-building",
id: teamBuildingId,
title: current.title,
image: current.image,
visitedAt: Date.now(),
});
} as const;
rememberRecentlyViewed(item);
void syncRecentlyViewed(item);
}
onLoad((query) => {
@@ -33,7 +33,7 @@ import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { fetchPublicWildArchiveDetail } from "@/lib/api";
import { goBack } from "@/lib/navigation";
import { rememberRecentlyViewed } from "@/lib/recentlyViewed";
import { rememberRecentlyViewed, syncRecentlyViewed } from "@/lib/recentlyViewed";
import { homeWildArchiveFallback, normalizeHomeWildArchiveDetail, type HomeWildArchive } from "@/pages/home/components/homeWildArchivesData";
const detail = ref<HomeWildArchive | null>(null);
@@ -60,13 +60,15 @@ function rememberCurrentDetail(archiveId: string) {
const current = detail.value;
if (!current || !archiveId) return;
rememberRecentlyViewed({
const item = {
type: "wild-archive",
id: archiveId,
title: current.title,
image: current.image,
visitedAt: Date.now(),
});
} as const;
rememberRecentlyViewed(item);
void syncRecentlyViewed(item);
}
onLoad((query) => {