Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
4dac32d89c
9 files changed
+632
-48
No files matched your search
@@ -170,9 +170,9 @@ export function disagree(obj?: Object) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将人工审核且已完成的钱包退款退回审核中,服务端会同步扣回退款金额并恢复库存占用。
|
||||
* 将人工审核且已完成的钱包退款退回审核中,服务端会同步扣回退款金额;默认重新占用库存占用。
|
||||
*
|
||||
* @param query 售后单 ID 查询参数
|
||||
* @param query 售后单 ID 及 deductStock(是否扣减库存,默认 true)查询参数
|
||||
* @returns 后端统一业务响应
|
||||
*/
|
||||
export function returnToReview(query?: Object) {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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="启用/禁用" />
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user