feat: add customer travel records and browse history
This commit is contained in:
1 parent
0139fe6382
commit
6d9a546918
79 files changed
+12362
-112
No files matched your search
@@ -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,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) => {
|
||||
|
||||
Reference in new issue
Block a user