feat:多餐段订餐模式
feat:骨架屏
This commit is contained in:
1 parent
ff5ac1dd12
commit
3e710e7ce3
28 files changed
+3673
-255
No files matched your search
@@ -33,12 +33,12 @@ 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_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
|
||||
# ----------------------------- end 开发环境 end ------------------------------------
|
||||
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
"name" : "资安信",
|
||||
"appid" : "__UNI__1BFE88F",
|
||||
"description" : "资安信后勤管理系统",
|
||||
"versionName" : "1.5.06",
|
||||
"versionCode" : 1506,
|
||||
"versionName" : "1.5.08",
|
||||
"versionCode" : 1508,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
@@ -265,6 +265,20 @@
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "daily-menu-confirm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订餐",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "guardian-ward-menu",
|
||||
"style": {
|
||||
"navigationBarTitleText": "被监护人菜谱",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "confirm",
|
||||
"style": {
|
||||
|
||||
@@ -83,13 +83,13 @@
|
||||
</view>
|
||||
<view class="day-summary">
|
||||
<view class="day-summary__item"><text class="day-summary__value">{{ dateGroup.payUserCount
|
||||
}}</text><text>人次</text></view>
|
||||
}}</text><text>人次</text></view>
|
||||
<view class="day-summary__item"><text class="day-summary__value">{{ dateGroup.timeCount
|
||||
}}</text><text>次数</text></view>
|
||||
}}</text><text>次数</text></view>
|
||||
<view class="day-summary__item"><text class="day-summary__value">{{ dateGroup.payCount
|
||||
}}</text><text>份数</text></view>
|
||||
}}</text><text>份数</text></view>
|
||||
<view class="day-summary__item"><text class="day-summary__value day-summary__value--price">{{
|
||||
formatPrice(dateGroup.totalPrice) }}</text><text>金额</text></view>
|
||||
formatPrice(dateGroup.totalPrice) }}</text><text>金额</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<view class="meal-metrics">
|
||||
<view class="meal-metric">
|
||||
<text class="meal-metric__value color-#0094ff">{{ numberValue(row.payUserCount)
|
||||
}}</text>
|
||||
}}</text>
|
||||
<text class="meal-metric__label">人数</text>
|
||||
</view>
|
||||
<view class="meal-metric">
|
||||
@@ -141,53 +141,49 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<tm-drawer v-model:show="filterShow" position="bottom" size="62%" title="筛选预约统计" :show-close="true"
|
||||
:show-footer="true" :show-cancel="false" :disabled-scroll="true" bg-color="#ffffff" @close="closeFilter">
|
||||
<view class="filter-body">
|
||||
<view class="sub-text-26 font-700 mb-12rpx">快捷日期</view>
|
||||
<view class="flex gap-10rpx flex-wrap">
|
||||
<view v-for="preset in datePresets" :key="preset.label" class="filter-chip"
|
||||
:class="{ 'filter-chip--active': isPresetActive(preset) }" @tap="applyPreset(preset)">
|
||||
{{ preset.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sub-text-26 font-700 mt-22rpx mb-10rpx">自定义日期</view>
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<tm-picker-date :model-value="toPickerValue(filterDraft.startDate)" type="day" format="YYYY-MM-DD"
|
||||
:start="datePickerMin" :end="datePickerMax" title="开始日期" @confirm="onStartDateConfirm">
|
||||
<view class="date-picker-box">{{ filterDraft.startDate }}</view>
|
||||
</tm-picker-date>
|
||||
<text class="second-text-22 shrink-0">至</text>
|
||||
<tm-picker-date :model-value="toPickerValue(filterDraft.endDate)" type="day" format="YYYY-MM-DD"
|
||||
:start="datePickerMin" :end="datePickerMax" title="结束日期" @confirm="onEndDateConfirm">
|
||||
<view class="date-picker-box">{{ filterDraft.endDate }}</view>
|
||||
</tm-picker-date>
|
||||
</view>
|
||||
<view class="second-text-20 mt-8rpx">单次最多查询 {{ MAX_RANGE_DAYS }} 天</view>
|
||||
|
||||
<view class="sub-text-26 font-700 mt-22rpx mb-12rpx">用餐时间段</view>
|
||||
<view class="flex gap-10rpx flex-wrap">
|
||||
<view v-for="item in mealOptions" :key="item.value || 'all'"
|
||||
class="filter-chip filter-chip--meal flex items-center"
|
||||
:class="{ 'filter-chip--active': filterDraft.hourType === item.value }"
|
||||
@tap="filterDraft.hourType = item.value">
|
||||
<tm-icon :name="item.icon" :size="22"
|
||||
:color="filterDraft.hourType === item.value ? '#0094ff' : '#64748b'" />
|
||||
<text class="ml-5rpx">{{ item.label }}</text>
|
||||
</view>
|
||||
</crud-layout>
|
||||
<tm-drawer v-model:show="filterShow" position="bottom" size="62%" title="筛选预约统计" :show-close="true"
|
||||
:show-footer="true" :show-cancel="false" :disabled-scroll="true" bg-color="#ffffff" @close="closeFilter">
|
||||
<view class="filter-body">
|
||||
<view class="sub-text-26 font-700 mb-12rpx">快捷日期</view>
|
||||
<view class="flex gap-10rpx flex-wrap">
|
||||
<view v-for="preset in datePresets" :key="preset.label" class="filter-chip"
|
||||
:class="{ 'filter-chip--active': isPresetActive(preset) }" @tap="applyPreset(preset)">
|
||||
{{ preset.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template #footer>
|
||||
<view class="filter-footer w-full flex gap-16rpx" @click.stop>
|
||||
<tm-button skin="thin" class="flex-1" @click="resetFilterDraft">重置</tm-button>
|
||||
<tm-button color="primary" class="flex-1" @click="applyFilter">查看统计</tm-button>
|
||||
<view class="sub-text-26 font-700 mt-22rpx mb-10rpx">自定义日期</view>
|
||||
<view class="flex items-center gap-10rpx">
|
||||
<tm-picker-date :model-value="toPickerValue(filterDraft.startDate)" type="day" format="YYYY-MM-DD"
|
||||
:start="datePickerMin" :end="datePickerMax" title="开始日期" @confirm="onStartDateConfirm">
|
||||
<view class="date-picker-box">{{ filterDraft.startDate }}</view>
|
||||
</tm-picker-date>
|
||||
<text class="second-text-22 shrink-0">至</text>
|
||||
<tm-picker-date :model-value="toPickerValue(filterDraft.endDate)" type="day" format="YYYY-MM-DD"
|
||||
:start="datePickerMin" :end="datePickerMax" title="结束日期" @confirm="onEndDateConfirm">
|
||||
<view class="date-picker-box">{{ filterDraft.endDate }}</view>
|
||||
</tm-picker-date>
|
||||
</view>
|
||||
<view class="second-text-20 mt-8rpx">单次最多查询 {{ MAX_RANGE_DAYS }} 天</view>
|
||||
<view class="sub-text-26 font-700 mt-22rpx mb-12rpx">用餐时间段</view>
|
||||
<view class="flex gap-10rpx flex-wrap">
|
||||
<view v-for="item in mealOptions" :key="item.value || 'all'"
|
||||
class="filter-chip filter-chip--meal flex items-center"
|
||||
:class="{ 'filter-chip--active': filterDraft.hourType === item.value }"
|
||||
@tap="filterDraft.hourType = item.value">
|
||||
<tm-icon :name="item.icon" :size="22"
|
||||
:color="filterDraft.hourType === item.value ? '#0094ff' : '#64748b'" />
|
||||
<text class="ml-5rpx">{{ item.label }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</tm-drawer>
|
||||
</crud-layout>
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<view class="filter-footer w-full flex gap-16rpx" @click.stop>
|
||||
<tm-button skin="thin" class="flex-1" @click="resetFilterDraft">重置</tm-button>
|
||||
<tm-button color="primary" class="flex-1" @click="applyFilter">查看统计</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="tab-list">
|
||||
<view v-if="visibleList.length > 0" class="tab-list">
|
||||
<view v-for="(v, i) in visibleList" :key="v.value" class="tab-item"
|
||||
:class="activeIndex === i ? 'tab-selected' : 'not-selected'" @tap="handleChange(v.value)">
|
||||
<view class="tab-item__content">
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
<template>
|
||||
<view class="bg-white px-30rpx py-24rpx shadow-main">
|
||||
<view v-if="state.loading" class="py-50rpx text-center second-text-26 color-#999">
|
||||
预约餐段加载中
|
||||
<view class="bg-white px-30rpx py-24rpx shadow-main" :aria-busy="state.loading">
|
||||
<view v-if="state.loading" aria-label="预约日期和餐段加载中">
|
||||
<view class="flex gap-12rpx overflow-hidden pb-12rpx">
|
||||
<base-skeleton v-for="index in 4" :key="index" class="shrink-0" width="142" height="92" round="12"
|
||||
color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-12rpx">
|
||||
<view v-for="index in 2" :key="index"
|
||||
class="reservation-option flex min-h-112rpx items-center gap-16rpx py-20rpx">
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="flex items-center gap-12rpx">
|
||||
<base-skeleton width="180" height="28" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
<base-skeleton width="96" height="32" round="8" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="mt-10rpx" width="66%" height="22" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="shrink-0" width="36" height="36" round="18" color="#dcecff"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template v-else-if="dateOptionList.length">
|
||||
<scroll-view scroll-x class="w-full whitespace-nowrap" :show-scrollbar="false">
|
||||
|
||||
@@ -94,22 +94,19 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeOrderTab == 'table-reservation'" class="rounded-b-24rpx overflow-hidden">
|
||||
<table-reservation-time-select class="rounded-b-16rpx" :mch-detail="state.mchDetail"
|
||||
@select="onSelectAppt" />
|
||||
<view class="h-20rpx" />
|
||||
<select-reserve-diners v-model="selectedDinerUserIds"
|
||||
:guardian-ward-enabled="guardianWardReserveEnabled" @change="onDinersChange" />
|
||||
<select-reserve-diners v-model="selectedDinerUserIds" :guardian-ward-enabled="guardianWardReserveEnabled"
|
||||
@change="onDinersChange" />
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<view class="h-20rpx" />
|
||||
<order-simple-car-list :take-out-setting="state?.parentCar?.takeOutSettingJson"
|
||||
:mch-name="state.mchDetail?.name ?? '未知商户'" :car-list="state.cartList" />
|
||||
<view class="h-20rpx" />
|
||||
</view>
|
||||
|
||||
<template #main-bottom>
|
||||
<view class="z-200 p-x-30rpx p-y-30rpx bg-white">
|
||||
<view class="flex items-center justify-between">
|
||||
@@ -124,7 +121,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<check-appt-modal ref="checkApptModalRef" @confirm="doPayTableReservation" />
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<crud-layout title="被监护人菜谱" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view class="pb-32rpx pt-14rpx" :aria-busy="state.loading">
|
||||
<view class="card min-h-92rpx flex items-center rounded-20rpx bg-white px-18rpx py-14rpx">
|
||||
<view class="h-56rpx w-56rpx flex-row-center shrink-0 rounded-16rpx bg-#eef8ff">
|
||||
<tm-icon name="user-line" :size="29" color="#0094ff" />
|
||||
</view>
|
||||
<view class="ml-13rpx min-w-0 flex-1">
|
||||
<view class="sub-text-27 show-line font-700">{{ state.wardName }}</view>
|
||||
<view class="second-text-20 mt-2rpx">未来未核销预约菜谱</view>
|
||||
</view>
|
||||
<view
|
||||
class="h-54rpx w-54rpx flex-row-center shrink-0 rounded-15rpx bg-#f5f8fb active:opacity-75"
|
||||
aria-label="刷新被监护人菜谱"
|
||||
@tap="loadWardMenus"
|
||||
>
|
||||
<tm-icon name="reset-right-line" :size="26" color="#64748b" :spin="state.loading" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<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>
|
||||
<view class="grid grid-cols-4 gap-8rpx">
|
||||
<view
|
||||
v-for="item in summaryItems"
|
||||
:key="item.key"
|
||||
class="min-w-0 flex items-center rounded-14rpx px-8rpx py-14rpx"
|
||||
:style="{ backgroundColor: item.background }"
|
||||
>
|
||||
<tm-icon :name="item.icon" :size="23" :color="item.color" />
|
||||
<view class="ml-7rpx min-w-0">
|
||||
<view class="show-line text-24rpx font-800 leading-31rpx" :style="{ color: item.color }">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
<view class="show-line mt-2rpx text-18rpx color-#94a3b8 leading-24rpx">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</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>
|
||||
|
||||
<view v-if="state.loading && !state.loaded" class="card flex-col-center rounded-20rpx bg-white py-64rpx">
|
||||
<tm-icon name="loader-4-line" :size="44" color="#0094ff" spin />
|
||||
<view class="second-text-24 mt-12rpx">正在加载菜谱</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="state.failed" class="card flex-col-center rounded-20rpx bg-white px-28rpx py-60rpx text-center">
|
||||
<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="second-text-22 mt-6rpx leading-34rpx">请检查网络或监护关系后重新加载</view>
|
||||
<tm-button class="mt-18rpx px-28rpx" size="s" skin="thin" @tap="loadWardMenus">重新加载</tm-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-else-if="state.loaded && !dateGroups.length"
|
||||
class="card flex-col-center rounded-20rpx bg-white px-28rpx py-60rpx text-center"
|
||||
>
|
||||
<view class="h-108rpx w-108rpx flex-row-center rounded-full bg-#f1f5f9">
|
||||
<tm-icon name="bowl-line" :size="52" color="#94a3b8" />
|
||||
</view>
|
||||
<view class="sub-text-26 mt-16rpx">暂无预约菜谱</view>
|
||||
<view class="second-text-22 mt-6rpx leading-34rpx">该被监护人目前没有今天及以后的未核销预约</view>
|
||||
<tm-button class="mt-18rpx px-28rpx" size="s" skin="thin" @tap="loadWardMenus">刷新看看</tm-button>
|
||||
</view>
|
||||
|
||||
<view v-for="dateGroup in dateGroups" :key="dateGroup.date" class="date-card card mb-14rpx overflow-hidden rounded-20rpx bg-white">
|
||||
<view class="date-card__head">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex min-w-0 items-center">
|
||||
<view class="h-52rpx w-52rpx flex-row-center shrink-0 rounded-14rpx bg-#eef8ff">
|
||||
<tm-icon name="calendar-event-line" :size="26" color="#0094ff" />
|
||||
</view>
|
||||
<view class="ml-12rpx min-w-0">
|
||||
<view class="sub-text-27 font-700">{{ formatDateTitle(dateGroup.date) }}</view>
|
||||
<view class="second-text-21 mt-2rpx">{{ formatDateSubTitle(dateGroup.date) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ml-12rpx shrink-0 rounded-full bg-#eef8ff px-13rpx py-7rpx text-20rpx color-#0094ff font-700">
|
||||
{{ dateGroup.orders.length }} 餐
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-14rpx grid grid-cols-3 rounded-12rpx bg-#f2f7fc px-4rpx py-12rpx">
|
||||
<view class="date-summary-item">
|
||||
<view class="date-summary-value">{{ dateGroup.orders.length }}</view>
|
||||
<view>预约</view>
|
||||
</view>
|
||||
<view class="date-summary-item">
|
||||
<view class="date-summary-value">{{ dateGroup.foodCount }}</view>
|
||||
<view>份数</view>
|
||||
</view>
|
||||
<view class="date-summary-item border-r-0!">
|
||||
<view class="date-summary-value text-19rpx!">¥{{ hooks.useMoney(dateGroup.totalPrice) }}</view>
|
||||
<view>金额</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bg-#f2f6fa p-14rpx">
|
||||
<view
|
||||
v-for="(order, orderIndex) in dateGroup.orders"
|
||||
:key="orderKey(order, orderIndex)"
|
||||
class="meal-card mb-12rpx overflow-hidden rounded-18rpx border-1rpx border-#e2eaf2 border-solid bg-white last:mb-0"
|
||||
>
|
||||
<view class="flex items-center justify-between border-b-1rpx border-#edf1f5 border-b-solid px-16rpx py-14rpx">
|
||||
<view class="meal-pill flex min-w-0 items-center border-1rpx border-solid" :style="mealMeta(order).style">
|
||||
<tm-icon :name="mealMeta(order).icon" :size="22" :color="mealMeta(order).color" />
|
||||
<view class="show-line ml-7rpx text-23rpx font-700">{{ mealTitle(order) }}</view>
|
||||
</view>
|
||||
<view class="show-line ml-12rpx max-w-280rpx text-20rpx color-#64748b">
|
||||
{{ order.merchantName || "食堂商户" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="order.carList?.length" class="px-16rpx">
|
||||
<view
|
||||
v-for="(product, productIndex) in order.carList"
|
||||
:key="productKey(product, productIndex)"
|
||||
class="flex items-center border-b-1rpx border-#edf1f5 border-b-solid py-14rpx last:border-b-0"
|
||||
>
|
||||
<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>
|
||||
<view class="ml-12rpx shrink-0 text-right">
|
||||
<view class="sub-text-23 font-700">×{{ normalizedCount(product.productCount) }}</view>
|
||||
<view class="mt-3rpx text-20rpx color-#f97316 font-700"> ¥{{ hooks.useMoney(productTotal(product)) }} </view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="second-text-21 px-16rpx py-22rpx text-center">
|
||||
{{ order.productDesc || "暂无菜品明细" }}
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between bg-#fafcff px-16rpx py-12rpx">
|
||||
<view class="second-text-19"> {{ mealTime(order) }} · 共 {{ orderFoodCount(order) }} 份 </view>
|
||||
<view class="text-22rpx color-#f97316 font-800">合计 ¥{{ hooks.useMoney(order.payPrice || 0) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import { hooks } from "@@/index"
|
||||
import { orderApi } from "@@/api"
|
||||
import { filePath } from "@@/utils/file"
|
||||
|
||||
/** 菜谱订单中的单个菜品明细。 */
|
||||
type WardMenuProduct = {
|
||||
/** 购物车明细 ID,仅用于列表渲染。 */
|
||||
id?: string | number
|
||||
/** 菜品业务 ID,来源于订单购物车快照。 */
|
||||
productId?: string | number
|
||||
/** 下单时保存的菜品名称快照。 */
|
||||
productName?: string
|
||||
/** 下单时保存的菜品图片路径。 */
|
||||
productPic?: string
|
||||
/** 菜品单价,单位为分。 */
|
||||
productPrice?: number
|
||||
/** 预约份数,空值按 0 处理。 */
|
||||
productCount?: number
|
||||
/** 当前菜品小计,单位为分。 */
|
||||
totalPrice?: number
|
||||
}
|
||||
|
||||
/** 被监护人一笔待核销预约订单的只读展示数据。 */
|
||||
type WardMenuOrder = {
|
||||
/** 预约订单 ID,仅用于列表稳定渲染。 */
|
||||
id?: string | number
|
||||
/** 预约使用日期,服务端格式为 yyyy-MM-dd。 */
|
||||
apptUseDate?: string
|
||||
/** 预约餐段标题,优先作为界面餐段名称。 */
|
||||
apptTitle?: string
|
||||
/** 预约配置返回的餐段协议编码。 */
|
||||
apptHourType?: string | number
|
||||
/** 订单快照中的餐段协议编码,用于兼容周期预约。 */
|
||||
hourType?: string | number
|
||||
/** 预约餐段开始时间,格式通常为 HH:mm:ss。 */
|
||||
apptStartTime?: string
|
||||
/** 预约餐段结束时间,格式通常为 HH:mm:ss。 */
|
||||
apptEndTime?: string
|
||||
/** 订单所属食堂商户名称。 */
|
||||
merchantName?: string
|
||||
/** 订单实际支付金额,单位为分。 */
|
||||
payPrice?: number
|
||||
/** 购物车中保存的菜品快照列表。 */
|
||||
carList?: WardMenuProduct[]
|
||||
/** 历史订单在无购物车明细时使用的商品摘要。 */
|
||||
productDesc?: string
|
||||
}
|
||||
|
||||
/** 按预约日期聚合后的菜谱分组。 */
|
||||
type WardMenuDateGroup = {
|
||||
/** 分组日期,格式为 yyyy-MM-dd。 */
|
||||
date: string
|
||||
/** 当日待核销预约订单,按餐段编码升序排列。 */
|
||||
orders: WardMenuOrder[]
|
||||
/** 当日全部预约菜品份数。 */
|
||||
foodCount: number
|
||||
/** 当日预约实付总额,单位为分。 */
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
/** 页面顶部概览指标。 */
|
||||
type WardMenuSummaryItem = {
|
||||
/** 指标唯一标识,仅用于列表渲染。 */
|
||||
key: "orders" | "days" | "foods" | "amount"
|
||||
/** 指标名称。 */
|
||||
label: string
|
||||
/** 已格式化的指标值。 */
|
||||
value: string | number
|
||||
/** TMUI Remix 图标名称。 */
|
||||
icon: string
|
||||
/** 指标图标和值的前景色。 */
|
||||
color: string
|
||||
/** 指标块背景色。 */
|
||||
background: string
|
||||
}
|
||||
|
||||
/** 餐段标签的跨端展示配置。 */
|
||||
type MealMeta = {
|
||||
/** 未提供预约标题时使用的餐段名称。 */
|
||||
label: string
|
||||
/** TMUI Remix 图标名称。 */
|
||||
icon: string
|
||||
/** 餐段标签前景色。 */
|
||||
color: string
|
||||
/** 餐段标签使用的 Vue 行内样式。 */
|
||||
style: Record<string, string>
|
||||
}
|
||||
|
||||
/** 页面路由参数。 */
|
||||
type WardMenuRouteOptions = {
|
||||
/** 被监护人用户 ID,服务端据此执行监护关系权限校验。 */
|
||||
wardUserId?: string
|
||||
/** 经过 URI 编码的被监护人展示名称。 */
|
||||
wardName?: string
|
||||
}
|
||||
|
||||
/** 页面响应式状态,离开页面后无需跨页面持久化。 */
|
||||
type WardMenuPageState = {
|
||||
/** 当前被监护人用户 ID,长整型始终按字符串保存。 */
|
||||
wardUserId: string
|
||||
/** 当前被监护人展示名称。 */
|
||||
wardName: string
|
||||
/** 已查询到的未来未核销预约订单。 */
|
||||
orders: WardMenuOrder[]
|
||||
/** 菜谱请求是否正在执行,用于阻止重复提交。 */
|
||||
loading: boolean
|
||||
/** 最近一次菜谱请求是否失败。 */
|
||||
failed: boolean
|
||||
/** 页面是否至少完成过一次请求,用于区分首屏加载和空数据。 */
|
||||
loaded: boolean
|
||||
/** 当前请求序号,用于丢弃过期响应。 */
|
||||
requestSequence: number
|
||||
}
|
||||
|
||||
/** 页面局部业务状态。 */
|
||||
const state = reactive<WardMenuPageState>({
|
||||
wardUserId: "",
|
||||
wardName: "被监护人",
|
||||
orders: [],
|
||||
loading: false,
|
||||
failed: false,
|
||||
loaded: false,
|
||||
requestSequence: 0,
|
||||
})
|
||||
|
||||
/** 把接口可能返回的空值或非法数字收敛为非负数。 */
|
||||
const normalizedNumber = (value: unknown) => {
|
||||
const parsedValue = Number(value)
|
||||
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : 0
|
||||
}
|
||||
|
||||
/** 把菜品份数收敛为界面可展示的非负整数。 */
|
||||
const normalizedCount = (value: unknown) => Math.floor(normalizedNumber(value))
|
||||
|
||||
/** 计算单个菜品小计,历史数据缺少小计时回退为单价乘份数。 */
|
||||
const productTotal = (product: WardMenuProduct) =>
|
||||
normalizedNumber(product.totalPrice) || normalizedNumber(product.productPrice) * normalizedCount(product.productCount)
|
||||
|
||||
/** 计算一笔预约订单包含的菜品总份数。 */
|
||||
const orderFoodCount = (order: WardMenuOrder) =>
|
||||
(order.carList || []).reduce((total, product) => total + normalizedCount(product.productCount), 0)
|
||||
|
||||
/** 全部待核销预约的菜品总份数。 */
|
||||
const totalFoodCount = computed(() => state.orders.reduce((total, order) => total + orderFoodCount(order), 0))
|
||||
|
||||
/** 全部待核销预约的实付总额,单位为分。 */
|
||||
const totalAmount = computed(() => state.orders.reduce((total, order) => total + normalizedNumber(order.payPrice), 0))
|
||||
|
||||
/**
|
||||
* 按日期聚合预约菜谱,日期异常的数据统一放到末尾,避免单条脏数据阻断整个页面。
|
||||
*/
|
||||
const dateGroups = computed<WardMenuDateGroup[]>(() => {
|
||||
const groupMap = new Map<string, WardMenuOrder[]>()
|
||||
state.orders.forEach((order) => {
|
||||
const date = dayjs(order.apptUseDate).isValid() ? dayjs(order.apptUseDate).format("YYYY-MM-DD") : "日期待确认"
|
||||
const currentOrders = groupMap.get(date) || []
|
||||
currentOrders.push(order)
|
||||
groupMap.set(date, currentOrders)
|
||||
})
|
||||
return Array.from(groupMap.entries())
|
||||
.map(([date, orders]) => {
|
||||
const sortedOrders = [...orders].sort((left, right) => mealCode(left).localeCompare(mealCode(right)))
|
||||
return {
|
||||
date,
|
||||
orders: sortedOrders,
|
||||
foodCount: sortedOrders.reduce((total, order) => total + orderFoodCount(order), 0),
|
||||
totalPrice: sortedOrders.reduce((total, order) => total + normalizedNumber(order.payPrice), 0),
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left.date.localeCompare(right.date))
|
||||
})
|
||||
|
||||
/** 页面顶部四项摘要,结构与订餐统计页保持一致。 */
|
||||
const summaryItems = computed<WardMenuSummaryItem[]>(() => [
|
||||
{ key: "orders", label: "预约", value: state.orders.length, icon: "calendar-check-line", color: "#0094ff", background: "#eef8ff" },
|
||||
{ key: "days", label: "天数", value: dateGroups.value.length, icon: "calendar-event-line", color: "#027a48", background: "#ecfdf3" },
|
||||
{ key: "foods", label: "份数", value: totalFoodCount.value, icon: "bowl-line", color: "#7c3aed", background: "#f4f0ff" },
|
||||
{
|
||||
key: "amount",
|
||||
label: "金额",
|
||||
value: `¥${hooks.useMoney(totalAmount.value)}`,
|
||||
icon: "money-cny-circle-line",
|
||||
color: "#f97316",
|
||||
background: "#fff4e8",
|
||||
},
|
||||
])
|
||||
|
||||
/** 安全解码路由中的展示名称,异常编码回退为原始文案。 */
|
||||
const decodeRouteText = (value?: string) => {
|
||||
if (!value) return "被监护人"
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch (_error) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取订单餐段编码,优先采用预约配置返回值。 */
|
||||
const mealCode = (order: WardMenuOrder) => String(order.apptHourType ?? order.hourType ?? "")
|
||||
|
||||
/** 根据餐段协议编码生成与订餐统计页一致的视觉标签。 */
|
||||
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" },
|
||||
}
|
||||
const selected = metaMap[mealCode(order)] || {
|
||||
label: "预约餐段",
|
||||
icon: "bowl-line",
|
||||
color: "#64748b",
|
||||
background: "#f1f5f9",
|
||||
border: "#dde5ed",
|
||||
}
|
||||
return {
|
||||
label: selected.label,
|
||||
icon: selected.icon,
|
||||
color: selected.color,
|
||||
style: {
|
||||
color: selected.color,
|
||||
backgroundColor: selected.background,
|
||||
borderColor: selected.border,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取预约餐段展示名称,服务端文案为空时按协议编码回退。 */
|
||||
const mealTitle = (order: WardMenuOrder) => order.apptTitle || mealMeta(order).label
|
||||
|
||||
/** 格式化预约时间范围,周期预约没有具体时段时给出稳定兜底。 */
|
||||
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}` : "时间以预约为准"
|
||||
}
|
||||
|
||||
/** 将日期转换为卡片主标题。 */
|
||||
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日")
|
||||
}
|
||||
|
||||
/** 将日期转换为完整日期和星期副标题。 */
|
||||
const formatDateSubTitle = (value: string) => {
|
||||
const date = dayjs(value)
|
||||
if (!date.isValid()) return "预约日期待确认"
|
||||
const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]
|
||||
return `${date.format("YYYY-MM-DD")} · ${weekdays[date.day()]}`
|
||||
}
|
||||
|
||||
/** 获取菜品图片,接口无图时使用项目现有品牌占位图。 */
|
||||
const productImage = (product: WardMenuProduct) => filePath(product.productPic) || "/static/logo/zax.png"
|
||||
|
||||
/** 生成预约订单稳定键,兼容极少量缺少订单 ID 的历史数据。 */
|
||||
const orderKey = (order: WardMenuOrder, index: number) => order.id || `${order.apptUseDate || "unknown"}-${mealCode(order)}-${index}`
|
||||
|
||||
/** 生成菜品稳定键,兼容极少量缺少购物车明细 ID 的历史数据。 */
|
||||
const productKey = (product: WardMenuProduct, index: number) =>
|
||||
product.id || product.productId || `${product.productName || "product"}-${index}`
|
||||
|
||||
/**
|
||||
* 查询被监护人未来未核销预约;后端会再次校验当前登录人和目标用户的有效监护关系。
|
||||
*/
|
||||
const loadWardMenus = async () => {
|
||||
if (state.loading) return
|
||||
if (!state.wardUserId) {
|
||||
state.failed = true
|
||||
state.loaded = true
|
||||
return
|
||||
}
|
||||
const currentSequence = ++state.requestSequence
|
||||
state.loading = true
|
||||
state.failed = false
|
||||
try {
|
||||
const response: any = await orderApi.userApptList({ dinerUserIds: [state.wardUserId] })
|
||||
if (response?.code !== 0) throw new Error(response?.msg || "查询菜谱失败")
|
||||
if (currentSequence !== state.requestSequence) return
|
||||
state.orders = Array.isArray(response?.data) ? response.data : []
|
||||
state.loaded = true
|
||||
} catch (error: any) {
|
||||
if (currentSequence !== state.requestSequence) return
|
||||
console.error("查询被监护人预约菜谱失败", error)
|
||||
state.orders = []
|
||||
state.failed = true
|
||||
state.loaded = true
|
||||
uni.showToast({ title: error?.message || "菜谱加载失败", icon: "none" })
|
||||
} finally {
|
||||
if (currentSequence === state.requestSequence) state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化路由参数并加载目标被监护人的预约菜谱。 */
|
||||
onLoad((options: WardMenuRouteOptions) => {
|
||||
state.wardUserId = String(options?.wardUserId || "")
|
||||
state.wardName = decodeRouteText(options?.wardName)
|
||||
loadWardMenus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.overview-card {
|
||||
padding: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
border: 1rpx solid rgba(215, 230, 244, 0.9);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 5rpx 18rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.date-card {
|
||||
box-shadow: 0 5rpx 18rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.date-card__head {
|
||||
padding: 18rpx 20rpx 16rpx;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.date-summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.25;
|
||||
border-right: 1rpx solid #e2ebf3;
|
||||
}
|
||||
|
||||
.date-summary-value {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #475569;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meal-card {
|
||||
box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.035);
|
||||
}
|
||||
|
||||
.meal-pill {
|
||||
max-width: 260rpx;
|
||||
padding: 7rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -28,6 +28,10 @@ const state = reactive<any>({
|
||||
backBtnType: 'back',
|
||||
orderDetail: {},
|
||||
failMessage: "",
|
||||
/** 批量订餐接口返回的整批实付金额,单位为分;其他支付场景保持为空并展示单笔订单金额。 */
|
||||
batchTotalPayPrice: null as number | null,
|
||||
/** 本次批量支付创建的订单数量,单订单入口保持为 1。 */
|
||||
batchOrderCount: 1,
|
||||
})
|
||||
|
||||
const titleText = computed(() => {
|
||||
@@ -37,7 +41,11 @@ const titleText = computed(() => {
|
||||
|
||||
const descText = computed(() => {
|
||||
if (state.phase === "loading") return "正在查询订单状态,请稍候..."
|
||||
if (state.phase === "success") return "支付金额已到账,可前往钱包查看最新余额"
|
||||
if (state.phase === "success") {
|
||||
return state.batchOrderCount > 1
|
||||
? `已完成 ${state.batchOrderCount} 笔订餐订单,可前往订单列表查看`
|
||||
: "支付金额已到账,可前往钱包查看最新余额"
|
||||
}
|
||||
return state.failMessage || "未查询到本次支付结果,请稍后在支付记录中确认"
|
||||
})
|
||||
|
||||
@@ -58,7 +66,7 @@ const currentAnimationSize = computed(() => {
|
||||
return { width: "280rpx", height: "280rpx" }
|
||||
})
|
||||
|
||||
const payOrderAmount = computed(() => Number(state.orderDetail?.payPrice || 0))
|
||||
const payOrderAmount = computed(() => state.batchTotalPayPrice ?? Number(state.orderDetail?.payPrice || 0))
|
||||
|
||||
const mchNameText = computed(() => state.orderDetail?.merchantName || "--")
|
||||
|
||||
@@ -110,6 +118,14 @@ const getOrder = async (polling = false) => {
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
const batchTotalPayPrice = Number(options.totalPayPrice)
|
||||
if (options.totalPayPrice !== undefined && Number.isFinite(batchTotalPayPrice) && batchTotalPayPrice >= 0) {
|
||||
state.batchTotalPayPrice = batchTotalPayPrice
|
||||
}
|
||||
const batchOrderCount = Number(options.orderCount)
|
||||
if (Number.isInteger(batchOrderCount) && batchOrderCount > 0) {
|
||||
state.batchOrderCount = batchOrderCount
|
||||
}
|
||||
if (options.orderId) {
|
||||
state.orderId = options.orderId || ""
|
||||
state.mchId = options.mchId || ""
|
||||
|
||||
@@ -49,7 +49,12 @@ const query = (page: number, pagesize: number) => {
|
||||
const toDetail = async (v: any) => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (isLogin) {
|
||||
router.jump("/pages/canteen/detail", { id: v.id })
|
||||
const mchTableReservationModeCodeEnum = constant.serviceEnum.mchTableReservationModeCodeEnum
|
||||
if (v.types == '1' && v.tableReservationConf.mode === mchTableReservationModeCodeEnum.DAILY_MENU) {
|
||||
router.jump("/pages/order/daily-menu-confirm", { merchantId: v.id })
|
||||
} else {
|
||||
router.jump("/pages/canteen/detail", { id: v.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,40 +93,17 @@ const inputClear = () => {
|
||||
|
||||
<template>
|
||||
<crud-layout title="商家" bg="/static/page/blue-bg.png">
|
||||
<z-paging
|
||||
ref="pageRef"
|
||||
height="100%"
|
||||
:fixed="false"
|
||||
v-model="pageHook.list"
|
||||
:default-page-size="10"
|
||||
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"
|
||||
>
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
|
||||
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">
|
||||
<template #top>
|
||||
<view class="mch-top">
|
||||
<view class="mch-search">
|
||||
<view class="mch-search__inner">
|
||||
<tm-icon class="mch-search__icon" name="search-line" :size="32" color="#94a3b8" />
|
||||
<tm-input
|
||||
v-model="state.pa.name"
|
||||
class="mch-search__input flex-1"
|
||||
type="text"
|
||||
height="72"
|
||||
minHeight="72"
|
||||
fontSize="28"
|
||||
:showClear="true"
|
||||
:focus="false"
|
||||
placeholder="搜索商家名称"
|
||||
:focus-highlight="false"
|
||||
color="transparent"
|
||||
@confirm="search"
|
||||
@clear="inputClear"
|
||||
/>
|
||||
<tm-input v-model="state.pa.name" class="mch-search__input flex-1" type="text" height="72"
|
||||
minHeight="72" fontSize="28" :showClear="true" :focus="false" placeholder="搜索商家名称"
|
||||
:focus-highlight="false" color="transparent" @confirm="search" @clear="inputClear" />
|
||||
<view class="mch-search__btn" @tap="search">搜索</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -131,26 +113,16 @@ const inputClear = () => {
|
||||
|
||||
<view class="mch-list">
|
||||
<view class="mch-panel">
|
||||
<view
|
||||
v-for="(v, i) in pageHook.list"
|
||||
:key="v.id || i"
|
||||
class="mch-item"
|
||||
:class="{ 'mch-item--last': i === pageHook.list.length - 1 }"
|
||||
@tap="toDetail(v)"
|
||||
>
|
||||
<image
|
||||
class="mch-item__logo"
|
||||
:src="filePath(v.logoPic) || '/static/icon/default-avatar.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-for="(v, i) in pageHook.list" :key="v.id || i" class="mch-item"
|
||||
:class="{ 'mch-item--last': i === pageHook.list.length - 1 }" @tap="toDetail(v)">
|
||||
<image class="mch-item__logo" :src="filePath(v.logoPic) || '/static/icon/default-avatar.png'"
|
||||
mode="aspectFill" />
|
||||
<view class="mch-item__body">
|
||||
<text class="mch-item__name">{{ v.name }}</text>
|
||||
<view v-if="jobTimeLines(v.jobTime).length" class="mch-item__times">
|
||||
<text
|
||||
v-for="(line, idx) in jobTimeLines(v.jobTime)"
|
||||
:key="idx"
|
||||
class="mch-item__time"
|
||||
>{{ line }}</text>
|
||||
<text v-for="(line, idx) in jobTimeLines(v.jobTime)" :key="idx"
|
||||
class="mch-item__time">{{ line
|
||||
}}</text>
|
||||
</view>
|
||||
<text v-if="v.addressDes" class="mch-item__addr">{{ v.addressDes }}</text>
|
||||
</view>
|
||||
|
||||
+30
-10
@@ -9,6 +9,7 @@ import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
|
||||
import AppHotUpdate from "@/sheep/components/app-version/app-hot-update.vue"
|
||||
import OrderCar from "@/sheep/components/order/order-car.vue"
|
||||
import OrderConfirmReceipt from "@/sheep/components/order/order-confirm-receipt.vue"
|
||||
import OrderListSkeleton from "@/sheep/components/order/order-list-skeleton.vue"
|
||||
|
||||
const appHotUpdateRef = ref<any>(null)
|
||||
const orderConfirmReceiptRef = ref<any>(null)
|
||||
@@ -93,10 +94,25 @@ const pageHook = hooks.usePage(
|
||||
null,
|
||||
"data.records",
|
||||
)
|
||||
const query = (page: number, pagesize: number) => {
|
||||
const isLogin = hooks.useStaticCheckLogin()
|
||||
if (!isLogin) return
|
||||
pageHook.startPage(page, pagesize, false, state.pa)
|
||||
/** 当前是否正在加载第一页订单,用于刷新期间以结构占位替换旧列表。 */
|
||||
const firstPageLoading = ref(true)
|
||||
/** 最新第一页请求序号,防止快速切换标签时旧请求提前关闭新请求的骨架。 */
|
||||
let firstPageRequestSequence = 0
|
||||
|
||||
/**
|
||||
* 查询订单分页数据,仅第一页控制整页骨架,后续分页继续沿用 z-paging 的加载状态。
|
||||
*/
|
||||
const query = async (page: number, pagesize: number) => {
|
||||
const isFirstPage = page === 1
|
||||
const requestSequence = isFirstPage ? ++firstPageRequestSequence : firstPageRequestSequence
|
||||
if (isFirstPage) firstPageLoading.value = true
|
||||
try {
|
||||
const isLogin = hooks.useStaticCheckLogin()
|
||||
if (!isLogin) return
|
||||
await pageHook.startPage(page, pagesize, false, state.pa)
|
||||
} finally {
|
||||
if (isFirstPage && requestSequence === firstPageRequestSequence) firstPageLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -104,7 +120,8 @@ const query = (page: number, pagesize: number) => {
|
||||
<crud-layout title="订单" bg="/static/page/blue-bg.png">
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
|
||||
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">
|
||||
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false"
|
||||
:hide-empty-view="firstPageLoading" @scroll="pageHook.scroll">
|
||||
<template #top>
|
||||
<view class="bg-white rounded-16rpx px-20rpx my-20rpx">
|
||||
<z-tabs :list="data.tabs" :scroll-count="4" @change="doTab" />
|
||||
@@ -123,11 +140,14 @@ const query = (page: number, pagesize: number) => {
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="card rounded-24rpx bg-white p-20rpx pb-0rpx mb-20rpx" v-for="(v, index) in pageHook.list"
|
||||
:key="v.id">
|
||||
<order-car :cardetail="v" :footerOperate="['refund', 'qrCodeDetail', 'expressTracking']"
|
||||
@confirmReceipt="orderConfirmReceiptRef.open(v.id, v.carList)" />
|
||||
</view>
|
||||
<order-list-skeleton v-if="firstPageLoading" />
|
||||
<template v-else>
|
||||
<view class="card rounded-24rpx bg-white p-20rpx pb-0rpx mb-20rpx"
|
||||
v-for="(v, index) in pageHook.list" :key="v.id">
|
||||
<order-car :cardetail="v" :footerOperate="['refund', 'qrCodeDetail', 'expressTracking']"
|
||||
@confirmReceipt="orderConfirmReceiptRef.open(v.id, v.carList)" />
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
<order-confirm-receipt ref="orderConfirmReceiptRef" @refresh="query" />
|
||||
<!-- #ifdef APP -->
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view class="guardian-ward-action-drawer-skeleton" aria-label="被监护人操作加载中">
|
||||
<view class="drawer-hero relative overflow-hidden bg-white px-20rpx pb-15rpx pt-6rpx">
|
||||
<view class="relative z-2 flex items-center">
|
||||
<base-skeleton width="74" height="74" round="20" color="#dcecff" dark-color="#323232" />
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<base-skeleton width="46%" height="28" round="8" color="#e8edf3" dark-color="#323232" />
|
||||
<view class="mt-7rpx flex items-center gap-6rpx">
|
||||
<base-skeleton width="64" height="27" round="14" color="#dcecff" dark-color="#323232" />
|
||||
<base-skeleton width="72" height="27" round="14" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="relative z-2 mt-13rpx grid grid-cols-2 gap-9rpx">
|
||||
<view class="rounded-16rpx border-1rpx border-#e8f0f8 border-solid bg-white px-14rpx py-11rpx">
|
||||
<base-skeleton width="42%" height="18" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-6rpx" width="60%" height="29" round="7" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="rounded-16rpx border-1rpx border-#e8f0f8 border-solid bg-white px-14rpx py-11rpx">
|
||||
<base-skeleton width="42%" height="18" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-7rpx" width="74%" height="24" round="7" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="relative z-2 mt-9rpx grid grid-cols-3 gap-7rpx">
|
||||
<view class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<base-skeleton width="30" height="30" round="15" color="#dcecff" dark-color="#323232" />
|
||||
<base-skeleton class="ml-6rpx" width="68" height="19" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-8rpx grid grid-cols-2 gap-10rpx">
|
||||
<base-skeleton width="76%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton width="88%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-16rpx border-1rpx border-#fed7aa border-solid bg-#fffaf3 px-11rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<base-skeleton width="30" height="30" round="15" color="#dcecff" dark-color="#323232" />
|
||||
<base-skeleton class="ml-5rpx" width="62" height="18" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="mt-8rpx" width="72%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="px-20rpx pt-14rpx">
|
||||
<view class="flex items-end justify-between">
|
||||
<view class="min-w-0 flex-1">
|
||||
<base-skeleton width="36%" height="26" round="7" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-5rpx" width="66%" height="18" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton width="48" height="27" round="14" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-10rpx flex-col gap-8rpx">
|
||||
<view
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="min-h-88rpx flex items-center rounded-18rpx border-1rpx border-#e8edf3 border-solid bg-white px-16rpx py-12rpx"
|
||||
>
|
||||
<base-skeleton width="54" height="54" round="16" color="#dcecff" dark-color="#323232" />
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<base-skeleton width="38%" height="25" round="7" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-6rpx" width="78%" height="19" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton width="25" height="25" round="13" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.guardian-ward-action-drawer-skeleton {
|
||||
min-height: 100%;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.drawer-hero {
|
||||
background: linear-gradient(118deg, #ffffff 0%, #ffffff 62%, #eef8ff 100%);
|
||||
}
|
||||
|
||||
.drawer-hero::after {
|
||||
position: absolute;
|
||||
right: -58rpx;
|
||||
top: -90rpx;
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 148, 255, 0.07);
|
||||
content: "";
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,26 @@
|
||||
<template>
|
||||
<tm-drawer v-model:show="show" position="right" size="88%" title="被监护人操作" :show-close="true" :show-footer="false"
|
||||
z-index="9000" bg-color="#f7faff" :content-margin="0">
|
||||
<view v-if="ward" class="drawer-content">
|
||||
<tm-drawer
|
||||
v-model:show="show"
|
||||
position="right"
|
||||
size="88%"
|
||||
title="被监护人操作"
|
||||
:show-close="true"
|
||||
:show-footer="false"
|
||||
z-index="9000"
|
||||
bg-color="#f7faff"
|
||||
:content-margin="0"
|
||||
>
|
||||
<guardian-ward-action-drawer-skeleton v-if="props.loading" />
|
||||
<view v-else-if="ward" class="drawer-content">
|
||||
<view class="drawer-hero relative overflow-hidden bg-white px-20rpx pb-15rpx pt-6rpx">
|
||||
<image class="drawer-hero__image" src="/static/illustration/guardian-action-hero.png" mode="aspectFit"
|
||||
aria-label="被监护人人脸、饭卡与预约服务插画" />
|
||||
<image
|
||||
class="drawer-hero__image"
|
||||
src="/static/illustration/guardian-action-hero.png"
|
||||
mode="aspectFit"
|
||||
aria-label="被监护人人脸、饭卡与预约服务插画"
|
||||
/>
|
||||
<view class="relative z-2 flex items-center">
|
||||
<view
|
||||
class="h-74rpx w-74rpx shrink-0 overflow-hidden rounded-20rpx border-4rpx border-white border-solid bg-#eef8ff">
|
||||
<view class="h-74rpx w-74rpx shrink-0 overflow-hidden rounded-20rpx border-4rpx border-white border-solid bg-#eef8ff">
|
||||
<image class="h-full w-full" :src="avatarUrl" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="ml-14rpx min-w-0 flex-1 pr-148rpx">
|
||||
@@ -26,19 +39,16 @@
|
||||
<view class="relative z-2 mt-13rpx grid grid-cols-2 gap-9rpx">
|
||||
<view class="rounded-16rpx border-1rpx border-#e8f0f8 border-solid bg-white px-14rpx py-11rpx">
|
||||
<view class="second-text-18">饭卡余额</view>
|
||||
<view class="mt-2rpx text-29rpx color-#1f2937 font-800">¥{{ hooks.useMoney(ward.balance || 0) }}
|
||||
</view>
|
||||
<view class="mt-2rpx text-29rpx color-#1f2937 font-800">¥{{ hooks.useMoney(ward.balance || 0) }} </view>
|
||||
</view>
|
||||
<view class="rounded-16rpx border-1rpx border-#e8f0f8 border-solid bg-white px-14rpx py-11rpx">
|
||||
<view class="second-text-18">脱敏卡号</view>
|
||||
<view class="mt-3rpx text-24rpx color-#1f2937 font-700">{{ ward.maskedCardNumber || "暂无饭卡" }}
|
||||
</view>
|
||||
<view class="mt-3rpx text-24rpx color-#1f2937 font-700">{{ ward.maskedCardNumber || "暂无饭卡" }} </view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="relative z-2 mt-9rpx grid grid-cols-3 gap-7rpx">
|
||||
<view
|
||||
class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<view class="h-30rpx w-30rpx flex-row-center rounded-full bg-#e3f3ff">
|
||||
<tm-icon name="emotion-happy-line" :size="17" color="#007ad6" />
|
||||
@@ -46,8 +56,11 @@
|
||||
<view class="ml-6rpx text-19rpx color-#334155 font-700">人脸情况</view>
|
||||
</view>
|
||||
<view class="mt-6rpx grid grid-cols-2">
|
||||
<view v-for="metric in faceMetrics" :key="metric.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#dcecf9 first:border-r-solid last:pl-10rpx">
|
||||
<view
|
||||
v-for="metric in faceMetrics"
|
||||
:key="metric.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#dcecf9 first:border-r-solid last:pl-10rpx"
|
||||
>
|
||||
<view class="show-line text-18rpx color-#64748b">{{ metric.label }}</view>
|
||||
<view class="ml-6rpx shrink-0 text-23rpx font-800" :style="{ color: metric.color }">
|
||||
{{ metric.value }}
|
||||
@@ -71,8 +84,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!ward.rechargeEnabled"
|
||||
class="relative z-2 mt-10rpx flex items-start rounded-15rpx bg-#fff7ed px-13rpx py-9rpx">
|
||||
<view v-if="!ward.rechargeEnabled" class="relative z-2 mt-10rpx flex items-start rounded-15rpx bg-#fff7ed px-13rpx py-9rpx">
|
||||
<tm-icon name="error-warning-line" :size="22" color="#c2410c" />
|
||||
<view class="ml-7rpx flex-1 text-19rpx color-#9a3412 leading-28rpx">
|
||||
{{ ward.disabledReason || "当前饭卡暂不可充值" }}
|
||||
@@ -86,26 +98,25 @@
|
||||
<view class="main-text-26">业务办理</view>
|
||||
<view class="second-text-18 mt-2rpx">选择一项服务,返回后列表自动刷新</view>
|
||||
</view>
|
||||
<view class="rounded-full bg-#eaf5ff px-10rpx py-4rpx text-18rpx color-#007ad6 font-700">3 项</view>
|
||||
<view class="rounded-full bg-#eaf5ff px-10rpx py-4rpx text-18rpx color-#007ad6 font-700"> {{ actions.length }} 项 </view>
|
||||
</view>
|
||||
<view class="mt-10rpx flex-col gap-8rpx">
|
||||
<view v-for="action in actions" :key="action.key"
|
||||
<view
|
||||
v-for="action in actions"
|
||||
:key="action.key"
|
||||
class="min-h-88rpx flex items-center rounded-18rpx border-1rpx border-solid px-16rpx py-12rpx"
|
||||
:class="action.disabled ? 'border-#e5e7eb bg-#f3f4f6 opacity-55' : 'active:opacity-80'"
|
||||
:style="action.disabled ? undefined : { backgroundColor: action.background, borderColor: action.borderColor }"
|
||||
@tap="handleAction(action)">
|
||||
<view class="h-54rpx w-54rpx flex-row-center shrink-0 rounded-16rpx"
|
||||
:style="{ backgroundColor: action.iconBg }">
|
||||
<tm-icon :name="action.icon" :size="27"
|
||||
:color="action.disabled ? '#9ca3af' : action.iconColor" />
|
||||
@tap="handleAction(action)"
|
||||
>
|
||||
<view class="h-54rpx w-54rpx flex-row-center shrink-0 rounded-16rpx" :style="{ backgroundColor: action.iconBg }">
|
||||
<tm-icon :name="action.icon" :size="27" :color="action.disabled ? '#9ca3af' : action.iconColor" />
|
||||
</view>
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<view class="sub-text-25 font-700" :class="action.disabled ? 'color-#9ca3af!' : ''">{{
|
||||
action.title }}</view>
|
||||
<view class="sub-text-25 font-700" :class="action.disabled ? 'color-#9ca3af!' : ''">{{ action.title }}</view>
|
||||
<view class="second-text-19 mt-2rpx show-line">{{ action.description }}</view>
|
||||
</view>
|
||||
<tm-icon name="arrow-right-s-line" :size="25"
|
||||
:color="action.disabled ? '#cbd5e1' : '#0094ff'" />
|
||||
<tm-icon name="arrow-right-s-line" :size="25" :color="action.disabled ? '#cbd5e1' : '#0094ff'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -123,21 +134,35 @@ import { computed } from "vue"
|
||||
import { hooks, router } from "@@/index"
|
||||
import { filePath } from "@@/utils/file"
|
||||
import type { GuardianWardRechargeItem } from "@@/api/wallet"
|
||||
import GuardianWardActionDrawerSkeleton from "./guardian-ward-action-drawer-skeleton.vue"
|
||||
|
||||
type WardAction = {
|
||||
key: "face-upload" | "face-records" | "recharge"
|
||||
/** 操作项唯一标识,仅用于前端分发对应页面跳转。 */
|
||||
key: "menu" | "face-upload" | "face-records" | "recharge"
|
||||
/** 操作项主标题。 */
|
||||
title: string
|
||||
/** 操作项补充说明,禁用时同时用于解释原因。 */
|
||||
description: string
|
||||
/** TMUI Remix 图标名称。 */
|
||||
icon: string
|
||||
/** 图标容器背景色。 */
|
||||
iconBg: string
|
||||
/** 图标前景色。 */
|
||||
iconColor: string
|
||||
/** 可操作状态下的卡片背景色。 */
|
||||
background: string
|
||||
/** 可操作状态下的卡片边框色。 */
|
||||
borderColor: string
|
||||
/** 是否禁止触发当前操作。 */
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
/** 右侧操作抽屉是否显示。 */
|
||||
modelValue: boolean
|
||||
/** 是否正在刷新被监护人数据;加载期间显示结构占位并阻止空态闪烁。 */
|
||||
loading?: boolean
|
||||
/** 当前选中的被监护人饭卡与预约摘要。 */
|
||||
ward?: GuardianWardRechargeItem
|
||||
}>()
|
||||
|
||||
@@ -177,9 +202,23 @@ const faceMetrics = computed(() => [
|
||||
])
|
||||
|
||||
/**
|
||||
* 操作项通过配置统一渲染,避免三类业务入口复制结构和交互状态。
|
||||
* 操作项通过配置统一渲染,避免各类业务入口复制结构和交互状态。
|
||||
*/
|
||||
const actions = computed<WardAction[]>(() => [
|
||||
{
|
||||
key: "menu",
|
||||
title: "查看菜谱",
|
||||
description:
|
||||
Number(props.ward?.pendingReservationCount || 0) > 0
|
||||
? `查看 ${props.ward?.pendingReservationCount} 笔待核销预约的日期、餐段与菜品`
|
||||
: "查看未来未核销预约的日期、餐段与菜品",
|
||||
icon: "bowl-line",
|
||||
iconBg: "#fff1e8",
|
||||
iconColor: "#c2410c",
|
||||
background: "#fffaf5",
|
||||
borderColor: "#fed7aa",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "face-upload",
|
||||
title: "上传人脸",
|
||||
@@ -224,6 +263,10 @@ const handleAction = (action: WardAction) => {
|
||||
const wardName = encodeURIComponent(props.ward.wardName || "被监护人")
|
||||
show.value = false
|
||||
setTimeout(() => {
|
||||
if (action.key === "menu") {
|
||||
router.jump("/pages/order/guardian-ward-menu", { wardUserId, wardName })
|
||||
return
|
||||
}
|
||||
if (action.key === "face-upload") {
|
||||
router.jump("/pages/human-face/guardian-ward-apply", { wardUserId, wardName })
|
||||
return
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view class="mt-14rpx flex-col gap-12rpx pb-32rpx" aria-label="被监护人列表加载中">
|
||||
<view
|
||||
v-for="index in props.count"
|
||||
:key="index"
|
||||
class="overflow-hidden rounded-24rpx border-1rpx border-#e8f0f8 border-solid bg-white px-20rpx pb-14rpx pt-13rpx shadow-main"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<base-skeleton width="72" height="72" round="20" color="#dcecff" dark-color="#323232" />
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<base-skeleton width="42%" height="28" round="8" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-7rpx" width="68%" height="20" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton width="26" height="26" round="13" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-10rpx grid grid-cols-3 gap-8rpx">
|
||||
<view class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<base-skeleton width="30" height="30" round="15" color="#dcecff" dark-color="#323232" />
|
||||
<base-skeleton class="ml-6rpx" width="68" height="19" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-8rpx grid grid-cols-2 gap-10rpx">
|
||||
<base-skeleton width="76%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton width="88%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-16rpx border-1rpx border-#fed7aa border-solid bg-#fffaf3 px-11rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<base-skeleton width="30" height="30" round="15" color="#dcecff" dark-color="#323232" />
|
||||
<base-skeleton class="ml-5rpx" width="62" height="18" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="mt-8rpx" width="72%" height="23" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-10rpx flex items-center justify-between border-t-1rpx border-#edf2f7 border-t-solid pt-10rpx">
|
||||
<base-skeleton width="42%" height="30" round="7" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton width="58" height="21" round="7" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** 被监护人列表骨架组件的输入参数。 */
|
||||
type Props = {
|
||||
/** 首屏占位卡片数量,默认三张以稳定常见手机首屏的滚动高度。 */
|
||||
count?: number
|
||||
}
|
||||
|
||||
/** 列表骨架默认渲染三张与真实被监护人卡片一致的占位卡。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 3,
|
||||
})
|
||||
</script>
|
||||
@@ -1,61 +1,89 @@
|
||||
<template>
|
||||
<crud-layout mainIsScroll title="图谱列表" bg="/static/page/blue-bg.png" navRightText="关系图谱" navRightIcon="records"
|
||||
@navRightClick="openRelationTree">
|
||||
<z-paging ref="pageRef" v-model="wards" height="100%" :fixed="false" :default-page-size="10"
|
||||
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="queryWards"
|
||||
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false">
|
||||
<crud-layout
|
||||
mainIsScroll
|
||||
title="图谱列表"
|
||||
bg="/static/page/blue-bg.png"
|
||||
navRightText="关系图谱"
|
||||
navRightIcon="records"
|
||||
@navRightClick="openRelationTree"
|
||||
>
|
||||
<z-paging
|
||||
ref="pageRef"
|
||||
v-model="wards"
|
||||
height="100%"
|
||||
:fixed="false"
|
||||
:default-page-size="10"
|
||||
safe-area-inset-bottom
|
||||
:loading-more-enabled="true"
|
||||
:show-scrollbar="false"
|
||||
@query="queryWards"
|
||||
:auto-clean-list-when-reload="false"
|
||||
:auto-scroll-to-top-when-reload="false"
|
||||
>
|
||||
<template #top>
|
||||
<view class="pt-16rpx">
|
||||
<view class="guardian-hero relative h-228rpx overflow-hidden rounded-24rpx bg-white shadow-main">
|
||||
<view class="relative z-2 h-full w-62% flex flex-col justify-center pl-22rpx">
|
||||
<view class="flex items-center gap-8rpx">
|
||||
<view class="main-text-28">被监护人服务台</view>
|
||||
<view v-if="queryCompleted"
|
||||
class="shrink-0 rounded-full bg-#eaf5ff px-10rpx py-4rpx text-19rpx color-#007ad6 font-700">
|
||||
<view v-if="queryCompleted" class="shrink-0 rounded-full bg-#eaf5ff px-10rpx py-4rpx text-19rpx color-#007ad6 font-700">
|
||||
{{ totalCount }} 人
|
||||
</view>
|
||||
<base-skeleton v-else width="58" height="28" round="14" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="second-text-20 mt-4rpx leading-30rpx">饭卡、人脸与预约状态集中查看</view>
|
||||
<view class="mt-10rpx grid grid-cols-3 gap-6rpx">
|
||||
<view
|
||||
class="col-span-2 min-w-0 rounded-13rpx border-1rpx border-#d8ecfb border-solid bg-#f7fbff px-9rpx py-6rpx">
|
||||
<view class="col-span-2 min-w-0 rounded-13rpx border-1rpx border-#d8ecfb border-solid bg-#f7fbff px-9rpx py-6rpx">
|
||||
<view class="flex items-center text-16rpx color-#475569 font-700">
|
||||
<tm-icon name="emotion-happy-line" :size="15" color="#007ad6" />
|
||||
<view class="ml-4rpx">人脸情况</view>
|
||||
</view>
|
||||
<view class="mt-2rpx grid grid-cols-2">
|
||||
<view v-for="item in heroFaceSummary" :key="item.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#d8ecfb first:border-r-solid last:pl-7rpx">
|
||||
<view
|
||||
v-for="item in heroFaceSummary"
|
||||
:key="item.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#d8ecfb first:border-r-solid last:pl-7rpx"
|
||||
>
|
||||
<view class="show-line text-15rpx color-#64748b">{{ item.label }}</view>
|
||||
<view class="ml-4rpx text-20rpx font-800" :style="{ color: item.color }">{{
|
||||
item.value }}</view>
|
||||
<view v-if="queryCompleted" class="ml-4rpx text-20rpx font-800" :style="{ color: item.color }">{{ item.value }}</view>
|
||||
<base-skeleton v-else class="ml-4rpx" width="22" height="20" round="6" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="min-w-0 rounded-13rpx border-1rpx border-#fed7aa border-solid bg-#fffaf3 px-8rpx py-6rpx">
|
||||
<view class="min-w-0 rounded-13rpx border-1rpx border-#fed7aa border-solid bg-#fffaf3 px-8rpx py-6rpx">
|
||||
<view class="flex items-center text-15rpx color-#7c4a19 font-700">
|
||||
<tm-icon name="bowl-line" :size="15" color="#c2410c" />
|
||||
<view class="show-line ml-3rpx">预约订单</view>
|
||||
</view>
|
||||
<view class="mt-2rpx flex items-baseline">
|
||||
<view class="text-15rpx color-#9a5b22">待核销</view>
|
||||
<view class="ml-4rpx text-20rpx color-#c2410c font-800">{{
|
||||
pendingReservationTotal }}</view>
|
||||
<view v-if="queryCompleted" class="ml-4rpx text-20rpx color-#c2410c font-800">
|
||||
{{ pendingReservationTotal }}
|
||||
</view>
|
||||
<base-skeleton v-else class="ml-4rpx" width="22" height="20" round="6" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<image class="guardian-hero__image" src="/static/illustration/guardian-service-hero.png"
|
||||
mode="aspectFit" aria-label="监护人陪同被监护人办理饭卡和人脸业务的插画" />
|
||||
<image
|
||||
class="guardian-hero__image"
|
||||
src="/static/illustration/guardian-service-hero.png"
|
||||
mode="aspectFit"
|
||||
aria-label="监护人陪同被监护人办理饭卡和人脸业务的插画"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="wards.length" class="mt-14rpx flex-col gap-12rpx pb-32rpx">
|
||||
<view v-for="ward in wards" :key="String(ward.wardUserId)"
|
||||
<guardian-ward-list-skeleton v-if="!queryCompleted" />
|
||||
|
||||
<view v-else-if="wards.length" class="mt-14rpx flex-col gap-12rpx pb-32rpx">
|
||||
<view
|
||||
v-for="ward in wards"
|
||||
:key="String(ward.wardUserId)"
|
||||
class="overflow-hidden rounded-24rpx border-1rpx border-#e8f0f8 border-solid bg-white px-20rpx pb-14rpx pt-13rpx shadow-main active:bg-#f6fbff"
|
||||
@tap="openActions(ward)">
|
||||
@tap="openActions(ward)"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="h-72rpx w-72rpx shrink-0 overflow-hidden rounded-20rpx bg-#eef8ff">
|
||||
<image class="h-full w-full" :src="avatarUrl(ward)" mode="aspectFill" />
|
||||
@@ -63,8 +91,7 @@
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<view class="flex items-center gap-8rpx">
|
||||
<view class="main-text-28 show-line min-w-0">{{ ward.wardName || "被监护人" }}</view>
|
||||
<view class="shrink-0 rounded-full px-10rpx py-4rpx text-18rpx font-700"
|
||||
:class="statusClass(ward)">
|
||||
<view class="shrink-0 rounded-full px-10rpx py-4rpx text-18rpx font-700" :class="statusClass(ward)">
|
||||
{{ ward.cardStatusName || "状态异常" }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -79,8 +106,7 @@
|
||||
<tm-icon name="arrow-right-s-line" :size="26" color="#94a3b8" />
|
||||
</view>
|
||||
<view class="mt-10rpx grid grid-cols-3 gap-8rpx">
|
||||
<view
|
||||
class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="col-span-2 rounded-16rpx border-1rpx border-#dcecf9 border-solid bg-#f7fbff px-12rpx py-9rpx">
|
||||
<view class="flex items-center">
|
||||
<view class="h-30rpx w-30rpx flex-row-center rounded-full bg-#e3f3ff">
|
||||
<tm-icon name="emotion-happy-line" :size="17" color="#007ad6" />
|
||||
@@ -88,8 +114,11 @@
|
||||
<view class="ml-6rpx text-19rpx color-#334155 font-700">人脸情况</view>
|
||||
</view>
|
||||
<view class="mt-6rpx grid grid-cols-2">
|
||||
<view v-for="metric in faceMetrics(ward)" :key="metric.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#dcecf9 first:border-r-solid last:pl-10rpx">
|
||||
<view
|
||||
v-for="metric in faceMetrics(ward)"
|
||||
:key="metric.key"
|
||||
class="flex min-w-0 items-baseline first:border-r-1rpx first:border-#dcecf9 first:border-r-solid last:pl-10rpx"
|
||||
>
|
||||
<view class="show-line text-18rpx color-#64748b">{{ metric.label }}</view>
|
||||
<view class="ml-6rpx shrink-0 text-23rpx font-800" :style="{ color: metric.color }">
|
||||
{{ metric.value }}
|
||||
@@ -112,12 +141,10 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="mt-10rpx flex items-center justify-between border-t-1rpx border-#edf2f7 border-t-solid pt-10rpx">
|
||||
<view class="mt-10rpx flex items-center justify-between border-t-1rpx border-#edf2f7 border-t-solid pt-10rpx">
|
||||
<view class="flex items-baseline">
|
||||
<view class="second-text-18 mr-8rpx">饭卡余额</view>
|
||||
<view class="text-30rpx color-#202938 font-800">¥{{ hooks.useMoney(ward.balance || 0) }}
|
||||
</view>
|
||||
<view class="text-30rpx color-#202938 font-800">¥{{ hooks.useMoney(ward.balance || 0) }} </view>
|
||||
</view>
|
||||
<view v-if="ward.rechargeEnabled" class="text-21rpx color-#059669 font-700">可充值</view>
|
||||
<view v-else class="show-line ml-16rpx max-w-330rpx text-20rpx color-#c2410c font-600">
|
||||
@@ -128,24 +155,24 @@
|
||||
</view>
|
||||
|
||||
<template #empty>
|
||||
<view class="mt-18rpx rounded-24rpx bg-white px-28rpx py-64rpx shadow-main">
|
||||
<view v-if="queryCompleted" class="mt-18rpx rounded-24rpx bg-white px-28rpx py-64rpx shadow-main">
|
||||
<view class="flex-col-center text-center">
|
||||
<tm-icon name="user-line" :size="54" color="#94a3b8" />
|
||||
<view class="main-text-27 mt-14rpx">暂无有效被监护人</view>
|
||||
<view class="second-text-21 mt-6rpx leading-34rpx">这里只展示当前登录用户直接且有效的监护关系</view>
|
||||
<view
|
||||
class="mt-22rpx h-88rpx min-w-230rpx flex-row-center rounded-full border-1rpx border-blue border-solid bg-white px-30rpx color-#0094ff text-25rpx font-700 active:bg-#eef8ff"
|
||||
@tap="openRelationTree">
|
||||
@tap="openRelationTree"
|
||||
>
|
||||
查看关系图谱
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</z-paging>
|
||||
|
||||
</crud-layout>
|
||||
<!-- App 端 tm-drawer 不会 Teleport,必须放在 crud-layout 外,避免被内容区 scroll-view 裁剪。 -->
|
||||
<guardian-ward-action-drawer v-model="drawerVisible" :ward="selectedWard" />
|
||||
<guardian-ward-action-drawer v-model="drawerVisible" :ward="selectedWard" :loading="!queryCompleted" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -157,11 +184,13 @@ import { walletApi } from "@@/api"
|
||||
import type { GuardianWardRechargeItem } from "@@/api/wallet"
|
||||
import { filePath } from "@@/utils/file"
|
||||
import GuardianWardActionDrawer from "./components/guardian-ward-action-drawer.vue"
|
||||
import GuardianWardListSkeleton from "./components/guardian-ward-list-skeleton.vue"
|
||||
|
||||
const pageRef = ref<any>(null)
|
||||
const wards = ref<GuardianWardRechargeItem[]>([])
|
||||
const wardCache = ref<GuardianWardRechargeItem[]>([])
|
||||
const totalCount = ref(0)
|
||||
/** 首次分页请求是否已经结束,用于控制首屏骨架,成功或失败后都必须退出加载态。 */
|
||||
const queryCompleted = ref(false)
|
||||
const selectedWard = ref<GuardianWardRechargeItem>()
|
||||
const drawerVisible = ref(false)
|
||||
@@ -222,16 +251,16 @@ const faceMetrics = (ward: GuardianWardRechargeItem): FaceMetric[] => [
|
||||
* 接口当前返回全部有效被监护人,页面按 z-paging 的页码切片回填,统一复用项目列表交互。
|
||||
*/
|
||||
const queryWards = async (page: number, pageSize: number) => {
|
||||
const isFirstPage = page === 1
|
||||
try {
|
||||
if (page === 1) {
|
||||
if (isFirstPage) {
|
||||
queryCompleted.value = false
|
||||
const response: any = await walletApi.guardianWardList()
|
||||
if (response?.code !== 0) throw new Error(response?.msg || "查询被监护人饭卡失败")
|
||||
wardCache.value = response?.data || []
|
||||
console.log("wardCache:", wardCache.value);
|
||||
console.log("wardCache:", wardCache.value)
|
||||
|
||||
totalCount.value = wardCache.value.length
|
||||
queryCompleted.value = true
|
||||
if (selectedWard.value) {
|
||||
selectedWard.value = wardCache.value.find((item) => String(item.wardUserId) === String(selectedWard.value?.wardUserId))
|
||||
}
|
||||
@@ -240,11 +269,13 @@ const queryWards = async (page: number, pageSize: number) => {
|
||||
pageRef.value?.complete(wardCache.value.slice(start, start + pageSize))
|
||||
} catch (error) {
|
||||
console.error("查询被监护人饭卡列表失败", error)
|
||||
if (page === 1) {
|
||||
if (isFirstPage) {
|
||||
wardCache.value = []
|
||||
totalCount.value = 0
|
||||
}
|
||||
pageRef.value?.complete(false)
|
||||
} finally {
|
||||
if (isFirstPage) queryCompleted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,24 @@ export default {
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 一次上传按日期餐段拆分的多组购物车,并按请求分组顺序返回父购物车 ID。
|
||||
*/
|
||||
batchSaveShopCar: (data: any) =>
|
||||
request({
|
||||
url: `/hard/hard-car/app/batch-save-or-upload`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 批量上传每日菜谱购物车,商品 ID 由服务端按 canteen_spu.id 解析。
|
||||
*/
|
||||
batchSaveDailyMenuCart: (data: any) =>
|
||||
request({
|
||||
url: `/hard/hard-car/app/daily-menu/batch-save-or-upload`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 预约时间列表
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,14 @@ export default {
|
||||
data,
|
||||
})
|
||||
},
|
||||
/** 使用每日菜谱父购物车一次创建多个日期餐段、多个用餐人的预约订单。 */
|
||||
createDailyMenuReserveOrders: (data: any) => {
|
||||
return request({
|
||||
url: "/hard/hard-order/app/create-daily-menu-reserve-orders",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 创建外卖订单(食堂外送)
|
||||
createTakeOutOrder: (data) => {
|
||||
return request({
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
<template>
|
||||
<tm-drawer v-model:show="drawerShow" position="bottom" :size="props.size" :title="props.title"
|
||||
:show-close="!props.submitting" :show-cancel="false" :show-footer="true" :overlay-click="false" :lazy="true"
|
||||
:z-index="props.zIndex" round="24" bg-color="#f7f9fc" @close="emitClose">
|
||||
<template #title>
|
||||
<text class="payment-drawer-title">{{ props.title }}</text>
|
||||
</template>
|
||||
<view class="payment-review">
|
||||
<view class="payment-review__heading">
|
||||
<view class="payment-review__icon">
|
||||
<tm-icon name="bank-card-line" :size="29" color="#0094ff" />
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="show-line text-22rpx font-700 color-#344054">{{ props.merchantName || "订单支付" }}</view>
|
||||
<view class="mt-2rpx text-19rpx color-#667085">本次应付</view>
|
||||
</view>
|
||||
<view class="payment-amount">
|
||||
<text class="payment-amount__symbol">¥</text>
|
||||
<text>{{ hooks.useMoney(props.totalAmount) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="props.summaryItems.length > 0" class="payment-summary">
|
||||
<view v-for="item in props.summaryItems" :key="item.key" class="payment-summary__item">
|
||||
<text class="payment-summary__value">{{ item.value }}</text>
|
||||
<text class="payment-summary__label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="props.checkItems.length > 0" class="payment-checks">
|
||||
<view class="payment-checks__heading">
|
||||
<text class="sub-text-23 font-700!">支付校验</text>
|
||||
<text class="payment-checks__status"
|
||||
:class="{ 'payment-checks__status--warning': !allChecksPassed }">
|
||||
{{ allChecksPassed ? "全部通过" : "请核对" }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-for="item in props.checkItems" :key="item.key" class="payment-check-row">
|
||||
<view class="payment-check-row__icon"
|
||||
:class="`payment-check-row__icon--${item.status || 'success'}`">
|
||||
<tm-icon :name="checkIcon(item.status)" :size="25" :color="checkColor(item.status)" />
|
||||
</view>
|
||||
<view class="ml-10rpx min-w-0 flex-1">
|
||||
<view class="text-22rpx font-600 color-#344054">{{ item.title }}</view>
|
||||
<view v-if="item.description" class="mt-1rpx text-19rpx color-#667085 show-line">
|
||||
{{ item.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<slot v-if="props.visiblePayCard">
|
||||
<view class="h-12rpx" />
|
||||
<view class="payment-method-panel">
|
||||
<pay-card :visiblePayBtn="false" :wallet-drawer-z-index="walletDrawerZIndex"
|
||||
:allowed-pay-types="props.allowedPayTypes" @selectPayType="selectPayType" />
|
||||
</view>
|
||||
</slot>
|
||||
<template v-else-if="resolvedPayType === FIXED_WALLET_PAY_TYPE">
|
||||
<view class="h-12rpx" />
|
||||
<view class="fixed-wallet-panel" :aria-busy="state.fixedWalletLoading">
|
||||
<view v-if="state.fixedWalletLoading" class="fixed-wallet-panel__row" aria-label="饭卡钱包加载中">
|
||||
<base-skeleton width="56" height="56" round="28" color="#e8edf3" dark-color="#323232" />
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<base-skeleton width="132" height="24" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-8rpx" width="42%" height="18" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton width="104" height="22" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view v-else-if="state.fixedWallet" class="fixed-wallet-panel__row">
|
||||
<view class="fixed-wallet-panel__icon">
|
||||
<tm-icon name="wallet-3-line" :size="29" color="#0094ff" />
|
||||
</view>
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<view class="show-line text-23rpx font-700 color-#344054">{{ fixedWalletName }}</view>
|
||||
<view class="mt-2rpx text-19rpx color-#667085">
|
||||
可用余额 ¥{{ hooks.useMoney(Number(state.fixedWallet.balance || 0)) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="fixed-wallet-panel__selected">
|
||||
<tm-icon name="checkbox-circle-fill" :size="23" color="#027a48" />
|
||||
<text>默认付款</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="fixed-wallet-panel__row fixed-wallet-panel__row--action" hover-class="tap-feedback"
|
||||
@tap="loadFixedWallet">
|
||||
<view class="fixed-wallet-panel__icon fixed-wallet-panel__icon--warning">
|
||||
<tm-icon :name="state.fixedWalletFailed ? 'refresh-line' : 'error-warning-line'" :size="29"
|
||||
color="#d97706" />
|
||||
</view>
|
||||
<view class="ml-14rpx min-w-0 flex-1">
|
||||
<view class="text-23rpx font-700 color-#344054">
|
||||
{{ state.fixedWalletFailed ? "饭卡钱包加载失败" : "未找到饭卡钱包" }}
|
||||
</view>
|
||||
<view class="mt-2rpx text-19rpx color-#667085">
|
||||
{{ state.fixedWalletFailed ? "点击重新加载" : "请先开通饭卡钱包后再支付" }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="flex-shrink-0 text-20rpx font-700 color-#0094ff">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="h-12rpx" />
|
||||
<template #footer>
|
||||
<view class="payment-footer">
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="show-line text-20rpx color-#667085">{{ selectedPayTypeText }}</view>
|
||||
<view class="mt-1rpx flex items-baseline gap-4rpx color-#e5484d">
|
||||
<text class="text-19rpx font-700">¥</text>
|
||||
<text class="text-30rpx font-800 payment-footer__amount">
|
||||
{{ hooks.useMoney(props.totalAmount) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<tm-button width="220" height="80" round="40" shadow="none" :loading="props.submitting"
|
||||
:disabled="confirmDisabled" :aria-label="props.submitting ? '正在支付' : props.confirmText"
|
||||
font-size="25" @click="!confirmDisabled && emitConfirm()">
|
||||
{{ props.submitting ? "支付中" : props.confirmText }}
|
||||
</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from "vue"
|
||||
|
||||
import { constant, hooks } from "@@/index"
|
||||
import { walletApi } from "@@/api"
|
||||
import PayCard from "@@/components/pay/pay-card.vue"
|
||||
|
||||
/** 平台钱包支付编码,属于现有支付接口协议,不可改为钱包类型编码。 */
|
||||
const FIXED_WALLET_PAY_TYPE = "30"
|
||||
|
||||
/** 固定钱包模式默认使用饭卡钱包的业务编码。 */
|
||||
const DEFAULT_WALLET_TYPE_CODE = constant.serviceEnum.walletTypeCodeEnum.EAT
|
||||
|
||||
/** 饭卡钱包的默认展示名称,在接口未返回名称时提供稳定兜底。 */
|
||||
const DEFAULT_WALLET_TYPE_NAME = constant.serviceEnum.walletTypeStatusEnum[DEFAULT_WALLET_TYPE_CODE]
|
||||
|
||||
/** 支付抽屉顶部展示的单项订单摘要。 */
|
||||
export type PaymentSummaryItem = {
|
||||
/** 摘要稳定键,仅用于跨端列表渲染。 */
|
||||
key: string
|
||||
/** 摘要标签,例如“日期餐段”或“用餐人”。 */
|
||||
label: string
|
||||
/** 已格式化的摘要值,由业务页面决定单位和文案。 */
|
||||
value: string
|
||||
}
|
||||
|
||||
/** 支付前校验状态,error 会阻止用户确认支付。 */
|
||||
export type PaymentCheckStatus = "success" | "warning" | "error"
|
||||
|
||||
/** 支付抽屉内展示的一项业务校验结果。 */
|
||||
export type PaymentCheckItem = {
|
||||
/** 校验项稳定键,仅用于跨端列表渲染。 */
|
||||
key: string
|
||||
/** 校验结论标题。 */
|
||||
title: string
|
||||
/** 可选的校验范围或结果补充说明。 */
|
||||
description?: string
|
||||
/** 校验状态;未传时按通过处理。 */
|
||||
status?: PaymentCheckStatus
|
||||
}
|
||||
|
||||
/** 支付抽屉最终确认时回传给业务页面的数据。 */
|
||||
export type PaymentSelection = {
|
||||
/** 用户选择或固定配置的平台支付类型编码。 */
|
||||
payType: string
|
||||
/** 钱包支付时选中的钱包类型 ID,其他支付方式为空。 */
|
||||
walletTypeId: string
|
||||
/** pay-card 返回或固定模式生成的完整支付方式配置,供业务接口读取扩展参数。 */
|
||||
payTypeDetail: any
|
||||
}
|
||||
|
||||
/** 全局支付确认抽屉的输入属性。 */
|
||||
type Props = {
|
||||
/** 抽屉显隐状态,由业务页面通过 v-model:show 控制。 */
|
||||
show?: boolean
|
||||
/** 抽屉标题。 */
|
||||
title?: string
|
||||
/** 当前订单所属商户名称。 */
|
||||
merchantName?: string
|
||||
/** 本次应付金额,单位为分。 */
|
||||
totalAmount?: number
|
||||
/** 订单数量、人数等紧凑摘要。 */
|
||||
summaryItems?: PaymentSummaryItem[]
|
||||
/** 支付前已完成的业务校验结果。 */
|
||||
checkItems?: PaymentCheckItem[]
|
||||
/** 页面是否正在提交支付,用于禁用关闭和防止重复提交。 */
|
||||
submitting?: boolean
|
||||
/** 主操作按钮文案。 */
|
||||
confirmText?: string
|
||||
/** 抽屉高度,沿用 tm-drawer 支持的尺寸格式。 */
|
||||
size?: string | number
|
||||
/** 抽屉层级,默认高于普通购物车抽屉以避免切换动画互相遮挡。 */
|
||||
zIndex?: string | number
|
||||
/** 是否展示支付方式选择器。 */
|
||||
visiblePayCard?: boolean
|
||||
/** 允许展示的平台支付类型编码;空数组表示沿用 pay-card 的全部可用方式。 */
|
||||
allowedPayTypes?: string[]
|
||||
/** 隐藏支付方式选择器时使用的固定平台支付类型,默认 30 表示钱包支付。 */
|
||||
defaultPayType?: string
|
||||
/** 固定钱包支付优先匹配的钱包类型业务编码,默认 1 表示饭卡钱包。 */
|
||||
defaultWalletTypeCode?: string | number
|
||||
/** 固定支付方式在抽屉底部展示的名称。 */
|
||||
defaultPayTypeText?: string
|
||||
}
|
||||
|
||||
/** 支付抽屉对外事件,具体创建订单和扣款仍由业务页面负责。 */
|
||||
const emit = defineEmits<{
|
||||
/** 同步抽屉显隐状态。 */
|
||||
"update:show": [show: boolean]
|
||||
/** 支付方式变化时同步 pay-card 的原始事件参数。 */
|
||||
selectPayType: [type: string, typeValue: any]
|
||||
/** 用户确认支付时回传标准化后的支付选择。 */
|
||||
confirm: [selection: PaymentSelection]
|
||||
/** 抽屉完成关闭动画后通知业务页面。 */
|
||||
close: []
|
||||
}>()
|
||||
|
||||
/** 属性默认值保证组件在缺少业务摘要时仍可独立展示。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
show: false,
|
||||
title: "确认支付",
|
||||
merchantName: "",
|
||||
totalAmount: 0,
|
||||
summaryItems: () => [],
|
||||
checkItems: () => [],
|
||||
submitting: false,
|
||||
confirmText: "确认支付",
|
||||
size: "60%",
|
||||
zIndex: 1200,
|
||||
visiblePayCard: true,
|
||||
allowedPayTypes: () => [],
|
||||
defaultPayType: "30",
|
||||
defaultWalletTypeCode: "1",
|
||||
defaultPayTypeText: "饭卡钱包支付",
|
||||
})
|
||||
|
||||
/** 当前支付方式选择,生命周期归属支付抽屉而非具体业务页面。 */
|
||||
const state = reactive({
|
||||
/** pay-card 当前选中的平台支付类型编码。 */
|
||||
payType: "",
|
||||
/** pay-card 当前选中项的完整配置。 */
|
||||
payTypeDetail: null as any,
|
||||
/** 固定支付模式匹配到的当前用户钱包记录,成功后包含真实 walletTypeId。 */
|
||||
fixedWallet: null as any,
|
||||
/** 固定钱包是否正在加载,期间禁止确认支付。 */
|
||||
fixedWalletLoading: false,
|
||||
/** 固定钱包最近一次请求是否失败,用于区分网络异常和用户未开通钱包。 */
|
||||
fixedWalletFailed: false,
|
||||
/** 固定钱包请求序号,用于丢弃抽屉快速开关或配置切换产生的过期响应。 */
|
||||
fixedWalletRequestSequence: 0,
|
||||
})
|
||||
|
||||
/** 将父页面显隐状态转换为 tm-drawer 所需的双向绑定,支付中不允许关闭。 */
|
||||
const drawerShow = computed({
|
||||
get: () => props.show,
|
||||
set: (show: boolean) => {
|
||||
if (!show && props.submitting) return
|
||||
emit("update:show", show)
|
||||
},
|
||||
})
|
||||
|
||||
/** 是否全部校验项均已通过;空校验列表不显示“全部通过”结论。 */
|
||||
const allChecksPassed = computed(
|
||||
() => props.checkItems.length > 0 && props.checkItems.every((item) => (item.status || "success") === "success"),
|
||||
)
|
||||
|
||||
/** 是否存在会阻止支付的失败校验项。 */
|
||||
const hasBlockingCheck = computed(() => props.checkItems.some((item) => item.status === "error"))
|
||||
|
||||
/** 当前实际使用的平台支付类型;隐藏选择器时使用业务页面传入的固定支付方式。 */
|
||||
const resolvedPayType = computed(() => props.visiblePayCard ? state.payType : String(props.defaultPayType || ""))
|
||||
|
||||
/** 当前实际使用的钱包类型 ID;固定模式必须取匹配到的用户钱包真实 ID。 */
|
||||
const resolvedWalletTypeId = computed(() => props.visiblePayCard
|
||||
? String(state.payTypeDetail?.extra?.walletTypeId || "")
|
||||
: String(state.fixedWallet?.walletTypeId || ""))
|
||||
|
||||
/** 未配置支付方式、钱包仍在加载、钱包类型缺失、校验失败或提交中时禁用主操作。 */
|
||||
const confirmDisabled = computed(() => !resolvedPayType.value
|
||||
|| (!props.visiblePayCard && state.fixedWalletLoading)
|
||||
|| (resolvedPayType.value === "30" && !resolvedWalletTypeId.value)
|
||||
|| hasBlockingCheck.value
|
||||
|| props.submitting)
|
||||
|
||||
/** pay-card 的钱包选择层必须高于当前支付抽屉,避免嵌套 Drawer 被遮挡。 */
|
||||
const walletDrawerZIndex = computed(() => {
|
||||
const currentZIndex = Number(props.zIndex)
|
||||
return Number.isFinite(currentZIndex) ? currentZIndex + 100 : 1300
|
||||
})
|
||||
|
||||
/** 固定钱包名称优先使用接口数据,避免商户别名或钱包名称调整后展示过期文案。 */
|
||||
const fixedWalletName = computed(() => String(state.fixedWallet?.walletTypeName || DEFAULT_WALLET_TYPE_NAME))
|
||||
|
||||
/** 固定支付方式文案;钱包模式明确展示当前匹配到的钱包类型。 */
|
||||
const fixedPayTypeText = computed(() => resolvedPayType.value === FIXED_WALLET_PAY_TYPE && state.fixedWallet
|
||||
? `${fixedWalletName.value}支付`
|
||||
: props.defaultPayTypeText)
|
||||
|
||||
/** 底部金额旁展示当前支付方式及加载结果,减少确认支付前的视线往返。 */
|
||||
const selectedPayTypeText = computed(() => {
|
||||
if (!props.visiblePayCard) {
|
||||
if (state.fixedWalletLoading) return "正在获取饭卡钱包"
|
||||
if (state.fixedWalletFailed) return "饭卡钱包加载失败"
|
||||
if (resolvedPayType.value === FIXED_WALLET_PAY_TYPE && !resolvedWalletTypeId.value) return "饭卡钱包不可用"
|
||||
return fixedPayTypeText.value
|
||||
}
|
||||
return state.payTypeDetail?.text || (state.payType ? "已选择支付方式" : "请选择支付方式")
|
||||
})
|
||||
|
||||
/**
|
||||
* 查询当前用户钱包并按业务编码匹配固定钱包;钱包类型编码只用于查找,提交仍使用记录的 walletTypeId。
|
||||
*/
|
||||
const loadFixedWallet = async () => {
|
||||
if (props.visiblePayCard || resolvedPayType.value !== FIXED_WALLET_PAY_TYPE) return
|
||||
const currentRequest = ++state.fixedWalletRequestSequence
|
||||
state.fixedWalletLoading = true
|
||||
state.fixedWalletFailed = false
|
||||
state.fixedWallet = null
|
||||
try {
|
||||
const { data, code, msg }: any = await walletApi.all()
|
||||
if (currentRequest !== state.fixedWalletRequestSequence) return
|
||||
if (code != 0) throw new Error(msg || "获取用户钱包列表失败")
|
||||
const records = Array.isArray(data?.records) ? data.records : []
|
||||
const targetWalletTypeCode = String(props.defaultWalletTypeCode || DEFAULT_WALLET_TYPE_CODE)
|
||||
state.fixedWallet = records.find(
|
||||
(wallet: any) => String(wallet?.walletTypeUniqueCode || "") === targetWalletTypeCode,
|
||||
) || null
|
||||
} catch (error) {
|
||||
if (currentRequest !== state.fixedWalletRequestSequence) return
|
||||
state.fixedWalletFailed = true
|
||||
state.fixedWallet = null
|
||||
console.error("固定饭卡钱包加载失败", error)
|
||||
} finally {
|
||||
if (currentRequest === state.fixedWalletRequestSequence) state.fixedWalletLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 支付抽屉打开时刷新固定钱包,关闭或切换模式后使未完成请求失效。 */
|
||||
watch(
|
||||
() => [props.show, props.visiblePayCard, props.defaultPayType, props.defaultWalletTypeCode] as const,
|
||||
([show, visiblePayCard, defaultPayType]) => {
|
||||
if (!show || visiblePayCard || String(defaultPayType || "") !== FIXED_WALLET_PAY_TYPE) {
|
||||
state.fixedWalletRequestSequence += 1
|
||||
state.fixedWalletLoading = false
|
||||
return
|
||||
}
|
||||
void loadFixedWallet()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 根据校验状态选择项目内 Remix Icon 图标。 */
|
||||
const checkIcon = (status: PaymentCheckStatus = "success") => {
|
||||
if (status === "error") return "close-circle-fill"
|
||||
if (status === "warning") return "error-warning-fill"
|
||||
return "checkbox-circle-fill"
|
||||
}
|
||||
|
||||
/** 根据校验状态返回语义色,图标和文字共同表达结果而不只依赖颜色。 */
|
||||
const checkColor = (status: PaymentCheckStatus = "success") => {
|
||||
if (status === "error") return "#d92d20"
|
||||
if (status === "warning") return "#d97706"
|
||||
return "#027a48"
|
||||
}
|
||||
|
||||
/** 接收 pay-card 选择并向业务页面透传原始支付方式。 */
|
||||
const selectPayType = (type: string, typeValue: any) => {
|
||||
state.payType = String(type || "")
|
||||
state.payTypeDetail = typeValue || null
|
||||
emit("selectPayType", state.payType, state.payTypeDetail)
|
||||
}
|
||||
|
||||
/** 校验支付方式后向业务页面提交标准化选择,公共组件不直接调用订单接口。 */
|
||||
const emitConfirm = () => {
|
||||
if (props.submitting) return
|
||||
if (hasBlockingCheck.value) {
|
||||
uni.showToast({ title: "存在未通过的支付校验", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (!resolvedPayType.value) {
|
||||
uni.showToast({ title: "请选择支付方式", icon: "none" })
|
||||
return
|
||||
}
|
||||
if (resolvedPayType.value === FIXED_WALLET_PAY_TYPE && !resolvedWalletTypeId.value) {
|
||||
uni.showToast({ title: "饭卡钱包不可用", icon: "none" })
|
||||
return
|
||||
}
|
||||
const payTypeDetail = props.visiblePayCard
|
||||
? state.payTypeDetail
|
||||
: {
|
||||
type: resolvedPayType.value,
|
||||
text: fixedPayTypeText.value,
|
||||
extra: state.fixedWallet,
|
||||
}
|
||||
emit("confirm", {
|
||||
payType: resolvedPayType.value,
|
||||
walletTypeId: resolvedWalletTypeId.value,
|
||||
payTypeDetail,
|
||||
})
|
||||
}
|
||||
|
||||
/** tm-drawer 完成关闭动画后透传关闭事件。 */
|
||||
const emitClose = () => {
|
||||
emit("close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payment-drawer-title {
|
||||
color: #101828;
|
||||
font-size: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-review {
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.payment-review__heading {
|
||||
display: flex;
|
||||
min-height: 96rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
}
|
||||
|
||||
.payment-review__icon {
|
||||
display: flex;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #eef8ff;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: baseline;
|
||||
color: #e5484d;
|
||||
font-size: 36rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
|
||||
&__symbol {
|
||||
margin-right: 3rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-summary {
|
||||
display: flex;
|
||||
border-top: 1px solid #f2f4f7;
|
||||
background: #f9fbfd;
|
||||
}
|
||||
|
||||
.payment-summary__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 80rpx;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8rpx;
|
||||
|
||||
&+&::before {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
bottom: 20rpx;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
background: #e4e7ec;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.payment-summary__value {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #344054;
|
||||
font-size: 22rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.payment-summary__label {
|
||||
margin-top: 2rpx;
|
||||
color: #667085;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
|
||||
.payment-checks {
|
||||
padding: 10rpx 20rpx 12rpx;
|
||||
border-top: 1px solid #f2f4f7;
|
||||
}
|
||||
|
||||
.payment-checks__heading {
|
||||
display: flex;
|
||||
height: 48rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.payment-checks__status {
|
||||
padding: 3rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #ecfdf3;
|
||||
color: #027a48;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&--warning {
|
||||
background: #fff7ed;
|
||||
color: #b54708;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-check-row {
|
||||
display: flex;
|
||||
min-height: 56rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.payment-check-row__icon {
|
||||
display: flex;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #ecfdf3;
|
||||
|
||||
&--warning {
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: #fef3f2;
|
||||
}
|
||||
}
|
||||
|
||||
/* pay-card 是全局支付方式组件,仅在确认支付 Drawer 内收敛到订餐页的卡片密度。 */
|
||||
.payment-method-panel :deep(.card) {
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 0 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.fixed-wallet-panel {
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.fixed-wallet-panel__row {
|
||||
display: flex;
|
||||
min-height: 92rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.fixed-wallet-panel__row--action {
|
||||
min-height: 104rpx;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.fixed-wallet-panel__icon {
|
||||
display: flex;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #eef8ff;
|
||||
|
||||
&--warning {
|
||||
background: #fff7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-wallet-panel__selected {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 5rpx;
|
||||
color: #027a48;
|
||||
font-size: 19rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 10rpx 24rpx calc(10rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.payment-footer__amount {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<view class="base-skeleton" aria-hidden="true" :style="skeletonStyle" @transitionend="handleTransitionEnd" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
||||
|
||||
import { getDefaultColor } from "@/uni_modules/tm-ui/libs/colors"
|
||||
import { useTmConfig } from "@/uni_modules/tm-ui/libs/config"
|
||||
import { covetUniNumber } from "@/uni_modules/tm-ui/libs/tool"
|
||||
|
||||
/** 全局骨架块的输入参数,核心字段与 TMUI tm-skeleton 保持一致。 */
|
||||
type Props = {
|
||||
/** 骨架宽度,数字按 TMUI 全局单位转换,也支持 px、rpx、百分比和 auto。 */
|
||||
width?: string | number
|
||||
/** 骨架高度,数字按 TMUI 全局单位转换,也支持 px、rpx、百分比和 auto。 */
|
||||
height?: string | number
|
||||
/** 浅色模式下的背景色,支持 TMUI 主题色名称或 CSS 颜色。 */
|
||||
color?: string
|
||||
/** 深色模式下的背景色,支持 TMUI 主题色名称或 CSS 颜色。 */
|
||||
darkColor?: string
|
||||
/** 骨架圆角,数字按 TMUI 全局单位转换。 */
|
||||
round?: string | number
|
||||
/** 单次透明度过渡时间,使用 CSS transition-duration 格式。 */
|
||||
duration?: string
|
||||
/** 是否启用呼吸动画,静态占位场景可关闭。 */
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
/** 全局骨架默认值与 TMUI 官方骨架组件保持相同视觉节奏。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: "auto",
|
||||
height: "12",
|
||||
color: "#e4e4e4",
|
||||
darkColor: "#323232",
|
||||
round: "3",
|
||||
duration: "900ms",
|
||||
animated: true,
|
||||
})
|
||||
|
||||
/** TMUI 全局配置,用于同步单位体系和明暗主题。 */
|
||||
const tmConfig = useTmConfig()
|
||||
|
||||
/** 当前动画透明度,在两个稳定状态间往返切换。 */
|
||||
const opacity = ref(1)
|
||||
|
||||
/** 首次动画延迟句柄,组件卸载或停用动画时必须释放。 */
|
||||
let animationTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** 解析后的骨架样式,尺寸、圆角及颜色均沿用 TMUI 规则。 */
|
||||
const skeletonStyle = computed(() => ({
|
||||
width: covetUniNumber(props.width, tmConfig.config.unit),
|
||||
height: covetUniNumber(props.height, tmConfig.config.unit),
|
||||
borderRadius: covetUniNumber(props.round, tmConfig.config.unit),
|
||||
backgroundColor: getDefaultColor(tmConfig.config.mode === "dark" ? props.darkColor : props.color),
|
||||
opacity: props.animated ? opacity.value : 1,
|
||||
transitionDuration: props.animated ? props.duration : "0ms",
|
||||
}))
|
||||
|
||||
/** 清理尚未触发的动画延迟,避免卸载后继续修改组件状态。 */
|
||||
const clearAnimationTimer = () => {
|
||||
if (animationTimer === null) return
|
||||
clearTimeout(animationTimer)
|
||||
animationTimer = null
|
||||
}
|
||||
|
||||
/** 从完整不透明状态启动骨架呼吸动画。 */
|
||||
const startAnimation = () => {
|
||||
clearAnimationTimer()
|
||||
opacity.value = 1
|
||||
if (!props.animated) return
|
||||
animationTimer = setTimeout(() => {
|
||||
opacity.value = 0.46
|
||||
animationTimer = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/** 每次透明度过渡结束后切换目标值,形成与 TMUI 一致的循环淡入淡出。 */
|
||||
const handleTransitionEnd = () => {
|
||||
if (!props.animated) return
|
||||
opacity.value = opacity.value === 1 ? 0.46 : 1
|
||||
}
|
||||
|
||||
/** 动画开关运行时变化时立即启动或恢复静态状态。 */
|
||||
watch(
|
||||
() => props.animated,
|
||||
(animated) => {
|
||||
if (animated) {
|
||||
startAnimation()
|
||||
return
|
||||
}
|
||||
clearAnimationTimer()
|
||||
opacity.value = 1
|
||||
},
|
||||
)
|
||||
|
||||
/** 组件挂载后延迟启动,避免首帧尚未绘制时丢失过渡动画。 */
|
||||
onMounted(startAnimation)
|
||||
|
||||
/** KeepAlive 页面重新激活时重启动画,防止恢复到半透明静止状态。 */
|
||||
onActivated(startAnimation)
|
||||
|
||||
/** 组件卸载前释放计时器,避免跨页面残留异步任务。 */
|
||||
onBeforeUnmount(clearAnimationTimer)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.base-skeleton {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.base-skeleton {
|
||||
opacity: 0.72 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<tm-drawer
|
||||
v-model:show="drawerShow"
|
||||
position="bottom"
|
||||
size="64%"
|
||||
title="订餐购物车"
|
||||
:show-close="true"
|
||||
:show-cancel="false"
|
||||
:show-footer="true"
|
||||
>
|
||||
<view v-if="props.draftGroups.length === 0" class="cart-empty">
|
||||
<view class="cart-empty__icon">
|
||||
<tm-icon name="shopping-cart-line" :size="36" color="#0094ff" />
|
||||
</view>
|
||||
<view class="sub-text-26">购物车还是空的</view>
|
||||
<view class="mt-6rpx text-21rpx color-#667085">选择日期和餐段后添加菜品</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="pb-12rpx">
|
||||
<view class="cart-toolbar">
|
||||
<view class="text-21rpx color-#667085"> {{ props.draftGroups.length }} 个餐段 · {{ props.totalFoodCount }} 份 </view>
|
||||
<view class="clear-button" hover-class="tap-feedback" @tap="emitClear">清空</view>
|
||||
</view>
|
||||
|
||||
<view v-for="draft in props.draftGroups" :key="draft.key" class="cart-group">
|
||||
<view class="cart-group__header">
|
||||
<view class="cart-group__meta">
|
||||
<text class="cart-group__date">{{ formatReservationDate(draft.apptDate) }}</text>
|
||||
<text class="meal-tag show-line">{{ draft.mealTitle }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="remove-hit-area"
|
||||
hover-class="tap-feedback"
|
||||
:aria-label="`移除${draft.mealTitle}选择`"
|
||||
@tap="emitRemoveGroup(draft.key)"
|
||||
>
|
||||
<view class="remove-button">
|
||||
<tm-icon name="delete-bin-line" :size="23" color="#d92d20" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-for="item in draft.items" :key="item.spuId || item.id" class="cart-food-row">
|
||||
<image class="cart-food-row__image" :src="foodImage(item)" mode="aspectFill" />
|
||||
<view class="ml-12rpx min-w-0 flex-1">
|
||||
<view class="show-line sub-text-24">{{ item.name || "未命名菜品" }}</view>
|
||||
<view class="mt-3rpx text-23rpx font-700 color-#e5484d">¥{{ hooks.useMoney(item.price || 0) }} </view>
|
||||
</view>
|
||||
<view class="quantity-control">
|
||||
<view class="quantity-hit-area" hover-class="tap-feedback" @tap="emitQuantityChange(draft.key, item, -1)">
|
||||
<view class="quantity-button quantity-button--secondary">
|
||||
<tm-icon name="subtract-line" :size="23" color="#0094ff" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="quantity-value">{{ item.quantity }}</view>
|
||||
<view
|
||||
class="quantity-hit-area"
|
||||
:class="{ 'quantity-hit-area--disabled': !canIncreaseFood(draft, item) }"
|
||||
hover-class="tap-feedback"
|
||||
@tap="canIncreaseFood(draft, item) && emitQuantityChange(draft.key, item, 1)"
|
||||
>
|
||||
<view class="quantity-button quantity-button--primary">
|
||||
<tm-icon name="add-line" :size="25" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template #footer>
|
||||
<view class="drawer-footer">
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="text-20rpx color-#667085">{{ props.selectedDinerCount }} 位用餐人合计</view>
|
||||
<view class="mt-1rpx text-32rpx font-800 color-#e5484d">¥{{ hooks.useMoney(props.totalPayableAmount) }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="drawer-submit"
|
||||
:class="{ 'drawer-submit--disabled': props.totalFoodCount === 0 }"
|
||||
hover-class="tap-feedback"
|
||||
@tap="props.totalFoodCount > 0 && emitSubmit()"
|
||||
>
|
||||
确认订餐
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import { hooks } from "@@/index"
|
||||
import { filePath } from "@@/utils/file"
|
||||
|
||||
import type { DraftFoodItem, ReservationDraft } from "./daily-menu-types"
|
||||
|
||||
/** 多日期餐段购物车抽屉的输入数据。 */
|
||||
type Props = {
|
||||
/** 抽屉是否显示,由页面通过 v-model:show 控制。 */
|
||||
show?: boolean
|
||||
/** 非空订餐草稿列表,按用户首次添加的顺序展示。 */
|
||||
draftGroups?: ReservationDraft[]
|
||||
/** 所有日期餐段的菜品总份数。 */
|
||||
totalFoodCount?: number
|
||||
/** 当前计价使用的用餐人数。 */
|
||||
selectedDinerCount?: number
|
||||
/** 所有用餐人的应付合计,单位为分。 */
|
||||
totalPayableAmount?: number
|
||||
/** 是否忽略库存限制,与商品卡片保持相同的库存展示和增减规则。 */
|
||||
ignoreStock?: boolean
|
||||
/** 是否限制每个日期餐段的草稿只能选择一份商品。 */
|
||||
singleProductOnlyInCart?: boolean
|
||||
}
|
||||
|
||||
/** 抽屉对外事件,业务数据仍由页面作为唯一状态源。 */
|
||||
const emit = defineEmits<{
|
||||
/** 同步抽屉显隐状态。 */
|
||||
"update:show": [show: boolean]
|
||||
/** 调整指定日期餐段中某个菜品的数量。 */
|
||||
"change-quantity": [draftKey: string, spuId: string, delta: number]
|
||||
/** 移除指定日期餐段的全部选择。 */
|
||||
"remove-group": [draftKey: string]
|
||||
/** 清空全部日期餐段的选择。 */
|
||||
clear: []
|
||||
/** 触发页面统一的提交校验。 */
|
||||
submit: []
|
||||
}>()
|
||||
|
||||
/** 抽屉属性默认值,空购物车也能独立展示完整状态。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
show: false,
|
||||
draftGroups: () => [],
|
||||
totalFoodCount: 0,
|
||||
selectedDinerCount: 1,
|
||||
totalPayableAmount: 0,
|
||||
ignoreStock: false,
|
||||
singleProductOnlyInCart: false,
|
||||
})
|
||||
|
||||
/** 将父页面显隐状态转换为 tm-drawer 所需的双向绑定。 */
|
||||
const drawerShow = computed({
|
||||
get: () => props.show,
|
||||
set: (show: boolean) => emit("update:show", show),
|
||||
})
|
||||
|
||||
/** 将预约日期格式化为抽屉标题使用的紧凑中文日期。 */
|
||||
const formatReservationDate = (value?: string) => {
|
||||
if (!value || !dayjs(value).isValid()) return "日期待确认"
|
||||
return dayjs(value).format("MM月DD日")
|
||||
}
|
||||
|
||||
/** 将菜品图片路径转换为跨端地址,并提供统一兜底图。 */
|
||||
const foodImage = (item: DraftFoodItem) => filePath(item.picUrl) || "/static/logo/zax.png"
|
||||
|
||||
/** 判断指定餐段菜品能否继续增加,业务单商品限制优先于库存配置。 */
|
||||
const canIncreaseFood = (draft: ReservationDraft, item: DraftFoodItem) => {
|
||||
const draftCount = draft.items.reduce((total, food) => total + Number(food.quantity || 0), 0)
|
||||
if (props.singleProductOnlyInCart && draftCount >= 1) return false
|
||||
return props.ignoreStock || Number(item.quantity || 0) < Number(item.stock || 0)
|
||||
}
|
||||
|
||||
/** 抛出指定菜品的数量增量,不在展示组件内直接修改草稿。 */
|
||||
const emitQuantityChange = (draftKey: string, item: DraftFoodItem, delta: -1 | 1) => {
|
||||
const spuId = String(item.spuId || "")
|
||||
if (!draftKey || !spuId) return
|
||||
emit("change-quantity", draftKey, spuId, delta)
|
||||
}
|
||||
|
||||
/** 请求页面移除指定日期餐段,二次确认由页面统一处理。 */
|
||||
const emitRemoveGroup = (draftKey: string) => {
|
||||
if (draftKey) emit("remove-group", draftKey)
|
||||
}
|
||||
|
||||
/** 请求页面清空全部购物车草稿。 */
|
||||
const emitClear = () => {
|
||||
emit("clear")
|
||||
}
|
||||
|
||||
/** 请求页面执行统一的订餐提交校验。 */
|
||||
const emitSubmit = () => {
|
||||
emit("submit")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cart-empty {
|
||||
display: flex;
|
||||
min-height: 300rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-empty__icon {
|
||||
display: flex;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #eef8ff;
|
||||
}
|
||||
|
||||
.cart-toolbar {
|
||||
display: flex;
|
||||
height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #f2f4f7;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
display: flex;
|
||||
min-width: 88rpx;
|
||||
height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #d92d20;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-group {
|
||||
border-bottom: 12rpx solid #f7f9fc;
|
||||
}
|
||||
|
||||
.cart-group__header {
|
||||
display: flex;
|
||||
min-height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.cart-group__meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.cart-group__date {
|
||||
flex-shrink: 0;
|
||||
color: #344054;
|
||||
font-size: 25rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.meal-tag {
|
||||
max-width: 220rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 4rpx 10rpx;
|
||||
border: 1px solid #d9efff;
|
||||
border-radius: 6rpx;
|
||||
background: #eef8ff;
|
||||
color: #0077cc;
|
||||
font-size: 19rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.remove-hit-area {
|
||||
display: flex;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
display: flex;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #fef3f2;
|
||||
}
|
||||
|
||||
.cart-food-row {
|
||||
display: flex;
|
||||
min-height: 108rpx;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
border-top: 1px solid #f2f4f7;
|
||||
|
||||
&__image {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8rpx;
|
||||
background: #f2f4f7;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity-hit-area {
|
||||
display: flex;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
touch-action: manipulation;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.38;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-button {
|
||||
display: flex;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&--secondary {
|
||||
border: 2rpx solid #b2ddff;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: #0094ff;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-value {
|
||||
min-width: 28rpx;
|
||||
color: #101828;
|
||||
font-size: 23rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 12rpx 24rpx calc(12rpx + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.drawer-submit {
|
||||
display: flex;
|
||||
min-width: 220rpx;
|
||||
height: 80rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 40rpx;
|
||||
background: #0094ff;
|
||||
color: #ffffff;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.42;
|
||||
}
|
||||
}
|
||||
|
||||
.tap-feedback {
|
||||
opacity: 0.68;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<view class="daily-food-card" :class="{ 'daily-food-card--sold-out': soldOut }">
|
||||
<image class="daily-food-card__image" :src="imageUrl" mode="aspectFill" :aria-label="`${displayName}菜品图片`" />
|
||||
<view class="ml-16rpx min-w-0 flex-1 py-2rpx">
|
||||
<view class="flex items-start justify-between gap-10rpx">
|
||||
<view class="show-line sub-text-26 color-#101828!">{{ displayName }}</view>
|
||||
<view v-if="soldOut" class="sold-out-tag">已售罄</view>
|
||||
</view>
|
||||
<view class="mt-5rpx flex items-center gap-8rpx text-20rpx color-#667085">
|
||||
<text>{{ props.ignoreStock ? "不限库存" : `库存 ${stock}` }}</text>
|
||||
<text class="color-#d0d5dd">|</text>
|
||||
<text>独立装餐</text>
|
||||
</view>
|
||||
<view class="food-price mt-5rpx">
|
||||
<text class="text-20rpx font-700">¥</text>
|
||||
<text class="ml-2rpx text-30rpx font-800">{{ hooks.useMoney(props.item.price || 0) }}</text>
|
||||
<text class="ml-3rpx text-19rpx color-#667085">/份</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quantity-control ml-4rpx" @tap.stop>
|
||||
<view
|
||||
v-if="props.quantity > 0"
|
||||
class="quantity-hit-area"
|
||||
hover-class="tap-feedback"
|
||||
:aria-label="`减少${displayName}数量`"
|
||||
@tap="changeQuantity(-1)"
|
||||
>
|
||||
<view class="quantity-button quantity-button--secondary">
|
||||
<tm-icon name="subtract-line" :size="25" color="#0094ff" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="props.quantity > 0" class="quantity-value">{{ props.quantity }}</view>
|
||||
<view
|
||||
class="quantity-hit-area"
|
||||
:class="{ 'quantity-hit-area--disabled': !canIncrease }"
|
||||
hover-class="tap-feedback"
|
||||
:aria-label="`增加${displayName}数量`"
|
||||
@tap="canIncrease && changeQuantity(1)"
|
||||
>
|
||||
<view class="quantity-button quantity-button--primary">
|
||||
<tm-icon name="add-line" :size="27" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
import { hooks } from "@@/index"
|
||||
import { filePath } from "@@/utils/file"
|
||||
|
||||
import type { DailyMenuItem } from "./daily-menu-types"
|
||||
|
||||
/** 紧凑菜品卡片的输入数据。 */
|
||||
type Props = {
|
||||
/** 每日菜谱接口返回的菜品数据。 */
|
||||
item: DailyMenuItem
|
||||
/** 当前日期餐段内已经选择的份数。 */
|
||||
quantity?: number
|
||||
/** 当前库存是否允许继续增加。 */
|
||||
canAdd?: boolean
|
||||
/** 是否忽略库存限制;开启后不展示售罄状态,并允许继续增加菜品。 */
|
||||
ignoreStock?: boolean
|
||||
/** 是否因库存之外的业务规则禁用增加按钮,例如当前餐段已达到单商品上限。 */
|
||||
addDisabled?: boolean
|
||||
}
|
||||
|
||||
/** 菜品数量变化事件,实际库存校验与草稿更新由页面统一处理。 */
|
||||
const emit = defineEmits<{
|
||||
/** 用户点击加减按钮时返回增量,取值为 -1 或 1。 */
|
||||
change: [delta: number]
|
||||
}>()
|
||||
|
||||
/** 卡片属性默认值,保证接口缺失可选字段时仍能稳定展示。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
quantity: 0,
|
||||
canAdd: false,
|
||||
ignoreStock: false,
|
||||
addDisabled: false,
|
||||
})
|
||||
|
||||
/** 菜品展示名称,接口为空时使用统一兜底文案。 */
|
||||
const displayName = computed(() => props.item.name || "未命名菜品")
|
||||
|
||||
/** 当前菜品可售库存,负值统一收敛为零。 */
|
||||
const stock = computed(() => Math.max(Number(props.item.stock || 0), 0))
|
||||
|
||||
/** 是否允许继续增加;业务禁用优先级高于忽略库存配置。 */
|
||||
const canIncrease = computed(() => !props.addDisabled && (props.ignoreStock || props.canAdd))
|
||||
|
||||
/** 当前菜品是否售罄;忽略库存时不展示售罄状态。 */
|
||||
const soldOut = computed(() => !props.ignoreStock && stock.value <= 0)
|
||||
|
||||
/** 跨 H5、iOS、Android 可访问的菜品图片地址。 */
|
||||
const imageUrl = computed(() => filePath(props.item.picUrl) || "/static/logo/zax.png")
|
||||
|
||||
/** 将用户的单次加减操作交给页面处理,组件自身不修改业务草稿。 */
|
||||
const changeQuantity = (delta: -1 | 1) => {
|
||||
emit("change", delta)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.daily-food-card {
|
||||
display: flex;
|
||||
min-height: 124rpx;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
border-top: 1px solid #f2f4f7;
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&--sold-out {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10rpx;
|
||||
background: #f2f4f7;
|
||||
}
|
||||
}
|
||||
|
||||
.sold-out-tag {
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #f2f4f7;
|
||||
color: #667085;
|
||||
font-size: 19rpx;
|
||||
}
|
||||
|
||||
.food-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #e5484d;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quantity-hit-area {
|
||||
display: flex;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
touch-action: manipulation;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.38;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-button {
|
||||
display: flex;
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&--secondary {
|
||||
border: 2rpx solid #b2ddff;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: #0094ff;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-value {
|
||||
min-width: 34rpx;
|
||||
color: #101828;
|
||||
font-size: 24rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tap-feedback {
|
||||
opacity: 0.68;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view class="daily-menu-food-skeleton" aria-label="菜谱加载中">
|
||||
<view v-for="index in props.count" :key="index" class="daily-menu-food-skeleton__row">
|
||||
<base-skeleton class="daily-menu-food-skeleton__image" width="100" height="100" round="10" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
<view class="daily-menu-food-skeleton__content">
|
||||
<base-skeleton width="58%" height="20" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-12rpx" width="72%" height="14" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
<base-skeleton class="mt-12rpx" width="38%" height="22" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<view class="daily-menu-food-skeleton__action">
|
||||
<base-skeleton width="54" height="54" round="27" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** 菜谱商品卡片骨架的输入参数。 */
|
||||
type Props = {
|
||||
/** 骨架行数,用于在接口返回前预留稳定的列表高度。 */
|
||||
count?: number
|
||||
}
|
||||
|
||||
/** 骨架默认展示三行,兼顾首屏信息量与小屏高度。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 3,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.daily-menu-food-skeleton {
|
||||
margin-top: 10rpx;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
min-height: 124rpx;
|
||||
align-items: center;
|
||||
padding: 10rpx 0;
|
||||
border-top: 1px solid #f2f4f7;
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
/** 每日菜谱接口返回的可选菜品。 */
|
||||
export type DailyMenuItem = {
|
||||
/** 每日菜谱记录 ID,仅用于列表渲染。 */
|
||||
id?: string | number
|
||||
/** 菜品 SPU ID,是购物车与库存校验使用的业务标识。 */
|
||||
spuId?: string | number
|
||||
/** 菜品展示名称,接口为空时由界面显示兜底名称。 */
|
||||
name?: string
|
||||
/** 菜品单价,单位为分。 */
|
||||
price?: number
|
||||
/** 菜品图片相对路径或完整地址。 */
|
||||
picUrl?: string
|
||||
/** 当前可售库存,数量上限以本次查询结果为准。 */
|
||||
stock?: number
|
||||
/** 菜谱所属餐段编码,来源于每日菜谱接口。 */
|
||||
hourType?: string | number
|
||||
}
|
||||
|
||||
/** 购物车内某一日期餐段下的菜品快照。 */
|
||||
export type DraftFoodItem = DailyMenuItem & {
|
||||
/** 用户为当前日期餐段选择的份数。 */
|
||||
quantity: number
|
||||
}
|
||||
|
||||
/** 按日期和餐段隔离的订餐草稿,切换筛选时仍保留其他餐段的选择。 */
|
||||
export type ReservationDraft = {
|
||||
/** 日期与餐段拼成的页面内唯一键,不作为服务端业务编码。 */
|
||||
key: string
|
||||
/** 批量上传后端返回的父购物车 ID;重新上传时由最新响应覆盖。 */
|
||||
carParentId?: string
|
||||
/** 预约使用日期,格式为 yyyy-MM-dd。 */
|
||||
apptDate: string
|
||||
/** 预约餐段编码,沿用后端 hourType 协议值。 */
|
||||
hourType: string
|
||||
/** 餐段展示名称,来源于预约选项。 */
|
||||
mealTitle: string
|
||||
/** 动态预约 ID;周期预约时为空。 */
|
||||
apptId: string
|
||||
/** 周期预约规则 ID;动态预约时可能为空。 */
|
||||
apptRuleId: string
|
||||
/** 预约时间类型,提交时沿用预约列表返回值。 */
|
||||
apptTimeType: string
|
||||
/** 当前日期餐段选择的菜品列表。 */
|
||||
items: DraftFoodItem[]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<view aria-label="订单列表加载中">
|
||||
<view v-for="index in props.count" :key="index" class="card mb-20rpx rounded-24rpx bg-white p-20rpx pb-0rpx">
|
||||
<view class="flex-col justify-center gap-20rpx pb-20rpx">
|
||||
<view class="flex-row">
|
||||
<base-skeleton width="90" height="90" round="8" color="#dcecff" dark-color="#323232" />
|
||||
<view class="ml-20rpx min-w-0 flex-1">
|
||||
<view class="flex items-center justify-between">
|
||||
<base-skeleton width="46%" height="26" round="7" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton width="84" height="28" round="14" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="mt-13rpx" width="72%" height="22" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center rounded-12rpx bg-[#fafafa] px-20rpx py-16rpx">
|
||||
<base-skeleton width="120" height="90" round="8" color="#dcecff" dark-color="#323232" />
|
||||
<view class="ml-16rpx min-w-0 flex-1">
|
||||
<base-skeleton width="72%" height="22" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-10rpx" width="48%" height="20" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
<view class="w-120rpx flex-col-center">
|
||||
<base-skeleton width="92" height="28" round="7" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-8rpx" width="68" height="20" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex justify-end">
|
||||
<base-skeleton width="116" height="48" round="24" color="#dcecff" dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** 订单列表骨架组件的输入参数。 */
|
||||
type Props = {
|
||||
/** 首屏占位订单数量,默认三张以覆盖常见手机首屏和短屏滚动区域。 */
|
||||
count?: number
|
||||
}
|
||||
|
||||
/** 默认渲染三张与订单卡片主体结构一致的占位卡。 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 3,
|
||||
})
|
||||
</script>
|
||||
@@ -1,10 +1,30 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="card rounded-24rpx bg-white p-20rpx">
|
||||
<view class="card rounded-24rpx bg-white p-20rpx" :aria-busy="state.payTypeLoading">
|
||||
<view v-if="visibleTitle" class="flex items-center justify-between">
|
||||
<view class="sub-text-24">选择支付方式</view>
|
||||
</view>
|
||||
<view v-if="isPay">
|
||||
<view v-if="state.payTypeLoading" class="pay-type-skeleton" aria-label="支付方式加载中">
|
||||
<view v-for="index in 2" :key="index" class="pay-type-skeleton__row">
|
||||
<base-skeleton class="shrink-0" width="50" height="50" round="10" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
<view class="ml-30rpx min-w-0 flex-1">
|
||||
<base-skeleton width="128" height="24" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-8rpx" width="58%" height="18" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="ml-20rpx shrink-0" width="40" height="40" round="20" color="#dcecff"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="state.payTypeLoadFailed" class="wallet-recharge-empty bg-white rounded-12rpx m-t-20rpx">
|
||||
<view class="wallet-recharge-empty__icon">!</view>
|
||||
<view class="sub-text-26 color-#333">支付方式加载失败</view>
|
||||
<tm-button class="mt-20rpx" width="180" height="88" round="44" skin="thin" @click="getUserPayTypeList">
|
||||
重新加载
|
||||
</tm-button>
|
||||
</view>
|
||||
<view v-else-if="isPay">
|
||||
<view v-if="walletPayDetail && visibleWalletPay"
|
||||
class="mt-20rpx px-10rpx pb-10rpx rounded-12rpx flex-col justify-items-start bg-#eff8ff"
|
||||
@tap="!walletPayDetail.disabled && (state.currentPayType = walletPayDetail.type)">
|
||||
@@ -35,7 +55,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(v, i) in state.payType" :key="i" @tap="!v.disabled && (state.currentPayType = v.type)">
|
||||
<view v-for="(v, i) in visiblePayTypes" :key="i" @tap="!v.disabled && (state.currentPayType = v.type)">
|
||||
<view v-if="['10', '20'].includes(v.type)"
|
||||
class="flex items-center py-20rpx pl-10rpx pr-22rpx rounded-16rpx">
|
||||
<image class="size-50rpx" :src="v.logoDir" mode="scaleToFill" />
|
||||
@@ -61,8 +81,8 @@
|
||||
class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-t-60rpx sub-text-30 color-white!" @tap="doPay">
|
||||
确认充值
|
||||
</view>
|
||||
|
||||
<tm-drawer v-model:show="state.visibleSelectWallet" title="选择钱包" :show-close="true" position="bottom" size="40%">
|
||||
<tm-drawer v-model:show="state.visibleSelectWallet" title="选择钱包" :show-close="true" position="bottom" size="40%"
|
||||
:z-index="props.walletDrawerZIndex">
|
||||
<view class="flex-col" v-for="(v, index) in state.walletList" :key="index" @click="selectTargetWalletType(v)">
|
||||
<view class="flex items-center p-x-30rpx p-y-20rpx">
|
||||
<view class="flex-1 flex items-center gap-16rpx">
|
||||
@@ -98,25 +118,41 @@ import ServiceText from "@@/components/common/service-text.vue"
|
||||
const emit = defineEmits(["selectPayType"])
|
||||
|
||||
export type Props = {
|
||||
/** 支付卡片内“选择钱包”抽屉的层级,嵌入其他 Drawer 时由外层传入更高值。 */
|
||||
walletDrawerZIndex?: string | number
|
||||
visibleTitle?: boolean
|
||||
visiblePayBtn?: boolean
|
||||
visibleWalletPay?: boolean
|
||||
/** 允许展示的平台支付类型编码;空数组表示展示接口返回的全部方式。 */
|
||||
allowedPayTypes?: string[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
walletDrawerZIndex: 1100,
|
||||
visibleTitle: true,
|
||||
visiblePayBtn: true,
|
||||
visibleWalletPay: true,
|
||||
allowedPayTypes: () => [],
|
||||
})
|
||||
|
||||
/** 根据业务组件声明的能力过滤支付方式,避免展示当前下单接口无法处理的渠道。 */
|
||||
const visiblePayTypes = computed(() => {
|
||||
if (!props.allowedPayTypes.length) return state.payType
|
||||
return state.payType.filter((payType: any) => props.allowedPayTypes.includes(String(payType?.type || "")))
|
||||
})
|
||||
|
||||
const isPay = computed(() => {
|
||||
return state.payType && state.payType.length > 0
|
||||
return visiblePayTypes.value.length > 0
|
||||
})
|
||||
|
||||
const walletPayDetail = computed(() => {
|
||||
return state.payType.find(v => v.type == "30") ?? null
|
||||
return visiblePayTypes.value.find((v: any) => v.type == "30") ?? null
|
||||
})
|
||||
|
||||
const state = reactive<any>({
|
||||
/** 支付方式配置是否正在加载,首次进入时用于控制等高骨架占位。 */
|
||||
payTypeLoading: true,
|
||||
/** 支付方式配置最近一次是否加载失败,用于提供明确的重试入口。 */
|
||||
payTypeLoadFailed: false,
|
||||
visibleSelectWallet: false,
|
||||
walletList: [],
|
||||
currentSelectWallet: null,
|
||||
@@ -148,6 +184,7 @@ const getAllWalletList = async () => {
|
||||
const { data, code }: any = await walletApi.all()
|
||||
if (code != 0) throw new Error(data?.msg || "获取用户钱包列表失败")
|
||||
const records = data?.records ?? []
|
||||
if (!records.length) return
|
||||
state.walletList = records
|
||||
state.payType.push({
|
||||
logoDir: "/static/logo/zax.png",
|
||||
@@ -166,42 +203,53 @@ const getAllWalletList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询用户可用支付方式,并确保成功、空数据和异常路径都能结束骨架状态。 */
|
||||
const getUserPayTypeList = async () => {
|
||||
const _pa = {}
|
||||
const { data } = await walletApi.getUserPaySettingList(_pa)
|
||||
state.payTypeList = data ?? []
|
||||
if (state.payTypeList.length) {
|
||||
const typeList = state.payType.map(v => v.type)
|
||||
for (const v of state.payTypeList) {
|
||||
if (!v.platformType) continue
|
||||
if (v.platformType == "30" && !typeList.includes("30")) {
|
||||
await getAllWalletList()
|
||||
state.payTypeLoading = true
|
||||
state.payTypeLoadFailed = false
|
||||
try {
|
||||
const _pa = {}
|
||||
const { data } = await walletApi.getUserPaySettingList(_pa)
|
||||
state.payTypeList = data ?? []
|
||||
if (state.payTypeList.length) {
|
||||
const typeList = state.payType.map(v => v.type)
|
||||
for (const v of state.payTypeList) {
|
||||
if (!v.platformType) continue
|
||||
if (v.platformType == "30" && !typeList.includes("30")) {
|
||||
await getAllWalletList()
|
||||
}
|
||||
if (v.platformType == "10" && !typeList.includes("10")) {
|
||||
state.payType.push({
|
||||
logoDir: "/static/pay/wechat-pay.jpg",
|
||||
text: "微信支付",
|
||||
desc: "",
|
||||
disabled: false,
|
||||
type: "10",
|
||||
value: "WECHAT"
|
||||
},)
|
||||
}
|
||||
if (v.platformType == "20" && !typeList.includes("20")) {
|
||||
state.payType.push({
|
||||
logoDir: "/static/pay/ali-pay.png",
|
||||
text: "支付宝支付",
|
||||
desc: "10亿用户都在用,真安全,更放心",
|
||||
disabled: false,
|
||||
type: "20",
|
||||
value: "ALI_PAY"
|
||||
},)
|
||||
}
|
||||
}
|
||||
if (v.platformType == "10" && !typeList.includes("10")) {
|
||||
state.payType.push({
|
||||
logoDir: "/static/pay/wechat-pay.jpg",
|
||||
text: "微信支付",
|
||||
desc: "",
|
||||
disabled: false,
|
||||
type: "10",
|
||||
value: "WECHAT"
|
||||
},)
|
||||
}
|
||||
if (v.platformType == "20" && !typeList.includes("20")) {
|
||||
state.payType.push({
|
||||
logoDir: "/static/pay/ali-pay.png",
|
||||
text: "支付宝支付",
|
||||
desc: "10亿用户都在用,真安全,更放心",
|
||||
disabled: false,
|
||||
type: "20",
|
||||
value: "ALI_PAY"
|
||||
},)
|
||||
const defaultPayType = visiblePayTypes.value[0]?.type
|
||||
const currentPayTypeAllowed = visiblePayTypes.value.some((payType: any) => payType.type == state.currentPayType)
|
||||
if (defaultPayType && !currentPayTypeAllowed) {
|
||||
state.currentPayType = defaultPayType
|
||||
}
|
||||
}
|
||||
const defaultPayType = state.payType[0].type
|
||||
if (defaultPayType && !state.currentPayType) {
|
||||
state.currentPayType = defaultPayType
|
||||
}
|
||||
} catch (error) {
|
||||
state.payTypeLoadFailed = true
|
||||
console.error("获取用户支付方式失败", error)
|
||||
} finally {
|
||||
state.payTypeLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +389,18 @@ const disposeAlipay = async (bizId: any) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pay-type-skeleton {
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
.pay-type-skeleton__row {
|
||||
display: flex;
|
||||
min-height: 90rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding: 20rpx 22rpx 20rpx 10rpx;
|
||||
}
|
||||
|
||||
.wallet-border {
|
||||
border: 1rpx solid #f8f8f8;
|
||||
margin: 15rpx 0;
|
||||
|
||||
@@ -1,30 +1,63 @@
|
||||
<template>
|
||||
<view class="card bg-white p-24rpx" @tap="openDrawer">
|
||||
<view class="flex items-center justify-between">
|
||||
<view>
|
||||
<view class="sub-text-30">用餐人</view>
|
||||
<view class="second-text-22 mt-6rpx color-#999">
|
||||
{{ props.guardianWardEnabled ? "每位用餐人将生成一笔独立订单" : "当前商户仅支持为本人订餐" }}
|
||||
<view class="card bg-white p-24rpx" :aria-busy="loading" @tap="openDrawer">
|
||||
<template v-if="loading">
|
||||
<view class="flex items-center justify-between gap-20rpx" aria-label="用餐人加载中">
|
||||
<view class="min-w-0 flex-1">
|
||||
<base-skeleton width="112" height="30" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-10rpx" width="72%" height="22" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="shrink-0" width="128" height="26" round="6" color="#dcecff"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="mt-18rpx" width="126" height="44" round="22" color="#dcecff" dark-color="#323232" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="flex items-center justify-between">
|
||||
<view>
|
||||
<view class="sub-text-30">用餐人</view>
|
||||
<view class="second-text-22 mt-6rpx color-#999">
|
||||
{{ props.guardianWardEnabled ? "每位用餐人将生成一笔独立订单" : "当前商户仅支持为本人订餐" }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="props.guardianWardEnabled" class="flex items-center color-#0094ff text-24rpx font-700">
|
||||
<text>已选 {{ selectedOptions.length }} 人</text>
|
||||
<tm-icon name="arrow-right-s-line" :size="26" color="#0094ff" />
|
||||
</view>
|
||||
<view v-else class="second-text-24 color-#999">仅本人</view>
|
||||
</view>
|
||||
<view class="mt-18rpx flex flex-wrap gap-12rpx">
|
||||
<view v-for="item in selectedOptions" :key="item.userId"
|
||||
class="flex items-center rounded-full bg-#eef8ff px-14rpx py-8rpx color-#0094ff text-22rpx">
|
||||
<text class="max-w-180rpx show-line">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="props.guardianWardEnabled" class="flex items-center color-#0094ff text-24rpx font-700">
|
||||
<text>已选 {{ selectedOptions.length }} 人</text>
|
||||
<tm-icon name="arrow-right-s-line" :size="26" color="#0094ff" />
|
||||
</view>
|
||||
<view v-else class="second-text-24 color-#999">仅本人</view>
|
||||
</view>
|
||||
<view class="mt-18rpx flex flex-wrap gap-12rpx">
|
||||
<view v-for="item in selectedOptions" :key="item.userId"
|
||||
class="flex items-center rounded-full bg-#eef8ff px-14rpx py-8rpx color-#0094ff text-22rpx">
|
||||
<text class="max-w-180rpx show-line">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<tm-drawer v-model:show="show" position="bottom" size="65%" title="选择用餐人" :show-close="true" :show-cancel="false"
|
||||
show-footer confirm-text="确认选择" @confirm="confirmSelection">
|
||||
<view v-if="loading" class="h-260rpx flex-col-center">
|
||||
<tm-icon name="loader-4-line" :size="40" color="#0094ff" spin />
|
||||
<text class="second-text-24 mt-12rpx">正在加载用餐人...</text>
|
||||
:show-footer="!loading" confirm-text="确认选择" @confirm="confirmSelection">
|
||||
<view v-if="loading" aria-label="用餐人列表加载中">
|
||||
<view v-for="index in 3" :key="index"
|
||||
class="mb-12rpx flex items-center rounded-18rpx border-1rpx border-solid border-#eef3f8 bg-#fafafa px-16rpx py-18rpx">
|
||||
<base-skeleton class="shrink-0" width="72" height="72" round="36" color="#dcecff"
|
||||
dark-color="#323232" />
|
||||
<view class="ml-16rpx min-w-0 flex-1">
|
||||
<base-skeleton width="42%" height="28" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-10rpx" width="62%" height="20" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="shrink-0" width="36" height="36" round="18" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<view class="mt-16rpx flex items-center justify-between rounded-16rpx bg-#f7f9fc px-18rpx py-16rpx">
|
||||
<view class="min-w-0 flex-1 pr-20rpx">
|
||||
<base-skeleton width="136" height="24" round="6" color="#e8edf3" dark-color="#323232" />
|
||||
<base-skeleton class="mt-10rpx" width="70%" height="20" round="6" color="#e8edf3"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
<base-skeleton class="shrink-0" width="84" height="48" round="24" color="#dcecff"
|
||||
dark-color="#323232" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view v-if="loadFailed"
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
// 商户订餐商品类型
|
||||
const mchTableReservationModeCodeEnum = {
|
||||
ONLONE_PRODUCT: "10",
|
||||
DAILY_MENU: "20",
|
||||
} as const
|
||||
|
||||
const mchTableReservationModeStatusEnum = {
|
||||
[mchTableReservationModeCodeEnum.ONLONE_PRODUCT]: "线上订餐产品",
|
||||
[mchTableReservationModeCodeEnum.DAILY_MENU]: "每日菜谱菜谱",
|
||||
}
|
||||
|
||||
// 钱包类型
|
||||
const walletTypeCodeEnum = {
|
||||
EAT: "1",
|
||||
PAY: "2",
|
||||
} as const
|
||||
|
||||
const walletTypeStatusEnum = {
|
||||
[walletTypeCodeEnum.EAT]: "饭卡钱包",
|
||||
[walletTypeCodeEnum.PAY]: "消费钱包",
|
||||
}
|
||||
|
||||
const hardwareOrderStatusEnum = {
|
||||
1: "创建订单(未支付)",
|
||||
5: "已经支付",
|
||||
@@ -157,4 +179,8 @@ export default {
|
||||
mapAddressTag,
|
||||
taskCheckStatusCodeEnum,
|
||||
taskCheckStatusEnum,
|
||||
mchTableReservationModeCodeEnum,
|
||||
mchTableReservationModeStatusEnum,
|
||||
walletTypeStatusEnum,
|
||||
walletTypeCodeEnum,
|
||||
}
|
||||
Reference in new issue
Block a user