diff --git a/src/api/canteen/canteenReport.ts b/src/api/canteen/canteenReport.ts index b3fbb456..65d295a2 100644 --- a/src/api/canteen/canteenReport.ts +++ b/src/api/canteen/canteenReport.ts @@ -5,7 +5,13 @@ export function fetchFaceImportLogPage(query?: Object) { return request({ url: "/canteen/canteenReport/faceImport/log/page", method: "get", params: query }) } export function delFaceImportLog(ids?: Object) { - return request({ url: "/canteen/canteenReport/faceImport/log", method: "delete", data: ids }) + return request({ + url: "/canteen/canteenReport/faceImport/log", + method: "delete", + data: ids, + // 大批量明细/刷卡分批物理删除可能较久 + timeout: 600000, + }) } export function fetchFaceDetailPage(query?: Object) { return request({ url: "/canteen/canteenReport/faceImport/detail/page", method: "get", params: query }) @@ -89,8 +95,20 @@ export function putFoodEnergy(obj?: Object) { export function delFoodEnergy(ids?: Object) { return request({ url: "/canteen/canteenReport/foodEnergy", method: "delete", data: ids }) } -export function clearFoodEnergyMonth(bizMonth: string) { - return request({ url: `/canteen/canteenReport/foodEnergy/month/${bizMonth}`, method: "delete" }) +export function clearFoodEnergyMonth(bizMonth: string, park?: string) { + return request({ + url: `/canteen/canteenReport/foodEnergy/month/${bizMonth}`, + method: "delete", + params: park ? { park } : undefined, + }) +} + +export function clearBoxImport(params: { bizMonth: string; park?: string }) { + return request({ url: "/canteen/canteenReport/boxImport/clear", method: "delete", params }) +} + +export function reportParks(bizMonth: string) { + return request({ url: "/canteen/canteenReport/parks", method: "get", params: { bizMonth } }) } /** 手工调整 */ @@ -161,6 +179,9 @@ export function delPerson(ids?: Object) { export function clearPersonAll() { return request({ url: "/canteen/canteenReport/person/all", method: "delete" }) } +export function clearPersonByMonth(bizMonth: string) { + return request({ url: "/canteen/canteenReport/person/month", method: "delete", params: { bizMonth } }) +} export function importPersonExcel(data: FormData, params: Object) { return request({ url: "/canteen/canteenReport/person/import", diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index 452ea087..82b6610c 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -233,3 +233,10 @@ export const dateTimeStr: string = "YYYY-MM-DD HH:mm:ss" export const dateStr: string = "YYYY-MM-DD" export const timeStr: string = "HH:mm:ss" + +/** 当前自然月,格式 yyyy-MM(报表业务月默认值) */ +export function currentBizMonth(): string { + const d = new Date() + const m = d.getMonth() + 1 + return `${d.getFullYear()}-${m < 10 ? `0${m}` : m}` +} diff --git a/src/views/canteen/canteenDailyMenu/importConfirmDialog.vue b/src/views/canteen/canteenDailyMenu/importConfirmDialog.vue index 65e797eb..f4a98dba 100644 --- a/src/views/canteen/canteenDailyMenu/importConfirmDialog.vue +++ b/src/views/canteen/canteenDailyMenu/importConfirmDialog.vue @@ -148,9 +148,14 @@ - + - + + + + + + @@ -198,6 +203,19 @@ const ALL_MEALS = ["早餐", "午餐", "晚餐"] as const const selectedCount = computed(() => rows.value.filter((r) => r.selected && !r.locked).length) +/** 把后端/Excel 各种日期写法统一成 yyyy-MM-dd,避免同一天拆成两个 Tab */ +const normalizeBizDate = (raw?: string) => { + const s = String(raw || "").trim() + if (!s) return "" + const head = s.split(/[ T]/)[0] || s + const m = head.match(/^(\d{4})[\/\-.](\d{1,2})[\/\-.](\d{1,2})$/) + if (!m) return head + const y = m[1] + const mo = String(Number(m[2])).padStart(2, "0") + const d = String(Number(m[3])).padStart(2, "0") + return `${y}-${mo}-${d}` +} + const dayTabs = computed(() => { const map = new Map { skipLockedCount: number }>() for (const r of rows.value) { - const date = String(r.bizDate || "").trim() + const date = normalizeBizDate(r.bizDate) if (!date) continue const tab = map.get(date) || { bizDate: date, @@ -231,7 +249,8 @@ const dayTabs = computed(() => { } // 优先用后端 daySummaries 的锁定标记补齐 for (const d of preview.value?.daySummaries || []) { - const exist = map.get(d.bizDate) + const key = normalizeBizDate(d.bizDate) + const exist = map.get(key) if (exist && d.locked) exist.locked = true } return [...map.values()].sort((a, b) => a.bizDate.localeCompare(b.bizDate)) @@ -283,7 +302,7 @@ const onRowMealsChange = (row: any) => { } const dayRows = computed(() => - rows.value.filter((r) => String(r.bizDate || "").trim() === activeDate.value), + rows.value.filter((r) => normalizeBizDate(r.bizDate) === activeDate.value), ) const dayStats = computed(() => { @@ -357,15 +376,17 @@ const open = (payload: { replaceDay.value = payload.replaceDay rows.value = (payload.preview?.rows || []).map((r: any) => { const mealNames = parseMealNames(r.mealName) + const bizDate = normalizeBizDate(r.bizDate) return { ...r, + bizDate, mealNames: mealNames.length ? mealNames : [], mealName: mealNames.join(",") || r.mealName, } }) mealFilters.value = [...ALL_MEALS] const dates = [ - ...new Set(rows.value.map((r) => String(r.bizDate || "").trim()).filter(Boolean)), + ...new Set(rows.value.map((r) => normalizeBizDate(r.bizDate)).filter(Boolean)), ].sort() activeDate.value = dates[0] || "" visible.value = true @@ -425,7 +446,8 @@ const submitConfirm = async () => { categoryName: r.categoryName, dishPriceYuan: toNumberOrUndef(r.dishPriceYuan), priceYuan: toNumberOrUndef(r.priceYuan), - stock: toNumberOrUndef(r.stock), + onlineStock: toNumberOrUndef(r.onlineStock), + offlineStock: toNumberOrUndef(r.offlineStock), ingredients: r.ingredients, id: r.id, spuId: r.spuId, diff --git a/src/views/canteen/canteenDailyMenu/index.vue b/src/views/canteen/canteenDailyMenu/index.vue index 82aa1a00..8f1de29d 100644 --- a/src/views/canteen/canteenDailyMenu/index.vue +++ b/src/views/canteen/canteenDailyMenu/index.vue @@ -245,18 +245,24 @@ - - + + + + + {{ displayPrice(row.price) }} - + 详情 + + 相关订单 + 编辑 @@ -275,6 +281,7 @@ + @@ -358,6 +365,7 @@ import { downBlobFile } from "/@/utils/other" const FormDialog = defineAsyncComponent(() => import("./form.vue")) const RowDialog = defineAsyncComponent(() => import("./rowDialog.vue")) +const OrderOrdersDialog = defineAsyncComponent(() => import("./orderOrdersDialog.vue")) const ImportConfirmDialog = defineAsyncComponent(() => import("./importConfirmDialog.vue")) const ImportLogDialog = defineAsyncComponent(() => import("./importLogDialog.vue")) const imageBaseUrl = import.meta.env.VITE_IMAGE_BASE_PATH @@ -371,6 +379,7 @@ const { userInfos } = storeToRefs(stores) const currentUserDeptId = ref(null) const formDialogRef = ref() const rowDialogRef = ref() +const orderOrdersDialogRef = ref() const loading = ref(true) const message = useMessage() // 消息弹窗 @@ -583,6 +592,17 @@ const openRowDetail = (row: any) => { rowDialogRef.value?.openDialog({ ...row, merchantId: row.merchantId || merchantId.value }, "view") } +const openRowOrders = (row: any) => { + if (!row?.spuId) { + useMessage().error("缺少菜品 SPU,无法查询订单") + return + } + orderOrdersDialogRef.value?.openDialog({ + ...row, + merchantId: row.merchantId || merchantId.value, + }) +} + const openRowEdit = (row: any) => { if (dayLocked.value) { useMessage().error("当日菜谱已锁定,请先解锁再编辑") diff --git a/src/views/canteen/canteenDailyMenu/orderOrdersDialog.vue b/src/views/canteen/canteenDailyMenu/orderOrdersDialog.vue new file mode 100644 index 00000000..66e66a80 --- /dev/null +++ b/src/views/canteen/canteenDailyMenu/orderOrdersDialog.vue @@ -0,0 +1,156 @@ + + + + 按商户、用餐日、餐段和本菜 SPU 查询含该商品的订单(含线上预约与线下消费)。 + + + + + + + + 未知用户 + + + + + {{ productCountOf(row) }} + + + + + + + + + + {{ displayPrice(row.payPrice, "元", 2) }} + + + + + + 详情 + + + + + + + + + + diff --git a/src/views/canteen/canteenDailyMenu/rowDialog.vue b/src/views/canteen/canteenDailyMenu/rowDialog.vue index d88b72d1..2c8c035f 100644 --- a/src/views/canteen/canteenDailyMenu/rowDialog.vue +++ b/src/views/canteen/canteenDailyMenu/rowDialog.vue @@ -18,7 +18,10 @@ {{ hourTypeLabel(detail.hourType) }} {{ formatDate(detail.startTime) }} {{ detail.sort ?? "-" }} - {{ detail.stock ?? "-" }} + {{ detail.onlineStock ?? "-" }} + {{ detail.remainOnlineStock ?? "-" }} + {{ detail.offlineStock ?? "-" }} + {{ detail.remainOfflineStock ?? "-" }} {{ displayPrice(detail.price) }} @@ -49,8 +52,11 @@ - - + + + + + @@ -91,7 +97,8 @@ const form = reactive({ startTime: "", hourType: undefined as number | undefined, sort: 0, - stock: 0, + onlineStock: 600, + offlineStock: 600, priceYuan: 0, description: "", }) @@ -125,7 +132,8 @@ const openDialog = async (row: any, openMode: "view" | "edit" = "view") => { form.startTime = formatDate(row.startTime) form.hourType = row.hourType form.sort = row.sort ?? 0 - form.stock = row.stock ?? 0 + form.onlineStock = row.onlineStock ?? 600 + form.offlineStock = row.offlineStock ?? 600 form.priceYuan = Number(displayPrice(row.price, "")) || 0 form.description = row.ingredients || "" @@ -137,7 +145,6 @@ const openDialog = async (row: any, openMode: "view" | "edit" = "view") => { detail.categoryName = row.categoryName || detail.categoryName if (openMode === "edit") { form.sort = spu.sort ?? form.sort - form.stock = spu.stock ?? form.stock form.priceYuan = Number(displayPrice(spu.price, "")) || form.priceYuan form.description = spu.description || "" } @@ -160,13 +167,14 @@ const onSubmit = async () => { startTime: form.startTime, spuId: form.spuId, hourType: form.hourType, + onlineStock: form.onlineStock, + offlineStock: form.offlineStock, }) const spuRes: any = await getSpu(form.spuId) const spu = spuRes.data || {} await putSpu({ ...spu, sort: form.sort, - stock: form.stock, price: storePrice(form.priceYuan), marketPrice: storePrice(form.priceYuan), description: form.description, diff --git a/src/views/canteen/canteenReportAdjust/index.vue b/src/views/canteen/canteenReportAdjust/index.vue index e4afa90a..e680693c 100644 --- a/src/views/canteen/canteenReportAdjust/index.vue +++ b/src/views/canteen/canteenReportAdjust/index.vue @@ -1,4 +1,4 @@ - + @@ -112,6 +112,7 @@ import { BasicTableProps, useTable } from "/@/hooks/table" import { fetchAdjustPage, addAdjust, putAdjust, delAdjust } from "/@/api/canteen/canteenReport" import { useMessage, useMessageBox } from "/@/hooks/message" +import { currentBizMonth } from "/@/utils/formatTime" const queryRef = ref() const showSearch = ref(true) @@ -119,7 +120,7 @@ const selectObjs = ref([]) as any const multiple = ref(true) const state: BasicTableProps = reactive({ - queryForm: { bizMonth: "2026-07" }, + queryForm: { bizMonth: "" }, pageList: fetchAdjustPage, }) @@ -127,7 +128,7 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableS const resetQuery = () => { queryRef.value?.resetFields() - state.queryForm = { bizMonth: "2026-07" } + state.queryForm = { bizMonth: "" } selectObjs.value = [] getDataList() } @@ -160,7 +161,7 @@ const formVisible = ref(false) const saving = ref(false) const form = reactive({ id: undefined, - bizMonth: "2026-07", + bizMonth: currentBizMonth(), adjustType: "", costCenter: "", park: "", @@ -171,7 +172,7 @@ const form = reactive({ const openForm = (row?: any) => { Object.assign(form, { id: undefined, - bizMonth: state.queryForm.bizMonth || "2026-07", + bizMonth: state.queryForm.bizMonth || currentBizMonth(), adjustType: "", costCenter: "", park: "", diff --git a/src/views/canteen/canteenReportBoard/index.vue b/src/views/canteen/canteenReportBoard/index.vue index d4dfac8a..6b542db0 100644 --- a/src/views/canteen/canteenReportBoard/index.vue +++ b/src/views/canteen/canteenReportBoard/index.vue @@ -288,13 +288,14 @@ + + diff --git a/src/views/canteen/canteenReportFaceImport/index.vue b/src/views/canteen/canteenReportFaceImport/index.vue index b97aa5b1..7d8eda7b 100644 --- a/src/views/canteen/canteenReportFaceImport/index.vue +++ b/src/views/canteen/canteenReportFaceImport/index.vue @@ -59,7 +59,8 @@ - 明细 + 明细 删除 @@ -146,6 +148,7 @@ 报表归属月:数据算进哪个月的报表(看板按这个月查)。 本文件日期段:这份 Excel 实际覆盖哪几天(如同月拆成 1–15、16–31 两份时必填)。 + 按表头名称识别列(自动忽略空格),人员编号/交易设备等用别名匹配;数字工号按整数入库。 大文件提交后后台异步导入,列表状态为「导入中」即可关闭弹窗,不必一直等接口返回。 @@ -212,16 +215,37 @@ - + + title="按整月已入库人脸明细写入刷卡。排除厂区:这些厂区不写入,且覆盖时保留其原有 FACE 数据。" /> + + + 按人员信息 + 按花名册 + + 二选一:人员信息取核对成本;花名册取人事范围(作核对成本) + + + + + + - 先清空该月全部 FACE 刷卡再写入 + 清空当月 FACE 再写入(已排除厂区会保留) @@ -349,9 +373,9 @@ - + + :title="`共 ${missPeople} 人、${missRows} 条刷卡行未命中${missLabel},已回退法律归属,并写入刷卡明细备注「${missRemarkTag}」。`" /> @@ -380,16 +404,19 @@ import { aggregateFaceToSwipe, fetchFaceOrgLevel2, delFaceImportLog, + fetchSettleParks, } from "/@/api/canteen/canteenReport" import { useMessage, useMessageBox } from "/@/hooks/message" +import { currentBizMonth } from "/@/utils/formatTime" import { useRouter } from "vue-router" const router = useRouter() const queryRef = ref() const showSearch = ref(true) +const deleteLoadingId = ref("") const state: BasicTableProps = reactive({ - queryForm: { bizMonth: "2026-07", status: "" }, + queryForm: { bizMonth: "", status: "" }, pageList: fetchFaceImportLogPage, }) @@ -397,7 +424,7 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTa const resetQuery = () => { queryRef.value?.resetFields() - state.queryForm = { bizMonth: "2026-07", status: "" } + state.queryForm = { bizMonth: "", status: "" } getDataList() } @@ -408,7 +435,10 @@ const goUnmatchedSwipe = () => { missVisible.value = false router.push({ path: "/canteen/canteenReportSwipe/index", - query: { bizMonth: aggForm.bizMonth || state.queryForm.bizMonth || "", remarkTag: "未命中人员信息" }, + query: { + bizMonth: aggForm.bizMonth || state.queryForm.bizMonth || "", + remarkTag: missRemarkTag.value, + }, }) } const goSettlePark = () => { @@ -428,7 +458,7 @@ const pollingImport = ref(false) const importUploadRef = ref() const importDateRange = ref(null) const importForm = reactive({ - bizMonth: "2026-07", + bizMonth: currentBizMonth(), replaceRange: true, orgLevel2s: [...PREFERRED_ORGS] as string[], }) @@ -488,7 +518,7 @@ const onImportDialogBeforeClose = (done: () => void) => { } const openImport = () => { - importForm.bizMonth = state.queryForm.bizMonth || "2026-07" + importForm.bizMonth = state.queryForm.bizMonth || currentBizMonth() importForm.replaceRange = true importForm.orgLevel2s = [...PREFERRED_ORGS] importDateRange.value = null @@ -565,23 +595,43 @@ const submitImport = async () => { const aggVisible = ref(false) const aggLoading = ref(false) +const parkOptions = ref([]) const aggForm = reactive({ - bizMonth: "2026-07", + bizMonth: currentBizMonth(), replaceFace: true, + matchMode: "PERSON" as "PERSON" | "ROSTER", + excludeParks: [] as string[], importLogId: "" as string, }) +const missRemarkTag = computed(() => (aggForm.matchMode === "ROSTER" ? "未命中花名册" : "未命中人员信息")) +const missLabel = computed(() => (aggForm.matchMode === "ROSTER" ? "花名册" : "人员信息")) +const missTitle = computed(() => `未命中${missLabel.value}`) + +const loadParkOptions = async () => { + try { + const res: any = await fetchSettleParks() + parkOptions.value = res.data || [] + } catch { + parkOptions.value = [] + } +} const openAggregate = () => { - aggForm.bizMonth = state.queryForm.bizMonth || "2026-07" + aggForm.bizMonth = state.queryForm.bizMonth || currentBizMonth() aggForm.replaceFace = true + aggForm.matchMode = "PERSON" + aggForm.excludeParks = [] aggForm.importLogId = "" + loadParkOptions() aggVisible.value = true } const submitAggregate = async () => { if (!aggForm.bizMonth) return useMessage().warning("请选择业务月") + const matchTip = aggForm.matchMode === "ROSTER" ? "按花名册" : "按人员信息" + const excludeTip = aggForm.excludeParks.length ? `,排除厂区:${aggForm.excludeParks.join("、")}` : "" try { await useMessageBox().confirm( - `确认将 ${aggForm.bizMonth} 全部已入库人脸明细(上半月+下半月)汇总写入刷卡?将覆盖该月 FACE 刷卡。刷脸→员工刷卡;B扫C 等保持支付方式原样(可再批量改为支付宝)。` + `确认将 ${aggForm.bizMonth} 全部已入库人脸明细汇总写入刷卡?匹配方式:${matchTip}(月份对月份)${excludeTip}。将覆盖该月 FACE 刷卡(排除厂区保留)。` ) } catch { return @@ -591,6 +641,8 @@ const submitAggregate = async () => { const res: any = await aggregateFaceToSwipe({ bizMonth: aggForm.bizMonth, replaceFace: aggForm.replaceFace, + matchMode: aggForm.matchMode, + excludeParks: aggForm.excludeParks.join(","), }) useMessage().success(res.msg || "汇总成功") aggVisible.value = false @@ -693,21 +745,24 @@ const openSkip = (row: any) => { } const handleDeleteLog = async (row: { id: string; fileName?: string; status?: string }) => { - if (row.status === "RUNNING") return + if (row.status === "RUNNING" || deleteLoadingId.value) return try { await useMessageBox().confirm( - `确认删除「${row.fileName || "该导入"}」?将删除该批人脸逐笔明细;若已汇总到刷卡明细,对应 FACE 来源行也会一并删除,之后可重新导入。`, + `确认删除「${row.fileName || "该导入"}」?将删除该批人脸逐笔明细;若已汇总到刷卡明细,对应 FACE 来源行也会一并删除,数据量较大时可能需要等待。`, "删除导入记录" ) } catch { return } + deleteLoadingId.value = row.id try { const res: any = await delFaceImportLog([row.id]) useMessage().success(res.msg || "删除成功") getDataList() } catch (err: any) { useMessage().error(err.msg || "删除失败") + } finally { + deleteLoadingId.value = "" } } diff --git a/src/views/canteen/canteenReportFoodEnergy/index.vue b/src/views/canteen/canteenReportFoodEnergy/index.vue index ccf1e913..90f57f96 100644 --- a/src/views/canteen/canteenReportFoodEnergy/index.vue +++ b/src/views/canteen/canteenReportFoodEnergy/index.vue @@ -32,6 +32,9 @@ 清空当月 + + 按厂区清空 + 删除 @@ -186,14 +189,39 @@ 按厂区汇总「结算总金额」,分类为食材 / 能耗 / 外协(空分类当食材)→ 看板「食材池」。人工列不进池,只进经营报表支出。 + + + + + + + + + + + + + + 取消 + 确认清空 + +