报表,每日库存
This commit is contained in:
1 parent
64a367894f
commit
2cd379cc57
15 files changed
+627
-83
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -148,9 +148,14 @@
|
||||
<el-input v-model="row.priceYuan" size="small" :disabled="row.locked" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="80">
|
||||
<el-table-column label="线上库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.stock" size="small" :disabled="row.locked" />
|
||||
<el-input v-model="row.onlineStock" size="small" :disabled="row.locked" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="线下库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.offlineStock" size="small" :disabled="row.locked" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="配料" min-width="120">
|
||||
@@ -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<string, {
|
||||
bizDate: string
|
||||
@@ -208,7 +226,7 @@ const dayTabs = computed(() => {
|
||||
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,
|
||||
|
||||
@@ -245,18 +245,24 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" width="72" align="center" sortable />
|
||||
<el-table-column label="库存" prop="stock" width="72" align="center" sortable />
|
||||
<el-table-column label="剩余库存" prop="remainStock" width="88" align="center" sortable />
|
||||
<el-table-column label="线上配额" prop="onlineStock" width="88" align="center" sortable />
|
||||
<el-table-column label="线上剩余" prop="remainOnlineStock" width="88" align="center" sortable />
|
||||
<el-table-column label="线下配额" prop="offlineStock" width="88" align="center" sortable />
|
||||
<el-table-column label="线下剩余" prop="remainOfflineStock" width="88" align="center" sortable />
|
||||
<el-table-column label="剩余合计" prop="remainStock" width="88" align="center" sortable />
|
||||
<el-table-column label="价格" width="96" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-red-600">{{ displayPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="300" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="view" text type="primary" @click="openRowDetail(row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button icon="Tickets" text type="primary" @click="openRowOrders(row)">
|
||||
相关订单
|
||||
</el-button>
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'canteen_canteenDailyMenu_edit'"
|
||||
@click="openRowEdit(row)">
|
||||
编辑
|
||||
@@ -275,6 +281,7 @@
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="refreshMenuData" />
|
||||
<row-dialog ref="rowDialogRef" @refresh="refreshMenuData" />
|
||||
<order-orders-dialog ref="orderOrdersDialogRef" />
|
||||
<copy-dialog ref="copyDialogRef" @success="fetchData" />
|
||||
|
||||
<el-dialog v-model="exportVisible" title="导出菜谱" width="480px" :close-on-click-modal="false" destroy-on-close>
|
||||
@@ -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("当日菜谱已锁定,请先解锁再编辑")
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="720px" destroy-on-close :title="drawerTitle" @closed="onClosed">
|
||||
<div class="mb-3 text-sm text-gray-500">
|
||||
按商户、用餐日、餐段和本菜 SPU 查询含该商品的订单(含线上预约与线下消费)。
|
||||
</div>
|
||||
<el-table :data="list" border stripe v-loading="loading" max-height="70vh"
|
||||
:empty-text="emptyText">
|
||||
<el-table-column label="下单人" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.userId && row.userId != -1" class="min-w-0">
|
||||
<user-info-cell :user="row" :show-gender="false" />
|
||||
</div>
|
||||
<span v-else class="text-gray-400">未知用户</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本菜份数" width="88" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ productCountOf(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="hard_order_status" :value="row.orderStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实付" width="96" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="text-red-600">{{ displayPrice(row.payPrice, "元", 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付时间" prop="payTime" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="88" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" @click="openOrderDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
small
|
||||
@current-change="loadOrders"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router"
|
||||
import { pageJoin } from "/@/api/hard/hard-order"
|
||||
import { displayPrice } from "/@/utils/formatMoney"
|
||||
import { useDict } from "/@/hooks/dict"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
|
||||
const UserInfoCell = defineAsyncComponent(() => import("/@/components/UserInfoCell/index.vue"))
|
||||
|
||||
const { hard_order_status } = useDict("hard_order_status")
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const menuRow = ref<any>(null)
|
||||
const page = reactive({ current: 1, size: 10, total: 0 })
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const name = menuRow.value?.name || "菜品"
|
||||
const date = formatDate(menuRow.value?.startTime)
|
||||
const meal = hourLabel(menuRow.value?.hourType)
|
||||
return `相关订单 · ${name} · ${date} ${meal}`
|
||||
})
|
||||
|
||||
const emptyText = computed(() => (loading.value ? "加载中…" : "暂无包含该菜的订单"))
|
||||
|
||||
const hourLabel = (hourType?: number | string) => {
|
||||
const map: Record<string, string> = { "1": "早餐", "2": "中餐", "3": "晚餐" }
|
||||
return map[String(hourType ?? "")] || "-"
|
||||
}
|
||||
|
||||
const formatDate = (value?: string) => {
|
||||
if (!value) return "-"
|
||||
return String(value).slice(0, 10)
|
||||
}
|
||||
|
||||
const productCountOf = (order: any) => {
|
||||
const spuId = String(menuRow.value?.spuId ?? "")
|
||||
const cars = Array.isArray(order?.carList) ? order.carList : []
|
||||
const hit = cars.filter((c: any) => String(c.productId ?? "") === spuId)
|
||||
if (!hit.length) return "-"
|
||||
return hit.reduce((sum: number, c: any) => sum + Number(c.productCount || 0), 0)
|
||||
}
|
||||
|
||||
const buildApptRange = (dateStr: string) => {
|
||||
const day = formatDate(dateStr)
|
||||
return [`${day} 00:00:00`, `${day} 23:59:59`]
|
||||
}
|
||||
|
||||
const loadOrders = async () => {
|
||||
const row = menuRow.value
|
||||
if (!row?.spuId || !row?.startTime || row?.hourType == null) {
|
||||
list.value = []
|
||||
page.total = 0
|
||||
return
|
||||
}
|
||||
const merchantId = row.merchantId
|
||||
if (!merchantId) {
|
||||
message.error("缺少商户信息")
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await pageJoin({
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
mchIds: [merchantId],
|
||||
productId: row.spuId,
|
||||
hourTypeCodes: [String(row.hourType)],
|
||||
apptUseDateTimeList: buildApptRange(row.startTime),
|
||||
isFindCarList: true,
|
||||
})
|
||||
list.value = data?.records || []
|
||||
page.total = Number(data?.total || 0)
|
||||
} catch (e: any) {
|
||||
list.value = []
|
||||
page.total = 0
|
||||
message.error(e?.msg || e?.message || "查询订单失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openOrderDetail = (row: any) => {
|
||||
router.push({ path: "/hard/hard-after-sale/order-refund", query: { id: String(row.id), type: "view" } })
|
||||
}
|
||||
|
||||
const onClosed = () => {
|
||||
menuRow.value = null
|
||||
list.value = []
|
||||
page.current = 1
|
||||
page.total = 0
|
||||
}
|
||||
|
||||
const openDialog = (row: any) => {
|
||||
menuRow.value = { ...row }
|
||||
page.current = 1
|
||||
visible.value = true
|
||||
nextTick(() => loadOrders())
|
||||
}
|
||||
|
||||
defineExpose({ openDialog })
|
||||
</script>
|
||||
@@ -18,7 +18,10 @@
|
||||
<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.stock ?? "-" }}</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>
|
||||
@@ -49,8 +52,11 @@
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :precision="0" class="w-full!" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库存" prop="stock">
|
||||
<el-input-number v-model="form.stock" :min="0" :precision="0" class="w-full!" />
|
||||
<el-form-item label="线上配额" prop="onlineStock">
|
||||
<el-input-number v-model="form.onlineStock" :min="0" :precision="0" class="w-full!" />
|
||||
</el-form-item>
|
||||
<el-form-item label="线下配额" prop="offlineStock">
|
||||
<el-input-number v-model="form.offlineStock" :min="0" :precision="0" class="w-full!" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格" prop="priceYuan">
|
||||
<el-input-number v-model="form.priceYuan" :min="0" :precision="2" class="w-full!" />
|
||||
@@ -91,7 +97,8 @@ const form = reactive({
|
||||
startTime: "",
|
||||
hourType: undefined as number | undefined,
|
||||
sort: 0,
|
||||
stock: 0,
|
||||
onlineStock: 600,
|
||||
offlineStock: 600,
|
||||
priceYuan: 0,
|
||||
description: "<p></p>",
|
||||
})
|
||||
@@ -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 || "<p></p>"
|
||||
|
||||
@@ -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 || "<p></p>"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-alert type="info" :closable="false" show-icon class="mb12">
|
||||
@@ -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<BasicTableProps>({
|
||||
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<any>({
|
||||
id: undefined,
|
||||
bizMonth: "2026-07",
|
||||
bizMonth: currentBizMonth(),
|
||||
adjustType: "",
|
||||
costCenter: "",
|
||||
park: "",
|
||||
@@ -171,7 +172,7 @@ const form = reactive<any>({
|
||||
const openForm = (row?: any) => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
bizMonth: state.queryForm.bizMonth || "2026-07",
|
||||
bizMonth: state.queryForm.bizMonth || currentBizMonth(),
|
||||
adjustType: "",
|
||||
costCenter: "",
|
||||
park: "",
|
||||
|
||||
@@ -288,13 +288,14 @@
|
||||
<script setup lang="ts" name="systemCanteenReportBoard">
|
||||
import { reportSummary, reportIncome, reportSupplierShare, reportSupplierShareAlloc, saveSupplierShareAlloc, reportBusiness } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
import other from "/@/utils/other"
|
||||
|
||||
const parks = ["敏惠园区", "敏实园区", "敏胜园区", "敏华园区", "湖州园区", "华智一期", "华智二期", "安吉园区"]
|
||||
const shareParks = ["敏惠园区", "敏实园区", "敏胜园区", "敏华园区", "湖州园区", "安吉园区", "华智一期"]
|
||||
const ALLOC_MANUAL_COLS = ["招待费"]
|
||||
|
||||
const bizMonth = ref("2026-07")
|
||||
const bizMonth = ref(currentBizMonth())
|
||||
const parks = ref<string[]>([])
|
||||
const shareParks = ref<string[]>([])
|
||||
const activeTab = ref("business")
|
||||
const shareSubTab = ref("cost")
|
||||
const loading = ref(false)
|
||||
@@ -580,6 +581,8 @@ const loadAll = async () => {
|
||||
reportBusiness(bizMonth.value),
|
||||
])
|
||||
summary.value = s.data
|
||||
parks.value = s.data?.parks || []
|
||||
shareParks.value = s.data?.shareParks || s.data?.parks || []
|
||||
incomeRows.value = i.data || []
|
||||
shareRows.value = sh.data || []
|
||||
shareParkChecks.value = s.data?.shareParkChecks || []
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
title="上传「包厢台账(汇总)」Excel,写入刷卡明细:收入类别=包厢收入,source=BOX。可到「刷卡明细」按收入类别筛选核对,再打开报表看板。"
|
||||
title="上传「包厢台账(汇总)」Excel,按表头名称识别列(成本中心/签单人工号/总计/园区等)。写入刷卡明细:收入类别=包厢收入。跳过行可在导入后查看原因。"
|
||||
/>
|
||||
<el-card shadow="never">
|
||||
<el-card shadow="never" class="mb-4">
|
||||
<template #header>导入包厢台账</template>
|
||||
<el-form label-width="110px" style="max-width: 520px">
|
||||
<el-form-item label="业务月" required>
|
||||
<el-date-picker v-model="bizMonth" type="month" value-format="YYYY-MM" />
|
||||
@@ -36,6 +37,45 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>清空包厢数据</template>
|
||||
<el-form label-width="110px" style="max-width: 520px">
|
||||
<el-form-item label="业务月" required>
|
||||
<el-date-picker v-model="clearForm.bizMonth" type="month" value-format="YYYY-MM" />
|
||||
</el-form-item>
|
||||
<el-form-item label="厂区">
|
||||
<el-select
|
||||
v-model="clearForm.park"
|
||||
filterable
|
||||
allow-create
|
||||
clearable
|
||||
default-first-option
|
||||
placeholder="不选则清空当月全部包厢"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in parkOptions" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="danger" plain :loading="clearLoading" @click="submitClear">确认清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="skipVisible" title="包厢导入跳过说明" width="720px" destroy-on-close>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
:title="`写入 ${skipWritten} 条,跳过 ${skipCount} 条。常见原因:空行、无金额、成本中心/工号/园区都为空。`"
|
||||
/>
|
||||
<pre class="skip-pre">{{ skipText || "无详情" }}</pre>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="skipVisible = false">知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,16 +83,39 @@
|
||||
<script setup lang="ts" name="systemCanteenReportBoxImport">
|
||||
import type { UploadFile, UploadInstance } from "element-plus"
|
||||
import { useRouter } from "vue-router"
|
||||
import { importBoxExcel } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { importBoxExcel, clearBoxImport, fetchSettleParks } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
|
||||
const router = useRouter()
|
||||
const bizMonth = ref("2026-07")
|
||||
const bizMonth = ref(currentBizMonth())
|
||||
const replaceMonth = ref(true)
|
||||
const file = ref<File | null>(null)
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
const parkOptions = ref<string[]>([])
|
||||
const clearLoading = ref(false)
|
||||
const clearForm = reactive({
|
||||
bizMonth: currentBizMonth(),
|
||||
park: "",
|
||||
})
|
||||
|
||||
const skipVisible = ref(false)
|
||||
const skipText = ref("")
|
||||
const skipCount = ref(0)
|
||||
const skipWritten = ref(0)
|
||||
|
||||
const loadParks = async () => {
|
||||
try {
|
||||
const res: any = await fetchSettleParks()
|
||||
parkOptions.value = res.data || []
|
||||
} catch {
|
||||
parkOptions.value = []
|
||||
}
|
||||
}
|
||||
onMounted(loadParks)
|
||||
|
||||
const onFileChange = (f: UploadFile) => {
|
||||
file.value = (f.raw as File) || null
|
||||
}
|
||||
@@ -77,13 +140,66 @@ const submit = async () => {
|
||||
useMessage().success(res.msg || "导入成功")
|
||||
file.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
const data = res.data || {}
|
||||
skipWritten.value = data.written || 0
|
||||
skipCount.value = data.skip || 0
|
||||
skipText.value = data.skipSummary || ""
|
||||
if (skipCount.value > 0) {
|
||||
skipVisible.value = true
|
||||
}
|
||||
} catch (err: any) {
|
||||
const data = err?.data || err?.response?.data?.data || {}
|
||||
if (data.skipSummary || data.skip) {
|
||||
skipWritten.value = data.written || 0
|
||||
skipCount.value = data.skip || 0
|
||||
skipText.value = data.skipSummary || err.msg || ""
|
||||
skipVisible.value = true
|
||||
}
|
||||
useMessage().error(err.msg || "导入失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitClear = async () => {
|
||||
if (!clearForm.bizMonth) return useMessage().warning("请选择业务月")
|
||||
const tip = clearForm.park
|
||||
? `确认清空 ${clearForm.bizMonth} 厂区「${clearForm.park}」的全部包厢刷卡?`
|
||||
: `确认清空 ${clearForm.bizMonth} 全部包厢刷卡?`
|
||||
try {
|
||||
await useMessageBox().confirm(tip)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
clearLoading.value = true
|
||||
try {
|
||||
const res: any = await clearBoxImport({
|
||||
bizMonth: clearForm.bizMonth,
|
||||
park: clearForm.park || undefined,
|
||||
})
|
||||
useMessage().success(res.msg || "已清空")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg || "清空失败")
|
||||
} finally {
|
||||
clearLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goSwipe = () => router.push("/canteen/canteenReportSwipe/index")
|
||||
const goBoard = () => router.push("/canteen/canteenReportBoard/index")
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.skip-pre {
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -59,7 +59,8 @@
|
||||
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
v-loading="state.loading || !!deleteLoadingId"
|
||||
element-loading-text="正在删除明细与关联刷卡,请稍候…"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@@ -106,11 +107,12 @@
|
||||
<el-table-column prop="createTime" label="导入时间" width="170" />
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">明细</el-button>
|
||||
<el-button link type="primary" :disabled="!!deleteLoadingId" @click="openDetail(row)">明细</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="row.status === 'RUNNING'"
|
||||
:loading="deleteLoadingId === row.id"
|
||||
:disabled="row.status === 'RUNNING' || (!!deleteLoadingId && deleteLoadingId !== row.id)"
|
||||
@click="handleDeleteLog(row)"
|
||||
>
|
||||
删除
|
||||
@@ -146,6 +148,7 @@
|
||||
<div style="line-height: 1.6">
|
||||
<div><b>报表归属月</b>:数据算进哪个月的报表(看板按这个月查)。</div>
|
||||
<div><b>本文件日期段</b>:这份 Excel 实际覆盖哪几天(如同月拆成 1–15、16–31 两份时必填)。</div>
|
||||
<div>按表头名称识别列(自动忽略空格),人员编号/交易设备等用别名匹配;数字工号按整数入库。</div>
|
||||
<div>大文件提交后后台异步导入,列表状态为「导入中」即可关闭弹窗,不必一直等接口返回。</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -212,16 +215,37 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 按月汇总 -->
|
||||
<el-dialog v-model="aggVisible" title="统计汇总到刷卡明细" width="520px" :close-on-click-modal="false">
|
||||
<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" />
|
||||
</el-form-item>
|
||||
<el-form-item label="匹配方式" required>
|
||||
<el-radio-group v-model="aggForm.matchMode">
|
||||
<el-radio label="PERSON">按人员信息</el-radio>
|
||||
<el-radio label="ROSTER">按花名册</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-xs text-gray-400 mt-1">二选一:人员信息取核对成本;花名册取人事范围(作核对成本)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排除厂区">
|
||||
<el-select
|
||||
v-model="aggForm.excludeParks"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="不选则汇总全部厂区"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in parkOptions" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</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>
|
||||
@@ -349,9 +373,9 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="missVisible" title="未命中人员信息" width="860px" destroy-on-close>
|
||||
<el-dialog v-model="missVisible" :title="missTitle" width="860px" destroy-on-close>
|
||||
<el-alert type="warning" :closable="false" show-icon class="mb-3"
|
||||
:title="`共 ${missPeople} 人、${missRows} 条刷卡行未命中人员信息,已回退法律归属,并写入刷卡明细备注「未命中人员信息」。`" />
|
||||
:title="`共 ${missPeople} 人、${missRows} 条刷卡行未命中${missLabel},已回退法律归属,并写入刷卡明细备注「${missRemarkTag}」。`" />
|
||||
<el-table :data="missList" border max-height="460" size="small">
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="empNo" label="工号" width="120" show-overflow-tooltip />
|
||||
@@ -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<BasicTableProps>({
|
||||
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<UploadInstance>()
|
||||
const importDateRange = ref<string[] | null>(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<string[]>([])
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
<el-button icon="Delete" plain type="danger" class="ml10" :disabled="!state.queryForm.bizMonth" @click="clearMonth">
|
||||
清空当月
|
||||
</el-button>
|
||||
<el-button icon="Delete" plain type="warning" class="ml10" @click="openClearPark">
|
||||
按厂区清空
|
||||
</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" @click="handleDelete(selectObjs)">
|
||||
删除
|
||||
</el-button>
|
||||
@@ -186,14 +189,39 @@
|
||||
按厂区汇总「结算总金额」,分类为食材 / 能耗 / 外协(空分类当食材)→ 看板「食材池」。人工列不进池,只进经营报表支出。
|
||||
</p>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="clearParkVisible" title="按厂区清空食材能耗" width="480px" :close-on-click-modal="false">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="业务月" required>
|
||||
<el-date-picker v-model="clearParkForm.bizMonth" type="month" value-format="YYYY-MM" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="厂区" required>
|
||||
<el-select
|
||||
v-model="clearParkForm.park"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入厂区"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in parkOptions" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="clearParkVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="clearParkLoading" @click="submitClearPark">确认清空</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemCanteenReportFoodEnergy">
|
||||
import type { UploadFile, UploadInstance } from "element-plus"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { fetchFoodEnergyPage, addFoodEnergy, putFoodEnergy, delFoodEnergy, clearFoodEnergyMonth } from "/@/api/canteen/canteenReport"
|
||||
import { fetchFoodEnergyPage, addFoodEnergy, putFoodEnergy, delFoodEnergy, clearFoodEnergyMonth, fetchSettleParks } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
import request from "/@/utils/request"
|
||||
|
||||
const queryRef = ref()
|
||||
@@ -203,7 +231,7 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: { bizMonth: "2026-07" },
|
||||
queryForm: { bizMonth: "" },
|
||||
pageList: fetchFoodEnergyPage,
|
||||
})
|
||||
|
||||
@@ -211,7 +239,7 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableS
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = { bizMonth: "2026-07" }
|
||||
state.queryForm = { bizMonth: "" }
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
@@ -253,13 +281,54 @@ const clearMonth = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearParkVisible = ref(false)
|
||||
const clearParkLoading = ref(false)
|
||||
const parkOptions = ref<string[]>([])
|
||||
const clearParkForm = reactive({
|
||||
bizMonth: "",
|
||||
park: "",
|
||||
})
|
||||
|
||||
const openClearPark = async () => {
|
||||
clearParkForm.bizMonth = state.queryForm.bizMonth || currentBizMonth()
|
||||
clearParkForm.park = state.queryForm.park || ""
|
||||
try {
|
||||
const res: any = await fetchSettleParks()
|
||||
parkOptions.value = res.data || []
|
||||
} catch {
|
||||
parkOptions.value = []
|
||||
}
|
||||
clearParkVisible.value = true
|
||||
}
|
||||
|
||||
const submitClearPark = async () => {
|
||||
if (!clearParkForm.bizMonth) return useMessage().warning("请选择业务月")
|
||||
if (!clearParkForm.park) return useMessage().warning("请选择厂区")
|
||||
try {
|
||||
await useMessageBox().confirm(`确认清空 ${clearParkForm.bizMonth} 厂区「${clearParkForm.park}」的全部食材能耗?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
clearParkLoading.value = true
|
||||
try {
|
||||
await clearFoodEnergyMonth(clearParkForm.bizMonth, clearParkForm.park)
|
||||
clearParkVisible.value = false
|
||||
getDataList()
|
||||
useMessage().success("已按厂区清空")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg || "清空失败")
|
||||
} finally {
|
||||
clearParkLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const exportExcel = () => {
|
||||
downBlobFile("/canteen/canteenReport/foodEnergy/export", { ...state.queryForm }, `食材能耗_${state.queryForm.bizMonth || ""}.xlsx`)
|
||||
}
|
||||
|
||||
const importVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importBizMonth = ref("2026-07")
|
||||
const importBizMonth = ref(currentBizMonth())
|
||||
const replaceMonth = ref(true)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importUploadRef = ref<UploadInstance>()
|
||||
@@ -271,7 +340,7 @@ const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
bizMonth: "2026-07",
|
||||
bizMonth: currentBizMonth(),
|
||||
bizDate: "",
|
||||
supplier: "",
|
||||
productName: "",
|
||||
@@ -284,7 +353,7 @@ const form = reactive<any>({
|
||||
const openForm = (row?: any) => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
bizMonth: state.queryForm.bizMonth || "2026-07",
|
||||
bizMonth: state.queryForm.bizMonth || currentBizMonth(),
|
||||
bizDate: "",
|
||||
supplier: "",
|
||||
productName: "",
|
||||
@@ -334,7 +403,7 @@ const saveForm = async () => {
|
||||
}
|
||||
|
||||
const openImport = () => {
|
||||
importBizMonth.value = state.queryForm.bizMonth || "2026-07"
|
||||
importBizMonth.value = state.queryForm.bizMonth || currentBizMonth()
|
||||
importVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item label="适用月" prop="bizMonth">
|
||||
<el-date-picker
|
||||
v-model="state.queryForm.bizMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="选择月份"
|
||||
style="width: 140px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="工号" prop="empNo">
|
||||
<el-input v-model="state.queryForm.empNo" clearable placeholder="工号" style="width: 140px" />
|
||||
</el-form-item>
|
||||
@@ -23,6 +32,7 @@
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="openForm()">新增</el-button>
|
||||
<el-button icon="upload-filled" type="primary" class="ml10" @click="openImport">Excel 导入</el-button>
|
||||
<el-button icon="download" plain type="primary" class="ml10" @click="exportExcel">导出</el-button>
|
||||
<el-button icon="Delete" plain type="warning" class="ml10" @click="clearMonth">清空当月</el-button>
|
||||
<el-button icon="Delete" plain type="danger" class="ml10" @click="clearAll">清空全部</el-button>
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" @click="handleDelete(selectObjs)">
|
||||
删除
|
||||
@@ -35,7 +45,7 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
title="无适用月。导入分「全量覆盖」与「新增导入」(同工号则更新)。手工新增:工号、姓名、成本分类、厂区必填。人脸汇总用工号查本表取核对成本。"
|
||||
title="人员信息按「适用月」维护(每月可不同)。人脸汇总选「按人员信息」时,用汇总月对同月工号取核对成本;也可改选「按花名册」。"
|
||||
/>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
@@ -47,6 +57,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" fixed />
|
||||
<el-table-column type="index" label="#" width="50" fixed />
|
||||
<el-table-column prop="bizMonth" label="适用月" width="90" fixed />
|
||||
<el-table-column prop="empNo" label="工号" width="120" fixed show-overflow-tooltip />
|
||||
<el-table-column prop="empName" label="姓名" width="100" />
|
||||
<el-table-column prop="costCenter" label="核对成本" min-width="160" show-overflow-tooltip />
|
||||
@@ -67,6 +78,9 @@
|
||||
|
||||
<el-dialog v-model="formVisible" :title="form.id ? '编辑人员信息' : '新增人员信息'" width="560px" :close-on-click-modal="false">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="适用月" required>
|
||||
<el-date-picker v-model="form.bizMonth" type="month" value-format="YYYY-MM" :disabled="!!form.id" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工号" required>
|
||||
<el-input v-model="form.empNo" clearable :disabled="!!form.id" />
|
||||
</el-form-item>
|
||||
@@ -104,13 +118,16 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
title="请优先上传 .xlsx(整本食堂报表,自动读「刷卡明细」)。若用 CSV/TSV,需 Tab/逗号分列且编码为 GBK 或 UTF-8;WPS 能正常显示不代表上传的是分列后的 xlsx。无工号行剔除,按工号去重。"
|
||||
title="请优先上传 .xlsx(整本食堂报表,自动读「刷卡明细」)。需指定适用月;可覆盖当月或同月同工号更新。无工号行剔除,按工号去重。"
|
||||
/>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="适用月" required>
|
||||
<el-date-picker v-model="importBizMonth" type="month" value-format="YYYY-MM" />
|
||||
</el-form-item>
|
||||
<el-form-item label="导入方式" required>
|
||||
<el-radio-group v-model="replaceAll">
|
||||
<el-radio :label="true">全量覆盖导入</el-radio>
|
||||
<el-radio :label="false">新增导入(同工号更新)</el-radio>
|
||||
<el-radio-group v-model="replaceMonth">
|
||||
<el-radio :label="true">覆盖当月</el-radio>
|
||||
<el-radio :label="false">同月同工号更新</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="Excel 文件" required>
|
||||
@@ -143,9 +160,11 @@ import {
|
||||
putPerson,
|
||||
delPerson,
|
||||
clearPersonAll,
|
||||
clearPersonByMonth,
|
||||
importPersonExcel,
|
||||
} from "/@/api/canteen/canteenReport"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -153,7 +172,7 @@ const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
queryForm: { bizMonth: "" },
|
||||
pageList: fetchPersonPage,
|
||||
})
|
||||
|
||||
@@ -161,7 +180,7 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableS
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = {}
|
||||
state.queryForm = { bizMonth: "" }
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
@@ -186,6 +205,23 @@ const handleDelete = async (ids: string[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearMonth = async () => {
|
||||
const month = state.queryForm.bizMonth
|
||||
if (!month) return useMessage().warning("请先在筛选里选择适用月")
|
||||
try {
|
||||
await useMessageBox().confirm(`确认清空 ${month} 的人员信息?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await clearPersonByMonth(month)
|
||||
getDataList()
|
||||
useMessage().success("已清空当月")
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg || "清空失败")
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = async () => {
|
||||
try {
|
||||
await useMessageBox().confirm("确认清空全部人员信息?此操作不可恢复。")
|
||||
@@ -202,13 +238,15 @@ const clearAll = async () => {
|
||||
}
|
||||
|
||||
const exportExcel = () => {
|
||||
downBlobFile("/canteen/canteenReport/person/export", {}, `人员信息.xlsx`)
|
||||
const month = state.queryForm.bizMonth || ""
|
||||
downBlobFile("/canteen/canteenReport/person/export", { bizMonth: month }, `人员信息${month ? "_" + month : ""}.xlsx`)
|
||||
}
|
||||
|
||||
const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
bizMonth: currentBizMonth(),
|
||||
empNo: "",
|
||||
empName: "",
|
||||
costCenter: "",
|
||||
@@ -222,6 +260,7 @@ const form = reactive<any>({
|
||||
const openForm = (row?: any) => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
bizMonth: state.queryForm.bizMonth || currentBizMonth(),
|
||||
empNo: "",
|
||||
empName: "",
|
||||
costCenter: "",
|
||||
@@ -238,6 +277,7 @@ const openForm = (row?: any) => {
|
||||
}
|
||||
|
||||
const saveForm = async () => {
|
||||
if (!form.bizMonth) return useMessage().warning("请选择适用月")
|
||||
if (!form.empNo) return useMessage().warning("请填写工号")
|
||||
if (!form.empName) return useMessage().warning("请填写姓名")
|
||||
if (!form.costCategory) return useMessage().warning("请填写成本分类")
|
||||
@@ -261,12 +301,14 @@ const saveForm = async () => {
|
||||
|
||||
const importVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const replaceAll = ref(true)
|
||||
const replaceMonth = ref(true)
|
||||
const importBizMonth = ref(currentBizMonth())
|
||||
const importFile = ref<File | null>(null)
|
||||
const importUploadRef = ref<UploadInstance>()
|
||||
|
||||
const openImport = () => {
|
||||
replaceAll.value = true
|
||||
replaceMonth.value = true
|
||||
importBizMonth.value = state.queryForm.bizMonth || currentBizMonth()
|
||||
importVisible.value = true
|
||||
}
|
||||
|
||||
@@ -275,16 +317,21 @@ const onImportFileChange = (file: UploadFile) => {
|
||||
}
|
||||
|
||||
const submitImport = async () => {
|
||||
if (!importBizMonth.value) return useMessage().warning("请选择适用月")
|
||||
if (!importFile.value) return useMessage().warning("请选择 Excel 文件")
|
||||
const fd = new FormData()
|
||||
fd.append("file", importFile.value)
|
||||
importLoading.value = true
|
||||
try {
|
||||
const res: any = await importPersonExcel(fd, { replaceAll: replaceAll.value })
|
||||
const res: any = await importPersonExcel(fd, {
|
||||
bizMonth: importBizMonth.value,
|
||||
replaceMonth: replaceMonth.value,
|
||||
})
|
||||
useMessage().success(res.msg || "导入成功")
|
||||
importVisible.value = false
|
||||
importFile.value = null
|
||||
importUploadRef.value?.clearFiles()
|
||||
state.queryForm.bizMonth = importBizMonth.value
|
||||
getDataList()
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg || "导入失败")
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3"
|
||||
title="花名册仍可导入备查。人脸汇总的核对成本已改为查「人员信息」(工号→核对成本)。请到人员信息导入客户刷卡明细汇总表后再汇总人脸。"
|
||||
title="花名册按适用月导入备查。人脸汇总时可二选一:按花名册(人事范围)或按人员信息(核对成本),均按业务月对同月匹配。"
|
||||
/>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
@@ -102,12 +102,13 @@ import type { UploadFile, UploadInstance } from "element-plus"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { fetchRosterPage, importRosterExcel } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: { bizMonth: "2026-07", empNo: "", empName: "", hrScope: "" },
|
||||
queryForm: { bizMonth: "", empNo: "", empName: "", hrScope: "" },
|
||||
pageList: fetchRosterPage,
|
||||
})
|
||||
|
||||
@@ -115,19 +116,19 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTa
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = { bizMonth: "2026-07", empNo: "", empName: "", hrScope: "" }
|
||||
state.queryForm = { bizMonth: "", empNo: "", empName: "", hrScope: "" }
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const importVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importBizMonth = ref("2026-07")
|
||||
const importBizMonth = ref(currentBizMonth())
|
||||
const replaceMonth = ref(true)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importUploadRef = ref<UploadInstance>()
|
||||
|
||||
const openImport = () => {
|
||||
importBizMonth.value = state.queryForm.bizMonth || "2026-07"
|
||||
importBizMonth.value = state.queryForm.bizMonth || currentBizMonth()
|
||||
importVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,14 @@
|
||||
<el-input v-model="form.settleCounter" placeholder="与人脸表「交易设备」一致" />
|
||||
</el-form-item>
|
||||
<el-form-item label="厂区" required>
|
||||
<el-select v-model="form.park" placeholder="选择厂区" style="width: 100%">
|
||||
<el-select
|
||||
v-model="form.park"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入厂区"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in parks" :key="p" :label="p" :value="p" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -326,14 +326,24 @@
|
||||
<script setup lang="ts" name="systemCanteenReportSwipe">
|
||||
import type { UploadFile, UploadInstance } from "element-plus"
|
||||
import { BasicTableProps, useTable } from "/@/hooks/table"
|
||||
import { fetchSwipePage, fetchSwipeSum, addSwipe, putSwipe, batchUpdateSwipe, delSwipe, clearSwipeMonth } from "/@/api/canteen/canteenReport"
|
||||
import { fetchSwipePage, fetchSwipeSum, addSwipe, putSwipe, batchUpdateSwipe, delSwipe, clearSwipeMonth, fetchSettleParks } from "/@/api/canteen/canteenReport"
|
||||
import { useMessage, useMessageBox } from "/@/hooks/message"
|
||||
import { currentBizMonth } from "/@/utils/formatTime"
|
||||
import request from "/@/utils/request"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const incomeTypes = ["员工刷卡", "支付宝", "包厢收入", "其他收入", "利润调整", "开票结算", "无法收回", "B扫C支付", "B扫C"]
|
||||
const parks = ["敏惠园区", "敏实园区", "敏胜园区", "敏华园区", "湖州园区", "华智一期", "华智二期", "安吉园区"]
|
||||
const parks = ref<string[]>([])
|
||||
|
||||
const loadParks = async () => {
|
||||
try {
|
||||
const res: any = await fetchSettleParks()
|
||||
parks.value = res.data || []
|
||||
} catch {
|
||||
parks.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -348,7 +358,7 @@ const amountSummary = ref<{
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {
|
||||
bizMonth: (route.query.bizMonth as string) || "2026-07",
|
||||
bizMonth: (route.query.bizMonth as string) || "",
|
||||
remarkTag: (route.query.remarkTag as string) || "",
|
||||
source: (route.query.source as string) || "",
|
||||
...(route.query.importLogId ? { importLogId: route.query.importLogId } : {}),
|
||||
@@ -384,12 +394,13 @@ const searchList = () => {
|
||||
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields()
|
||||
state.queryForm = { bizMonth: "2026-07" }
|
||||
state.queryForm = { bizMonth: "" }
|
||||
selectObjs.value = []
|
||||
searchList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadParks()
|
||||
loadAmountSummary()
|
||||
})
|
||||
|
||||
@@ -438,7 +449,7 @@ const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
bizMonth: "2026-07",
|
||||
bizMonth: currentBizMonth(),
|
||||
bizDate: "",
|
||||
empNo: "",
|
||||
empName: "",
|
||||
@@ -458,7 +469,7 @@ const form = reactive<any>({
|
||||
const openForm = (row?: any) => {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
bizMonth: state.queryForm.bizMonth || "2026-07",
|
||||
bizMonth: state.queryForm.bizMonth || currentBizMonth(),
|
||||
bizDate: "",
|
||||
empNo: "",
|
||||
empName: "",
|
||||
@@ -571,7 +582,7 @@ const submitBatch = async () => {
|
||||
|
||||
const importVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importBizMonth = ref("2026-07")
|
||||
const importBizMonth = ref(currentBizMonth())
|
||||
const replaceMonth = ref(true)
|
||||
const importFile = ref<File | null>(null)
|
||||
const importUploadRef = ref<UploadInstance>()
|
||||
@@ -580,7 +591,7 @@ const errorTitle = ref("导入失败")
|
||||
const errorData = ref<any[]>([])
|
||||
|
||||
const openImport = () => {
|
||||
importBizMonth.value = state.queryForm.bizMonth || "2026-07"
|
||||
importBizMonth.value = state.queryForm.bizMonth || currentBizMonth()
|
||||
importVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user