Merge remote-tracking branch 'origin/master'

This commit is contained in:
zjf committed 2026-09-17 14:36:06 +08:00
commit 4dac32d89c
9 files changed
+632 -48

No files matched your search

+2 -2
View File
@@ -170,9 +170,9 @@ export function disagree(obj?: Object) {
}
/**
* 将人工审核且已完成的钱包退款退回审核中,服务端会同步扣回退款金额并恢复库存占用。
* 将人工审核且已完成的钱包退款退回审核中,服务端会同步扣回退款金额;默认重新占用库存占用。
*
* @param query 售后单 ID 查询参数
* @param query 售后单 ID 及 deductStock(是否扣减库存,默认 true)查询参数
* @returns 后端统一业务响应
*/
export function returnToReview(query?: Object) {
+41
View File
@@ -0,0 +1,41 @@
import request from "/@/utils/request"
import type { AsyncTaskProgress } from "/@/api/asyncTask"
/** 购物车商品记录异步导出提交接口的统一响应。 */
export interface HardCarRecordExportTaskResponse {
/** 后端创建的初始异步任务进度。 */
data: AsyncTaskProgress
/** 后端返回的任务提交提示。 */
msg?: string
}
/**
* 以购物车商品明细为维度分页查询记录,一行对应一条商品快照及其所属订单摘要。
*
* @param query 商品、订单、用户、部门、时间及身份筛选条件
*/
export function pageJoin(query: any) {
return request({
url: "/hard/hard-car-record/page-join",
method: "post",
params: {
current: query.current,
size: query.size,
},
data: query,
})
}
/**
* 按购物车商品记录页面当前筛选条件提交异步分批导出任务。
*
* @param query 商品记录筛选条件;carIds 非空时仅导出当前勾选的商品明细行
* @returns 包含任务初始进度的统一响应
*/
export function submitExport(query: Record<string, any>) {
return request<unknown, HardCarRecordExportTaskResponse>({
url: "/hard/hard-car-record/export-task",
method: "post",
data: query,
})
}
+11 -1
View File
@@ -71,6 +71,9 @@ const prop = defineProps({
},
})
/** 错误明细弹窗最多渲染的行数,防止超大错误列表拖垮表格;与后端返回上限保持一致。 */
const MAX_ERROR_ROWS = 1000
/** Element Plus 上传组件实例,用于提交和清理文件。 */
const uploadRef = ref()
/** 当前已选择的 Excel 文件列表,作为上传列表展示和提交前校验的统一数据源。 */
@@ -133,7 +136,14 @@ const handleFileSuccess = (response: any) => {
if (response.code === 1) {
useMessage().error(response.msg ? response.msg : "导入失败,以下数据不合法")
state.errorVisible = true
state.errorData = Array.isArray(response.data) ? response.data : response.data?.errors || []
const fullErrors: any[] = Array.isArray(response.data) ? response.data : response.data?.errors || []
// 兜底截断:即使后端未限制,也只渲染前 MAX_ERROR_ROWS 行,避免大文件全错时卡死弹窗
if (fullErrors.length > MAX_ERROR_ROWS) {
state.errorData = fullErrors.slice(0, MAX_ERROR_ROWS)
useMessage().warning(`错误共 ${fullErrors.length} 条,仅展示前 ${MAX_ERROR_ROWS} 条`)
} else {
state.errorData = fullErrors
}
uploadRef.value.clearFiles()
return
}
+4 -11
View File
@@ -18,11 +18,7 @@
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item style="width: 250px" label="班级" prop="studentInfo.className">
<el-input
v-model="state.queryForm.studentInfo.className"
placeholder="请输入班级名称"
clearable
/>
<el-input v-model="state.queryForm.studentInfo.className" placeholder="请输入班级名称" clearable />
</el-form-item>
<template #more>
@@ -35,14 +31,11 @@
<el-form-item style="width: 250px" label="编制情况" prop="staffingType">
<dict-select v-model="state.queryForm.staffingType" dict-code="user_staffing_type" />
</el-form-item>
<user-identity-query
v-model:identity-types="state.queryForm.identityTypes"
<user-identity-query v-model:identity-types="state.queryForm.identityTypes"
v-model:student-info="state.queryForm.studentInfo"
v-model:teacher-info="state.queryForm.teacherInfo"
v-model:has-face="state.queryForm.hasFace"
:exclude-fields="['className']"
:persistent-student-fields="['className']"
/>
v-model:has-face="state.queryForm.hasFace" :exclude-fields="['className']"
:persistent-student-fields="['className']" />
<el-form-item style="width: 180px" label="启用状态" prop="lockFlag">
<dict-select v-model="state.queryForm.lockFlag" dict-code="lock_flag"
placeholder="启用/禁用" />
+7 -32
View File
@@ -131,9 +131,7 @@
<div class="w-1/2 h-[30px] flex justify-start items-center"
v-if="canReturnToReview(row)" v-auth="'hard_hardAfterSale_change'">
<el-button icon="RefreshLeft" link type="primary"
:loading="returningAfterSaleId === row.id"
:disabled="returningAfterSaleId !== null && returningAfterSaleId !== row.id"
@click="handleReturnToReview(row)">
@click="returnReviewDialogRef.openDialog(row)">
退回到审核前
</el-button>
</div>
@@ -154,12 +152,14 @@
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<shop-car ref="shopCarDialogRef" />
<!-- 退回审核确认(可选是否扣减库存) -->
<return-review-dialog ref="returnReviewDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="systemHardAfterSale">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { pageJoin, delObjs, returnToReview } from "/@/api/hard/hard-after-sale"
import { pageJoin, delObjs } from "/@/api/hard/hard-after-sale"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { getDicts } from "/@/api/admin/dict"
@@ -176,11 +176,14 @@ const FormDialog = defineAsyncComponent(() => import("./form.vue"))
const ShopCar = defineAsyncComponent(() => import("../components/shop-car.vue"))
/** 自动退款技术任务列表和尝试日志。 */
const AutoRefundTask = defineAsyncComponent(() => import("./auto-refund-task.vue"))
/** 退回审核确认弹框:选择是否扣减库存后提交。 */
const ReturnReviewDialog = defineAsyncComponent(() => import("./return-review-dialog.vue"))
// 定义查询字典
const { hard_sale_status } = useDict("hard_sale_status")
// 定义变量内容
const formDialogRef = ref()
const shopCarDialogRef = ref()
const returnReviewDialogRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
@@ -197,8 +200,6 @@ interface OrderTypeTab {
const activeOrderType = ref<string | null>(null)
/** 表格上方的订单类型页签,字典加载前仍保留“全部”入口。 */
const orderTypeTabs = ref<OrderTypeTab[]>([{ label: "全部", value: null }])
/** 正在执行退回审核的售后单 ID;非空时禁用其他行的同类操作,避免连续误点。 */
const returningAfterSaleId = ref<string | number | null>(null)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
@@ -330,32 +331,6 @@ const doRefund = (row: any) => {
const canReturnToReview = (row: any) =>
!row.autoRefundTaskType && !row.autoRefundTaskStatus && String(row.status) === "50"
/**
* 确认后退回人工审核;服务端会强制扣回原退款金额并重新占用已恢复库存。
*
* @param row 当前人工审核售后单
*/
const handleReturnToReview = async (row: any) => {
try {
await useMessageBox().confirm(
`确认将该售后退回审核中吗?系统会从用户钱包扣回 ${displayPrice(row.refundPrice)},余额不足时可能形成负余额,并重新占用本次退款商品库存。`,
)
} catch {
return
}
returningAfterSaleId.value = row.id
try {
await returnToReview({ id: row.id })
useMessage().success("已退回审核中")
await getDataList(false)
} catch (err: any) {
useMessage().error(err?.msg || "退回审核失败")
} finally {
returningAfterSaleId.value = null
}
}
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
@@ -0,0 +1,77 @@
<template>
<el-dialog title="退回审核确认" v-model="visible" width="45%" :close-on-click-modal="false" draggable>
<el-alert type="warning" :closable="false" show-icon
:title="`确认将该售后退回审核中吗?系统会从用户钱包扣回 ${displayPrice(form.refundPrice)},余额不足时可能形成负余额。`" />
<el-form label-width="100px" style="margin-top: 12px">
<el-form-item label="是否扣减库存">
<el-radio-group v-model="form.deductStock">
<el-radio :label="true" border>是</el-radio>
<el-radio :label="false" border>否</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div v-if="!form.deductStock" class="keep-stock-tip">
选择"否"后:本次退回不再重新占用本次退款商品库存,后续再次同意该售后时也不会重复恢复库存;请自行核对库存数据。
</div>
<template #footer>
<el-button type="primary" :loading="loading" @click="onSubmit">确 定</el-button>
<el-button :disabled="loading" @click="visible = false">取 消</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="return-review-dialog">
import { useMessage } from "/@/hooks/message"
import { returnToReview } from "/@/api/hard/hard-after-sale"
import { displayPrice } from "/@/utils/formatMoney"
const emit = defineEmits(["refresh"])
// 定义变量内容
const visible = ref(false)
const loading = ref(false)
const form = reactive<any>({
id: undefined,
refundPrice: 0,
/** 是否重新占用本次退款恢复的商品库存;默认是,与后端参数默认值保持一致。 */
deductStock: true,
})
// 打开弹窗:每次打开重置为默认扣减库存
const openDialog = (row: any) => {
visible.value = true
form.id = row.id
form.refundPrice = row.refundPrice
form.deductStock = true
}
// 提交退回审核
const onSubmit = async () => {
try {
loading.value = true
await returnToReview({ id: form.id, deductStock: form.deductStock })
useMessage().success("已退回审核中")
visible.value = false
emit("refresh")
} catch (err: any) {
useMessage().error(err?.msg || "退回审核失败")
} finally {
loading.value = false
}
}
// 暴露变量
defineExpose({
openDialog,
})
</script>
<style scoped>
.keep-stock-tip {
padding: 0 12px;
font-size: 13px;
line-height: 20px;
color: var(--el-color-warning);
}
</style>
+461
View File
@@ -0,0 +1,461 @@
<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" :props="{
label: 'name',
children: 'children',
value: 'id',
disabled: 'isLockNone',
}">
<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="searchRecords" @reset="resetQuery">
<el-form-item style="width: 250px" label="商品名称" prop="productName">
<el-input v-model="state.queryForm.productName" placeholder="请输入商品名称" clearable />
</el-form-item>
<el-form-item style="width: 250px" label="用户" prop="userId">
<select-user v-model="state.queryForm.userId" />
</el-form-item>
<el-form-item style="width: 250px" label="商户" prop="mchIds">
<select-mch deptMch multiple v-model="state.queryForm.mchIds"
:apiParams="{ parentIsNull: false }" />
</el-form-item>
<el-form-item style="width: 300px" label="订单状态" prop="orderStatusCodes">
<dict-select v-model="state.queryForm.orderStatusCodes" dict-code="hard_order_status"
multiple />
</el-form-item>
<template #more>
<el-form-item style="width: 250px" label="商户类型" prop="merchantTypeCodes">
<dict-select v-model="state.queryForm.merchantTypeCodes" dict-code="merchant_type"
multiple />
</el-form-item>
<el-form-item style="width: 250px" label="设备类型" prop="hardTypeCodes">
<dict-select v-model="state.queryForm.hardTypeCodes" dict-code="hard_type" multiple />
</el-form-item>
<el-form-item style="width: 250px" label="用户部门" prop="userDeptId">
<select-dept v-model="state.queryForm.userDeptId" />
</el-form-item>
<el-form-item style="width: 250px" label="用餐时间段" prop="hourTypeCodes">
<dict-select v-model="state.queryForm.hourTypeCodes" dict-code="daily_menu" multiple />
</el-form-item>
<el-form-item style="width: 300px" label="支付方式" prop="orderPayTypeCodes">
<dict-select v-model="state.queryForm.orderPayTypeCodes" dict-code="hard_pay_type"
multiple />
</el-form-item>
<el-form-item style="width: 450px" label="预约时间" prop="apptUseDateTimeList">
<el-date-picker v-model="state.queryForm.apptUseDateTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
:default-time="dateRangeDefaultTime" />
</el-form-item>
<el-form-item style="width: 450px" label="核销时间" prop="verificationTimeList">
<el-date-picker v-model="state.queryForm.verificationTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
:default-time="dateRangeDefaultTime" />
</el-form-item>
<el-form-item style="width: 450px" label="下单时间" prop="createTimeList">
<el-date-picker v-model="state.queryForm.createTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
</el-form-item>
<el-form-item style="width: 450px" label="支付时间" prop="payTimeList">
<el-date-picker v-model="state.queryForm.payTimeList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" />
</el-form-item>
<user-identity-query v-model:identity-types="state.queryForm.identityTypes"
v-model:student-info="state.queryForm.studentInfo"
v-model:teacher-info="state.queryForm.teacherInfo"
v-model:has-face="state.queryForm.hasFace" />
</template>
</SearchPanel>
<div class="flex justify-between items-center">
<el-tabs class="flex-1" v-if="tabList && tabList.length" v-model="activeName"
@tab-click="handleClick">
<div v-for="v in tabList" :key="v.value">
<el-tab-pane :label="v.label" :name="v.value">
<div></div>
</el-tab-pane>
</div>
</el-tabs>
<right-toolbar v-model:showSearch="showSearch" :export="'hard_hardCarRecord_export'"
@exportExcel="exportExcel" class="ml10" style="float: right; margin-right: 20px"
@queryTable="getDataList">
</right-toolbar>
</div>
<el-table ref="recordTableRef" :data="state.dataList" v-loading="state.loading" border
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle" row-key="id">
<el-table-column type="selection" reserve-selection width="44" fixed="left" align="center" />
<el-table-column type="index" label="#" width="50" />
<el-table-column width="240px" label="商品信息">
<template #default="{ row }">
<div class="flex items-center gap-2 min-w-0">
<preview-image v-if="row.productPic" width="40px" height="40px" :src="row.productPic" />
<div class="min-w-0 flex-1">
<el-tooltip :content="row.productName" placement="top-start"
:disabled="!row.productName">
<div class="text-line primary-line font-medium">{{ row.productName || "-" }}</div>
</el-tooltip>
<div class="kv">
<span class="kv-label">单价</span>
<span class="kv-value">{{ displayPrice(row.productPrice, "元", 2) }}</span>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column width="110px" label="数量/小计" prop="productCount" align="center">
<template #default="{ row }">
<div class="leading-[22px]">
<span class="text-[15px] font-semibold">{{ row.productCount ?? 0 }}</span>
<span
class="text-xs font-normal text-[var(--el-text-color-secondary)]">份</span>
</div>
<div class="text-xs leading-[18px] color-success">
{{ displayPrice(row.totalPrice, "元", 2) }}
</div>
</template>
</el-table-column>
<el-table-column width="260" label="购买人">
<template #default="{ row }">
<div v-if="row.userId && row.userId != -1" class="min-w-0">
<user-info-cell :user="row" :show-gender="false" />
</div>
<div v-else>
<span>未知用户</span>
</div>
</template>
</el-table-column>
<el-table-column width="300" label="身份信息">
<template #default="{ row }">
<user-identity-info v-if="row.userId && row.userId != -1" :user="row" />
<span v-else class="text-[var(--el-text-color-placeholder)]">-</span>
</template>
</el-table-column>
<el-table-column width="250px" label="订单信息">
<template #default="{ row }">
<div class="flex items-center gap-1 mb-[2px]">
<dict-tag :options="hard_order_status" :value="row.orderStatus" />
<span class="text-xs text-[var(--el-text-color-secondary)]">
{{ dictLabel(hard_order_type, row.orderType) }}
</span>
</div>
<div v-if="orderSubLine(row)" class="kv">
<span class="kv-label">支付</span>
<span class="kv-value">{{ orderSubLine(row) }}</span>
</div>
<div v-if="row.orderNo" class="kv">
<span class="kv-label">单号</span>
<el-tooltip :content="row.orderNo" placement="top-start">
<span class="kv-value">{{ row.orderNo }}</span>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column width="200px" label="商户设备">
<template #default="{ row }">
<el-tooltip :content="row.merchantName" placement="top-start"
:disabled="!row.merchantName">
<div class="text-line primary-line">{{ row.merchantName || "-" }}</div>
</el-tooltip>
<div v-if="hardLine(row)" class="kv">
<span class="kv-label">设备</span>
<span class="kv-value">{{ hardLine(row) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="时间信息" prop="orderCreateTime" width="220px">
<template #default="{ row }">
<div v-if="row.orderCreateTime" class="kv">
<span class="kv-label">下单</span>
<span class="kv-value">{{ row.orderCreateTime }}</span>
</div>
<div v-if="row.payTime" class="kv">
<span class="kv-label">支付</span>
<span class="kv-value">{{ row.payTime }}</span>
</div>
<div v-if="row.apptUseDate" class="kv">
<span class="kv-label">预约</span>
<span class="kv-value">{{ row.apptUseDate }}</span>
</div>
<div v-if="row.verificationTime" class="kv">
<span class="kv-label">核销</span>
<span class="kv-value">{{ row.verificationTime }}</span>
</div>
<!-- verificationType 为空的历史已核销订单按人工核销展示 -->
<div v-if="row.verificationTime" class="kv">
<el-tag v-if="row.verificationType === '2'" type="warning" size="small">自动核销</el-tag>
<el-tag v-else type="info" size="small">人工核销</el-tag>
</div>
</template>
</el-table-column>
<el-table-column width="170px" label="退款信息">
<template #default="{ row }">
<template v-if="row.refundStatus || (row.refundPrice && row.refundPrice != 0)">
<div class="kv">
<span class="kv-label">退款额</span>
<span class="kv-value color-success">
{{ displayPrice(row.refundPrice, "元", 2) }}
</span>
</div>
<div class="kv">
<span class="kv-label">状态</span>
<dict-tag :options="hard_refund_status" :value="row.refundStatus" />
</div>
<div v-if="row.refundTime" class="kv">
<span class="kv-label">时间</span>
<span class="kv-value">{{ row.refundTime }}</span>
</div>
</template>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="100">
<template #default="{ row }">
<el-button icon="InfoFilled" link type="primary" style="margin: 0"
@click="openOrderDetail(row)">
订单详情
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</template>
</DraggableDepTree>
</div>
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig" @success="handleExportSuccess"
@failure="handleExportFailure" />
</div>
</template>
<script setup lang="ts" name="hardCarRecord">
import { BasicTableProps, useTable } from "/@/hooks/table";
import * as hardCarRecord from "/@/api/hard/hard-car-record";
import { getDicts } from "/@/api/admin/dict";
import { useMessage } from "/@/hooks/message";
import { useDict } from "/@/hooks/dict";
import { displayPrice } from "/@/utils/formatMoney";
import { createEmptyUserIdentityStudentQuery, createEmptyUserIdentityTeacherQuery } from "/@/components/UserIdentityQuery/types";
import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask";
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue";
import { downBlobFile } from "/@/utils/other";
const { push } = useRouter();
/** 从字典集合中取编码对应的文本,未知编码原样返回,空值返回空串。 */
const dictLabel = (options: Array<{ label: string; value: any }> | undefined | null, value: unknown): string => {
if (value === null || value === undefined || value === "") return "";
const hit = (options ?? []).find((item: any) => String(item.value) === String(value));
return hit ? hit.label : String(value);
};
/** 订单次要信息行:支付方式与用餐时段以中点连接,空字段自动折叠。 */
const orderSubLine = (row: any): string => {
return [dictLabel(hard_pay_type.value, row.payType), dictLabel(daily_menu.value, row.hourType)]
.filter(Boolean)
.join(" · ");
};
/** 设备次要信息行:设备类型与设备名以中点连接,均无设备信息时返回空。 */
const hardLine = (row: any): string => {
return [dictLabel(hard_type.value, row.hardType), row.hardName].filter(Boolean).join(" · ");
};
// 定义查询字典
const { hard_pay_type, hard_order_type, hard_order_status, hard_type, hard_refund_status, daily_menu } = useDict(
"hard_pay_type",
"hard_order_type",
"hard_order_status",
"hard_type",
"hard_refund_status",
"daily_menu",
);
// 定义变量内容
const queryRef = ref();
const showSearch = ref(true);
const tabList = ref<any>([{}]);
const activeName = ref(null);
const draggableDepTreeRef = ref();
/** Element Plus 商品记录表格引用,用于筛选变化时清空跨页保留选择。 */
const recordTableRef = ref();
/** 跨页勾选的商品明细行主键;导出时勾选行优先于当前筛选条件。 */
const selectObjs = ref<Array<string | number>>([]);
/** 购物车记录异步导出进度弹窗引用,用于提交成功后启动统一轮询。 */
const exportProgressRef = ref<InstanceType<typeof AsyncTaskProgressComponent>>();
/** 导出任务从提交到终态的本地互斥状态,防止重复创建并覆盖当前跟踪任务。 */
const exportSubmitting = ref(false);
/** 购物车记录导出进度配置,与后端导出任务 Redis 业务命名空间保持一致。 */
const exportTaskConfig: AsyncTaskPollingConfig = {
progressUrl: "/hard/async-task/progress",
redisKey: "hard:carRecord:export",
showDialogOnFinished: false,
waitingMessage: "正在分批生成购物车记录导出文件,请耐心等待进度达到 100%",
downloadButtonText: "下载 Excel",
};
/** 日期范围默认覆盖开始日零点至结束日最后一秒,避免遗漏结束日期内的记录。 */
const dateRangeDefaultTime = [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)];
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
// 列表查询和异步导出共用这些数组字段,保证多选条件的筛选语义完全一致。
productName: "",
mchIds: [],
orderStatusCodes: [],
merchantTypeCodes: [],
hardTypeCodes: [],
hourTypeCodes: [],
orderPayTypeCodes: [],
identityTypes: [],
studentInfo: createEmptyUserIdentityStudentQuery(),
teacherInfo: createEmptyUserIdentityTeacherQuery(),
hasFace: "",
},
pageList: hardCarRecord.pageJoin,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state);
onMounted(async () => {
const { data } = await getDicts("hard_order_type");
data.unshift({ label: "全部", value: null });
tabList.value = data;
});
/** 使用新的筛选条件查询商品记录;筛选上下文变化时清空跨页勾选,避免导出混入不可见记录。 */
const searchRecords = () => {
clearSelection();
getDataList();
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
clearSelection();
state.queryForm.userDeptIds = [];
draggableDepTreeRef.value.resetData();
getDataList();
};
/** 订单类型 Tab 切换后按新类型重新查询;跨页勾选随筛选上下文一起清空。 */
const handleClick = (e: any) => {
clearSelection();
state.queryForm.orderType = tabList.value[e.index].value;
getDataList();
};
/** 跳转到订单详情页查看该商品明细归属的完整订单。 */
const openOrderDetail = (row: any) => {
push({ path: "/hard/hard-after-sale/order-refund", query: { id: row.orderId, type: "view" } });
};
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
clearSelection();
state.queryForm.userDeptIds = checkedTreeNodes.map((item: any) => item.id);
state.queryForm.isInclude = !isDeepTree;
getDataList();
};
/** 同步 Element Plus 跨页保留选择到页面勾选快照,供导出接口优先使用。 */
const selectionChangHandle = (rows: any[]) => {
selectObjs.value = rows.map((row) => row.id);
};
/** 清空 Element Plus 内部 reserve-selection 与页面勾选快照,确保后续分页不会恢复旧选择。 */
const clearSelection = () => {
selectObjs.value = [];
recordTableRef.value?.clearSelection();
};
/** 按当前筛选条件或勾选明细提交异步分批导出任务。 */
const exportExcel = async () => {
if (exportSubmitting.value) {
useMessage().warning("已有购物车记录导出任务正在处理");
return;
}
exportSubmitting.value = true;
try {
const response = await hardCarRecord.submitExport({
...state.queryForm,
carIds: [...selectObjs.value],
});
if (!response.data?.taskId) {
exportSubmitting.value = false;
useMessage().error("导出任务提交成功,但响应中缺少任务ID");
return;
}
useMessage().success(response.msg || "购物车记录导出任务已提交");
exportProgressRef.value?.start(response.data);
} catch (error: any) {
exportSubmitting.value = false;
useMessage().error(error?.msg || "购物车记录导出任务提交失败");
}
};
/** 异步导出成功后下载文件中心返回的完整记录文件。 */
const handleExportSuccess = async (progress: AsyncTaskProgress) => {
exportSubmitting.value = false;
if (!progress.resultFileUrl) {
useMessage().error("导出完成,但未返回文件下载地址");
return;
}
useMessage().success(progress.message || "购物车记录导出完成");
await downBlobFile(progress.resultFileUrl, {}, progress.resultFileName || "购物车记录.xlsx");
};
/** 异步导出失败时展示后台任务返回的受控错误信息。 */
const handleExportFailure = (progress: AsyncTaskProgress) => {
exportSubmitting.value = false;
useMessage().error(progress.message || "购物车记录导出失败");
};
</script>
<style scoped lang="scss">
.text-line {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 单元格主体行:商品名、商户名等一级信息。 */
.primary-line {
font-size: 13px;
line-height: 20px;
}
/* 单元格次要键值行:短标签 + 值紧凑左对齐,替代旧的表格式 label 两端对齐。 */
.kv {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
line-height: 18px;
margin-bottom: 2px;
&:last-child {
margin-bottom: 0;
}
}
.kv-label {
flex-shrink: 0;
color: var(--el-text-color-secondary);
}
.kv-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -1,7 +1,7 @@
<!-- 不绑定发票的充值/扣款导入组件:集中维护导入场景、充值类型、接口和模板。 -->
<template>
<upload-excel ref="uploadExcelRef" title="充值导入(不绑定发票)" :url="currentUploadUrl" :temp-url="currentTempUrl"
@refreshDataList="handleRefreshDataList">
:async-task-config="currentAsyncTaskConfig" @refreshDataList="handleRefreshDataList">
<template #before-upload="{ uploading }">
<el-form class="mb20" :model="form" label-width="90px">
<el-form-item label="导入类型" prop="importType" required class="mb20">
@@ -22,6 +22,7 @@
</template>
<script setup lang="ts" name="wallet-recharge-apply-unbind-import-excel">
import type { AsyncTaskPollingConfig } from "/@/api/asyncTask"
import UploadExcel from "/@/components/Upload/Excel.vue"
import { useDict } from "/@/hooks/dict"
import { useUserInfo } from "/@/stores/userInfo"
@@ -46,6 +47,8 @@ interface WalletRechargeApplyImportOption {
resetTempUrl: string
/** 当前导入场景要求的按钮权限。 */
permission: string
/** 当前导入场景对应的异步进度 Redis Key 命名空间。 */
redisKey: string
}
/** 导入成功后通知申请列表刷新。 */
@@ -61,6 +64,7 @@ const importOptions: WalletRechargeApplyImportOption[] = [
tempUrl: "/admin/sys-file/local/file/wallet_recharge.xlsx",
resetTempUrl: "/admin/sys-file/local/file/wallet_reset_recharge.xlsx",
permission: "wallet_walletRechargeApply_unbind_def",
redisKey: "wallet:recharge:import:default",
},
{
value: "student",
@@ -70,6 +74,7 @@ const importOptions: WalletRechargeApplyImportOption[] = [
tempUrl: "/admin/sys-file/local/file/wallet_student_recharge.xlsx",
resetTempUrl: "/admin/sys-file/local/file/wallet_student_reset_recharge.xlsx",
permission: "wallet_walletRechargeApply_unbind_studen",
redisKey: "wallet:recharge:import:student",
},
]
@@ -123,6 +128,16 @@ const currentTempUrl = computed(() => form.bizType == "4"
: currentImportOption.value.tempUrl
)
/** 当前导入场景的通用异步进度配置,接口返回任务 ID 后由上传组件轮询并展示进度条。 */
const currentAsyncTaskConfig = computed<AsyncTaskPollingConfig>(() => ({
progressUrl: "/wallet/async-task/progress",
redisKey: currentImportOption.value.redisKey,
pollIntervalMs: 1000,
pollTimeoutMs: 2 * 60 * 60 * 1000,
maxConsecutiveErrors: 5,
waitingMessage: "正在导入充值明细,请耐心等待进度达到 100%",
}))
/** 转发导入成功事件,由申请列表重新加载数据。 */
const handleRefreshDataList = () => {
emit("refreshDataList")
@@ -132,7 +132,8 @@
</div>
<upload-excel ref="uploadExcelRef" title="导入" url="/wallet/wallet-recharge-apply/read-topup-excel"
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx" @refreshDataList="getDataList(false)" />
temp-url="/admin/sys-file/local/file/wallet_recharge.xlsx" :async-task-config="topupAsyncTaskConfig"
@refreshDataList="getDataList(false)" />
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
@@ -147,6 +148,7 @@
<script setup lang="ts" name="systemWalletRechargeInvoice">
import { BasicTableProps, useTable } from "/@/hooks/table"
import type { AsyncTaskPollingConfig } from "/@/api/asyncTask"
import * as WalletRechargeInvoiceApi from "/@/api/wallet/wallet-recharge-invoice"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { displayPrice } from "/@/utils/formatMoney"
@@ -170,6 +172,16 @@ const { push } = useRouter()
// 定义变量内容
const formDialogRef = ref()
const uploadExcelRef = ref()
/** 发票充值导入的异步进度配置,接口返回任务 ID 后由上传组件轮询并展示进度条。 */
const topupAsyncTaskConfig: AsyncTaskPollingConfig = {
progressUrl: "/wallet/async-task/progress",
redisKey: "wallet:recharge:import:default",
pollIntervalMs: 1000,
pollTimeoutMs: 2 * 60 * 60 * 1000,
maxConsecutiveErrors: 5,
waitingMessage: "正在导入充值明细,请耐心等待进度达到 100%",
}
// 搜索变量
const queryRef = ref()
const processDrawerRef = ref()