feat:钱包模块导出都改为分批,前端显示导出进度条的模式。

fix:修改各个统计模块统计的数量不一致的情况。
fix:修改支付宝分账退款失败的情况
This commit is contained in:
dzc committed 2026-08-26 17:48:55 +08:00
1 parent 452f0839fd
commit b5ad897188
18 files changed
+497 -260

No files matched your search

+8 -6
View File
@@ -8,7 +8,7 @@ export interface CanteenUserReservationQuery {
current?: number
/** 每页用户数量。 */
size?: number
/** 用餐日期闭区间,固定为开始、结束两个 YYYY-MM-DD 日期,最大跨度为两个自然月。 */
/** 统计日期闭区间;已核销按实际核销日期,其他状态按预约用餐日期,最大跨度为两个自然月。 */
apptUseDateList: [string, string]
/** 姓名、登录账号或手机号关键字。 */
nameOrPhone?: string
@@ -42,6 +42,8 @@ export interface CanteenUserReservationOrder {
id: string | number
/** 用餐时间段字典编码。 */
hourType?: string
/** 订单原预约用餐日期;已核销按核销日查询时用于展示跨日预约。 */
apptUseDate?: string
/** 下单时保存的商品描述。 */
productDesc?: string
/** 预约所属食堂名称。 */
@@ -115,13 +117,13 @@ export interface CanteenUserReservationDishStatistics {
/** 预约统计的通用汇总结构。金额单位为分。 */
export interface CanteenUserReservationStatisticsSummary {
/** 用户-用餐日期记录数。 */
/** 用户-统计日期记录数。 */
userCount?: number
/** 当前范围内去重用户数。 */
uniqueUserCount?: number
/** 已订餐用户-用餐日期记录数。 */
/** 已订餐用户-统计日期记录数。 */
reservedUserCount?: number
/** 未订餐用户-用餐日期记录数。 */
/** 未订餐用户-统计日期记录数。 */
unreservedUserCount?: number
/** 有效预约订单数。 */
orderCount?: number
@@ -137,7 +139,7 @@ export interface CanteenUserReservationStatisticsSummary {
/** 预约每日统计结构。 */
export interface CanteenUserReservationDailyStatistics extends CanteenUserReservationStatisticsSummary {
/** 用餐日期,格式为 YYYY-MM-DD。 */
/** 统计日期,已核销取核销日期,其他状态取预约用餐日期。 */
date?: string
}
@@ -155,7 +157,7 @@ export interface CanteenUserReservationStatisticsResponse {
}
/**
* 按用户和用餐日期范围分页查询每日食堂预约,后端会保留每天没有预约日志的权限范围用户。
* 按用户和统计日期范围分页查询每日食堂预约,后端会保留没有预约日志的权限范围用户日期行。
*/
export function canteenUserReservationPage(query: CanteenUserReservationQuery) {
return request({
+18
View File
@@ -1,4 +1,13 @@
import request from "/@/utils/request"
import type { AsyncTaskProgress } from "/@/api/asyncTask"
/** 充值扣款水单异步导出提交接口的统一响应。 */
export interface WalletRechargeImportExportTaskResponse {
/** 后端创建的初始异步任务进度。 */
data: AsyncTaskProgress
/** 后端返回的任务提交提示。 */
msg?: string
}
export function pageJoin(query: any) {
return request({
@@ -58,3 +67,12 @@ export function recover(params?: Object) {
params,
})
}
/** 提交充值扣款水单异步分片导出任务。 */
export function submitExport(query: Record<string, unknown>) {
return request<unknown, WalletRechargeImportExportTaskResponse>({
url: "/wallet/wallet-recharge-import/export-async",
method: "post",
data: query,
})
}
+43
View File
@@ -1,4 +1,13 @@
import request from "/@/utils/request"
import type { AsyncTaskProgress } from "/@/api/asyncTask"
/** 钱包流水异步导出提交接口的统一响应。 */
export interface WalletTransactionExportTaskResponse {
/** 后端创建的初始异步任务进度。 */
data: AsyncTaskProgress
/** 后端返回的任务提交提示。 */
msg?: string
}
/** 流水备注的前后端统一字符上限,与 wallet_transaction.remark 字段长度保持一致。 */
export const WALLET_TRANSACTION_REMARK_MAX_LENGTH = 256
@@ -138,6 +147,40 @@ export function tkTotal(data: any) {
})
}
/** 提交充值记录异步分片导出任务。 */
export function submitCzExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({ url: "/wallet/wallet-transaction/export-cz-async", method: "post", data: query })
}
/** 提交记账扣款异步分片导出任务。 */
export function submitJzExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({ url: "/wallet/wallet-transaction/export-jz-async", method: "post", data: query })
}
/** 提交消费记录异步分片导出任务。 */
export function submitXfExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({ url: "/wallet/wallet-transaction/export-xf-async", method: "post", data: query })
}
/** 提交退款记录异步分片导出任务。 */
export function submitTkExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({ url: "/wallet/wallet-transaction/export-tk-async", method: "post", data: query })
}
/** 提交食堂退还记录异步分片导出任务。 */
export function submitThExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({ url: "/wallet/wallet-transaction/export-th-async", method: "post", data: query })
}
/** 提交个人充值统计异步分片导出任务。 */
export function submitCzTotalPersonExport(query: Record<string, unknown>) {
return request<unknown, WalletTransactionExportTaskResponse>({
url: "/wallet/wallet-transaction/export-cz-total-person-async",
method: "post",
data: query,
})
}
export function updateBalance(obj?: Object) {
return request({
url: "/wallet/wallet-transaction/update-balance",
@@ -1,91 +1,74 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<SearchPanel
v-show="showSearch"
ref="queryRef"
:model="state.queryForm"
:loading="Boolean(state.loading)"
:search-text="$t('common.queryBtn')"
:reset-text="$t('common.resetBtn')"
@search="doSearch"
@reset="resetQuery"
>
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm"
:loading="Boolean(state.loading)" :search-text="$t('common.queryBtn')"
:reset-text="$t('common.resetBtn')" @search="doSearch" @reset="resetQuery">
<el-form-item style="width: 250px" label="用餐时间段" prop="hourType">
<dict-select v-model="state.queryForm.hourType" dict-code="daily_menu" />
</el-form-item>
<el-form-item style="width: 250px" label="食堂" prop="merchantId">
<select-mch v-model="state.queryForm.merchantId" deptMch :apiParams="{ parentIsNull: false, types: '1' }" />
<select-mch v-model="state.queryForm.merchantId" deptMch
:apiParams="{ parentIsNull: false, types: '1' }" />
</el-form-item>
<el-form-item style="width: 450px" label="统计日期" prop="statisticsDateTimeList">
<el-date-picker
v-model="state.queryForm.statisticsDateTimeList"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
:default-time="dateRangeDefaultTime"
:clearable="false"
/>
<el-form-item style="width: 500px" prop="statisticsDateTimeList">
<template #label>
<span class="inline-flex items-center gap-1">
{{ statisticsDateColumnLabel }}
<el-tooltip :content="statisticsScopeDescription" placement="top">
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-date-picker v-model="state.queryForm.statisticsDateTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
:default-time="dateRangeDefaultTime" :clearable="false" />
</el-form-item>
</SearchPanel>
<div class="flex justify-between items-center">
<el-tabs class="flex-1" v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="tab in sourceTabs" :key="tab.name" :label="tab.label" :name="tab.name" />
<el-tab-pane v-for="tab in sourceTabs" :key="tab.name" :label="tab.label"
:name="tab.name" />
</el-tabs>
<right-toolbar
v-model:showSearch="showSearch"
:export="'hard_canteenDeptTotal_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
@queryTable="getDataList"
/>
<right-toolbar v-model:showSearch="showSearch" :export="'hard_canteenDeptTotal_export'"
@exportExcel="exportExcel" class="ml10 mr20" @queryTable="getDataList" />
</div>
<el-table
:data="displayRows"
v-loading="state.loading"
border
row-key="_statisticsRowKey"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
:summary-method="getSummaries"
:show-summary="true"
:row-class-name="statisticsRowClassName"
@sort-change="sortChangeHandle"
>
<el-alert class="mb-3" :title="statisticsScopeDescription" type="info"
:closable="false" show-icon />
<el-table :data="displayRows" v-loading="state.loading" border row-key="_statisticsRowKey"
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
:summary-method="getSummaries" :show-summary="true" :row-class-name="statisticsRowClassName"
@sort-change="sortChangeHandle">
<el-table-column type="expand" width="50">
<template #default="{ row }">
<div class="mx-[50px] my-2">
<div class="flex items-center gap-2 mb-2 text-sm text-[var(--el-text-color-secondary)]">
<div
class="flex items-center gap-2 mb-2 text-sm text-[var(--el-text-color-secondary)]">
<span class="font-medium text-[var(--el-text-color-primary)]">部门餐段明细</span>
<span>{{ rowContext(row) }}</span>
</div>
<el-table
:data="displayChildRows(row)"
border
row-key="_statisticsRowKey"
<el-table :data="displayChildRows(row)" border row-key="_statisticsRowKey"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
:summary-method="getSummaries"
:show-summary="true"
:row-class-name="statisticsRowClassName"
>
:header-cell-style="tableStyle.headerCellStyle" :summary-method="getSummaries"
:show-summary="true" :row-class-name="statisticsRowClassName">
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column prop="userDeptName" align="center" label="部门" width="200" />
<el-table-column label="统计维度" min-width="140">
<template #default="{ row: child }">
<div class="flex items-center gap-2" :class="dimensionIndentClass(child)">
<el-icon v-if="isSourceLevel(child)" class="text-[var(--el-text-color-secondary)]"><Right /></el-icon>
<span :class="isTotalLevel(child) ? 'font-semibold' : ''">{{ statisticsLevelName(child) }}</span>
<div class="flex items-center gap-2"
:class="dimensionIndentClass(child)">
<el-icon v-if="isSourceLevel(child)"
class="text-[var(--el-text-color-secondary)]">
<Right />
</el-icon>
<span :class="isTotalLevel(child) ? 'font-semibold' : ''">{{
statisticsLevelName(child) }}</span>
</div>
</template>
</el-table-column>
@@ -95,36 +78,42 @@
</template>
</el-table-column>
<el-table-column label="来源口径" width="150" align="center">
<template #default="{ row: child }"
><el-tag size="small" effect="plain" :type="child.saleSource ? sourceTagType(child.saleSource) : 'info'">{{
child.saleSource ? child.saleSourceName || sourceName(child.saleSource) : aggregateSourceName
}}</el-tag></template
>
<template #default="{ row: child }"><el-tag size="small" effect="plain"
:type="child.saleSource ? sourceTagType(child.saleSource) : 'info'">{{
child.saleSource ? child.saleSourceName ||
sourceName(child.saleSource) : aggregateSourceName
}}</el-tag></template>
</el-table-column>
<el-table-column label="交易人数" width="120" align="center" prop="payUserCount">
<el-table-column :label="peopleColumnLabel" width="120" align="center"
prop="payUserCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.payUserCount || 0 }}人 </span>
</template>
</el-table-column>
<el-table-column label="交易次数" width="120" align="center" prop="timeCount">
<el-table-column :label="orderColumnLabel" width="120" align="center"
prop="timeCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.timeCount || 0 }}次 </span>
<span class="color-primary"> {{ row.timeCount || 0 }}单 </span>
</template>
</el-table-column>
<el-table-column label="售出商品数量" width="140" align="center" prop="payCount">
<el-table-column :label="quantityColumnLabel" width="140" align="center"
prop="payCount">
<template #default="{ row }">
<span class="color-success"> {{ row.payCount || 0 }}份 </span>
</template>
</el-table-column>
<el-table-column label="总价" width="130" prop="totalPrice" align="center">
<el-table-column :label="amountColumnLabel" width="130" prop="totalPrice"
align="center">
<template #default="{ row }">
<span>{{ displayPrice(row.totalPrice) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="{ row }">
<el-button icon="Tickets" text type="primary" @click="doOpenDialog(row)"> 购买人员 </el-button>
<el-button icon="Tickets" text type="primary" @click="openProductDialog(row)"> 菜品详情 </el-button>
<el-button icon="Tickets" text type="primary"
@click="doOpenDialog(row)"> 订单明细 </el-button>
<el-button icon="Tickets" text type="primary"
@click="openProductDialog(row)"> 菜品详情 </el-button>
</template>
</el-table-column>
</el-table>
@@ -132,22 +121,27 @@
</template>
</el-table-column>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="统计日期" align="center" width="130" prop="createTime"
><template #default="{ row }">{{ parseTime(row.createTime, "{y}-{m}-{d}") }}</template></el-table-column
>
<el-table-column :label="statisticsDateColumnLabel" align="center" width="130"
prop="createTime"><template #default="{ row }">{{ parseTime(row.createTime, "{y}-{m}-{d}")
}}</template></el-table-column>
<el-table-column label="食堂" align="center" width="200" prop="mchName" />
<el-table-column label="统计维度" min-width="150">
<template #default="{ row }">
<div class="flex items-center gap-2" :class="dimensionIndentClass(row)">
<el-icon v-if="isSourceLevel(row)" class="text-[var(--el-text-color-secondary)]"><Right /></el-icon>
<span :class="isTotalLevel(row) ? 'font-semibold' : ''">{{ statisticsLevelName(row) }}</span>
<el-icon v-if="isSourceLevel(row)" class="text-[var(--el-text-color-secondary)]">
<Right />
</el-icon>
<span :class="isTotalLevel(row) ? 'font-semibold' : ''">{{ statisticsLevelName(row)
}}</span>
</div>
</template>
</el-table-column>
<el-table-column label="来源口径" align="center" width="150" prop="saleSourceName">
<template #default="{ row }">
<el-tag size="small" effect="plain" :type="row.saleSource ? sourceTagType(row.saleSource) : 'info'">
{{ row.saleSource ? row.saleSourceName || sourceName(row.saleSource) : aggregateSourceName }}
<el-tag size="small" effect="plain"
:type="row.saleSource ? sourceTagType(row.saleSource) : 'info'">
{{ row.saleSource ? row.saleSourceName || sourceName(row.saleSource) :
aggregateSourceName }}
</el-tag>
</template>
</el-table-column>
@@ -156,30 +150,32 @@
<span class="color-danger"> {{ row.deptCount || 0 }}部门 </span>
</template>
</el-table-column>
<el-table-column label="交易人数" width="120" align="center" prop="payUserCount">
<el-table-column :label="peopleColumnLabel" width="120" align="center" prop="payUserCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.payUserCount || 0 }}人 </span>
</template>
</el-table-column>
<el-table-column label="交易次数" width="120" align="center" prop="timeCount">
<el-table-column :label="orderColumnLabel" width="120" align="center" prop="timeCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.timeCount || 0 }}次 </span>
<span class="color-primary"> {{ row.timeCount || 0 }}单 </span>
</template>
</el-table-column>
<el-table-column label="售出商品数量" width="140" align="center" prop="payCount">
<el-table-column :label="quantityColumnLabel" width="140" align="center" prop="payCount">
<template #default="{ row }">
<span class="color-success"> {{ row.payCount || 0 }}份 </span>
</template>
</el-table-column>
<el-table-column label="总价" width="130" prop="totalPrice" align="center">
<el-table-column :label="amountColumnLabel" width="130" prop="totalPrice" align="center">
<template #default="{ row }">
<span>{{ displayPrice(row.totalPrice) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<template #default="{ row }">
<el-button icon="Tickets" text type="primary" @click="doOpenDialog(row)"> 购买人员 </el-button>
<el-button icon="Tickets" text type="primary" @click="openProductDialog(row)"> 菜品详情 </el-button>
<el-button icon="Tickets" text type="primary" @click="doOpenDialog(row)"> 订单明细
</el-button>
<el-button icon="Tickets" text type="primary" @click="openProductDialog(row)"> 菜品详情
</el-button>
</template>
</el-table-column>
</el-table>
@@ -187,12 +183,8 @@
</DraggableDepTree>
<dept-person-total-table ref="deptPersonTotalTableRef" />
<product-total-table ref="productTotalTableRef" />
<async-task-progress-component
ref="exportProgressRef"
:config="exportTaskConfig"
@success="handleExportSuccess"
@failure="handleExportFailure"
/>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</div>
</template>
@@ -204,7 +196,7 @@ import { displayPrice } from "/@/utils/formatMoney"
import { useDict } from "/@/hooks/dict"
import { useMessage } from "/@/hooks/message"
import { parseTime } from "/@/utils/formatTime"
import { Right } from "@element-plus/icons-vue"
import { QuestionFilled, Right } from "@element-plus/icons-vue"
import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { downBlobFile } from "/@/utils/other"
@@ -260,6 +252,39 @@ const aggregateSourceName = computed(() =>
activeName.value === "all" ? "线下 + 预约" : sourceTabs.find((tab) => tab.name === activeName.value)?.label || "当前来源"
)
/** 当前是否只查看预约核销来源,用于切换日期与四项指标的业务文案。 */
const isReservationSource = computed(() => activeName.value === hardTotalApi.SALE_SOURCE.RESERVATION_VERIFIED)
/** 日期列随销售来源展示真实业务发生时间,全部来源同时包含支付日期和核销日期。 */
const statisticsDateColumnLabel = computed(() => {
if (isReservationSource.value) return "核销日期"
if (activeName.value === hardTotalApi.SALE_SOURCE.OFFLINE_PURCHASE) return "支付日期"
return "支付/核销日期"
})
/** 预约核销页的人数标题,其他来源保留通用交易名称。 */
const peopleColumnLabel = computed(() => (isReservationSource.value ? "核销人数" : "交易人数"))
/** 预约核销页的订单标题,其他来源保留通用交易名称。 */
const orderColumnLabel = computed(() => (isReservationSource.value ? "核销订单" : "交易订单"))
/** 预约核销页的份数标题,突出页面展示的是扣除退款后的实际结果。 */
const quantityColumnLabel = computed(() => (isReservationSource.value ? "实际核销份数" : "售出份数"))
/** 预约核销页的金额标题,突出页面展示的是扣除退款后的实际结果。 */
const amountColumnLabel = computed(() => (isReservationSource.value ? "实际核销金额" : "交易金额"))
/** 当前标签页面向业务人员的统计说明,明确日期依据和退款扣减方式。 */
const statisticsScopeDescription = computed(() => {
if (isReservationSource.value) {
return "这里统计已经完成取餐核销的预约,日期按实际核销当天计算;发生退款时,只扣除实际退款的菜品份数和金额。"
}
if (activeName.value === hardTotalApi.SALE_SOURCE.OFFLINE_PURCHASE) {
return "这里统计现场购买并已付款的订单,日期按付款当天计算。"
}
return "这里合并显示现场购买和预约核销。现场购买按付款日期统计,预约订餐按实际核销日期统计;预约退款只扣除实际退款的菜品份数和金额。"
})
// table hook
const { sortChangeHandle, tableStyle } = useTable(state)
@@ -432,8 +457,8 @@ const getSummaries = (param: any) => {
const totalData = data.some((row: any) => row.statisticsLevel === hardTotalApi.STATISTICS_LEVEL.DATE_TOTAL)
? data.filter((row: any) => row.statisticsLevel === hardTotalApi.STATISTICS_LEVEL.DATE_TOTAL)
: data.some((row: any) => row.statisticsLevel === hardTotalApi.STATISTICS_LEVEL.HOUR_TOTAL)
? data.filter((row: any) => row.statisticsLevel === hardTotalApi.STATISTICS_LEVEL.HOUR_TOTAL)
: data
? data.filter((row: any) => row.statisticsLevel === hardTotalApi.STATISTICS_LEVEL.HOUR_TOTAL)
: data
columns.forEach((i: any, index: number) => {
if (totalColumns.includes(i.property)) {
const total = totalData
@@ -8,7 +8,17 @@
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm"
:loading="Boolean(state.loading)" :search-text="$t('common.queryBtn')"
:reset-text="$t('common.resetBtn')" @search="doSearch" @reset="resetQuery">
<el-form-item style="width: 450px" label="统计日期" prop="statisticsDateTimeList">
<el-form-item style="width: 500px" prop="statisticsDateTimeList">
<template #label>
<span class="inline-flex items-center gap-1">
{{ statisticsDateColumnLabel }}
<el-tooltip :content="statisticsScopeDescription" placement="top">
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-date-picker v-model="state.queryForm.statisticsDateTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
:default-time="dateRangeDefaultTime" :clearable="false" />
@@ -26,9 +36,10 @@
<el-tab-pane v-for="tab in sourceTabs" :key="tab.name" :label="tab.label"
:name="tab.name" />
</el-tabs>
<right-toolbar v-model:showSearch="showSearch" :export="canExport"
@exportExcel="exportExcel" class="ml10 mr20" @queryTable="getDataList" />
<right-toolbar v-model:showSearch="showSearch" :export="canExport" @exportExcel="exportExcel"
class="ml10 mr20" @queryTable="getDataList" />
</div>
<el-alert class="mb-3" :title="statisticsScopeDescription" type="info" :closable="false" show-icon />
<el-table :data="displayRows" v-loading="state.loading" border row-key="_statisticsRowKey"
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
:summary-method="getSummaries" :show-summary="true" :row-class-name="statisticsRowClassName"
@@ -49,22 +60,22 @@
:header-cell-style="tableStyle.headerCellStyle" row-class-name="warning-row">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="userDeptName" align="center" label="部门" width="200" />
<el-table-column label="交易人数" width="150" align="center" prop="payUserCount">
<el-table-column :label="peopleColumnLabel" width="150" align="center" prop="payUserCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.payUserCount || 0 }}人 </span>
</template>
</el-table-column>
<el-table-column label="交易次数" width="150" align="center" prop="timeCount">
<el-table-column :label="orderColumnLabel" width="150" align="center" prop="timeCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.timeCount || 0 }}次 </span>
<span class="color-primary"> {{ row.timeCount || 0 }}单 </span>
</template>
</el-table-column>
<el-table-column label="售出商品数量" width="150" align="center" prop="payCount">
<el-table-column :label="quantityColumnLabel" width="150" align="center" prop="payCount">
<template #default="{ row }">
<span class="color-success"> {{ row.payCount || 0 }}份 </span>
</template>
</el-table-column>
<el-table-column label="总价" width="150" prop="totalPrice" align="center">
<el-table-column :label="amountColumnLabel" width="150" prop="totalPrice" align="center">
<template #default="{ row }">
<span>{{ displayPrice(row.totalPrice) }}</span>
</template>
@@ -75,7 +86,7 @@
</el-table-column>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="统计日期" align="center" width="130" prop="createTime">
<el-table-column :label="statisticsDateColumnLabel" align="center" width="130" prop="createTime">
<template #default="{ row }">
<span>{{ parseTime(row.createTime, "{y}-{m}-{d}") }}</span>
</template>
@@ -106,22 +117,22 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="交易人数" width="150" align="center" prop="payUserCount">
<el-table-column :label="peopleColumnLabel" width="150" align="center" prop="payUserCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.payUserCount || 0 }}人 </span>
</template>
</el-table-column>
<el-table-column label="交易次数" width="150" align="center" prop="timeCount">
<el-table-column :label="orderColumnLabel" width="150" align="center" prop="timeCount">
<template #default="{ row }">
<span class="color-primary"> {{ row.timeCount || 0 }}次 </span>
<span class="color-primary"> {{ row.timeCount || 0 }}单 </span>
</template>
</el-table-column>
<el-table-column label="售出商品数量" width="150" align="center" prop="payCount">
<el-table-column :label="quantityColumnLabel" width="150" align="center" prop="payCount">
<template #default="{ row }">
<span class="color-success"> {{ row.payCount || 0 }}份 </span>
</template>
</el-table-column>
<el-table-column label="总价" width="150" prop="totalPrice" align="center">
<el-table-column :label="amountColumnLabel" width="150" prop="totalPrice" align="center">
<template #default="{ row }">
<span>{{ displayPrice(row.totalPrice) }}</span>
</template>
@@ -138,15 +149,11 @@
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<product-total-table ref="productTotalTableRef" />
<async-task-progress-component
ref="exportProgressRef"
:config="exportTaskConfig"
@success="handleExportSuccess"
@failure="handleExportFailure"
/>
</div>
<product-total-table ref="productTotalTableRef" />
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig" @success="handleExportSuccess"
@failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="canteen-product-appt-total">
@@ -156,7 +163,7 @@ import { displayPrice } from "/@/utils/formatMoney"
import { useDict } from "/@/hooks/dict"
import { useMessage } from "/@/hooks/message"
import { parseTime } from "/@/utils/formatTime"
import { Right } from "@element-plus/icons-vue"
import { QuestionFilled, Right } from "@element-plus/icons-vue"
import { auths } from "/@/utils/authFunction"
import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
@@ -227,6 +234,39 @@ const displayRows = computed(() =>
const aggregateSourceName = computed(() => (activeName.value === "all" ? "线下 + 预约" : sourceTabs.find((tab) => tab.name === activeName.value)?.label || "当前来源"))
/** 当前是否只查看预约核销,用于切换日期和统计指标的业务名称。 */
const isReservationSource = computed(() => activeName.value === hardTotalApi.SALE_SOURCE.RESERVATION_VERIFIED)
/** 日期标题随来源展示付款日期或实际核销日期。 */
const statisticsDateColumnLabel = computed(() => {
if (isReservationSource.value) return "核销日期"
if (activeName.value === hardTotalApi.SALE_SOURCE.OFFLINE_PURCHASE) return "支付日期"
return "支付/核销日期"
})
/** 预约核销页的人数标题,其他来源保留通用交易名称。 */
const peopleColumnLabel = computed(() => (isReservationSource.value ? "核销人数" : "交易人数"))
/** 预约核销页的订单标题,其他来源保留通用交易名称。 */
const orderColumnLabel = computed(() => (isReservationSource.value ? "核销订单" : "交易订单"))
/** 预约核销页的份数标题,突出页面展示的是扣除退款后的实际结果。 */
const quantityColumnLabel = computed(() => (isReservationSource.value ? "实际核销份数" : "售出份数"))
/** 预约核销页的金额标题,突出页面展示的是扣除退款后的实际结果。 */
const amountColumnLabel = computed(() => (isReservationSource.value ? "实际核销金额" : "交易金额"))
/** 当前标签页面向业务人员的统计说明,明确日期依据和退款扣减方式。 */
const statisticsScopeDescription = computed(() => {
if (isReservationSource.value) {
return "这里统计已经完成取餐核销的预约,日期按实际核销当天计算;发生退款时,只扣除实际退款的菜品份数和金额。"
}
if (activeName.value === hardTotalApi.SALE_SOURCE.OFFLINE_PURCHASE) {
return "这里统计现场购买并已付款的订单,日期按付款当天计算。"
}
return "这里合并显示现场购买和预约核销。现场购买按付款日期统计,预约订餐按实际核销日期统计;预约退款只扣除实际退款的菜品份数和金额。"
})
/** 按当前筛选条件提交按日期分批、流式写入的异步导出任务。 */
const exportExcel = async () => {
if (exportSubmitting.value) {
@@ -2,7 +2,14 @@
## 功能边界
`index.vue` 展示“用户 × 用餐日期”的食堂预约汇总。查询的用户主数据来自 UPMS 当前操作者的数据权限范围,预约订单来自 Hardware;因此即使用户当天没有订餐,只要处于权限范围内,也可能返回“未订餐”行。
`index.vue` 展示“用户 × 统计日期”的食堂预约汇总。查询的用户主数据来自 UPMS 当前操作者的数据权限范围,预约订单来自 Hardware;因此即使用户当天没有订餐,只要处于权限范围内,也可能返回“未订餐”行。
## 日期、核销与退款口径
- 查询“已核销”时,只统计 `hard_order.order_status = 20` 的单笔订单,日期取 `verification_time`;不能再按用户整行是否全部核销来排除其中已经核销的订单。
- 查询“待核销”时,只统计 `hard_order.order_status = 5` 的单笔订单,日期取预约用餐日期;“部分核销”仍表示同一用户、同一预约日期同时存在已核销和待核销订单。
- 菜品份数和金额先按原购物车商品汇总,再扣除 `hard_after_sale.status = 50` 的完成退款。部分退款只扣已退款份数和金额,全额退款后净数量为零的商品、订单和对应订餐人数不再计入已订餐统计。
- 人员页选择“已核销”后,其人数、订单数、菜品份数和金额应与商品统计页“预约核销”来源在相同日期、食堂、餐段及用户范围下保持一致。
本页面的用户身份及人脸筛选复用全局 `UserIdentityQuery` 组件,查询条件放在搜索面板的“更多条件”区域。新增或调整身份字段时,必须同时检查前端组件、预约接口类型、Hardware 查询 DTO、预约服务转交逻辑和 UPMS 用户 SQL。
@@ -22,7 +29,7 @@
## 数据链路
1. `src/views/hard/appt/canteen-user-appt-total/index.vue` 持有查询表单默认值,并通过 `user-identity-query` 写入身份及人脸条件。
1. `src/views/hard/appt/canteen-user-appt-total/index.vue` 持有查询表单默认值,并通过 `user-identity-query` 写入身份及人脸条件;已核销筛选下表格日期展示为核销日期,订单明细额外保留原预约日期。
2. `src/api/hard/hard-total-appt.ts` 将条件提交到列表、统计和导出接口;三者共用同一组身份字段。
3. `HardCanteenUserApptQueryDto` 实现 `BaseIsIncludeTreeInterface` 和 `BaseUserIdentityQueryInterface`,承载部门包含关系及身份条件。
4. Hardware 的预约列表服务和导出服务复制查询快照后,把身份条件转为 UPMS `UserDTO`,避免异步导出或分页预筛选丢字段。
@@ -33,4 +40,4 @@
- 新增身份类型或扩展字段时,先修改 `src/components/UserIdentityQuery` 的配置和类型,再同步 Java 查询接口、DTO、UPMS SQL 以及本页文档。
- 修改查询字段名或匹配规则前,检查 PC 页面、统计抽屉、同步/异步导出、Feign `UserDTO` 和所有调用方;保持旧字段向后兼容。
- 不要在页面内新建 Axios 实例,也不要把身份查询改为直接访问数据库或新增数据库字段。
- 修改日期、部门或身份查询逻辑后,至少静态核对列表、统计、导出三条请求都携带同一查询快照;完整构建和测试按项目集中验证流程执行。
- 修改日期、部门或身份查询逻辑后,至少静态核对列表、统计、导出三条请求都携带同一查询快照,并确认已核销使用核销日期、待核销及部分核销使用预约日期;完整构建和测试按项目集中验证流程执行。
@@ -1,6 +1,8 @@
<template>
<el-drawer v-model="visible" title="预约统计" size="min(920px, 92vw)" destroy-on-close @closed="resetState">
<div v-loading="loading" class="flex h-full min-h-0 flex-col">
<el-alert class="mb-3 shrink-0" type="info" :closable="false" show-icon
:title="statisticsScopeDescription" />
<el-empty v-if="!loading && !hasStatistics" description="当前筛选条件暂无预约统计数据" />
<el-tabs v-else v-model="activeTab" class="min-h-0 flex-1" stretch>
<el-tab-pane label="按日统计" name="daily">
@@ -57,6 +59,17 @@ const message = useMessage()
/** 统计请求序号,用于忽略关闭抽屉或重新打开后迟到的旧响应。 */
let requestSequence = 0
/** 当前状态筛选对应的日期、核销和完成退款净值口径。 */
const statisticsScopeDescription = computed(() => {
if (query.value?.verificationStatus === "VERIFIED") {
return "已核销按实际核销日期统计,只计状态为 20 的订单;菜品份数和金额已扣除完成退款。"
}
if (query.value?.verificationStatus === "UNVERIFIED") {
return "待核销按预约用餐日期统计,只计状态为 5 的订单;菜品份数和金额已扣除完成退款。"
}
return "当前按预约用餐日期统计;部分核销保留用户维度判断,菜品份数和金额均为完成退款后的净值。"
})
/** 当前餐段展示选项;选中餐段筛选时只展示该餐段。 */
const mealOptions = computed(() => {
const options = (daily_menu.value || [])
@@ -6,7 +6,7 @@
<el-descriptions-item label="去重用户">{{ summary.uniqueUserCount ?? 0 }}</el-descriptions-item>
<el-descriptions-item label="预约订单">{{ summary.orderCount ?? 0 }} 单</el-descriptions-item>
<el-descriptions-item label="菜品份数">{{ summary.dishCount ?? 0 }} 份</el-descriptions-item>
<el-descriptions-item label="预约总价">{{ displayPrice(summary.totalPrice ?? 0) }}</el-descriptions-item>
<el-descriptions-item label="预约净额">{{ displayPrice(summary.totalPrice ?? 0) }}</el-descriptions-item>
</el-descriptions>
<div class="border-l-2 border-primary pl-2 text-sm font-medium text-[var(--el-text-color-primary)]">餐段统计</div>
@@ -8,7 +8,17 @@
<SearchPanel ref="queryRef" :model="state.queryForm" title="预约查询" :loading="Boolean(state.loading)"
:search-text="$t('common.queryBtn')" :reset-text="$t('common.resetBtn')"
:hidden-active-count="advancedFilterCount" @search="doSearch" @reset="resetQuery">
<el-form-item style="width: 450px" label="用餐日期" prop="apptUseDateList">
<el-form-item style="width: 450px" prop="apptUseDateList">
<template #label>
<span class="inline-flex items-center gap-1">
统计日期
<el-tooltip :content="statisticsScopeDescription" placement="top">
<el-icon class="cursor-help text-[var(--el-text-color-secondary)]">
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-date-picker v-model="state.queryForm.apptUseDateList" type="daterange"
value-format="YYYY-MM-DD" :clearable="false" unlink-panels range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" @change="handleDateRangeChange" />
@@ -56,6 +66,8 @@
</right-toolbar>
</div>
<el-alert class="mb-3" :title="statisticsScopeDescription" type="info" :closable="false" show-icon />
<el-table :data="state.dataList" v-loading="state.loading" border :row-key="getRowKey"
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle">
<el-table-column label="用户信息" min-width="280" fixed="left">
@@ -68,7 +80,8 @@
<user-identity-info :user="row" />
</template>
</el-table-column>
<el-table-column prop="apptUseDate" label="用餐日期" width="120" align="center" />
<el-table-column prop="apptUseDate" :label="statisticsDateColumnLabel" width="120"
align="center" />
<el-table-column label="是否订餐" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.reservationStatus === 'RESERVED' ? 'success' : 'info'"
@@ -77,7 +90,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="当日订餐总价" width="130" align="right">
<el-table-column :label="statisticsTotalPriceColumnLabel" width="130" align="right">
<template #default="{ row }">
<span
:class="row.totalPrice ? 'font-medium text-[var(--el-text-color-primary)]' : 'text-[var(--el-text-color-placeholder)]'">
@@ -103,9 +116,9 @@
<el-tag v-else type="info" size="small" effect="plain">未订餐</el-tag>
</div>
</div>
<el-tooltip content="查看当天全部餐段预约" placement="top">
<el-tooltip :content="reservationDetailTooltip" placement="top">
<el-button class="shrink-0" link type="primary" :icon="Tickets"
aria-label="查看当天全部餐段预约" @click="openReservationDetail(row)" />
:aria-label="reservationDetailTooltip" @click="openReservationDetail(row)" />
</el-tooltip>
</div>
</template>
@@ -132,12 +145,16 @@
:title="order.merchantName || '食堂信息未记录'">
{{ order.merchantName || "食堂信息未记录" }}
</div>
<div v-if="shouldShowOriginalApptDate(row, order)"
class="truncate text-xs text-[var(--el-text-color-secondary)]">
预约日期:{{ order.apptUseDate }}
</div>
</div>
</div>
<span v-else class="min-w-0 flex-1 text-[var(--el-text-color-placeholder)]">-</span>
<el-tooltip content="查看当天全部订餐内容" placement="top">
<el-tooltip :content="reservationDetailTooltip" placement="top">
<el-button class="shrink-0" link type="primary" :icon="Tickets"
aria-label="查看当天全部订餐内容" @click="openReservationDetail(row)" />
:aria-label="reservationDetailTooltip" @click="openReservationDetail(row)" />
</el-tooltip>
</div>
</template>
@@ -182,9 +199,12 @@
<el-dialog v-model="detailDialog.visible" :title="detailDialogTitle" width="980px" destroy-on-close
@closed="resetReservationDetailDialog">
<el-table v-loading="detailDialog.loading" :data="detailDialog.orders" border max-height="520"
empty-text="当天没有订餐记录" :cell-style="tableStyle.cellStyle"
:empty-text="detailDialogEmptyText" :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column type="index" label="#" width="55" align="center" />
<el-table-column prop="apptUseDate" label="预约日期" width="120" align="center">
<template #default="{ row }">{{ row.apptUseDate || "-" }}</template>
</el-table-column>
<el-table-column label="餐段" width="110" align="center">
<template #default="{ row }">{{ getMealSegmentLabel(row.hourType) }}</template>
</el-table-column>
@@ -237,7 +257,7 @@ import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { downBlobFile } from "/@/utils/other"
import { displayPrice } from "/@/utils/formatMoney"
import * as hardTotalApptApi from "/@/api/hard/hard-total-appt"
import { Tickets } from "@element-plus/icons-vue"
import { QuestionFilled, Tickets } from "@element-plus/icons-vue"
import dayjs from "dayjs"
import {
createEmptyUserIdentityStudentQuery,
@@ -300,7 +320,7 @@ interface UserReservationRow {
studentInfo?: StudentIdentityInfo | null
/** 老师身份扩展资料;没有老师身份时为空。 */
teacherInfo?: TeacherIdentityInfo | null
/** 当前查询的用餐日期,格式为 YYYY-MM-DD。 */
/** 当前查询的统计日期;已核销筛选取核销日期,其他筛选取预约用餐日期。 */
apptUseDate: string
/** 当前筛选口径下是否存在有效预约。 */
reservationStatus: ReservationStatus
@@ -328,7 +348,7 @@ interface ReservationDetailDialogState {
loading: boolean
/** 当前查看的用户姓名。 */
userName: string
/** 当前查看的用餐日期。 */
/** 当前查看的统计日期。 */
apptUseDate: string
/** 当前用户当天的完整有效预约明细。 */
orders: ReservationOrder[]
@@ -410,10 +430,44 @@ const advancedFilterCount = computed(() => {
].filter((value) => (Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null && value !== "")).length
})
/** 当前详情弹窗标题,明确展示用户和用餐日期。 */
/** 当前状态筛选对应的统计日期口径。 */
const statisticsDateColumnLabel = computed(() =>
state.queryForm.verificationStatus === "VERIFIED" ? "核销日期" : "用餐日期",
)
/** 当前状态筛选对应的金额列口径,金额始终为完成退款后的净额。 */
const statisticsTotalPriceColumnLabel = computed(() =>
state.queryForm.verificationStatus === "VERIFIED" ? "实际核销金额" : "实际订餐金额",
)
/** 当前查询状态面向业务人员的统计说明,明确日期依据和退款扣减方式。 */
const statisticsScopeDescription = computed(() => {
if (state.queryForm.verificationStatus === "VERIFIED") {
return "这里显示已经完成取餐核销的预约,日期按实际核销当天计算;发生退款时,只扣除实际退款的菜品份数和金额。"
}
if (state.queryForm.verificationStatus === "UNVERIFIED") {
return "这里显示已经付款但还没有核销的预约,日期按预约用餐当天计算;发生退款时,只扣除实际退款的菜品份数和金额。"
}
if (state.queryForm.verificationStatus === "PARTIAL") {
return "这里显示同一天既有已核销订单、又有待核销订单的人员,日期按预约用餐当天计算;发生退款时,只扣除实际退款的菜品份数和金额。"
}
return "这里按预约用餐日期显示全部订餐情况;发生退款时,只扣除实际退款的菜品份数和金额。"
})
/** 详情按钮说明;已核销筛选展示当前核销日和当前条件下的订单,避免误查预约日期。 */
const reservationDetailTooltip = computed(() =>
state.queryForm.verificationStatus === "VERIFIED" ? "查看该核销日当前条件下的预约" : "查看当天全部订餐内容",
)
/** 详情空状态文案随统计日期口径变化。 */
const detailDialogEmptyText = computed(() =>
state.queryForm.verificationStatus === "VERIFIED" ? "该核销日没有预约核销记录" : "当天没有订餐记录",
)
/** 当前详情弹窗标题,明确展示用户和统计日期口径。 */
const detailDialogTitle = computed(() => {
const context = [detailDialog.userName, detailDialog.apptUseDate].filter(Boolean).join(" · ")
return context ? `${context} 订餐详情` : "订餐详情"
return context ? `${context}(${statisticsDateColumnLabel.value})订餐详情` : "订餐详情"
})
/**
@@ -494,6 +548,17 @@ function getSegmentTotalPrice(row: UserReservationRow, hourType: string): number
return getSegmentOrders(row, hourType).reduce((total, order) => total + Number(order.totalPrice || 0), 0)
}
/**
* 判断已核销订单是否跨预约日期展示,帮助操作人员区分核销日和原预约日。
*
* @param row 当前用户统计日期行
* @param order 当前行内的有效预约订单
* @returns 已核销筛选且预约日期与核销日期不同时返回 true
*/
function shouldShowOriginalApptDate(row: UserReservationRow, order: ReservationOrder): boolean {
return state.queryForm.verificationStatus === "VERIFIED" && Boolean(order.apptUseDate) && order.apptUseDate !== row.apptUseDate
}
/**
* 获取餐段编码对应的展示文案。
*
@@ -535,7 +600,7 @@ function getVerificationTagType(status: VerificationStatus): "success" | "warnin
}
/**
* 校验用餐日期闭区间,前端规则与后端的两个自然月限制保持一致。
* 校验统计日期闭区间,前端规则与后端的两个自然月限制保持一致。
*
* @param showMessage 校验失败时是否显示提示
* @returns 日期范围完整、顺序正确且未超过两个月时返回 true
@@ -544,16 +609,16 @@ function validateDateRange(showMessage = true): boolean {
const dateRange = state.queryForm.apptUseDateList as string[] | undefined
let errorMessage = ""
if (!Array.isArray(dateRange) || dateRange.length !== 2 || !dateRange[0] || !dateRange[1]) {
errorMessage = "请选择开始和结束用餐日期"
errorMessage = "请选择开始和结束统计日期"
} else {
const startDate = dayjs(dateRange[0])
const endDate = dayjs(dateRange[1])
if (!startDate.isValid() || !endDate.isValid()) {
errorMessage = "用餐日期格式不正确"
errorMessage = "统计日期格式不正确"
} else if (endDate.isBefore(startDate, "day")) {
errorMessage = "用餐结束日期不能早于开始日期"
errorMessage = "统计结束日期不能早于开始日期"
} else if (endDate.isAfter(startDate.add(MAX_QUERY_MONTHS, "month"), "day")) {
errorMessage = "用餐日期范围最多查询两个月"
errorMessage = "统计日期范围最多查询两个月"
}
}
if (errorMessage && showMessage) message.warning(errorMessage)
@@ -617,17 +682,23 @@ function handleGetSelectNode(_curClickedNode: any, checkedTreeNodes: any[], isDe
}
/**
* 打开用户当天完整预约详情,并忽略列表上的食堂、餐段和状态筛选。
* 打开用户统计日期内的预约详情。已核销按当前核销日结果展示,其他状态查询原预约日完整记录。
*
* @param row 当前用户和用餐日期对应的列表行
* @param row 当前用户和统计日期对应的列表行
*/
async function openReservationDetail(row: UserReservationRow) {
const requestSequence = ++detailRequestSequence
detailDialog.visible = true
detailDialog.loading = true
detailDialog.userName = row.name || row.userName || row.username || "未命名用户"
detailDialog.apptUseDate = row.apptUseDate
detailDialog.orders = []
if (state.queryForm.verificationStatus === "VERIFIED") {
// 行日期此时是核销日期,直接展示当前查询已返回的核销订单,避免详情接口按预约日期误查跨日订单。
detailDialog.loading = false
detailDialog.orders = [...row.orders]
return
}
detailDialog.loading = true
try {
const response = await hardTotalApptApi.getCanteenUserReservationDetail({
userId: row.userId,
@@ -1,14 +1,14 @@
<template>
<el-dialog title="钱包余额明细" v-model="visible" width="60%" :close-on-click-modal="false" draggable>
<el-dialog title="钱包余额明细" v-model="visible" width="70%" :close-on-click-modal="false" draggable>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @sort-change="sortChangeHandle">
<el-table-column align="center" width="200" label="编号" prop="id" />
<el-table-column align="center" width="200" label="钱包编号" prop="walletId" />
<el-table-column align="center" width="200" label="交易类型" prop="title" />
<el-table-column align="center" label="交易金额" prop="price">
<el-table-column align="center" width="200" label="交易金额" prop="price">
<template #default="{ row }"> {{ displayPrice(row.price) }} </template>
</el-table-column>
<el-table-column align="center" label="钱包余额" prop="balance">
<el-table-column align="center" width="200" label="钱包余额" prop="balance">
<template #default="{ row }"> {{ displayPrice(row.balance) }} </template>
</el-table-column>
<el-table-column align="center" label="备注" prop="remark" min-width="180" show-overflow-tooltip />
+15 -7
View File
@@ -106,6 +106,8 @@
v-bind="state.pagination" />
</DraggableDepTree>
</div>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
@@ -113,6 +115,8 @@
import { BasicTableProps, useTable } from "/@/hooks/table"
import * as walletTransactionApi from "/@/api/wallet/wallet-transaction"
import { displayPrice } from "/@/utils/formatMoney"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const state: BasicTableProps = reactive<BasicTableProps>({
isPage: false,
@@ -133,7 +137,15 @@ const draggableDepTreeRef = ref()
const tableChildrenMap = ref(new Map<string, any>())
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 个人充值统计导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:cz-total",
taskName: "个人充值统计导出",
fallbackFileName: "个人充值统计.xlsx",
submitTask: walletTransactionApi.submitCzTotalPersonExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -173,13 +185,9 @@ const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepT
getDataList()
}
// 导出excel
/** 按当前统计筛选提交个人充值明细异步导出任务,金额继续以元传给后端。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-cz-total-person",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"钱包充值统计.xlsx",
)
void submitExport({ ...state.queryForm })
}
// 多选事件
+16 -8
View File
@@ -129,16 +129,20 @@
</el-table-column>
</el-table>
</el-drawer>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageCz, czTotal, delObjs } from "/@/api/wallet/wallet-transaction"
import { pageCz, czTotal, delObjs, submitCzExport } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
@@ -176,7 +180,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 充值记录导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:cz",
taskName: "充值记录导出",
fallbackFileName: "充值记录.xlsx",
submitTask: submitCzExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -231,13 +243,9 @@ const getSummaries = (param: any) => {
return totalRow
}
// 导出excel
/** 按当前筛选条件提交充值记录异步导出任务,不携带列表页码。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-cz",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"充值记录.xlsx",
)
void submitExport({ ...state.queryForm })
}
// 多选事件
+16 -8
View File
@@ -100,16 +100,20 @@
</el-table-column>
</el-table>
</el-drawer>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { pageJz, jzTotal } from "/@/api/wallet/wallet-transaction"
import { pageJz, jzTotal, submitJzExport } from "/@/api/wallet/wallet-transaction"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
/** 所有钱包流水列表共用的备注编辑表单。 */
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
@@ -145,7 +149,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 记账扣款导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:jz",
taskName: "记账扣款导出",
fallbackFileName: "记账扣款记录.xlsx",
submitTask: submitJzExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -185,13 +197,9 @@ const viewBalanceStatistics = async () => {
}
// 导出excel
/** 按当前筛选条件提交记账扣款异步导出任务,不携带列表页码。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-jz",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"消费记录.xlsx",
)
void submitExport({ ...state.queryForm })
}
// 多选事件
+11 -53
View File
@@ -125,14 +125,13 @@
<script setup lang="ts" name="systemWalletManage">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { fetchListJoinQb, delObjs, qbTotal, submitQbExport } from "/@/api/wallet/wallet-manage"
import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import { formatDate } from "/@/utils/formatTime"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { downBlobFile } from "/@/utils/other"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const { wallet_type, yes_no_type } = useDict("wallet_type", "yes_no_type")
// 定义变量内容
@@ -148,18 +147,6 @@ const balanceStatisticsLoading = ref(false)
const balanceStatistics = reactive<any>({
})
/** 月度钱包记录异步导出进度弹窗引用,用于提交成功后启动统一轮询。 */
const exportProgressRef = ref<InstanceType<typeof AsyncTaskProgressComponent>>()
/** 月度钱包记录导出提交中的本地互斥状态,防止接口返回前连续创建重复任务。 */
const exportSubmitting = ref(false)
/** 月度钱包记录导出进度配置,与后端导出任务的 Redis 命名空间保持一致。 */
const exportTaskConfig: AsyncTaskPollingConfig = {
progressUrl: "/wallet/async-task/progress",
redisKey: "wallet:manage:export:record",
showDialogOnFinished: false,
waitingMessage: "正在生成钱包记录导出文件,请耐心等待进度达到 100%",
downloadButtonText: "下载 Excel",
}
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
@@ -175,6 +162,14 @@ const state: BasicTableProps = reactive<BasicTableProps>({
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 月度钱包记录导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:manage:export:record",
taskName: "钱包记录导出",
fallbackFileName: "钱包记录.xlsx",
submitTask: submitQbExport,
})
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
@@ -213,45 +208,8 @@ const drawerClose = () => {
}
/** 提交月度钱包记录异步导出任务,并将当前筛选条件作为独立快照交给后台处理。 */
const exportExcel = async () => {
if (exportSubmitting.value) return
exportSubmitting.value = true
try {
const response = await submitQbExport({ ...state.queryForm })
if (!response.data?.taskId) {
useMessage().error("导出任务提交成功,但响应中缺少任务ID")
return
}
useMessage().success(response.msg || "钱包记录导出任务已提交")
exportProgressRef.value?.start(response.data)
} catch (error: any) {
useMessage().error(error?.msg || "钱包记录导出任务提交失败")
} finally {
exportSubmitting.value = false
}
}
/**
* 钱包记录导出成功后下载文件中心结果。
*
* @param progress 后端成功终态,必须包含结果文件地址
*/
const handleExportSuccess = async (progress: AsyncTaskProgress) => {
if (!progress.resultFileUrl) {
useMessage().error("导出完成,但未返回文件下载地址")
return
}
useMessage().success(progress.message || "钱包记录导出完成")
await downBlobFile(progress.resultFileUrl, {}, progress.resultFileName || "钱包记录.xlsx")
}
/**
* 钱包记录导出失败时展示后台任务的受控错误信息。
*
* @param progress 后端失败终态
*/
const handleExportFailure = (progress: AsyncTaskProgress) => {
useMessage().error(progress.message || "钱包记录导出失败")
const exportExcel = () => {
void submitExport({ ...state.queryForm })
}
const getSummaries = (param: any) => {
@@ -39,9 +39,9 @@
</SearchPanel>
<el-row>
<div class="mb8" style="width: 100%">
<right-toolbar v-model:showSearch="showSearch" :export="'wallet_walletRechargeApply_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right"
@queryTable="getDataList"></right-toolbar>
<right-toolbar v-model:showSearch="showSearch"
:export="'wallet_walletRechargeImport_export'" @exportExcel="exportExcel"
class="ml10 mr20" style="float: right" @queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
@@ -105,6 +105,8 @@
</template>
</DraggableDepTree>
</div>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig" @success="handleExportSuccess"
@failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="wallet-recharge-water">
@@ -113,7 +115,9 @@ import { displayPrice } from "/@/utils/formatMoney"
import { useDict } from "/@/hooks/dict"
import { useMessage, useMessageBox } from "/@/hooks/message"
import * as walletRechargeImportApi from "/@/api/wallet/wallet-recharge-import"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const { wallet_biz_recharge, wallet_type, check_status, wallet_transaction_tag } = useDict(
"wallet_biz_recharge",
@@ -144,7 +148,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 充值扣款水单导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:recharge-water",
taskName: "充值扣款水单导出",
fallbackFileName: "充值扣款水单.xlsx",
submitTask: walletRechargeImportApi.submitExport,
})
// 恢复操作
const handleRecover = async (id: string) => {
@@ -174,9 +186,9 @@ const getSummaries = (param: any) => {
return totalRow
}
// 导出excel
/** 按当前筛选条件提交充值扣款水单异步导出任务,不传递页面多选 Ref。 */
const exportExcel = () => {
postDownBlobFile("/wallet/wallet-recharge-import/export", Object.assign(state.queryForm, { ids: selectObjs }), "充值/扣款流水.xlsx")
void submitExport({ ...state.queryForm })
}
// 多选事件
+16 -8
View File
@@ -99,17 +99,21 @@
</el-table-column>
</el-table>
</el-drawer>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageTh, thTotal } from "/@/api/wallet/wallet-transaction"
import { pageTh, thTotal, submitThExport } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import * as hardAfterSaleApi from "/@/api/hard/hard-after-sale"
import { displayPrice } from "/@/utils/formatMoney"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
/** 所有钱包流水列表共用的备注编辑表单。 */
const TransactionRemarkForm = defineAsyncComponent(() => import("../components/transaction-remark-form.vue"))
@@ -147,7 +151,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 食堂退还记录导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:th",
taskName: "食堂退还记录导出",
fallbackFileName: "食堂退还记录.xlsx",
submitTask: submitThExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -194,13 +206,9 @@ const doRefund = async (row: any) => {
push({ path: "/hard/hard-after-sale/order-refund", query: { id: data.orderId, type: "view" } })
}
// 导出excel
/** 按当前筛选条件提交食堂退还记录异步导出任务,不携带列表页码。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-th",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"退还各自食堂记录.xlsx",
)
void submitExport({ ...state.queryForm })
}
const getSummaries = (param: any) => {
+16 -8
View File
@@ -109,17 +109,21 @@
</el-table-column>
</el-table>
</el-drawer>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageTk, tkTotal } from "/@/api/wallet/wallet-transaction"
import { pageTk, tkTotal, submitTkExport } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import * as hardAfterSaleApi from "/@/api/hard/hard-after-sale"
import { displayPrice } from "/@/utils/formatMoney"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const showDrawer = ref(false)
const balanceStatisticsLoading = ref(false)
@@ -158,7 +162,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 退款记录导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:tk",
taskName: "退款记录导出",
fallbackFileName: "退款记录.xlsx",
submitTask: submitTkExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -205,13 +217,9 @@ const doRefund = async (row: any) => {
push({ path: "/hard/hard-after-sale/order-refund", query: { id: data.orderId, type: "view" } })
}
// 导出excel
/** 按当前筛选条件提交退款记录异步导出任务,不携带列表页码。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-tk",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"退款记录.xlsx",
)
void submitExport({ ...state.queryForm })
}
const getSummaries = (param: any) => {
+16 -8
View File
@@ -115,17 +115,21 @@
</el-table-column>
</el-table>
</el-drawer>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="systemWalletRecharge">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageXf, xfTotal } from "/@/api/wallet/wallet-transaction"
import { pageXf, xfTotal, submitXfExport } from "/@/api/wallet/wallet-transaction"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import { deptTree } from "/@/api/admin/dept"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types"
import { useWalletUserExport } from "../composables/useWalletUserExport"
const { push } = useRouter()
@@ -165,7 +169,15 @@ const state: BasicTableProps = reactive<BasicTableProps>({
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state)
/** 消费记录导出复用统一提交互斥、进度轮询和文件下载流程。 */
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useWalletUserExport({
redisKey: "wallet:user:export:xf",
taskName: "消费记录导出",
fallbackFileName: "消费记录.xlsx",
submitTask: submitXfExport,
})
// 清空搜索条件
const resetQuery = () => {
@@ -203,13 +215,9 @@ const viewBalanceStatistics = async () => {
}
}
// 导出excel
/** 按当前筛选条件提交消费记录异步导出任务,不携带列表页码。 */
const exportExcel = () => {
postDownBlobFile(
"/wallet/wallet-transaction/export-xf",
Object.assign(state.queryForm, { current: state.pagination?.current, size: state.pagination?.size }),
"消费记录.xlsx",
)
void submitExport({ ...state.queryForm })
}
const doRefund = (row: any) => {