feat:菜谱页面适配国际化

feat:钱包页面增加人员选择
This commit is contained in:
dzc committed 2026-10-10 12:05:20 +08:00
1 parent f10575a873
commit bc85e793d2
16 files changed
+863 -183

No files matched your search

+4 -4
View File
@@ -35,15 +35,15 @@ VITE_OAUTH2_SOCIAL_CLIENT='social:social'
# 是否开启websocket 消息接受,
VITE_WEBSOCKET_ENABLE = true
# --------------------------- start 开发环境 start ----------------------------------
# VITE_APP_DEV_SERVICE = https://www.zianxin.com/api
VITE_APP_DEV_SERVICE = https://dzc-test-1.fy.takin.cc
VITE_APP_DEV_SERVICE = https://www.zianxin.com/api
# VITE_APP_DEV_SERVICE = https://dzc-test-1.fy.takin.cc
# VITE_APP_DEV_SERVICE = http://127.0.0.1:9999
# VITE_APP_DEV_SERVICE = https://www.zianxin.com/api
# VITE_APP_DEV_SERVICE = https://dzc-test-1.fy.takin.cc
# VITE_APP_DEV_SERVICE = http://127.0.0.1:9999
# VITE_IMAGE_DEV_PATH = https://www.zianxin.com/api
VITE_IMAGE_DEV_PATH = https://dzc-test-1.fy.takin.cc
VITE_IMAGE_DEV_PATH = https://www.zianxin.com/api
# VITE_IMAGE_DEV_PATH = https://dzc-test-1.fy.takin.cc
# VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
# VITE_IMAGE_DEV_PATH = https://www.zianxin.com/api
# VITE_IMAGE_DEV_PATH = https://dzc-test-1.fy.takin.cc
+94
View File
@@ -1,6 +1,100 @@
export default {
mobile: {
order: {
wardMenu: {
myTitle: "My Meals",
wardTitle: "Ward's Meals",
mapMyTitle: "My Meal Map",
mapWardTitle: "Ward's Meal Map",
mapEntry: "Meal Map",
subtitleDaily: "Daily dining records",
subtitleMap: "Daily dining overview",
userSwitchAria: "Switch dining person",
userRefreshAria: "Refresh menu",
pickerTitle: "Select dining person",
selfLabel: "Self",
relationFallback: "Related person",
wardFallback: "Ward",
defaultSubtitle: "Upcoming unverified reservations",
queryRelationsFail: "Failed to query related users",
overviewTitle: "Dining Overview",
updating: "Updating",
totalDays: "{count} day(s) in total",
dailyTitle: "Daily Dining",
orderCount: "{count} order(s)",
summaryReserve: "Reserved",
summaryOthers: "Others",
summaryDays: "Days",
summaryAmount: "Amount",
legendPending: "Pending",
legendVerified: "Verified",
legendOffline: "Offline etc.",
legendNone: "No records",
prevMonthAria: "Previous month",
nextMonthAria: "Next month",
mapLoadingAria: "Meal map loading",
mapLoadFail: "Failed to load meal map",
checkNetworkReload: "Check your network and try again",
checkNetworkOrRelation: "Check your network or guardianship and try again",
reload: "Reload",
cellAria: "{date}, {pending} pending, {verified} verified, {offline} offline, tap to view daily details",
pillPending: "P",
pillVerified: "V",
pillOffline: "O",
monthTotalPendingPrefix: "Pending this month:",
monthTotalVerifiedPrefix: "Verified:",
monthTotalOfflinePrefix: "Offline etc.:",
monthTotalSuffix: "order(s)",
monthEmpty: "No dining records this month",
dayLoadingAria: "Daily details loading",
dayLoading: "Loading daily details",
dayLoadFail: "Failed to load daily details",
dayEmpty: "No dining records on this day",
queryMapFail: "Failed to query meal map",
queryDayFail: "Failed to query daily details",
ordersLoadingAria: "Dining records loading",
ordersLoadFail: "Failed to load dining records",
emptyDescSelf: "No dining records in the recent 15 days",
emptyDescWard: "This ward has no dining records in the recent 15 days",
emptyTitle: "No dining records",
swipeHint: "Swipe or tap a date above to switch",
queryOrdersFail: "Failed to query dining records",
daySummaryPortions: "Portions",
reserveGroupTitle: "Reserved · {count} order(s)",
otherGroupTitle: "Other spending · {count} order(s)",
verifiedBadge: "Verified",
completedBadge: "Completed",
merchantFallback: "Canteen merchant",
productFallback: "Unnamed item",
productAria: "View the order details of {name}",
perPortion: "/ portion",
originalPortions: "Bought {count}",
refundedPortions: "Refunded {count}",
noProducts: "No item details",
paidAt: "Paid at {time}",
totalPortions: "{count} portion(s) in total",
orderTotal: "Total ¥{amount}",
missingOrderInfo: "Order info missing, details unavailable",
apptTimeFallback: "Time subject to reservation",
unsureDate: "Date to be confirmed",
apptDateUnsure: "Reservation date to be confirmed",
todayLabel: "Today",
tomorrowLabel: "Tomorrow",
pendingShort: "TBD",
weekdayShort: "Sun,Mon,Tue,Wed,Thu,Fri,Sat",
weekdayFull: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
mapWeekdayHeaders: "Su,Mo,Tu,We,Th,Fr,Sa",
kindOffline: "Offline spending",
kindMachine: "Payment terminal",
kindTakeout: "Takeout",
kindMall: "Mall",
kindOther: "Other spending",
mealBreakfast: "Breakfast",
mealLunch: "Lunch",
mealDinner: "Dinner",
mealNight: "Late-night",
mealFallback: "Reserved meal",
},
title: "Meals",
filter: "Filter",
filterAria: "Open filter",
+94
View File
@@ -1,6 +1,100 @@
export default {
mobile: {
order: {
wardMenu: {
myTitle: "我的用餐",
wardTitle: "被监护人用餐",
mapMyTitle: "我的用餐地图",
mapWardTitle: "被监护人用餐地图",
mapEntry: "用餐地图",
subtitleDaily: "每日用餐记录",
subtitleMap: "每日用餐总览",
userSwitchAria: "切换查看对象",
userRefreshAria: "刷新菜谱",
pickerTitle: "选择查看对象",
selfLabel: "本人",
relationFallback: "关系人",
wardFallback: "被监护人",
defaultSubtitle: "未来未核销预约菜谱",
queryRelationsFail: "查询关系人失败",
overviewTitle: "用餐概览",
updating: "更新中",
totalDays: "共 {count} 天",
dailyTitle: "每日用餐",
orderCount: "{count} 笔",
summaryReserve: "预约",
summaryOthers: "其他",
summaryDays: "天数",
summaryAmount: "金额",
legendPending: "待核销",
legendVerified: "已核销",
legendOffline: "线下等",
legendNone: "无记录",
prevMonthAria: "上一个月",
nextMonthAria: "下一个月",
mapLoadingAria: "预约地图加载中",
mapLoadFail: "预约地图加载失败",
checkNetworkReload: "请检查网络后重新加载",
checkNetworkOrRelation: "请检查网络或监护关系后重新加载",
reload: "重新加载",
cellAria: "{date},待核销 {pending} 单,已核销 {verified} 单,线下等 {offline} 单,点击查看当日明细",
pillPending: "待",
pillVerified: "已",
pillOffline: "线",
monthTotalPendingPrefix: "本月待核销",
monthTotalVerifiedPrefix: "已核销",
monthTotalOfflinePrefix: "线下等",
monthTotalSuffix: "单",
monthEmpty: "本月暂无用餐记录",
dayLoadingAria: "当日明细加载中",
dayLoading: "正在加载当日明细",
dayLoadFail: "当日明细加载失败",
dayEmpty: "这一天暂无用餐记录",
queryMapFail: "查询用餐地图失败",
queryDayFail: "查询当日明细失败",
ordersLoadingAria: "用餐记录加载中",
ordersLoadFail: "用餐记录加载失败",
emptyDescSelf: "您近期(前后 15 天)暂无用餐记录",
emptyDescWard: "该被监护人近期(前后 15 天)暂无用餐记录",
emptyTitle: "暂无用餐记录",
swipeHint: "左右滑动或点击上方日期切换",
queryOrdersFail: "查询用餐记录失败",
daySummaryPortions: "份数",
reserveGroupTitle: "预约订单 · {count} 笔",
otherGroupTitle: "其他消费 · {count} 笔",
verifiedBadge: "已核销",
completedBadge: "已完成",
merchantFallback: "食堂商户",
productFallback: "未命名菜品",
productAria: "查看{name}所属订单详情",
perPortion: "/ 份",
originalPortions: "原购 {count} 份",
refundedPortions: "已退款 {count} 份",
noProducts: "暂无菜品明细",
paidAt: "{time} 支付",
totalPortions: "共 {count} 份",
orderTotal: "合计 ¥{amount}",
missingOrderInfo: "订单信息缺失,暂时无法查看详情",
apptTimeFallback: "时间以预约为准",
unsureDate: "日期待确认",
apptDateUnsure: "预约日期待确认",
todayLabel: "今天",
tomorrowLabel: "明天",
pendingShort: "待定",
weekdayShort: "周日,周一,周二,周三,周四,周五,周六",
weekdayFull: "星期日,星期一,星期二,星期三,星期四,星期五,星期六",
mapWeekdayHeaders: "日,一,二,三,四,五,六",
kindOffline: "线下消费",
kindMachine: "消费机",
kindTakeout: "外卖",
kindMall: "商城",
kindOther: "其他消费",
mealBreakfast: "早餐",
mealLunch: "午餐",
mealDinner: "晚餐",
mealNight: "宵夜",
mealFallback: "预约餐段",
},
title: "订餐",
filter: "筛选",
filterAria: "打开筛选",
+94
View File
@@ -1,6 +1,100 @@
export default {
mobile: {
order: {
wardMenu: {
myTitle: "我的用餐",
wardTitle: "被監護人用餐",
mapMyTitle: "我的用餐地圖",
mapWardTitle: "被監護人用餐地圖",
mapEntry: "用餐地圖",
subtitleDaily: "每日用餐記錄",
subtitleMap: "每日用餐總覽",
userSwitchAria: "切換查看對象",
userRefreshAria: "刷新菜譜",
pickerTitle: "選擇查看對象",
selfLabel: "本人",
relationFallback: "關係人",
wardFallback: "被監護人",
defaultSubtitle: "未來未核銷預約菜譜",
queryRelationsFail: "查詢關係人失敗",
overviewTitle: "用餐概覽",
updating: "更新中",
totalDays: "共 {count} 天",
dailyTitle: "每日用餐",
orderCount: "{count} 筆",
summaryReserve: "預約",
summaryOthers: "其他",
summaryDays: "天數",
summaryAmount: "金額",
legendPending: "待核銷",
legendVerified: "已核銷",
legendOffline: "線下等",
legendNone: "無記錄",
prevMonthAria: "上一個月",
nextMonthAria: "下一個月",
mapLoadingAria: "預約地圖載入中",
mapLoadFail: "預約地圖載入失敗",
checkNetworkReload: "請檢查網絡後重新載入",
checkNetworkOrRelation: "請檢查網絡或監護關係後重新載入",
reload: "重新載入",
cellAria: "{date},待核銷 {pending} 單,已核銷 {verified} 單,線下等 {offline} 單,點擊查看當日明細",
pillPending: "待",
pillVerified: "已",
pillOffline: "線",
monthTotalPendingPrefix: "本月待核銷",
monthTotalVerifiedPrefix: "已核銷",
monthTotalOfflinePrefix: "線下等",
monthTotalSuffix: "單",
monthEmpty: "本月暫無用餐記錄",
dayLoadingAria: "當日明細載入中",
dayLoading: "正在載入當日明細",
dayLoadFail: "當日明細載入失敗",
dayEmpty: "這一天暫無用餐記錄",
queryMapFail: "查詢用餐地圖失敗",
queryDayFail: "查詢當日明細失敗",
ordersLoadingAria: "用餐記錄載入中",
ordersLoadFail: "用餐記錄載入失敗",
emptyDescSelf: "您近期(前後 15 天)暫無用餐記錄",
emptyDescWard: "該被監護人近期(前後 15 天)暫無用餐記錄",
emptyTitle: "暫無用餐記錄",
swipeHint: "左右滑動或點擊上方日期切換",
queryOrdersFail: "查詢用餐記錄失敗",
daySummaryPortions: "份數",
reserveGroupTitle: "預約訂單 · {count} 筆",
otherGroupTitle: "其他消費 · {count} 筆",
verifiedBadge: "已核銷",
completedBadge: "已完成",
merchantFallback: "食堂商戶",
productFallback: "未命名菜品",
productAria: "查看{name}所屬訂單詳情",
perPortion: "/ 份",
originalPortions: "原購 {count} 份",
refundedPortions: "已退款 {count} 份",
noProducts: "暫無菜品明細",
paidAt: "{time} 支付",
totalPortions: "共 {count} 份",
orderTotal: "合計 ¥{amount}",
missingOrderInfo: "訂單信息缺失,暫時無法查看詳情",
apptTimeFallback: "時間以預約為準",
unsureDate: "日期待確認",
apptDateUnsure: "預約日期待確認",
todayLabel: "今天",
tomorrowLabel: "明天",
pendingShort: "待定",
weekdayShort: "週日,週一,週二,週三,週四,週五,週六",
weekdayFull: "星期日,星期一,星期二,星期三,星期四,星期五,星期六",
mapWeekdayHeaders: "日,一,二,三,四,五,六",
kindOffline: "線下消費",
kindMachine: "消費機",
kindTakeout: "外賣",
kindMall: "商城",
kindOther: "其他消費",
mealBreakfast: "早餐",
mealLunch: "午餐",
mealDinner: "晚餐",
mealNight: "宵夜",
mealFallback: "預約餐段",
},
title: "訂餐",
filter: "篩選",
filterAria: "打開篩選",
+28
View File
@@ -8,6 +8,32 @@ export default {
detailTitle: "Meal Card",
userCardSubtitle: "Wallet overview",
relationListTitle: "{name}'s Wallets",
queryWardCardFail: "Failed to load meal card",
wardCardFallback: "Meal card",
wardNoCard: "No meal card",
wardNoCardDesc: "This person has not opened a meal card yet",
viewTransactions: "Transactions",
wardCardInactive: "Inactive",
wardCardActive: "Active",
wardCardLost: "Lost",
wardCardLocked: "Locked",
wardCardCancelled: "Cancelled",
wardCardUnknown: "Unknown",
guardianRechargeTitle: "Recharge for ward",
currentBalance: "Current balance",
@@ -244,6 +270,8 @@ export default {
txTransferRefund: "Transfer refund",
txSysRemarkLabel: "System note",
txRemarkLabel: "Remark",
txRemarkExpand: "View remark",
+28
View File
@@ -8,6 +8,32 @@ export default {
detailTitle: "饭卡",
userCardSubtitle: "钱包总览",
relationListTitle: "{name}的钱包",
queryWardCardFail: "查询饭卡信息失败",
wardCardFallback: "饭卡",
wardNoCard: "未开通饭卡",
wardNoCardDesc: "该对象尚未开通饭卡,暂无钱包信息",
viewTransactions: "查看流水",
wardCardInactive: "未激活",
wardCardActive: "已激活",
wardCardLost: "已挂失",
wardCardLocked: "已锁卡",
wardCardCancelled: "已销卡",
wardCardUnknown: "状态异常",
guardianRechargeTitle: "为被监护人充值",
currentBalance: "当前余额",
@@ -244,6 +270,8 @@ export default {
txTransferRefund: "转账退款",
txSysRemarkLabel: "系统备注",
txRemarkLabel: "备注",
txRemarkExpand: "查看备注",
+28
View File
@@ -8,6 +8,32 @@ export default {
detailTitle: "飯卡",
userCardSubtitle: "錢包總覽",
relationListTitle: "{name}的錢包",
queryWardCardFail: "查詢飯卡信息失敗",
wardCardFallback: "飯卡",
wardNoCard: "未開通飯卡",
wardNoCardDesc: "該對象尚未開通飯卡,暫無錢包信息",
viewTransactions: "查看流水",
wardCardInactive: "未激活",
wardCardActive: "已激活",
wardCardLost: "已掛失",
wardCardLocked: "已鎖卡",
wardCardCancelled: "已銷卡",
wardCardUnknown: "狀態異常",
guardianRechargeTitle: "為被監護人充值",
currentBalance: "當前餘額",
@@ -244,6 +270,8 @@ export default {
txTransferRefund: "轉賬退款",
txSysRemarkLabel: "系統備註",
txRemarkLabel: "備註",
txRemarkExpand: "查看備註",
+30 -25
View File
@@ -1,15 +1,15 @@
<template>
<crud-layout :title="pageTitle" mainIsScroll bg="/static/page/blue-bg.png" nav-right-text="用餐地图"
<crud-layout :title="pageTitle" mainIsScroll bg="/static/page/blue-bg.png" :nav-right-text="t('mobile.order.wardMenu.mapEntry')"
navRightIcon="address" @navRightClick="openMenuMap">
<view class="pb-32rpx pt-14rpx" :aria-busy="state.loading">
<ward-menu-user-card :default-user-id="state.wardUserId" :default-name="state.wardName"
:default-is-self="state.currentUser" :refreshing="state.loading" subtitle="每日用餐记录"
:default-is-self="state.currentUser" :refreshing="state.loading" :subtitle="t('mobile.order.wardMenu.subtitleDaily')"
@change="onWardUserChange" @refresh="loadWardMenus" />
<view class="overview-card mt-14rpx">
<view class="flex items-center justify-between px-4rpx pb-12rpx">
<view class="sub-text-25 font-700">用餐概览</view>
<view class="second-text-20">{{ state.loading ? "更新中" : `共 ${dateGroups.length} 天` }}</view>
<view class="sub-text-25 font-700">{{ t('mobile.order.wardMenu.overviewTitle') }}</view>
<view class="second-text-20">{{ state.loading ? t('mobile.order.wardMenu.updating') : t('mobile.order.wardMenu.totalDays', { count: dateGroups.length }) }}</view>
</view>
<view class="grid grid-cols-4 gap-8rpx">
<view v-for="item in summaryItems" :key="item.key"
@@ -28,12 +28,12 @@
</view>
<view class="flex items-center justify-between px-4rpx pb-10rpx pt-18rpx">
<view class="main-text-28">每日用餐</view>
<view v-if="state.loaded && !state.failed" class="second-text-20">{{ state.orders.length }} 笔</view>
<view class="main-text-28">{{ t('mobile.order.wardMenu.dailyTitle') }}</view>
<view v-if="state.loaded && !state.failed" class="second-text-20">{{ t('mobile.order.wardMenu.orderCount', { count: state.orders.length }) }}</view>
</view>
<!-- 首载骨架:按日历条 + 日卡 + 餐卡结构占位,刷新时保留旧内容不闪骨架 -->
<view v-if="state.loading && !state.loaded" aria-label="用餐记录加载中">
<view v-if="state.loading && !state.loaded" :aria-label="t('mobile.order.wardMenu.ordersLoadingAria')">
<view class="card rounded-20rpx bg-white px-16rpx py-14rpx">
<view class="flex gap-8rpx">
<view v-for="index in 7" :key="index" class="flex flex-1 flex-col-center gap-8rpx">
@@ -78,9 +78,9 @@
<view class="h-108rpx w-108rpx flex-row-center rounded-full bg-#fff4e8">
<tm-icon name="error-warning-line" :size="52" color="#d97706" />
</view>
<view class="sub-text-26 mt-16rpx">用餐记录加载失败</view>
<view class="sub-text-26 mt-16rpx">{{ t('mobile.order.wardMenu.ordersLoadFail') }}</view>
<view class="second-text-22 mt-6rpx leading-34rpx">{{ failureDescription }}</view>
<tm-button class="mt-18rpx px-28rpx" size="s" skin="thin" @tap="loadWardMenus">重新加载</tm-button>
<tm-button class="mt-18rpx px-28rpx" size="s" skin="thin" @tap="loadWardMenus">{{ t('mobile.order.wardMenu.reload') }}</tm-button>
</view>
<!-- 整体无用餐记录时同样保留日历条结构,空态在单日区域内以紧凑横条表达 -->
@@ -99,8 +99,9 @@
color="#94a3b8" />
</view>
<view class="ml-12rpx min-w-0">
<view class="sub-text-24 font-700">{{ dateGroups.length ? '这一天暂无用餐记录' : '暂无用餐记录' }}</view>
<view class="second-text-20 mt-2rpx show-line">{{ dateGroups.length ? '左右滑动或点击上方日期切换' :
<view class="sub-text-24 font-700">{{ dateGroups.length ? t('mobile.order.wardMenu.dayEmpty') : t('mobile.order.wardMenu.emptyTitle') }}
</view>
<view class="second-text-20 mt-2rpx show-line">{{ dateGroups.length ? t('mobile.order.wardMenu.swipeHint') :
emptyDescription }}</view>
</view>
</view>
@@ -118,6 +119,7 @@
<script setup lang="ts">
import { computed, reactive } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import { useI18n } from "vue-i18n"
import dayjs from "dayjs"
import { hooks, router } from "@@/index"
@@ -134,6 +136,8 @@ import {
type WardMenuOrder,
} from "@/sheep/components/order/ward-menu"
const { t } = useI18n()
/** 页面顶部概览指标。 */
type WardMenuSummaryItem = {
/** 指标唯一标识,仅用于列表渲染。 */
@@ -185,7 +189,7 @@ type WardMenuPageState = {
/** 页面局部业务状态。 */
const state = reactive<WardMenuPageState>({
wardUserId: "",
wardName: "被监护人",
wardName: "",
currentUser: false,
selectedDate: dayjs().format("YYYY-MM-DD"),
orders: [],
@@ -196,13 +200,14 @@ const state = reactive<WardMenuPageState>({
})
/** 页面标题根据查询目标区分本人和被监护人。 */
const pageTitle = computed(() => (state.currentUser ? "我的用餐" : "被监护人用餐"))
const pageTitle = computed(() =>
t(state.currentUser ? "mobile.order.wardMenu.myTitle" : "mobile.order.wardMenu.wardTitle"))
/** 用餐记录请求失败时,本人无需提示检查监护关系。 */
const failureDescription = computed(() => (state.currentUser ? "请检查网络后重新加载" : "请检查网络或监护关系后重新加载"))
const failureDescription = computed(() =>
t(state.currentUser ? "mobile.order.wardMenu.checkNetworkReload" : "mobile.order.wardMenu.checkNetworkOrRelation"))
/** 空记录提示根据当前查询目标展示。 */
const emptyDescription = computed(() =>
state.currentUser ? "您近期(前后 15 天)暂无用餐记录" : "该被监护人近期(前后 15 天)暂无用餐记录",
)
t(state.currentUser ? "mobile.order.wardMenu.emptyDescSelf" : "mobile.order.wardMenu.emptyDescWard"))
/** 窗口内预约订单笔数。 */
const reserveCount = computed(() => state.orders.filter((order) => isReserveOrder(order)).length)
@@ -238,12 +243,12 @@ const ordersOf = (date: string) => dateGroups.value.find((item) => item.date ===
/** 页面顶部四项摘要:预约与线下等其他类型分开展示,突出每日用餐构成。 */
const summaryItems = computed<WardMenuSummaryItem[]>(() => [
{ key: "reserve", label: "预约", value: reserveCount.value, icon: "calendar-check-line", color: "#0094ff", background: "#eef8ff" },
{ key: "others", label: "其他", value: otherCount.value, icon: "store-2-line", color: "#1570ef", background: "#eaf3ff" },
{ key: "days", label: "天数", value: dateGroups.value.length, icon: "calendar-event-line", color: "#027a48", background: "#ecfdf3" },
{ key: "reserve", label: t("mobile.order.wardMenu.summaryReserve"), value: reserveCount.value, icon: "calendar-check-line", color: "#0094ff", background: "#eef8ff" },
{ key: "others", label: t("mobile.order.wardMenu.summaryOthers"), value: otherCount.value, icon: "store-2-line", color: "#1570ef", background: "#eaf3ff" },
{ key: "days", label: t("mobile.order.wardMenu.summaryDays"), value: dateGroups.value.length, icon: "calendar-event-line", color: "#027a48", background: "#ecfdf3" },
{
key: "amount",
label: "金额",
label: t("mobile.order.wardMenu.summaryAmount"),
value: `¥${hooks.useMoney(totalAmount.value)}`,
icon: "money-cny-circle-line",
color: "#f97316",
@@ -253,7 +258,7 @@ const summaryItems = computed<WardMenuSummaryItem[]>(() => [
/** 安全解码路由中的展示名称,异常编码回退为原始文案。 */
const decodeRouteText = (value?: string) => {
if (!value) return "被监护人"
if (!value) return t("mobile.order.wardMenu.wardFallback")
try {
return decodeURIComponent(value)
} catch (_error) {
@@ -281,7 +286,7 @@ const loadWardMenus = async () => {
startDate: dayjs().subtract(15, "day").format("YYYY-MM-DD"),
endDate: dayjs().add(15, "day").format("YYYY-MM-DD"),
})
if (response?.code !== 0) throw new Error(response?.msg || "查询用餐记录失败")
if (response?.code !== 0) throw new Error(response?.msg || t("mobile.order.wardMenu.queryOrdersFail"))
if (currentSequence !== state.requestSequence) return
state.orders = Array.isArray(response?.data) ? response.data : []
state.loaded = true
@@ -291,7 +296,7 @@ const loadWardMenus = async () => {
state.orders = []
state.failed = true
state.loaded = true
uni.showToast({ title: error?.message || "用餐记录加载失败", icon: "none" })
uni.showToast({ title: error?.message || t("mobile.order.wardMenu.ordersLoadFail"), icon: "none" })
} finally {
if (currentSequence === state.requestSequence) state.loading = false
}
@@ -300,7 +305,7 @@ const loadWardMenus = async () => {
/** 切换查看对象后同步页面身份、标题并重新加载对应用餐记录;重置数据避免闪现上一位的记录。 */
const onWardUserChange = (user: { userId: string; name: string; isSelf: boolean }) => {
state.wardUserId = user.userId
state.wardName = user.name || (user.isSelf ? "本人" : "被监护人")
state.wardName = user.name || t(user.isSelf ? "mobile.order.wardMenu.selfLabel" : "mobile.order.wardMenu.wardFallback")
state.currentUser = user.isSelf
state.orders = []
state.loaded = false
@@ -322,7 +327,7 @@ const openMenuMap = () => {
onLoad((options: WardMenuRouteOptions) => {
state.wardUserId = String(options?.wardUserId || "")
state.currentUser = options?.currentUser === "1"
state.wardName = state.currentUser && !options?.wardName ? "本人" : decodeRouteText(options?.wardName)
state.wardName = state.currentUser && !options?.wardName ? t("mobile.order.wardMenu.selfLabel") : decodeRouteText(options?.wardName)
uni.setNavigationBarTitle({ title: pageTitle.value })
// 未携带查看对象时等待用户卡片自动选中本人后经 change 事件触发加载。
if (state.wardUserId) loadWardMenus()
+60 -36
View File
@@ -3,38 +3,40 @@
<view class="pb-32rpx pt-14rpx" :aria-busy="state.loading">
<!-- 查看对象筛选:与每日用餐页同款用户卡片,切换后重新加载当月地图 -->
<ward-menu-user-card :default-user-id="state.wardUserId" :default-name="state.wardName"
:default-is-self="state.currentUser" :refreshing="state.loading" subtitle="每日用餐总览"
@change="onWardUserChange" @refresh="loadMap" />
:default-is-self="state.currentUser" :refreshing="state.loading"
:subtitle="t('mobile.order.wardMenu.subtitleMap')" @change="onWardUserChange" @refresh="loadMap" />
<!-- 月份切换 + 图例 -->
<view class="card mt-14rpx rounded-20rpx bg-white px-20rpx py-18rpx">
<view class="flex items-center justify-between">
<view class="h-52rpx w-52rpx flex-row-center rounded-14rpx bg-#f5f8fb active:opacity-75"
:class="{ 'opacity-30!': !canPrev }" role="button" aria-label="上一个月" @tap="changeMonth(-1)">
:class="{ 'opacity-30!': !canPrev }" role="button"
:aria-label="t('mobile.order.wardMenu.prevMonthAria')" @tap="changeMonth(-1)">
<tm-icon name="arrow-left-s-line" :size="26" color="#334155" />
</view>
<view class="sub-text-28 font-700">{{ monthLabel }}</view>
<view class="h-52rpx w-52rpx flex-row-center rounded-14rpx bg-#f5f8fb active:opacity-75"
:class="{ 'opacity-30!': !canNext }" role="button" aria-label="下一个月" @tap="changeMonth(1)">
:class="{ 'opacity-30!': !canNext }" role="button"
:aria-label="t('mobile.order.wardMenu.nextMonthAria')" @tap="changeMonth(1)">
<tm-icon name="arrow-right-s-line" :size="26" color="#334155" />
</view>
</view>
<view class="mt-16rpx flex flex-wrap items-center justify-center gap-x-24rpx gap-y-8rpx">
<view class="flex items-center gap-8rpx">
<view class="h-12rpx w-12rpx rounded-full bg-#f5a623" />
<text class="second-text-20">待核销</text>
<text class="second-text-20">{{ t('mobile.order.wardMenu.legendPending') }}</text>
</view>
<view class="flex items-center gap-8rpx">
<view class="h-12rpx w-12rpx rounded-full bg-#12b76a" />
<text class="second-text-20">已核销</text>
<text class="second-text-20">{{ t('mobile.order.wardMenu.legendVerified') }}</text>
</view>
<view class="flex items-center gap-8rpx">
<view class="h-12rpx w-12rpx rounded-full bg-#1570ef" />
<text class="second-text-20">线下等</text>
<text class="second-text-20">{{ t('mobile.order.wardMenu.legendOffline') }}</text>
</view>
<view class="flex items-center gap-8rpx">
<view class="h-12rpx w-12rpx rounded-full bg-#e4e7ec" />
<text class="second-text-20">无记录</text>
<text class="second-text-20">{{ t('mobile.order.wardMenu.legendNone') }}</text>
</view>
</view>
</view>
@@ -42,12 +44,13 @@
<!-- 日历地图网格 -->
<view class="card mt-14rpx rounded-20rpx bg-white px-10rpx py-14rpx">
<view class="grid grid-cols-7 pb-12rpx">
<view v-for="week in WEEKDAY_HEADERS" :key="week" class="text-center text-21rpx color-#98a2b3">
<view v-for="week in weekdayHeaders" :key="week" class="text-center text-21rpx color-#98a2b3">
{{ week }}
</view>
</view>
<view v-if="state.loading" class="grid grid-cols-7 gap-6rpx" aria-label="预约地图加载中">
<view v-if="state.loading" class="grid grid-cols-7 gap-6rpx"
:aria-label="t('mobile.order.wardMenu.mapLoadingAria')">
<view v-for="index in 35" :key="index" class="map-cell flex-col-center">
<base-skeleton width="58%" height="88" round="10" color="#e8edf3" dark-color="#323232" />
</view>
@@ -57,9 +60,10 @@
<view class="h-96rpx w-96rpx flex-row-center rounded-full bg-#fff4e8">
<tm-icon name="error-warning-line" :size="44" color="#d97706" />
</view>
<view class="sub-text-24 mt-14rpx">预约地图加载失败</view>
<view class="second-text-20 mt-6rpx">请检查网络后重新加载</view>
<tm-button class="mt-16rpx px-28rpx" size="s" skin="thin" @tap="loadMap">重新加载</tm-button>
<view class="sub-text-24 mt-14rpx">{{ t('mobile.order.wardMenu.mapLoadFail') }}</view>
<view class="second-text-20 mt-6rpx">{{ t('mobile.order.wardMenu.checkNetworkReload') }}</view>
<tm-button class="mt-16rpx px-28rpx" size="s" skin="thin" @tap="loadMap">{{
t('mobile.order.wardMenu.reload') }}</tm-button>
</view>
<template v-else>
@@ -68,7 +72,7 @@
class="map-cell active:opacity-80" :class="{
'map-cell--empty': !cell.blank && !cell.pending && !cell.verified && !cell.offline,
'map-cell--today': !cell.blank && cell.isToday,
}" role="button" :aria-label="cell.blank ? undefined : `${cell.date},待核销 ${cell.pending} 单,已核销 ${cell.verified} 单,线下等 ${cell.offline} 单,点击查看当日明细`"
}" role="button" :aria-label="cell.blank ? undefined : t('mobile.order.wardMenu.cellAria', { date: cell.date, pending: cell.pending, verified: cell.verified, offline: cell.offline })"
@tap="openDayDetail(cell)">
<template v-if="!cell.blank">
<view class="map-cell__day" :class="{ 'map-cell__day--today': cell.isToday }">
@@ -76,15 +80,15 @@
</view>
<view v-if="cell.pending || cell.verified || cell.offline" class="map-cell__status">
<view v-if="cell.pending" class="map-pill map-pill--pending">
<text>待</text>
<text>{{ t('mobile.order.wardMenu.pillPending') }}</text>
<text class="font-800">{{ cell.pending }}</text>
</view>
<view v-if="cell.verified" class="map-pill map-pill--verified">
<text>已</text>
<text>{{ t('mobile.order.wardMenu.pillVerified') }}</text>
<text class="font-800">{{ cell.verified }}</text>
</view>
<view v-if="cell.offline" class="map-pill map-pill--offline">
<text>线</text>
<text>{{ t('mobile.order.wardMenu.pillOffline') }}</text>
<text class="font-800">{{ cell.offline }}</text>
</view>
</view>
@@ -95,11 +99,18 @@
<!-- 月度汇总 -->
<view v-if="monthTotals.pending || monthTotals.verified || monthTotals.offline"
class="mt-14rpx flex flex-wrap items-center justify-center gap-x-24rpx gap-y-6rpx rounded-12rpx bg-#f2f7fc py-12rpx text-21rpx color-#667081">
<text>本月待核销 <text class="font-700 color-#b45309">{{ monthTotals.pending }}</text> 单</text>
<text>已核销 <text class="font-700 color-#027a48">{{ monthTotals.verified }}</text> 单</text>
<text>线下等 <text class="font-700 color-#1570ef">{{ monthTotals.offline }}</text> 单</text>
<text>{{ t('mobile.order.wardMenu.monthTotalPendingPrefix') }} <text
class="font-700 color-#b45309">{{
monthTotals.pending }}</text> {{ t('mobile.order.wardMenu.monthTotalSuffix') }}</text>
<text>{{ t('mobile.order.wardMenu.monthTotalVerifiedPrefix') }} <text
class="font-700 color-#027a48">{{
monthTotals.verified }}</text> {{ t('mobile.order.wardMenu.monthTotalSuffix') }}</text>
<text>{{ t('mobile.order.wardMenu.monthTotalOfflinePrefix') }} <text
class="font-700 color-#1570ef">{{
monthTotals.offline }}</text> {{ t('mobile.order.wardMenu.monthTotalSuffix') }}</text>
</view>
<view v-else class="mt-14rpx text-center text-21rpx color-#98a2b3">本月暂无用餐记录</view>
<view v-else class="mt-14rpx text-center text-21rpx color-#98a2b3">{{
t('mobile.order.wardMenu.monthEmpty') }}</view>
</template>
</view>
</view>
@@ -109,14 +120,16 @@
<tm-drawer v-model:show="state.showDayDrawer" position="bottom" size="72%" :title="drawerTitle" :show-close="true"
:show-footer="false" :show-cancel="false" :width-cover-center="true" bg-color="#eef2f7">
<view class="pb-20rpx">
<view v-if="state.dayLoading" class="flex-col-center py-48rpx" aria-label="当日明细加载中">
<view v-if="state.dayLoading" class="flex-col-center py-48rpx"
:aria-label="t('mobile.order.wardMenu.dayLoadingAria')">
<tm-icon name="loader-4-line" :size="44" color="#0094ff" spin />
<view class="second-text-22 mt-12rpx">正在加载当日明细</view>
<view class="second-text-22 mt-12rpx">{{ t('mobile.order.wardMenu.dayLoading') }}</view>
</view>
<view v-else-if="state.dayFailed" class="flex-col-center py-40rpx text-center">
<view class="second-text-21">当日明细加载失败</view>
<tm-button class="mt-14rpx px-28rpx" size="s" skin="thin" @tap="loadDayOrders">重新加载</tm-button>
<view class="second-text-21">{{ t('mobile.order.wardMenu.dayLoadFail') }}</view>
<tm-button class="mt-14rpx px-28rpx" size="s" skin="thin" @tap="loadDayOrders">{{
t('mobile.order.wardMenu.reload') }}</tm-button>
</view>
<!-- 与每日用餐页共用同一张日卡,全类型订单带“已核销/已完成”徽标 -->
@@ -126,7 +139,7 @@
<view class="h-52rpx w-52rpx flex-row-center shrink-0 rounded-14rpx bg-#f1f5f9">
<tm-icon name="calendar-event-line" :size="26" color="#94a3b8" />
</view>
<view class="ml-12rpx sub-text-24 font-700">这一天暂无用餐记录</view>
<view class="ml-12rpx sub-text-24 font-700">{{ t('mobile.order.wardMenu.dayEmpty') }}</view>
</view>
</view>
</tm-drawer>
@@ -135,12 +148,21 @@
<script setup lang="ts">
import { computed, reactive } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import { useI18n } from "vue-i18n"
import dayjs from "dayjs"
import { orderApi } from "@@/api"
import WardMenuUserCard from "@/sheep/components/order/ward-menu-user-card.vue"
import WardMenuDayCard from "@/sheep/components/order/ward-menu-day-card.vue"
import { formatWeekdayText, type WardMenuOrder } from "@/sheep/components/order/ward-menu"
import {
formatDayTitle,
formatMonthTitle,
formatWeekdayText,
mapWeekdayHeaders,
type WardMenuOrder,
} from "@/sheep/components/order/ward-menu"
const { t } = useI18n()
/** 后端返回的单日聚合数据。 */
type WardMapDay = {
@@ -206,14 +228,16 @@ const state = reactive({
/** 日明细请求序号,避免较早响应覆盖较新的日期。 */
let dayRequestSequence = 0
const WEEKDAY_HEADERS = ["日", "一", "二", "三", "四", "五", "六"] as const
/** 当前语言的日历表头短星期。 */
const weekdayHeaders = computed(() => mapWeekdayHeaders())
const todayText = dayjs().format("YYYY-MM-DD")
/** 月份浏览范围:过去 12 个月到未来 2 个月,避免无界查询。 */
const minMonth = dayjs().subtract(12, "month").format("YYYY-MM")
const maxMonth = dayjs().add(2, "month").format("YYYY-MM")
const pageTitle = computed(() => (state.currentUser ? "我的用餐地图" : "被监护人用餐地图"))
const monthLabel = computed(() => dayjs(`${state.month}-01`).format("YYYY年M月"))
const pageTitle = computed(() =>
t(state.currentUser ? "mobile.order.wardMenu.mapMyTitle" : "mobile.order.wardMenu.mapWardTitle"))
const monthLabel = computed(() => formatMonthTitle(`${state.month}-01`))
const canPrev = computed(() => state.month > minMonth)
const canNext = computed(() => state.month < maxMonth)
@@ -270,7 +294,7 @@ const loadMap = async () => {
endDate: monthStart.endOf("month").format("YYYY-MM-DD"),
})
if (sequence !== state.requestSequence) return
if (response?.code !== 0) throw new Error(response?.msg || "查询用餐地图失败")
if (response?.code !== 0) throw new Error(response?.msg || t("mobile.order.wardMenu.queryMapFail"))
state.days = Array.isArray(response?.data) ? response.data : []
state.loaded = true
} catch (error: any) {
@@ -279,7 +303,7 @@ const loadMap = async () => {
state.days = []
state.failed = true
state.loaded = true
uni.showToast({ title: error?.message || "用餐地图加载失败", icon: "none" })
uni.showToast({ title: error?.message || t("mobile.order.wardMenu.mapLoadFail"), icon: "none" })
} finally {
if (sequence === state.requestSequence) state.loading = false
}
@@ -296,7 +320,7 @@ const changeMonth = (delta: number) => {
/** 切换查看对象后同步页面身份、标题并重新加载当月地图;重置数据避免闪现上一位的分布。 */
const onWardUserChange = (user: { userId: string; name: string; isSelf: boolean }) => {
state.wardUserId = user.userId
state.wardName = user.name || (user.isSelf ? "本人" : "被监护人")
state.wardName = user.name || t(user.isSelf ? "mobile.order.wardMenu.selfLabel" : "mobile.order.wardMenu.wardFallback")
state.currentUser = user.isSelf
state.days = []
state.loaded = false
@@ -307,7 +331,7 @@ const onWardUserChange = (user: { userId: string; name: string; isSelf: boolean
/** 抽屉标题:日期 + 星期,如“9月18日 · 星期四”。 */
const drawerTitle = computed(() =>
state.activeDay ? `${dayjs(state.activeDay).format("M月D日")} · ${formatWeekdayText(state.activeDay)}` : "",
state.activeDay ? `${formatDayTitle(state.activeDay)} · ${formatWeekdayText(state.activeDay)}` : "",
)
/** 点击某日格子打开明细抽屉并查询当天订单。 */
@@ -331,14 +355,14 @@ const loadDayOrders = async () => {
date: state.activeDay,
})
if (sequence !== dayRequestSequence) return
if (response?.code !== 0) throw new Error(response?.msg || "查询当日明细失败")
if (response?.code !== 0) throw new Error(response?.msg || t("mobile.order.wardMenu.queryDayFail"))
state.dayOrders = Array.isArray(response?.data) ? response.data : []
} catch (error: any) {
if (sequence !== dayRequestSequence) return
console.error("查询用餐当日明细失败", error)
state.dayOrders = []
state.dayFailed = true
uni.showToast({ title: error?.message || "当日明细加载失败", icon: "none" })
uni.showToast({ title: error?.message || t("mobile.order.wardMenu.dayLoadFail"), icon: "none" })
} finally {
if (sequence === dayRequestSequence) state.dayLoading = false
}
+43 -16
View File
@@ -121,35 +121,47 @@ const query = (page: number, pagesize: number) => {
}
/**
* 切换全部、收入或支出筛选,并从第一页重新加载;筛选变化后收起已展开的备注。
* 切换全部、收入或支出筛选,并从第一页重新加载;筛选变化后恢复默认展开。
*/
const doTab = (e: any) => {
if (state.pa.type != e) {
state.pa.type = e
expandedRemarkKey.value = null
collapsedRemarkKeys.value = new Set()
pageRef.value.reload(true)
}
}
/** 当前展开备注的流水键,手风琴模式下同屏仅展开一条。 */
const expandedRemarkKey = ref<string | null>(null)
/** 当前被收起的流水键集合;备注默认全部展开,这里只记录用户手动收起的行。 */
const collapsedRemarkKeys = ref<Set<string>>(new Set())
/** 流水记录是否带有可展示备注;空白备注不提供展开交互。 */
const hasRemark = (v: any) => Boolean(String(v?.remark || "").trim())
/** 流水记录是否带有系统备注;模板生成的交易描述,空白内容不参与展开交互。 */
const hasSysRemark = (v: any) => Boolean(String(v?.sysRemark || "").trim())
/** 流水记录是否带有人工备注。 */
const hasUserRemark = (v: any) => Boolean(String(v?.remark || "").trim())
/** 流水记录是否带有任一可展示备注;空白备注不提供展开交互。 */
const hasRemark = (v: any) => hasSysRemark(v) || hasUserRemark(v)
/** 用流水 ID 生成展开键,历史数据缺失 ID 时退回列表下标。 */
const remarkKey = (v: any, i: number) => String(v?.id ?? i)
/** 判断当前流水备注是否已展开。 */
const isRemarkOpen = (v: any, i: number) => expandedRemarkKey.value === remarkKey(v, i)
/** 判断当前流水备注是否展开;未被手动收起的行默认展开。 */
const isRemarkOpen = (v: any, i: number) => !collapsedRemarkKeys.value.has(remarkKey(v, i))
/**
* 点击带备注的流水切换展开;手风琴效果下再次点击或切换其他行会收起当前备注。
* 点击带备注的流水切换展开状态;默认展开后首次点击为收起该行,再次点击恢复。
*/
const toggleRemark = (v: any, i: number) => {
if (!hasRemark(v)) return
const key = remarkKey(v, i)
expandedRemarkKey.value = expandedRemarkKey.value === key ? null : key
const next = new Set(collapsedRemarkKeys.value)
if (next.has(key)) {
next.delete(key)
} else {
next.add(key)
}
collapsedRemarkKeys.value = next
}
</script>
@@ -201,14 +213,21 @@ const toggleRemark = (v: any, i: number) => {
{{ hooks.useTime(v.createTime, "") }}
</text>
<tm-icon v-if="hasRemark(v)" class="m-l-8rpx"
:name="isRemarkOpen(v, i) ? 'arrow-up-s-line' : 'arrow-down-s-line'" :size="22" color="#999999" />
:name="isRemarkOpen(v, i) ? 'arrow-up-s-line' : 'arrow-down-s-line'" :size="22"
color="#999999" />
</view>
</view>
<view v-if="hasRemark(v)" class="tx-remark-wrap"
:class="isRemarkOpen(v, i) ? 'tx-remark-wrap--open' : ''">
<view class="tx-remark-panel p-x-20rpx p-t-20rpx p-b-24rpx">
<view class="second-text-22">{{ t('mobile.wallet.txRemarkLabel') }}</view>
<view class="tx-remark-content sub-text-26">{{ v.remark }}</view>
<view v-if="hasSysRemark(v)" class="tx-remark-block">
<view class="second-text-22">{{ t('mobile.wallet.txSysRemarkLabel') }}</view>
<view class="tx-remark-content sub-text-26">{{ v.sysRemark }}</view>
</view>
<view v-if="hasUserRemark(v)" class="tx-remark-block">
<view class="second-text-22">{{ t('mobile.wallet.txRemarkLabel') }}</view>
<view class="tx-remark-content sub-text-26">{{ v.remark }}</view>
</view>
</view>
</view>
</view>
@@ -222,8 +241,9 @@ const toggleRemark = (v: any, i: number) => {
background: #f7fafc;
}
/* 手风琴容器:max-height 过渡在 H5、Android、iOS WebView 上均可靠收起;
上限 400rpx 约容纳 10 行备注,超长内容由 overflow 截断。 */
/* 折叠容器:默认展开(初始渲染即带 --open,无首帧动画),max-height 过渡
在 H5、Android、iOS WebView 上均可靠收起;上限 800rpx 约容纳 256 字
系统备注加常见人工备注,超长内容由 overflow 截断。 */
.tx-remark-wrap {
max-height: 0;
overflow: hidden;
@@ -231,7 +251,7 @@ const toggleRemark = (v: any, i: number) => {
}
.tx-remark-wrap--open {
max-height: 400rpx;
max-height: 800rpx;
}
.tx-remark-panel {
@@ -239,6 +259,13 @@ const toggleRemark = (v: any, i: number) => {
border-top: 1rpx solid #edf2f7;
}
/* 系统备注与人工备注同屏时以虚线分隔:区分区块性质,且不与面板实线边界混淆。 */
.tx-remark-block+.tx-remark-block {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e2e8f0;
}
/* 备注原文完整展示,超长连续字符也允许换行。 */
.tx-remark-content {
margin-top: 6rpx;
+235 -45
View File
@@ -12,6 +12,7 @@ import { usePayStore } from "@@/stores/pay"
import { useUserStore } from "@@/stores/user"
import InformationStreamAd from "@@/components/ad/information-stream-ad.vue"
import WardMenuUserCard from "@/sheep/components/order/ward-menu-user-card.vue"
const { t } = useI18n()
@@ -20,6 +21,20 @@ const state = reactive<any>({
showFreezeDialog: false,
showQrCodeDialog: false,
currentCard: {},
/** 当前查看对象是否本人;钱包列表仅本人可查,关系人展示饭卡摘要。 */
currentUser: true,
/** 关系人查看目标用户 ID。 */
wardUserId: "",
/** 关系人展示名称,用于页面标题。 */
wardName: "",
/** 关系人饭卡摘要(监护充值上下文),含余额、脱敏卡号与状态。 */
wardContext: null as any,
/** 饭卡摘要请求是否进行中。 */
wardLoading: false,
/** 饭卡摘要是否至少完成过一次请求,用于区分首载骨架与刷新。 */
wardLoaded: false,
/** 饭卡摘要最近一次请求是否失败。 */
wardFailed: false,
})
/** 当前登录用户及其钱包身份配置,充值入口只允许命中直属商户配置的身份使用。 */
const userStore = useUserStore()
@@ -37,20 +52,124 @@ const freezeForm = reactive<any>({
remark: "",
})
/** 页面标题:本人显示我的钱包,关系人显示“{name}的钱包”。 */
const pageTitle = computed(() => (state.currentUser
? t("mobile.wallet.listTitle")
: t("mobile.wallet.relationListTitle", { name: state.wardName || t("mobile.relation.identityWard") })))
onLoad(() => { })
onShow(async () => {
const isLogin = await hooks.useCheckLogin()
if (!isLogin) return
await userStore._getUserZsMch()
if (pageRef.value) {
pageRef.value.reload(false)
// 本人视图刷新钱包列表;关系人视图从充值页返回时同步刷新饭卡余额。
if (state.currentUser) {
if (pageRef.value) {
pageRef.value.reload(false)
}
} else if (state.wardUserId) {
loadWardContext()
}
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, walletApi.all, null, "data.records")
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa)
/** 列表请求进行中状态,驱动卡片刷新图标。 */
const refreshing = ref(false)
const query = async (page: number, pagesize: number) => {
refreshing.value = true
try {
await pageHook.startPage(page, pagesize, false, state.pa)
} finally {
refreshing.value = false
}
}
/** 卡片刷新图标状态:跟随当前视图(钱包列表或饭卡摘要)的请求。 */
const cardRefreshing = computed(() => (state.currentUser ? refreshing.value : state.wardLoading))
/**
* 查询关系人饭卡摘要:后端暂无关系人钱包列表接口,复用监护充值上下文
* 下发的饭卡余额、脱敏卡号、状态与充值开关。
*/
const loadWardContext = async () => {
if (!state.wardUserId || state.currentUser) return
state.wardLoading = true
state.wardFailed = false
try {
const response: any = await walletApi.guardianWardRechargeContext({ wardUserId: state.wardUserId })
if (response?.code !== 0) throw new Error(response?.msg || t("mobile.wallet.queryWardCardFail"))
state.wardContext = response?.data || null
} catch (error: any) {
console.error("查询关系人饭卡摘要失败", error)
state.wardContext = null
state.wardFailed = true
uni.showToast({ title: error?.message || t("mobile.wallet.queryWardCardFail"), icon: "none" })
} finally {
state.wardLoaded = true
state.wardLoading = false
}
}
/** 饭卡状态协议编码到标签的映射;0未激活 1激活 2挂失 3冻结 4锁卡 5销卡。 */
const wardStatusTag = computed(() => {
const status = String(state.wardContext?.ward?.cardStatus ?? "")
const metaMap: Record<string, { key: string; color: string }> = {
0: { key: "wardCardInactive", color: "info" },
1: { key: "wardCardActive", color: "success" },
2: { key: "wardCardLost", color: "error" },
3: { key: "statusFrozen", color: "error" },
4: { key: "wardCardLocked", color: "error" },
5: { key: "wardCardCancelled", color: "error" },
}
const selected = metaMap[status] || { key: "wardCardUnknown", color: "info" }
return { text: t(`mobile.wallet.${selected.key}`), color: selected.color }
})
/** 卡片刷新:本人刷新钱包列表,关系人刷新饭卡摘要。 */
const refreshWallets = () => {
if (state.currentUser) {
pageRef.value?.reload(true)
} else {
loadWardContext()
}
}
/** 卡片首次 change 只用于确立默认对象(自动选中本人),跳过重复刷新。 */
let cardTargetInitialized = false
/** 切换查看对象:本人回到钱包列表,关系人加载饭卡摘要并同步标题。 */
const onWardUserChange = (user: { userId: string; name: string; isSelf: boolean }) => {
state.currentUser = user.isSelf
state.wardUserId = user.isSelf ? "" : user.userId
state.wardName = user.name || t(user.isSelf ? "mobile.order.wardMenu.selfLabel" : "mobile.order.wardMenu.wardFallback")
state.wardContext = null
state.wardLoaded = false
state.wardFailed = false
uni.setNavigationBarTitle({ title: pageTitle.value })
if (!cardTargetInitialized) {
cardTargetInitialized = true
return
}
if (user.isSelf) {
pageRef.value?.reload(true)
} else {
loadWardContext()
}
}
/** 查看关系人钱包流水,走 detail 页的关系人受保护模式。 */
const goWardTransactions = () => {
router.jump("/pages/wallet/detail", {
relationFlow: "1",
wardUserId: state.wardUserId,
wardName: encodeURIComponent(state.wardName),
})
}
/** 为关系人饭卡充值,复用充值页的监护模式。 */
const goWardRecharge = () => {
router.jump("/pages/wallet/recharge", { wardUserId: state.wardUserId })
}
const openBindDialog = (v: any) => {
@@ -155,59 +274,130 @@ const canRecharge = (wallet: any) => wallet?.walletTypeUniqueCode == 1
</script>
<template>
<crud-layout :title="t('mobile.wallet.listTitle')" bg="/static/page/blue-bg.png">
<crud-layout :title="pageTitle" bg="/static/page/blue-bg.png">
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query"
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
<view class="h-30rpx" />
<view class="card flex-col rounded-20rpx p-x-30rpx p-y-20rpx m-b-30rpx" v-for="(v, i) in pageHook.list"
:key="i" @tap="router.jump('/pages/wallet/detail', { id: v.id })">
<view class="flex-col">
<view class="flex-row items-center justify-between">
<view class="flex-row-center">
<text v-if="v.cardNumber" class="second-text-28">{{ t('mobile.wallet.cardNumber') }} {{
v.cardNumber }}</text>
<template #top>
<ward-menu-user-card :default-user-id="state.wardUserId" :default-name="state.wardName"
:default-is-self="state.currentUser" :refreshing="cardRefreshing"
:subtitle="t('mobile.wallet.userCardSubtitle')" @change="onWardUserChange"
@refresh="refreshWallets" />
<view class="h-20rpx" />
</template>
<!-- 本人视图:钱包列表(原有行为) -->
<template v-if="state.currentUser">
<view class="card flex-col rounded-20rpx p-x-30rpx p-y-20rpx m-b-30rpx" v-for="(v, i) in pageHook.list"
:key="i" @tap="router.jump('/pages/wallet/detail', { id: v.id })">
<view class="flex-col">
<view class="flex-row items-center justify-between">
<view class="flex-row-center">
<text v-if="v.cardNumber" class="second-text-28">{{ t('mobile.wallet.cardNumber') }} {{
v.cardNumber }}</text>
</view>
<view class="second-text-50">
<text v-if="v.cardNumber" class="iconfont icon-fuzhi"
@tap.stop="hooks.useCopy(v.cardNumber)" />
<text v-if="v.walletTypeUniqueCode != '2'" class="m-l-20rpx iconfont icon-erweima"
@tap.stop="doPreviewQrCode(v)" />
</view>
</view>
<view class="second-text-50">
<text v-if="v.cardNumber" class="iconfont icon-fuzhi"
@tap.stop="hooks.useCopy(v.cardNumber)" />
<text v-if="v.walletTypeUniqueCode != '2'" class="m-l-20rpx iconfont icon-erweima"
@tap.stop="doPreviewQrCode(v)" />
<view v-if="v.cardNumber" class="h-1px m-t-10rpx bg-#999 bg-opacity-5" />
</view>
<view class="flex-row items-center justify-between m-t-10rpx">
<view class="flex-row items-center">
<image class="size-50rpx rounded-full" :src="filePath(v.walletTypeIcon)"
mode="aspectFill" />
<text class="m-l-25rpx sub-text-28">
{{ v.walletTypeName }}
</text>
</view>
<view v-if="v.cardNumber">
<tm-tag v-if="v.isFreeze == 0" size="s" color="success">{{ t('mobile.wallet.statusNormal')
}}</tm-tag>
<tm-tag v-if="v.isFreeze == 1" size="s" color="error">{{ t('mobile.wallet.statusFrozen')
}}</tm-tag>
</view>
</view>
<view class="flex-row items-center justify-between m-t-10rpx">
<text class="sub-text-40 color-red!"> ¥{{ hooks.useMoney(v.balance) }} </text>
</view>
<view class="flex-row items-center justify-end space-x-20rpx m-t-10rpx">
<!-- <view v-if="v.walletTypeUniqueCode == 1 && !v.cardNumber" class="botder_btn" @tap.stop="openBindDialog(v)">{{ t('mobile.wallet.bind') }}</view> -->
<view v-if="v.isRule == '1'" class="botder_btn" @tap.stop="doTransferMoney(v)">{{
t('mobile.wallet.transfer') }}</view>
<view v-if="v.walletTypeUniqueCode == 1 && v.cardNumber && v.isFreeze != 1" class="botder_btn"
@tap.stop="openFreezeDialog(v)">
{{ t('mobile.wallet.applyFreeze') }}
</view>
<view v-if="canRecharge(v)" class="botder_btn"
@tap.stop="router.jump('/pages/wallet/recharge', { id: v.id })">
{{ t('mobile.wallet.goRecharge') }}
</view>
</view>
<view v-if="v.cardNumber" class="h-1px m-t-10rpx bg-#999 bg-opacity-5" />
</view>
<view class="flex-row items-center justify-between m-t-10rpx">
<view class="flex-row items-center">
<image class="size-50rpx rounded-full" :src="filePath(v.walletTypeIcon)" mode="aspectFill" />
</template>
<!-- 关系人视图:饭卡摘要(后端暂无关系人钱包列表接口) -->
<template v-else>
<view v-if="state.wardLoading && !state.wardLoaded"
class="card flex-col rounded-20rpx p-x-30rpx p-y-20rpx m-b-30rpx">
<base-skeleton width="30%" height="26" round="7" color="#e8edf3" dark-color="#323232" />
<base-skeleton class="m-t-16rpx" width="46%" height="34" round="8" color="#e8edf3"
dark-color="#323232" />
<base-skeleton class="m-t-14rpx" width="60%" height="48" round="10" color="#dcecff"
dark-color="#323232" />
</view>
<view v-else-if="state.wardFailed"
class="card flex-col-center rounded-20rpx bg-white p-y-48rpx text-center">
<view class="h-96rpx w-96rpx flex-row-center rounded-full bg-#fff4e8">
<tm-icon name="error-warning-line" :size="44" color="#d97706" />
</view>
<view class="sub-text-24 m-t-14rpx">{{ t('mobile.wallet.queryWardCardFail') }}</view>
<tm-button class="m-t-16rpx p-x-28rpx" size="s" skin="thin" @tap="loadWardContext">{{
t('mobile.wallet.reload') }}</tm-button>
</view>
<view v-else-if="!state.wardContext?.ward?.walletOpened"
class="card flex items-center rounded-20rpx bg-white p-x-18rpx p-y-20rpx m-b-30rpx">
<view class="h-52rpx w-52rpx flex-row-center shrink-0 rounded-14rpx bg-#f1f5f9">
<tm-icon name="bowl-line" :size="26" color="#94a3b8" />
</view>
<view class="m-l-12rpx min-w-0">
<view class="sub-text-24 font-700">{{ t('mobile.wallet.wardNoCard') }}</view>
<view class="second-text-20 m-t-2rpx show-line">{{ t('mobile.wallet.wardNoCardDesc') }}</view>
</view>
</view>
<view v-else class="card flex-col rounded-20rpx p-x-30rpx p-y-20rpx m-b-30rpx">
<view class="flex-row items-center justify-between">
<text v-if="state.wardContext.ward.maskedCardNumber" class="second-text-28">
{{ t('mobile.wallet.cardNumber') }} {{ state.wardContext.ward.maskedCardNumber }}
</text>
<tm-tag :color="wardStatusTag.color" size="s">{{ wardStatusTag.text }}</tm-tag>
</view>
<view v-if="state.wardContext.ward.maskedCardNumber" class="h-1px m-t-10rpx bg-#999 bg-opacity-5" />
<view class="flex-row items-center m-t-10rpx">
<view class="h-50rpx w-50rpx flex-row-center rounded-full bg-#eef8ff">
<tm-icon name="bank-card-line" :size="26" color="#0094ff" />
</view>
<text class="m-l-25rpx sub-text-28">
{{ v.walletTypeName }}
{{ state.wardContext.ward.walletTypeName || t('mobile.wallet.wardCardFallback') }}
</text>
</view>
<view v-if="v.cardNumber">
<tm-tag v-if="v.isFreeze == 0" size="s" color="success">{{ t('mobile.wallet.statusNormal')
}}</tm-tag>
<tm-tag v-if="v.isFreeze == 1" size="s" color="error">{{ t('mobile.wallet.statusFrozen')
}}</tm-tag>
<view class="flex-row items-center justify-between m-t-10rpx">
<text class="sub-text-40 color-red!"> ¥{{ hooks.useMoney(state.wardContext.ward.balance) }}
</text>
</view>
<view class="flex-row items-center justify-end space-x-20rpx m-t-10rpx">
<view class="botder_btn" @tap="goWardTransactions">{{ t('mobile.wallet.viewTransactions') }}
</view>
<view v-if="state.wardContext.ward.rechargeEnabled" class="botder_btn" @tap="goWardRecharge">
{{ t('mobile.wallet.goRecharge') }}
</view>
</view>
</view>
<view class="flex-row items-center justify-between m-t-10rpx">
<text class="sub-text-40 color-red!"> ¥{{ hooks.useMoney(v.balance) }} </text>
</view>
<view class="flex-row items-center justify-end space-x-20rpx m-t-10rpx">
<!-- <view v-if="v.walletTypeUniqueCode == 1 && !v.cardNumber" class="botder_btn" @tap.stop="openBindDialog(v)">{{ t('mobile.wallet.bind') }}</view> -->
<view v-if="v.isRule == '1'" class="botder_btn" @tap.stop="doTransferMoney(v)">{{
t('mobile.wallet.transfer') }}</view>
<view v-if="v.walletTypeUniqueCode == 1 && v.cardNumber && v.isFreeze != 1" class="botder_btn"
@tap.stop="openFreezeDialog(v)">
{{ t('mobile.wallet.applyFreeze') }}
</view>
<view v-if="canRecharge(v)" class="botder_btn"
@tap.stop="router.jump('/pages/wallet/recharge', { id: v.id })">
{{ t('mobile.wallet.goRecharge') }}
</view>
</view>
</view>
</template>
<template #bottom>
<information-stream-ad v-if="!state.showQrCodeDialog" />
</template>
@@ -66,6 +66,24 @@ const isLoginPage = computed(() => {
return router.isLoginPage()
})
/** 左侧按钮是否占据导航位。 */
const hasLeftSlot = computed(() =>
Boolean(props.leftText) || (props.showLeft && !isTabbar.value && !isLoginPage.value))
/**
* 标题可用宽度:左右按钮为绝对定位不参与布局,这里按其存在与形态
* 动态预留两侧安全距离,超长标题在剩余宽度内单行省略。
*/
const titleStyle = computed(() => {
const left = props.leftText ? 140 : (hasLeftSlot.value ? 96 : 24)
const right = props.rightText ? 180 : (props.rightIcon ? 96 : 24)
return {
color: props.titleColor,
fontWeight: props.titleFontWeight,
maxWidth: `calc(100% - ${left + right}rpx)`,
}
})
function doBackClick() {
router.back()
}
@@ -80,7 +98,7 @@ function doBackClick() {
<text class="sicon-back icon" v-if="isHistiry" />
<text class="sicon-home icon" v-else />
</view>
<view class="navbar_title" :style="{ color: titleColor, fontWeight: props.titleFontWeight }">
<view class="navbar_title" :style="titleStyle">
{{ props.title }}
</view>
<view v-if="props.rightIcon || props.rightText" class="navbar_right" @tap="emit('rightClick')">
@@ -140,6 +158,9 @@ function doBackClick() {
&_title {
font-size: 30rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from "vue"
import { useI18n } from "vue-i18n"
import { hooks, router } from "@@/index"
import {
@@ -24,6 +25,8 @@ import {
type WardMenuOrder,
} from "@/sheep/components/order/ward-menu"
const { t } = useI18n()
/** 组件属性;每日用餐页与地图页历史明细共用同一张日卡。 */
const props = withDefaults(
defineProps<{
@@ -72,7 +75,7 @@ const totalPrice = computed(() => sortedOrders.value.reduce((total, order) => to
*/
const goOrderDetail = (order: WardMenuOrder) => {
if (!order.id) {
uni.showToast({ title: "订单信息缺失,暂时无法查看详情", icon: "none" })
uni.showToast({ title: t("mobile.order.wardMenu.missingOrderInfo"), icon: "none" })
return
}
router.jump("/pages/order/detail", { id: order.id })
@@ -93,25 +96,25 @@ const goOrderDetail = (order: WardMenuOrder) => {
</view>
</view>
<view class="ml-12rpx shrink-0 rounded-full bg-#eef8ff px-13rpx py-7rpx text-20rpx color-#0094ff font-700">
{{ sortedOrders.length }} 笔
{{ t('mobile.order.wardMenu.orderCount', { count: sortedOrders.length }) }}
</view>
</view>
<view class="mt-14rpx grid grid-cols-4 rounded-12rpx bg-#f2f7fc px-4rpx py-12rpx">
<view class="date-summary-item">
<view class="date-summary-value">{{ reserveCount }}</view>
<view>预约</view>
<view>{{ t('mobile.order.wardMenu.summaryReserve') }}</view>
</view>
<view class="date-summary-item">
<view class="date-summary-value">{{ otherCount }}</view>
<view>其他</view>
<view>{{ t('mobile.order.wardMenu.summaryOthers') }}</view>
</view>
<view class="date-summary-item">
<view class="date-summary-value">{{ foodCount }}</view>
<view>份数</view>
<view>{{ t('mobile.order.wardMenu.daySummaryPortions') }}</view>
</view>
<view class="date-summary-item border-r-0!">
<view class="date-summary-value text-18rpx!">¥{{ hooks.useMoney(totalPrice) }}</view>
<view>金额</view>
<view>{{ t('mobile.order.wardMenu.summaryAmount') }}</view>
</view>
</view>
</view>
@@ -126,7 +129,7 @@ const goOrderDetail = (order: WardMenuOrder) => {
<view v-if="isGroupFirst(orderIndex)" class="mb-10rpx flex items-center gap-8rpx">
<view class="h-6rpx w-24rpx rounded-full" :class="isReserveOrder(order) ? 'bg-#0094ff' : 'bg-#1570ef'" />
<text class="text-20rpx font-700 color-#475569">
{{ isReserveOrder(order) ? `预约订单 · ${reserveCount} 笔` : `其他消费 · ${otherCount} 笔` }}
{{ isReserveOrder(order) ? t('mobile.order.wardMenu.reserveGroupTitle', { count: reserveCount }) : t('mobile.order.wardMenu.otherGroupTitle', { count: otherCount }) }}
</text>
</view>
<view
@@ -140,10 +143,10 @@ const goOrderDetail = (order: WardMenuOrder) => {
<view class="ml-12rpx flex min-w-0 shrink-0 items-center gap-8rpx">
<view v-if="isVerifiedOrder(order)"
class="shrink-0 rounded-full bg-#e2f5ea px-10rpx py-3rpx text-18rpx font-700 color-#027a48">
{{ isReserveOrder(order) ? "已核销" : "已完成" }}
{{ isReserveOrder(order) ? t('mobile.order.wardMenu.verifiedBadge') : t('mobile.order.wardMenu.completedBadge') }}
</view>
<view class="show-line max-w-200rpx text-20rpx color-#64748b">
{{ order.merchantName || "食堂商户" }}
{{ order.merchantName || t('mobile.order.wardMenu.merchantFallback') }}
</view>
</view>
</view>
@@ -154,15 +157,15 @@ const goOrderDetail = (order: WardMenuOrder) => {
:key="productKey(product, productIndex)"
class="flex items-center border-b-1rpx border-#edf1f5 border-b-solid py-14rpx last:border-b-0 active:opacity-70"
role="button"
:aria-label="`查看${product.productName || '菜品'}所属订单详情`"
:aria-label="t('mobile.order.wardMenu.productAria', { name: product.productName || t('mobile.order.wardMenu.productFallback') })"
@tap.stop="goOrderDetail(order)"
>
<image class="h-86rpx w-86rpx shrink-0 rounded-12rpx bg-#f1f5f9" :src="productImage(product)" mode="aspectFill" />
<view class="ml-13rpx min-w-0 flex-1">
<view class="sub-text-24 show-line font-700">{{ product.productName || "未命名菜品" }}</view>
<view class="second-text-19 mt-4rpx"> ¥{{ hooks.useMoney(product.productPrice || 0) }} / 份 </view>
<view class="sub-text-24 show-line font-700">{{ product.productName || t('mobile.order.wardMenu.productFallback') }}</view>
<view class="second-text-19 mt-4rpx"> ¥{{ hooks.useMoney(product.productPrice || 0) }} {{ t('mobile.order.wardMenu.perPortion') }} </view>
<view v-if="refundedProductCount(product)" class="mt-3rpx text-18rpx color-#64748b">
原购 {{ originalProductCount(product) }} 份 · 已退款 {{ refundedProductCount(product) }} 份
{{ t('mobile.order.wardMenu.originalPortions', { count: originalProductCount(product) }) }} · {{ t('mobile.order.wardMenu.refundedPortions', { count: refundedProductCount(product) }) }}
</view>
</view>
<view class="ml-12rpx shrink-0 text-right">
@@ -173,14 +176,14 @@ const goOrderDetail = (order: WardMenuOrder) => {
</view>
</view>
<view v-else class="second-text-21 px-16rpx py-22rpx text-center">
{{ order.productDesc || "暂无菜品明细" }}
{{ order.productDesc || t('mobile.order.wardMenu.noProducts') }}
</view>
<view class="flex items-center justify-between bg-#fafcff px-16rpx py-12rpx">
<view class="second-text-19">
{{ orderTimeText(order) }}{{ orderFoodCount(order) ? ` · 共 ${orderFoodCount(order)} 份` : "" }}
{{ orderTimeText(order) }}{{ orderFoodCount(order) ? ` · ${t('mobile.order.wardMenu.totalPortions', { count: orderFoodCount(order) })}` : "" }}
</view>
<view class="text-22rpx color-#f97316 font-800">合计 ¥{{ hooks.useMoney(effectiveOrderPrice(order)) }}</view>
<view class="text-22rpx color-#f97316 font-800">{{ t('mobile.order.wardMenu.orderTotal', { amount: hooks.useMoney(effectiveOrderPrice(order)) }) }}</view>
</view>
</view>
</view>
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue"
import { useI18n } from "vue-i18n"
import dayjs from "dayjs"
const { t } = useI18n()
/** 日历条中的单个日期项。 */
interface DayItem {
/** 日期,格式 YYYY-MM-DD;脏数据日期使用固定占位文案。 */
@@ -26,15 +29,16 @@ const emit = defineEmits<{
const instance = getCurrentInstance()
const WEEKDAY_LABELS = ["日", "一", "二", "三", "四", "五", "六"] as const
/** 当前语言的完整短星期文案(如“周日”/“Sun”),按下标取用。 */
const weekdayLabels = computed(() => t("mobile.order.wardMenu.weekdayShort").split(","))
const todayText = dayjs().format("YYYY-MM-DD")
/** 单元格短星期文案;今天显示“今天”,脏数据日期显示“待定”。 */
const weekdayLabel = (item: DayItem, active: boolean) => {
if (isToday(item) && !active) return "今天"
if (item.invalid) return "待定"
if (isToday(item) && !active) return t("mobile.order.wardMenu.todayLabel")
if (item.invalid) return t("mobile.order.wardMenu.pendingShort")
const date = dayjs(item.date)
return date.isValid() ? `周${WEEKDAY_LABELS[date.day()]}` : "待定"
return date.isValid() ? (weekdayLabels.value[date.day()] ?? t("mobile.order.wardMenu.pendingShort")) : t("mobile.order.wardMenu.pendingShort")
}
/** 单元格日数字;脏数据日期展示占位符。 */
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { computed, ref } from "vue"
import { useI18n } from "vue-i18n"
import { walletApi } from "@@/api"
const { t } = useI18n()
/** 可查看菜谱的对象:本人及关系人。 */
interface WardUserOption {
/** 用户 ID,后端 Long 在前端统一按字符串处理。 */
@@ -31,7 +34,7 @@ const props = withDefaults(
defaultUserId: "",
defaultName: "",
defaultIsSelf: false,
subtitle: "未来未核销预约菜谱",
subtitle: "",
refreshing: false,
},
)
@@ -66,7 +69,7 @@ const isUserSelected = (userId: string) => currentUser.value.userId === userId
const loadOptions = async () => {
try {
const response: any = await walletApi.relatedUserList()
if (response?.code !== 0) throw new Error(response?.msg || "查询关系人失败")
if (response?.code !== 0) throw new Error(response?.msg || t("mobile.order.wardMenu.queryRelationsFail"))
const source: any[] = Array.isArray(response?.data) ? response.data : []
const hasMarkedSelf = source.some((item) => Boolean(item?.currentUser))
const mapped = source
@@ -74,7 +77,7 @@ const loadOptions = async () => {
const isSelf = Boolean(item?.currentUser) || (!hasMarkedSelf && index === 0)
return {
userId: String(item?.wardUserId || item?.userId || ""),
name: item?.wardName || item?.name || (isSelf ? "本人" : "关系人"),
name: item?.wardName || item?.name || (isSelf ? t("mobile.order.wardMenu.selfLabel") : t("mobile.order.wardMenu.relationFallback")),
isSelf,
}
})
@@ -130,11 +133,11 @@ const selectUser = (user: WardUserOption) => {
<view
class="ml-13rpx min-w-0 flex-1"
role="button"
:aria-label="canSwitch ? '切换查看对象' : ''"
:aria-label="canSwitch ? t('mobile.order.wardMenu.userSwitchAria') : ''"
@tap="togglePicker"
>
<view class="flex items-center min-w-0">
<text class="sub-text-27 show-line font-700">{{ currentUser.name || '本人' }}</text>
<text class="sub-text-27 show-line font-700">{{ currentUser.name || t('mobile.order.wardMenu.selfLabel') }}</text>
<tm-icon
v-if="canSwitch"
class="ml-6rpx shrink-0 transition-transform duration-200"
@@ -144,13 +147,13 @@ const selectUser = (user: WardUserOption) => {
color="#0094ff"
/>
</view>
<view class="second-text-20 mt-2rpx">{{ subtitle }}</view>
<view class="second-text-20 mt-2rpx">{{ subtitle || t('mobile.order.wardMenu.defaultSubtitle') }}</view>
</view>
</template>
<view
class="h-54rpx w-54rpx flex-row-center shrink-0 rounded-15rpx bg-#f5f8fb active:opacity-75"
role="button"
aria-label="刷新菜谱"
:aria-label="t('mobile.order.wardMenu.userRefreshAria')"
@tap="emit('refresh')"
>
<tm-icon name="reset-right-line" :size="26" color="#64748b" :spin="refreshing" />
@@ -159,7 +162,7 @@ const selectUser = (user: WardUserOption) => {
<!-- 关系人切换面板:内联展开,避免 App 端浮层被滚动容器裁剪 -->
<view v-if="showPicker && canSwitch" class="card mt-10rpx rounded-20rpx bg-white px-16rpx py-16rpx">
<view class="second-text-21 pb-10rpx">选择查看对象</view>
<view class="second-text-21 pb-10rpx">{{ t('mobile.order.wardMenu.pickerTitle') }}</view>
<view class="ward-user-grid">
<view
v-for="item in options"
@@ -175,8 +178,8 @@ const selectUser = (user: WardUserOption) => {
:size="21"
:color="isUserSelected(item.userId) ? '#0094ff' : '#64748b'"
/>
<text class="ward-user-chip__name ml-6rpx min-w-0">{{ item.name }}</text>
<text v-if="item.isSelf" class="ward-user-chip__self ml-5rpx shrink-0">本人</text>
<text class="ward-user-chip__name ml-6rpx min-w-0 show-line-2">{{ item.name }}</text>
<text v-if="item.isSelf" class="ward-user-chip__self ml-5rpx shrink-0">{{ t('mobile.order.wardMenu.selfLabel') }}</text>
</view>
</view>
</view>
@@ -219,14 +222,11 @@ const selectUser = (user: WardUserOption) => {
border-color: #0094ff;
}
/* 名称超长时按 show-line-2 两行截断,避免长名把网格行撑高破坏对齐。 */
.ward-user-chip__name {
flex: 1;
line-height: 1.35;
overflow: visible;
text-align: left;
text-overflow: clip;
white-space: normal;
word-break: break-all;
}
.ward-user-chip__self {
+62 -22
View File
@@ -1,9 +1,39 @@
import dayjs from "dayjs"
import { i18n } from "@/i18n"
import { filePath } from "@@/utils/file"
/** 脏数据日期在聚合时的固定占位文案。 */
export const WARD_MENU_UNSURE_DATE = "日期待确认"
/** wardMenu 文案命名空间前缀。 */
const W = "mobile.order.wardMenu"
/** 取当前语言下的 wardMenu 文案;在渲染或 computed 内调用可跟随语言切换。 */
const wt = (key: string, params?: Record<string, unknown>) =>
String(i18n.global.t(`${W}.${key}`, params as any))
/** 当前语言码;未识别的语言回落到简体中文格式。 */
const currentLocale = () => {
const locale = String(i18n.global.locale.value || "zh-cn")
return locale === "en" || locale === "zh-hk" ? locale : "zh-cn"
}
/** 按语言区分的日期格式串;dayjs 未加载中文 locale,中文直接用字面量字符。 */
const LOCALE_DATE_FORMATS = {
"zh-cn": { monthTitle: "YYYY年M月", dayTitle: "M月D日", cardTitle: "MM月DD日" },
"zh-hk": { monthTitle: "YYYY年M月", dayTitle: "M月D日", cardTitle: "MM月DD日" },
en: { monthTitle: "MMM YYYY", dayTitle: "MMM D", cardTitle: "MMM DD" },
} as const
/** 逗号分隔的多值文案拆分为数组,如星期、日历表头。 */
const splitList = (key: string) => wt(key).split(",")
/**
* 脏数据日期在聚合时的固定占位哨兵:仅作为分组键内部使用,
* 展示层经 unsureDateLabel() 翻译,哨兵文案本身不再直接露出。
*/
export const WARD_MENU_UNSURE_DATE = "__ward_menu_unsure_date__"
/** 脏数据日期的展示文案。 */
export const unsureDateLabel = () => wt("unsureDate")
/** 菜谱订单中的单个菜品明细。 */
export type WardMenuProduct = {
@@ -173,13 +203,13 @@ export type OrderKindMeta = MealMeta
/** 按订单类型生成展示标签;未识别的类型统一落到灰色的“其他消费”。 */
export const orderKindMeta = (order: WardMenuOrder): OrderKindMeta => {
const metaMap: Record<string, Omit<OrderKindMeta, "style"> & { background: string; border: string }> = {
"1": { label: "线下消费", icon: "store-2-line", color: "#1570ef", background: "#eaf3ff", border: "#c5ddfb" },
"9": { label: "消费机", icon: "bank-card-line", color: "#1570ef", background: "#eaf3ff", border: "#c5ddfb" },
"10": { label: "外卖", icon: "motorbike-line", color: "#0e9384", background: "#e6f6f3", border: "#bfe8e0" },
"7": { label: "商城", icon: "shopping-bag-3-line", color: "#6941c6", background: "#f4f0ff", border: "#e0d6fa" },
"1": { label: wt("kindOffline"), icon: "store-2-line", color: "#1570ef", background: "#eaf3ff", border: "#c5ddfb" },
"9": { label: wt("kindMachine"), icon: "bank-card-line", color: "#1570ef", background: "#eaf3ff", border: "#c5ddfb" },
"10": { label: wt("kindTakeout"), icon: "motorbike-line", color: "#0e9384", background: "#e6f6f3", border: "#bfe8e0" },
"7": { label: wt("kindMall"), icon: "shopping-bag-3-line", color: "#6941c6", background: "#f4f0ff", border: "#e0d6fa" },
}
const selected = metaMap[String(order.orderType ?? "")] || {
label: "其他消费",
label: wt("kindOther"),
icon: "coins-line",
color: "#64748b",
background: "#f1f5f9",
@@ -211,13 +241,13 @@ export const isVerifiedOrder = (order: WardMenuOrder) => String(order.orderStatu
/** 根据餐段协议编码生成与订餐统计页一致的视觉标签。 */
export const mealMeta = (order: WardMenuOrder): MealMeta => {
const metaMap: Record<string, Omit<MealMeta, "style"> & { background: string; border: string }> = {
"1": { label: "早餐", icon: "sun-line", color: "#d98900", background: "#fff7e7", border: "#f8dfad" },
"2": { label: "午餐", icon: "bowl-line", color: "#007de3", background: "#edf7ff", border: "#cce8fb" },
"3": { label: "晚餐", icon: "moon-line", color: "#7651d6", background: "#f4f0ff", border: "#e1d8fb" },
"4": { label: "宵夜", icon: "moon-fill", color: "#4f46b8", background: "#eef0ff", border: "#d8dcfa" },
"1": { label: wt("mealBreakfast"), icon: "sun-line", color: "#d98900", background: "#fff7e7", border: "#f8dfad" },
"2": { label: wt("mealLunch"), icon: "bowl-line", color: "#007de3", background: "#edf7ff", border: "#cce8fb" },
"3": { label: wt("mealDinner"), icon: "moon-line", color: "#7651d6", background: "#f4f0ff", border: "#e1d8fb" },
"4": { label: wt("mealNight"), icon: "moon-fill", color: "#4f46b8", background: "#eef0ff", border: "#d8dcfa" },
}
const selected = metaMap[mealCode(order)] || {
label: "预约餐段",
label: wt("mealFallback"),
icon: "bowl-line",
color: "#64748b",
background: "#f1f5f9",
@@ -243,42 +273,52 @@ export const mealTime = (order: WardMenuOrder) => {
const trimSeconds = (value?: string) => (value ? value.slice(0, 5) : "")
const start = trimSeconds(order.apptStartTime)
const end = trimSeconds(order.apptEndTime)
return start && end ? `${start}-${end}` : "时间以预约为准"
return start && end ? `${start}-${end}` : wt("apptTimeFallback")
}
/** 获取订单时间文案:预约单显示预约时段,线下等其他类型显示支付完成时间。 */
export const orderTimeText = (order: WardMenuOrder) => {
if (!isReserveOrder(order)) {
const paidAt = order.payTime ? String(order.payTime).slice(11, 16) : ""
return paidAt ? `${paidAt} 支付` : ""
return paidAt ? wt("paidAt", { time: paidAt }) : ""
}
return mealTime(order)
}
/** 将日期转换为卡片主标题。 */
/** 将日期转换为卡片主标题;今天/明天用自然语言,其余按语言格式化。 */
export const formatDateTitle = (value: string) => {
const date = dayjs(value)
if (!date.isValid()) return value || "日期待确认"
if (date.isSame(dayjs(), "day")) return "今天"
if (date.isSame(dayjs().add(1, "day"), "day")) return "明天"
return date.format("MM月DD日")
if (!date.isValid()) return unsureDateLabel()
if (date.isSame(dayjs(), "day")) return wt("todayLabel")
if (date.isSame(dayjs().add(1, "day"), "day")) return wt("tomorrowLabel")
return date.format(LOCALE_DATE_FORMATS[currentLocale()].cardTitle)
}
/** 将日期转换为星期文案。 */
export const formatWeekdayText = (value: string) => {
const date = dayjs(value)
if (!date.isValid()) return ""
const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]
return weekdays[date.day()]
return splitList("weekdayFull")[date.day()] || ""
}
/** 将日期转换为完整日期和星期副标题。 */
export const formatDateSubTitle = (value: string) => {
const date = dayjs(value)
if (!date.isValid()) return "预约日期待确认"
if (!date.isValid()) return wt("apptDateUnsure")
return `${date.format("YYYY-MM-DD")} · ${formatWeekdayText(value)}`
}
/** 用餐地图月份标题,如“2026年10月”/“Oct 2026”。 */
export const formatMonthTitle = (value: string) =>
dayjs(value).format(LOCALE_DATE_FORMATS[currentLocale()].monthTitle)
/** 用餐地图抽屉标题的日期部分,如“10月18日”/“Oct 18”。 */
export const formatDayTitle = (value: string) =>
dayjs(value).format(LOCALE_DATE_FORMATS[currentLocale()].dayTitle)
/** 用餐地图日历表头短星期数组。 */
export const mapWeekdayHeaders = () => splitList("mapWeekdayHeaders")
/** 获取菜品图片,接口无图时使用项目现有品牌占位图。 */
export const productImage = (product: WardMenuProduct) => filePath(product.productPic) || "/static/logo/zax.png"