每日菜品优化

This commit is contained in:
zjf committed 2026-09-14 19:56:16 +08:00
1 parent 2cd379cc57
commit f32ca28caa
6 files changed
+437 -51

No files matched your search

+9
View File
@@ -158,6 +158,15 @@ export function setDayLock(data: Object) {
})
}
/** 按订单净销量重算当日线上/线下 sold(不改配额) */
export function syncChannelSold(params: { merchantId: string | number; mealDate: string }) {
return request({
url: "/canteen/canteenDailyMenu/syncChannelSold",
method: "put",
params,
})
}
/** 分页查询导入日志 */
export function pageImportLog(params: Object) {
return request({
+3 -1
View File
@@ -234,9 +234,11 @@ export const dateStr: string = "YYYY-MM-DD"
export const timeStr: string = "HH:mm:ss"
/** 当前自然月,格式 yyyy-MM(报表业务月默认值) */
/** 报表业务月默认值:上一个自然月(本月统计上月),格式 yyyy-MM */
export function currentBizMonth(): string {
const d = new Date()
d.setDate(1)
d.setMonth(d.getMonth() - 1)
const m = d.getMonth() + 1
return `${d.getFullYear()}-${m < 10 ? `0${m}` : m}`
}
@@ -16,12 +16,12 @@
<span>更新 <b class="text-blue-600">{{ preview?.totalUpdate || 0 }}</b></span>
<span>锁定跳过 <b class="text-orange-500">{{ preview?.totalSkipLocked || 0 }}</b></span>
<span class="text-gray-300">|</span>
<span>已勾选导入 <b>{{ selectedCount }}</b> / 共 {{ rows.length }} 行</span>
<span>已勾选导入 <b>{{ selectedMenuCount }}</b> 道 / Excel {{ rows.length }} 行</span>
</div>
<!-- 按日切换:点哪天只看哪天 -->
<div v-if="dayTabs.length" class="mb-3">
<div class="text-xs text-gray-400 mb-1">点击日期查看当天数据</div>
<div class="text-xs text-gray-400 mb-1">点击日期查看当天数据(计数按排菜道数:套餐整组算 1)</div>
<div class="flex flex-wrap gap-2">
<button
v-for="d in dayTabs"
@@ -36,7 +36,10 @@
>
<div class="day-tab__date">{{ d.bizDate }}</div>
<div class="day-tab__meta">
<span>共{{ d.rowCount }}行</span>
<span>共{{ d.rowCount }}道</span>
<span v-if="d.excelRowCount && d.excelRowCount !== d.rowCount" class="text-gray-400">
({{ d.excelRowCount }}行)
</span>
<span v-if="d.locked" class="text-red-500">已锁定</span>
<template v-else>
<span class="text-green-600">新{{ d.insertCount }}</span>
@@ -52,14 +55,14 @@
<div class="text-sm">
<span class="font-medium">{{ activeDate || "-" }}</span>
<span class="sep">|</span>
<span>当日合计 <b>{{ dayStats.total }}</b></span>
<span>当日合计 <b>{{ dayStats.total }}</b> 道</span>
<span class="sep">|</span>
<span>早餐 {{ dayStats.breakfast }}</span>
<span>午餐 {{ dayStats.lunch }}</span>
<span>晚餐 {{ dayStats.dinner }}</span>
<span v-if="dayStats.other" class="text-orange-500">其他 {{ dayStats.other }}</span>
<span class="sep">|</span>
<span>已勾选 {{ daySelectedCount }}</span>
<span>已勾选 {{ daySelectedMenuCount }} 道</span>
</div>
<div class="flex items-center gap-2 ml-auto flex-wrap">
<span class="text-sm text-gray-500">餐段</span>
@@ -168,13 +171,23 @@
<span class="text-xs text-gray-500">{{ row.id || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="菜品SPU" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-xs text-gray-500">{{ row.spuId || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="子SPU" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-xs text-gray-500">{{ row.childSpuId || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="说明" min-width="160" show-overflow-tooltip prop="tip" />
</el-table>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="confirming" @click="submitConfirm">
确认导入(全部已勾选 {{ selectedCount }} 行)
确认导入(全部已勾选 {{ selectedMenuCount }} 道)
</el-button>
</template>
</el-dialog>
@@ -203,6 +216,38 @@ const ALL_MEALS = ["早餐", "午餐", "晚餐"] as const
const selectedCount = computed(() => rows.value.filter((r) => r.selected && !r.locked).length)
/** 同一套餐多行共用 groupKey,按「一道排菜」去重计数 */
const scheduleGroupKey = (r: any) => {
if (r?.groupKey) return String(r.groupKey)
const date = normalizeBizDate(r?.bizDate)
const meals = parseMealNames(r?.mealNames?.length ? r.mealNames : r?.mealName).join(",")
const id = String(r?.id || "").trim()
if (id) return `id:${id}|${date}|${meals}`
const combo = String(r?.comboName || "").trim()
if (combo && !isSingleComboMarker(combo)) return `new:${date}|${meals}|${combo}`
return `new:${date}|${meals}|${String(r?.dishName || "").trim()}`
}
const isSingleComboMarker = (combo: string) => {
const c = String(combo || "").trim()
if (!c) return false
if (["无", "-", "无套餐", "不套餐"].includes(c)) return true
return c.startsWith("单品") || c.startsWith("单点")
}
const countUniqueGroups = (list: any[], predicate?: (r: any) => boolean) => {
const keys = new Set<string>()
for (const r of list) {
if (predicate && !predicate(r)) continue
keys.add(scheduleGroupKey(r))
}
return keys.size
}
const selectedMenuCount = computed(() =>
countUniqueGroups(rows.value, (r) => r.selected && !r.locked),
)
/** 把后端/Excel 各种日期写法统一成 yyyy-MM-dd,避免同一天拆成两个 Tab */
const normalizeBizDate = (raw?: string) => {
const s = String(raw || "").trim()
@@ -221,10 +266,12 @@ const dayTabs = computed(() => {
bizDate: string
locked: boolean
rowCount: number
excelRowCount: number
insertCount: number
updateCount: number
skipLockedCount: number
}>()
const seenGroup = new Set<string>()
for (const r of rows.value) {
const date = normalizeBizDate(r.bizDate)
if (!date) continue
@@ -232,26 +279,40 @@ const dayTabs = computed(() => {
bizDate: date,
locked: false,
rowCount: 0,
excelRowCount: 0,
insertCount: 0,
updateCount: 0,
skipLockedCount: 0,
}
tab.rowCount++
if (r.locked || r.action === "SKIP_LOCKED") {
tab.excelRowCount++
const gk = `${date}::${scheduleGroupKey(r)}`
if (!seenGroup.has(gk)) {
seenGroup.add(gk)
tab.rowCount++
if (r.locked || r.action === "SKIP_LOCKED") {
tab.locked = true
tab.skipLockedCount++
} else if (r.action === "UPDATE") {
tab.updateCount++
} else {
tab.insertCount++
}
} else if (r.locked || r.action === "SKIP_LOCKED") {
tab.locked = true
tab.skipLockedCount++
} else if (r.action === "UPDATE") {
tab.updateCount++
} else {
tab.insertCount++
}
map.set(date, tab)
}
// 优先用后端 daySummaries 的锁定标记补齐
// 后端 daySummaries 已按排菜组统计,优先覆盖
for (const d of preview.value?.daySummaries || []) {
const key = normalizeBizDate(d.bizDate)
const exist = map.get(key)
if (exist && d.locked) exist.locked = true
if (!exist) continue
if (d.locked) exist.locked = true
if (typeof d.rowCount === "number") exist.rowCount = d.rowCount
if (typeof d.excelRowCount === "number") exist.excelRowCount = d.excelRowCount
if (typeof d.insertCount === "number") exist.insertCount = d.insertCount
if (typeof d.updateCount === "number") exist.updateCount = d.updateCount
if (typeof d.skipLockedCount === "number") exist.skipLockedCount = d.skipLockedCount
}
return [...map.values()].sort((a, b) => a.bizDate.localeCompare(b.bizDate))
})
@@ -307,14 +368,17 @@ const dayRows = computed(() =>
const dayStats = computed(() => {
const stats = { total: 0, breakfast: 0, lunch: 0, dinner: 0, other: 0 }
const seen = new Set<string>()
for (const r of dayRows.value) {
const gk = scheduleGroupKey(r)
if (seen.has(gk)) continue
seen.add(gk)
stats.total++
const meals = parseMealNames(r.mealNames?.length ? r.mealNames : r.mealName)
if (!meals.length) {
stats.other++
continue
}
// 一行多餐时,各餐段计数都 +1(方便排查)
let hit = false
for (const meal of meals) {
if (meal === "早餐") {
@@ -337,6 +401,10 @@ const daySelectedCount = computed(
() => dayRows.value.filter((r) => r.selected && !r.locked).length,
)
const daySelectedMenuCount = computed(() =>
countUniqueGroups(dayRows.value, (r) => r.selected && !r.locked),
)
const filteredRows = computed(() => {
let list = dayRows.value
const selectedMeals = mealFilters.value || []
@@ -451,6 +519,7 @@ const submitConfirm = async () => {
ingredients: r.ingredients,
id: r.id,
spuId: r.spuId,
childSpuId: r.childSpuId,
}
}),
}
@@ -180,6 +180,15 @@
<el-button @click="addFood(2)" v-auth="'canteen_canteenDailyMenu_add'">+ 中餐</el-button>
<el-button @click="addFood(3)" v-auth="'canteen_canteenDailyMenu_add'">+ 晚餐</el-button>
<el-button icon="Refresh" @click="loadAdminList">刷新</el-button>
<el-button
plain
type="warning"
:loading="syncSoldLoading"
v-auth="'canteen_canteenDailyMenu_edit'"
@click="syncSoldFromOrders"
>
按订单重算销量
</el-button>
</div>
</el-col>
</el-row>
@@ -353,6 +362,7 @@ import {
previewDailyMenuImport,
listDayLocks,
setDayLock,
syncChannelSold,
} from "/@/api/canteen/canteenDailyMenu"
import { getAllSimple as getCanteenCategory } from "/@/api/canteen/canteenCategory"
import { useTable } from "/@/hooks/table"
@@ -725,6 +735,7 @@ const delteData = async (tid: number) => {
const dayLocked = ref(false)
const dayLockLoading = ref(false)
const syncSoldLoading = ref(false)
const weekLockedDates = ref<Set<string>>(new Set())
const normalizeLockDate = (v: any) => {
@@ -798,6 +809,36 @@ const onDayLockChange = async (val: string | number | boolean) => {
}
}
/** 按当日订单净销量覆盖 online/offline sold,刷新剩余展示 */
const syncSoldFromOrders = async () => {
if (!merchantId.value || !timedata) {
message.warning("请先选择商户和日期")
return
}
try {
await ElMessageBox.confirm(
`将按订单重算 ${timedata} 的线上/线下销量(配额不变;无订单的菜品销量置 0)。是否继续?`,
"按订单重算销量",
{ type: "warning", confirmButtonText: "重算", cancelButtonText: "取消" }
)
} catch {
return
}
syncSoldLoading.value = true
try {
const res: any = await syncChannelSold({
merchantId: merchantId.value,
mealDate: timedata,
})
message.success(res.msg || "重算完成")
await loadAdminList()
} catch (err: any) {
message.error(err?.msg || "重算失败")
} finally {
syncSoldLoading.value = false
}
}
const importVisible = ref(false)
const importLoading = ref(false)
const importCategoryId = ref<string | number>()
+296 -31
View File
@@ -4,35 +4,81 @@
v-model="visible"
:close-on-click-modal="false"
draggable
width="640px"
:width="mode === 'view' ? '720px' : '640px'"
destroy-on-close
class="daily-menu-row-dialog"
>
<div v-loading="loading">
<el-descriptions v-if="mode === 'view'" :column="2" border>
<el-descriptions-item label="菜品">{{ detail.name || "-" }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag v-if="Number(detail.spuType) === 2" type="warning" size="small">套餐</el-tag>
<el-tag v-else type="info" size="small">单品</el-tag>
</el-descriptions-item>
<el-descriptions-item label="分类">{{ detail.categoryName || "-" }}</el-descriptions-item>
<el-descriptions-item label="餐段">{{ hourTypeLabel(detail.hourType) }}</el-descriptions-item>
<el-descriptions-item label="日期">{{ formatDate(detail.startTime) }}</el-descriptions-item>
<el-descriptions-item label="排序">{{ detail.sort ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="线上配额">{{ detail.onlineStock ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="线上剩余">{{ detail.remainOnlineStock ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="线下配额">{{ detail.offlineStock ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="线下剩余">{{ detail.remainOfflineStock ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="价格">
<span class="text-red-600">{{ displayPrice(detail.price) }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="Number(detail.spuType) === 2" label="套餐组成" :span="2">
{{ formatComboItems(detail.comboItems) || "-" }}
</el-descriptions-item>
<el-descriptions-item label="配料/详情" :span="2">
<!-- 查看:自定义排版,避免 descriptions 标签被挤成竖排 -->
<div v-if="mode === 'view'" class="dish-detail">
<div class="dish-detail__header">
<div class="dish-detail__title-wrap">
<div class="dish-detail__title" :title="detail.name">{{ detail.name || "-" }}</div>
<el-tag v-if="Number(detail.spuType) === 2" type="warning" size="small">套餐</el-tag>
<el-tag v-else type="info" size="small">单品</el-tag>
</div>
<div class="dish-detail__price">{{ displayPrice(detail.price) }}</div>
</div>
<div class="dish-detail__meta">
<div class="meta-item">
<span class="meta-label">分类</span>
<span class="meta-value">{{ detail.categoryName || "-" }}</span>
</div>
<div class="meta-item">
<span class="meta-label">餐段</span>
<span class="meta-value">{{ hourTypeLabel(detail.hourType) }}</span>
</div>
<div class="meta-item">
<span class="meta-label">日期</span>
<span class="meta-value">{{ formatDate(detail.startTime) }}</span>
</div>
<div class="meta-item">
<span class="meta-label">排序</span>
<span class="meta-value">{{ detail.sort ?? "-" }}</span>
</div>
<div class="meta-item">
<span class="meta-label">线上配额</span>
<span class="meta-value">{{ detail.onlineStock ?? "-" }}</span>
</div>
<div class="meta-item">
<span class="meta-label">线上剩余</span>
<span class="meta-value">{{ detail.remainOnlineStock ?? "-" }}</span>
</div>
<div class="meta-item">
<span class="meta-label">线下配额</span>
<span class="meta-value">{{ detail.offlineStock ?? "-" }}</span>
</div>
<div class="meta-item">
<span class="meta-label">线下剩余</span>
<span class="meta-value">{{ detail.remainOfflineStock ?? "-" }}</span>
</div>
</div>
<div v-if="Number(detail.spuType) === 2" class="dish-detail__section">
<div class="section-title">
套餐组成
<span class="section-count">共 {{ comboItemList.length }} 项</span>
</div>
<ul v-if="comboItemList.length" class="combo-list">
<li v-for="(item, idx) in comboItemList" :key="itemKey(item, idx)" class="combo-item">
<span class="combo-index">{{ idx + 1 }}</span>
<div class="combo-body">
<div class="combo-name">{{ item.name }}</div>
<div v-if="item.sub" class="combo-sub">{{ item.sub }}</div>
</div>
<span v-if="item.priceText" class="combo-price">{{ item.priceText }}</span>
</li>
</ul>
<div v-else class="section-empty">暂无组成菜</div>
</div>
<div class="dish-detail__section">
<div class="section-title">配料 / 详情</div>
<div v-if="detail.ingredients" class="rich-html" v-html="detail.ingredients" />
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<div v-else class="section-empty">-</div>
</div>
</div>
<el-form v-else ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="菜品">
@@ -109,8 +155,46 @@ const rules = {
const hourTypeLabel = (hourType?: number) => HOUR_TYPE_LABEL[hourType || 0] || "-"
const formatDate = (value?: string) => (value ? String(value).substring(0, 10) : "-")
const formatComboItems = (comboItems: any[] = []) =>
comboItems.map((c) => c.name || c.childSpuName).filter(Boolean).join("、")
/** 套餐组成:拆成列表项,长中英混排名尽量分行展示 */
const comboItemList = computed(() => {
const items = Array.isArray(detail.comboItems) ? detail.comboItems : []
return items
.map((c: any) => {
const raw = String(c?.name || c?.childSpuName || "").trim()
if (!raw) return null
const parts = splitDishName(raw)
const priceNum = c?.price
const priceText =
priceNum === null || priceNum === undefined || priceNum === ""
? ""
: displayPrice(priceNum)
return {
name: parts.primary,
sub: parts.secondary,
priceText,
id: c?.childSpuId || c?.id,
}
})
.filter(Boolean) as { name: string; sub: string; priceText: string; id?: string | number }[]
})
const itemKey = (item: { id?: string | number; name: string }, idx: number) =>
item.id != null ? String(item.id) : `${idx}-${item.name}`
/** 尽量把英文/日期后缀放到副行,主行保留中文菜名 */
const splitDishName = (raw: string) => {
const oneLine = raw.replace(/\s+/g, " ").trim()
const m = oneLine.match(/^(.+?)([A-Za-z].*)$/)
if (m && m[1].trim().length >= 2) {
return { primary: m[1].trim(), secondary: m[2].trim() }
}
if (oneLine.includes("\n")) {
const [first, ...rest] = oneLine.split("\n").map((s) => s.trim()).filter(Boolean)
return { primary: first || oneLine, secondary: rest.join(" ") }
}
return { primary: oneLine, secondary: "" }
}
const resetDetail = () => {
Object.keys(detail).forEach((k) => delete detail[k])
@@ -125,6 +209,7 @@ const openDialog = async (row: any, openMode: "view" | "edit" = "view") => {
...row,
ingredients: row.ingredients || "",
categoryName: row.categoryName || "-",
comboItems: row.comboItems || [],
})
form.menuId = row.id
form.spuId = row.spuId
@@ -143,6 +228,9 @@ const openDialog = async (row: any, openMode: "view" | "edit" = "view") => {
const spu = res.data || {}
detail.ingredients = spu.description || row.ingredients || ""
detail.categoryName = row.categoryName || detail.categoryName
if (Array.isArray(spu.comboItems) && spu.comboItems.length) {
detail.comboItems = spu.comboItems
}
if (openMode === "edit") {
form.sort = spu.sort ?? form.sort
form.priceYuan = Number(displayPrice(spu.price, "")) || form.priceYuan
@@ -194,13 +282,190 @@ defineExpose({ openDialog })
</script>
<style scoped>
.rich-html {
line-height: 1.6;
max-height: 320px;
overflow: auto;
.dish-detail {
display: flex;
flex-direction: column;
gap: 16px;
}
.dish-detail__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.dish-detail__title-wrap {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
flex: 1;
}
.dish-detail__title {
font-size: 16px;
font-weight: 600;
line-height: 1.45;
color: var(--el-text-color-primary);
word-break: break-word;
}
.dish-detail__price {
flex-shrink: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-color-danger);
line-height: 1.4;
}
.dish-detail__meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 20px;
padding: 12px 14px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
}
.meta-item {
display: flex;
align-items: baseline;
gap: 10px;
min-width: 0;
}
.meta-label {
flex-shrink: 0;
width: 4.5em;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.meta-value {
min-width: 0;
color: var(--el-text-color-primary);
font-size: 13px;
word-break: break-word;
}
.dish-detail__section {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-title {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.section-count {
font-size: 12px;
font-weight: 400;
color: var(--el-text-color-secondary);
}
.section-empty {
color: var(--el-text-color-placeholder);
font-size: 13px;
padding: 8px 0;
}
.combo-list {
list-style: none;
margin: 0;
padding: 0;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
overflow: hidden;
max-height: 280px;
overflow-y: auto;
}
.combo-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--el-bg-color);
}
.combo-item:last-child {
border-bottom: none;
}
.combo-item:nth-child(even) {
background: var(--el-fill-color-blank);
}
.combo-index {
flex-shrink: 0;
width: 22px;
height: 22px;
margin-top: 1px;
border-radius: 50%;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-size: 12px;
line-height: 22px;
text-align: center;
}
.combo-body {
flex: 1;
min-width: 0;
}
.combo-name {
font-size: 13px;
line-height: 1.45;
color: var(--el-text-color-primary);
word-break: break-word;
}
.combo-sub {
margin-top: 2px;
font-size: 12px;
line-height: 1.4;
color: var(--el-text-color-secondary);
word-break: break-word;
}
.combo-price {
flex-shrink: 0;
font-size: 13px;
color: var(--el-color-danger);
line-height: 1.45;
}
.rich-html {
line-height: 1.6;
max-height: 240px;
overflow: auto;
word-break: break-word;
padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
font-size: 13px;
}
.rich-html :deep(img) {
max-width: 100%;
}
.rich-html :deep(p) {
margin: 0 0 0.5em;
}
.rich-html :deep(p:last-child) {
margin-bottom: 0;
}
</style>
@@ -217,7 +217,7 @@
<!-- 按月汇总 -->
<el-dialog v-model="aggVisible" title="统计汇总到刷卡明细" width="580px" :close-on-click-modal="false">
<el-alert type="warning" :closable="false" show-icon class="mb-3"
title="按整月已入库人脸明细写入刷卡。排除厂区:这些厂区不写入,且覆盖时保留其原有 FACE 数据。" />
title="按整月已入库人脸明细写入刷卡。排除厂区:这些厂区不写入,并清除其当月 FACE,看板不再显示该厂区。" />
<el-form label-width="120px">
<el-form-item label="业务月" required>
<el-date-picker v-model="aggForm.bizMonth" type="month" value-format="YYYY-MM" />
@@ -245,7 +245,7 @@
</el-form-item>
<el-form-item label="覆盖当月FACE">
<el-switch v-model="aggForm.replaceFace" />
<span class="ml-2 text-xs text-gray-400">清空当月 FACE 再写入(已排除厂区会保留)</span>
<span class="ml-2 text-xs text-gray-400">清空当月 FACE 再写入(排除厂区一并清除、不再展示)</span>
</el-form-item>
</el-form>
<template #footer>
@@ -631,7 +631,7 @@ const submitAggregate = async () => {
const excludeTip = aggForm.excludeParks.length ? `,排除厂区:${aggForm.excludeParks.join("、")}` : ""
try {
await useMessageBox().confirm(
`确认将 ${aggForm.bizMonth} 全部已入库人脸明细汇总写入刷卡?匹配方式:${matchTip}(月份对月份)${excludeTip}。将覆盖该月 FACE 刷卡(排除厂区保留)。`
`确认将 ${aggForm.bizMonth} 全部已入库人脸明细汇总写入刷卡?匹配方式:${matchTip}(月份对月份)${excludeTip}。将覆盖该月 FACE 刷卡(排除厂区会清除并不再展示)。`
)
} catch {
return