Files
one-pipe-system/src/views/finance/refund/index.vue
luo d3b5f55c64
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 3m41s
feat:更换企业授权
2026-06-22 10:36:33 +08:00

946 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ArtTableFullScreen>
<div class="refund-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
show-expand
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog" v-if="hasAuth('refund:create')"
>创建退款申请</ElButton
>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="refundList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:actionsWidth="220"
:inlineActionsCount="2"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 创建退款申请对话框 -->
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
<!-- 退款凭证预览 -->
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
<!-- 审批通过对话框 -->
<ElDialog
v-model="approveDialogVisible"
title="审批通过"
width="500px"
@closed="handleApproveDialogClosed"
>
<ElForm
ref="approveFormRef"
:model="approveForm"
:rules="approveRules"
label-width="120px"
>
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="申请退款金额">
<span>{{ formatCurrency(currentRefund?.requested_refund_amount) }}</span>
</ElFormItem>
<ElFormItem label="实际退款金额" prop="approved_refund_amount">
<ElInputNumber
v-model="approveForm.approved_refund_amount"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则默认使用申请金额"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
不填则默认使用申请金额
</div>
</ElFormItem>
<ElFormItem label="审批备注">
<ElInput
v-model="approveForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入审批备注"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="approveDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleApproveRefund" :loading="approveLoading">
确认通过
</ElButton>
</div>
</template>
</ElDialog>
<!-- 审批拒绝对话框 -->
<ElDialog
v-model="rejectDialogVisible"
title="审批拒绝"
width="500px"
@closed="handleRejectDialogClosed"
>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="拒绝原因" prop="reject_reason">
<ElInput
v-model="rejectForm.reject_reason"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入拒绝原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
<ElButton type="danger" @click="handleRejectRefund" :loading="rejectLoading">
确认拒绝
</ElButton>
</div>
</template>
</ElDialog>
<!-- 退回对话框 -->
<ElDialog
v-model="returnDialogVisible"
title="驳回申请"
width="30%"
@closed="handleReturnDialogClosed"
>
<ElForm ref="returnFormRef" :model="returnForm" :rules="returnRules" label-width="80">
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="退回备注">
<ElInput
v-model="returnForm.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入退回备注"
/>
</ElFormItem>
</ElForm>
<div class="return-dialog-tip">
驳回后该订单不可再申请退款若还需申请退款点击审核拒绝
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="returnDialogVisible = false">取消</ElButton>
<ElButton type="warning" @click="handleReturnRefund" :loading="returnLoading">
确认退回
</ElButton>
</div>
</template>
</ElDialog>
<!-- 重新提交对话框 -->
<ElDialog
v-model="resubmitDialogVisible"
title="重新提交退款申请"
width="500px"
@closed="handleResubmitDialogClosed"
>
<ElForm
ref="resubmitFormRef"
:model="resubmitForm"
:rules="resubmitRules"
label-width="120px"
>
<ElFormItem label="退款单号">
<span>{{ currentRefund?.refund_no }}</span>
</ElFormItem>
<ElFormItem label="申请退款金额">
<ElInputNumber
v-model="resubmitForm.requested_refund_amount"
:min="1"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则使用原金额"
/>
</ElFormItem>
<ElFormItem label="实收金额">
<ElInputNumber
v-model="resubmitForm.actual_received_amount"
:min="1"
:precision="2"
:step="1"
style="width: 100%"
placeholder="不填则使用原金额"
/>
</ElFormItem>
<ElFormItem label="退款原因">
<ElInput
v-model="resubmitForm.refund_reason"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入退款原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
确认提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RefundService, ShopService, OrderService } from '@/api/modules'
import { ElMessage, ElTag, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
Refund,
RefundQueryParams,
RefundStatus,
ApproveRefundRequest,
RejectRefundRequest,
ReturnRefundRequest,
ResubmitRefundRequest
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import { useAuth } from '@/composables/useAuth'
import CreateRefundDialog from '@/components/business/CreateRefundDialog.vue'
defineOptions({ name: 'RefundList' })
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const approveLoading = ref(false)
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const returnDialogVisible = ref(false)
const resubmitDialogVisible = ref(false)
const currentRefund = ref<Refund | null>(null)
const refundVoucherFileKey = ref('')
// 搜索表单初始值
const initialSearchState: RefundQueryParams = {
status: undefined,
order_id: undefined,
shop_id: undefined,
asset_identifier: ''
}
// 搜索表单
const searchForm = reactive<RefundQueryParams>({ ...initialSearchState })
// 店铺选项
const shopOptions = ref<any[]>([])
// 订单选项
const orderOptions = ref<any[]>([])
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '店铺',
prop: 'shop_id',
type: 'select',
placeholder: '请选择店铺',
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query)
}
},
{
label: '订单号',
prop: 'order_id',
type: 'select',
placeholder: '请选择订单号',
options: () =>
orderOptions.value.map((order) => ({
label: `${order.order_no}`,
value: order.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchOrdersForFilter(query)
}
},
{
label: '资产标识',
prop: 'asset_identifier',
type: 'input',
placeholder: '请输入ICCID或设备虚拟号',
config: {
clearable: true
}
},
{
label: '状态',
prop: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待审批', value: 1 },
{ label: '已通过', value: 2 },
{ label: '已拒绝', value: 3 },
{ label: '已退回', value: 4 }
],
config: {
clearable: true
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '退款单号', prop: 'refund_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '订单号', prop: 'order_no' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '资产类型', prop: 'asset_type' },
{ label: '申请退款金额', prop: 'requested_refund_amount' },
{ label: '实际退款金额', prop: 'approved_refund_amount' },
{ label: '实收金额', prop: 'actual_received_amount' },
{ label: '状态', prop: 'status' },
{ label: '退款原因', prop: 'refund_reason' },
{ label: '拒绝原因', prop: 'reject_reason' },
{ label: '审批备注', prop: 'remark' },
{ label: '资产重置', prop: 'asset_reset' },
{ label: '佣金扣除', prop: 'commission_deducted' },
{ label: '创建时间', prop: 'created_at' },
{ label: '审批时间', prop: 'processed_at' }
]
const approveFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const returnFormRef = ref<FormInstance>()
const resubmitFormRef = ref<FormInstance>()
const approveRules = reactive<FormRules>({})
const rejectRules = reactive<FormRules>({
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const returnRules = reactive<FormRules>({})
const resubmitRules = reactive<FormRules>({})
const approveForm = reactive<ApproveRefundRequest>({
approved_refund_amount: undefined,
remark: ''
})
const rejectForm = reactive<RejectRefundRequest>({
reject_reason: ''
})
const returnForm = reactive<ReturnRefundRequest>({
remark: ''
})
const resubmitForm = reactive<{
requested_refund_amount?: number
actual_received_amount?: number
refund_reason?: string
}>({
requested_refund_amount: undefined,
actual_received_amount: undefined,
refund_reason: ''
})
const refundList = ref<Refund[]>([])
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number | null | undefined): string => {
if (amount === undefined || amount === null || Number.isNaN(amount)) return '-'
return `¥${(amount / 100).toFixed(2)}`
}
// 获取状态标签类型
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
1: 'warning', // 待审批
2: 'success', // 已通过
3: 'danger', // 已拒绝
4: 'info' // 已退回
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: RefundStatus): string => {
const statusMap: Record<RefundStatus, string> = {
1: '待审批',
2: '已通过',
3: '已拒绝',
4: '已退回'
}
return statusMap[status] || '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'refund_no',
label: '退款单号',
minWidth: 210,
formatter: (row: Refund) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.refund_no
)
}
},
{
prop: 'shop_name',
label: '店铺名称',
width: 160
},
{
prop: 'order_no',
label: '订单号',
width: 180,
showOverflowTooltip: true
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 200,
showOverflowTooltip: true
},
{
prop: 'asset_type',
label: '资产类型',
width: 100,
formatter: (row: Refund) =>
row.asset_type === 'device'
? '设备'
: row.asset_type === 'card'
? '单卡'
: row.asset_type === 'iot_card'
? 'IoT卡'
: row.asset_type || '-'
},
{
prop: 'requested_refund_amount',
label: '申请退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.requested_refund_amount)
},
{
prop: 'approved_refund_amount',
label: '实际退款金额',
width: 150,
formatter: (row: Refund) => formatCurrency(row.approved_refund_amount)
},
{
prop: 'actual_received_amount',
label: '实收金额',
width: 120,
formatter: (row: Refund) => formatCurrency(row.actual_received_amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
}
},
{
prop: 'refund_reason',
label: '退款原因',
minWidth: 200,
formatter: (row: Refund) => row.refund_reason || '-'
},
{
prop: 'reject_reason',
label: '拒绝原因',
minWidth: 200,
formatter: (row: Refund) => row.reject_reason || '-'
},
{
prop: 'remark',
label: '审批备注',
minWidth: 200,
formatter: (row: Refund) => row.remark || '-'
},
{
prop: 'asset_reset',
label: '资产重置',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.asset_reset ? 'success' : 'info' }, () =>
row.asset_reset ? '是' : '否'
)
}
},
{
prop: 'commission_deducted',
label: '佣金扣除',
width: 100,
formatter: (row: Refund) => {
return h(ElTag, { type: row.commission_deducted ? 'success' : 'info' }, () =>
row.commission_deducted ? '是' : '否'
)
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: Refund) => formatDateTime(row.created_at)
},
{
prop: 'processed_at',
label: '审批时间',
width: 180,
formatter: (row: Refund) => (row.processed_at ? formatDateTime(row.processed_at) : '-')
}
])
onMounted(() => {
getTableData()
searchShops('')
searchOrdersForFilter('')
})
let isFirstActivation = true
onActivated(() => {
if (!isFirstActivation) {
getTableData()
}
isFirstActivation = false
})
// 搜索店铺(用于搜索表单)
const searchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search shops failed:', error)
}
}
// 搜索订单(用于搜索表单)
const searchOrdersForFilter = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.order_no = query
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search orders failed:', error)
}
}
// 获取退款申请列表
const getTableData = async () => {
loading.value = true
try {
const assetIdentifier = searchForm.asset_identifier?.trim()
const params: RefundQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
status: searchForm.status,
order_id: searchForm.order_id,
shop_id: searchForm.shop_id,
asset_identifier: assetIdentifier || undefined
}
const res = await RefundService.getRefunds(params)
if (res.code === 0) {
refundList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error: any) {
console.error(error)
ElMessage.error(error?.message || '重新提交失败')
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建对话框
const showCreateDialog = () => {
createDialogVisible.value = true
}
// 创建退款申请成功
const handleCreateSuccess = () => {
getTableData()
}
// 显示审批通过对话框
const handleShowApprove = (row: Refund) => {
currentRefund.value = row
approveDialogVisible.value = true
}
// 审批通过对话框关闭后的清理
const handleApproveDialogClosed = () => {
approveFormRef.value?.resetFields()
approveForm.approved_refund_amount = undefined
approveForm.remark = ''
currentRefund.value = null
}
// 审批通过
const handleApproveRefund = async () => {
if (!approveFormRef.value || !currentRefund.value) return
await approveFormRef.value.validate(async (valid) => {
if (valid) {
approveLoading.value = true
try {
const data: ApproveRefundRequest = {
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
remark: approveForm.remark || undefined
}
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.approveRefund(refundId, data)
ElMessage.success('审批通过成功')
approveDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
approveLoading.value = false
}
}
})
}
// 显示审批拒绝对话框
const handleShowReject = (row: Refund) => {
currentRefund.value = row
rejectDialogVisible.value = true
}
// 审批拒绝对话框关闭后的清理
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.reject_reason = ''
currentRefund.value = null
}
// 审批拒绝
const handleRejectRefund = async () => {
if (!rejectFormRef.value || !currentRefund.value) return
await rejectFormRef.value.validate(async (valid) => {
if (valid) {
rejectLoading.value = true
try {
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.rejectRefund(refundId, rejectForm)
ElMessage.success('审批拒绝成功')
rejectDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
rejectLoading.value = false
}
}
})
}
// 显示退回对话框
const handleShowReturn = (row: Refund) => {
currentRefund.value = row
returnDialogVisible.value = true
}
// 退回对话框关闭后的清理
const handleReturnDialogClosed = () => {
returnFormRef.value?.resetFields()
returnForm.remark = ''
currentRefund.value = null
}
// 退回
const handleReturnRefund = async () => {
if (!returnFormRef.value || !currentRefund.value) return
await returnFormRef.value.validate(async (valid) => {
if (valid) {
returnLoading.value = true
try {
const refundId = currentRefund.value?.id
if (!refundId) return
await RefundService.returnRefund(refundId, returnForm)
ElMessage.success('退回成功')
returnDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
returnLoading.value = false
}
}
})
}
// 显示重新提交对话框
const handleShowResubmit = (row: Refund) => {
currentRefund.value = row
// 预填充原有数据
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
resubmitForm.refund_reason = row.refund_reason
resubmitDialogVisible.value = true
}
// 重新提交对话框关闭后的清理
const handleResubmitDialogClosed = () => {
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_reason = ''
currentRefund.value = null
}
// 重新提交
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !currentRefund.value) return
await resubmitFormRef.value.validate(async (valid) => {
if (valid) {
resubmitLoading.value = true
try {
const refundId = currentRefund.value?.id
if (!refundId) return
const data: ResubmitRefundRequest = {
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined
}
await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功')
resubmitDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
resubmitLoading.value = false
}
}
})
}
// 处理名称点击
const handleNameClick = (row: Refund) => {
if (hasAuth('refund:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 查看详情
const handleViewDetail = (row: Refund) => {
router.push({
path: `${RoutesAlias.RefundManagement}/detail/${row.id}`
})
}
// 获取操作按钮
const getActions = (row: Refund) => {
const actions: any[] = []
const voucherKey = row.refund_voucher_key
// 待审批状态可以打回重填、审批通过或审批拒绝
if (row.status === 1) {
// 审批通过
if (hasAuth('refund:approve')) {
actions.push({
label: '审批通过',
handler: () => handleShowApprove(row),
type: 'primary'
})
}
// 审批拒绝
if (hasAuth('refund:reject')) {
actions.push({
label: '审批拒绝',
handler: () => handleShowReject(row),
type: 'danger'
})
}
// 驳回
if (hasAuth('refund:return')) {
actions.push({
label: '驳回',
handler: () => handleShowReturn(row),
type: 'primary'
})
}
}
// 只有已退回状态可以重新提交
if (row.status === 4 && hasAuth('refund:resubmit')) {
actions.push({
label: '重新提交',
handler: () => handleShowResubmit(row),
type: 'primary'
})
}
if (voucherKey && hasAuth('refund:view_voucher')) {
actions.push({
label: '查看退款凭证',
handler: () => handleViewRefundVoucher(row),
type: 'primary'
})
}
return actions
}
const handleViewRefundVoucher = (row: Refund) => {
const voucherKey = row.refund_voucher_key
if (!voucherKey) return
refundVoucherFileKey.value = voucherKey
}
</script>
<style scoped lang="scss">
.refund-page {
height: 100%;
.return-dialog-tip {
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
color: var(--el-color-primary);
text-align: center;
}
}
</style>