Files
one-pipe-system/src/views/finance/agent-recharge/index.vue
luo 5e7f5eaba4
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m3s
fix: some
2026-09-18 10:31:14 +08:00

1677 lines
54 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="agent-recharge-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('agent_recharge:create') && canCreateRecharge"
>{{ createButtonLabel }}</ElButton
>
<ElButton v-if="hasAuth('agent_recharge:export')" @click="exportDialogVisible = true">
导出
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="rechargeList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:actions="getActions"
:inlineActionsCount="1"
:actionsWidth="160"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 创建充值订单对话框 -->
<ElDialog
v-model="createDialogVisible"
:title="createDialogTitle"
width="40%"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="充值金额" prop="amount">
<ElInputNumber
v-model="createForm.amount"
:min="minimumAmountYuan"
:max="maximumAmountYuan"
:precision="2"
:step="0.01"
style="width: 100%"
placeholder="请输入充值金额(元)"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
金额范围¥{{ minimumAmountYuan.toFixed(2) }} - ¥{{
maximumAmountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}}
</div>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
:loading="paymentMethodsLoading"
>
<ElOption
v-for="option in availablePaymentMethodOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
<div
v-if="
createMode === 'online' &&
!paymentMethodsLoading &&
onlinePaymentMethods.length === 0
"
class="online-recharge-empty-hint"
>
当前暂无可用在线支付方式,暂时无法提交充值。
</div>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="店铺" prop="shop_id">
<ElCascader
v-model="createForm.shop_id"
:options="shopCascadeOptions"
:props="shopCascadeProps"
placeholder="请选择店铺"
filterable
clearable
style="width: 100%"
@change="handleShopChange"
/>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="支付凭证"
prop="payment_voucher_key"
>
<VoucherUpload
ref="uploadRef"
v-model="createForm.payment_voucher_key"
voucher-name="支付凭证"
accept="image/*"
content-type="image/jpeg"
@uploading-change="voucherUploading = $event"
@change="handleVoucherChange"
/>
</ElFormItem>
<ElFormItem
v-if="createMode === 'offline'"
label="收款方式"
prop="offline_payment_method_id"
>
<ElSelect
v-model="createForm.offline_payment_method_id"
placeholder="请选择收款方式"
style="width: 100%"
filterable
:loading="paymentMethodOptionsLoading"
>
<ElOption
v-for="item in offlinePaymentMethodOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
v-if="createForm.payment_method === 'offline'"
label="交易流水号"
prop="external_transaction_no"
>
<div class="external-transaction-row">
<ElInput
v-model="createForm.external_transaction_no"
placeholder="请输入交易流水号,或点击右侧识别凭证预填"
:disabled="ocrLoading"
/>
<ElButton
:loading="ocrLoading"
:disabled="!offlineVoucherKeys.length"
@click="handleRecognizeVoucher"
>
识别凭证
</ElButton>
</div>
<div class="external-transaction-tip">
识别结果仅供参考,请对照凭证核对后再提交。
</div>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="其他凭证">
<VoucherUpload
ref="otherUploadRef"
v-model="createForm.other_voucher_key"
voucher-name="其他凭证"
:max-count="5"
accept="image/*"
content-type="image/jpeg"
@uploading-change="otherVoucherUploading = $event"
/>
</ElFormItem>
<ElFormItem v-if="createMode === 'offline'" label="运营备注" prop="remark">
<ElInput
v-model="createForm.remark"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入运营备注(可选)"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleCreateRecharge"
:loading="createLoading || voucherUploading || paymentMethodsLoading"
:disabled="createSubmitDisabled"
>
{{ voucherUploading ? '凭证上传中...' : createButtonLabel }}
</ElButton>
</div>
</template>
</ElDialog>
<!-- 在线充值二维码对话框 -->
<ElDialog
v-model="onlineQrDialogVisible"
title="扫码完成充值"
width="430px"
align-center
@closed="handleQrDialogClosed"
>
<div class="online-recharge-qr-dialog">
<template v-if="qrContent">
<div class="online-recharge-qr-dialog__code-surface">
<QrcodeVue
:value="qrContent"
:size="240"
level="H"
background="#FFFFFF"
foreground="#000000"
render-as="canvas"
/>
</div>
<ElTag
v-if="onlinePaymentMethodLabel"
class="online-recharge-qr-dialog__method"
type="primary"
effect="dark"
size="large"
>
{{ onlinePaymentMethodLabel }}扫码支付
</ElTag>
</template>
<ElEmpty v-else description="暂未获取到支付二维码" />
<div v-if="onlineQrRecharge" class="online-recharge-qr-dialog__summary">
<div>充值单号:{{ onlineQrRecharge.recharge_no || '-' }}</div>
<div>充值金额:{{ formatCurrency(onlineQrRecharge.amount) }}</div>
<div v-if="onlineWalletBalance !== null">
当前主钱包余额:{{ formatCurrency(onlineWalletBalance) }}
</div>
<ElTag :type="getStatusType(onlinePaymentStatus?.status || onlineQrRecharge.status)">
{{ onlinePaymentStatus?.status_name || onlineQrRecharge.status_name || '-' }}
</ElTag>
<div class="online-recharge-qr-dialog__hint">
{{ onlinePaymentStatusMessage }}
</div>
<div v-if="paymentStatusLoading" class="online-recharge-qr-dialog__loading">
正在查询支付状态...
</div>
</div>
</div>
<template #footer>
<ElButton @click="onlineQrDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
<!-- 导出任务对话框 -->
<ExportTaskCreateDialog
v-model="exportDialogVisible"
scene="agent_recharge"
:query="exportQuery"
confirm-permission="agent_recharge:export"
title="导出代理充值"
/>
<!-- 确认线下支付对话框 -->
<ElDialog
v-model="confirmPayDialogVisible"
title="确认线下充值"
width="25%"
@closed="handleConfirmPayDialogClosed"
>
<ElForm
ref="confirmPayFormRef"
:model="confirmPayForm"
:rules="confirmPayRules"
label-width="80"
>
<ElFormItem label="充值单号">
<span>{{ currentRecharge?.recharge_no }}</span>
</ElFormItem>
<ElFormItem label="充值金额">
<span>{{ formatCurrency(currentRecharge?.amount || 0) }}</span>
</ElFormItem>
<ElFormItem label="操作密码" prop="operation_password">
<ElInput
v-model="confirmPayForm.operation_password"
type="password"
placeholder="请输入超级管理员统一设置的操作密码"
show-password
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="confirmPayDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmPay" :loading="confirmPayLoading">
确认支付
</ElButton>
</div>
</template>
</ElDialog>
<!-- 拒绝充值订单对话框 -->
<ElDialog
v-model="rejectDialogVisible"
title="拒绝充值订单"
width="500px"
@closed="handleRejectDialogClosed"
>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
<ElFormItem label="充值单号">
<span>{{ currentRecharge?.recharge_no }}</span>
</ElFormItem>
<ElFormItem label="拒绝原因" prop="rejection_reason">
<ElInput
v-model="rejectForm.rejection_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="handleRejectRecharge" :loading="rejectLoading">
确认拒绝
</ElButton>
</div>
</template>
</ElDialog>
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import QrcodeVue from 'qrcode.vue'
import {
AgentRechargeService,
CommissionService,
EmployeeCollectionService,
ShopService
} from '@/api/modules'
import {
ElMessage,
ElMessageBox,
ElTag,
ElButton,
ElCascader,
ElInput,
ElInputNumber,
ElSelect,
ElOption
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
AgentRecharge,
AgentRechargeQueryParams,
AgentRechargeStatus,
AgentRechargeOnlinePaymentMethod,
AgentRechargePaymentMethod,
AgentRechargePaymentStatusResponse,
CreateAgentRechargeRequest,
ConfirmOfflinePaymentRequest,
EmployeeCollectionPaymentMethod,
RejectAgentRechargeRequest
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import {
getApprovalStatusText,
getCurrentApproverSummaryText,
getProcessingStatusText
} from '@/utils/business/approvalSummary'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { AUDIT_PERMISSIONS } from '@/config/constants/audit'
import {
resolveAuditResourceTarget,
resolveFinanceAuditTarget
} from '@/utils/business/auditNavigation'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import ExportTaskCreateDialog from '@/components/business/ExportTaskCreateDialog.vue'
import { buildAgentRechargeActions } from './agentRechargeActions'
import { formatRejectionReason } from './agentRechargeDisplay'
import { normalizeCollectionList } from '@/views/finance/employee-collection/employeeCollectionDisplay'
import {
amountYuanToFen,
createOnlineRechargeRequestId,
isOnlineRechargeTerminal
} from './agentRechargeOnline'
defineOptions({ name: 'AgentRechargeList' })
const router = useRouter()
const { hasAuth } = useAuth()
const userStore = useUserStore()
const isAgentAccount = computed(() => Number(userStore.info.user_type) === 3)
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
const isRestrictedCustomerRole = computed(() => [3, 4].includes(Number(userStore.info.user_type)))
const hiddenInternalColumnProps = new Set([
'approval_provider',
'submitter_name',
'approval_status',
'current_approver_summary',
'processing_status_name',
'remark',
'rejection_reason'
])
const shouldShowInternalColumn = (prop?: string) =>
!isRestrictedCustomerRole.value || !hiddenInternalColumnProps.has(prop || '')
const canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
const createMode = ref<'online' | 'offline'>(isAgentAccount.value ? 'online' : 'offline')
const createDialogTitle = computed(() =>
createMode.value === 'online' ? '代理钱包在线扫码充值' : '创建平台线下代充'
)
const createButtonLabel = computed(() => (isAgentAccount.value ? '立即充值' : '创建充值订单'))
const loading = ref(false)
const createLoading = ref(false)
const voucherUploading = ref(false)
const confirmPayLoading = ref(false)
const rejectLoading = ref(false)
const triggerApprovalLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const exportDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const onlineQrDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const onlineQrRecharge = ref<AgentRecharge | null>(null)
const onlinePaymentStatus = ref<AgentRechargePaymentStatusResponse | null>(null)
const onlineWalletBalance = ref<number | null>(null)
const onlinePaymentMethods = ref<AgentRechargeOnlinePaymentMethod[]>([])
const paymentMethodsLoading = ref(false)
const paymentStatusLoading = ref(false)
const qrContent = ref('')
const onlineRequestId = ref<string | null>(null)
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
const ONLINE_MIN_RECHARGE_AMOUNT_FEN = 10000
const ONLINE_MAX_RECHARGE_AMOUNT_FEN = 100000000
const paymentMethodsBounds = reactive({
min_amount: ONLINE_MIN_RECHARGE_AMOUNT_FEN,
max_amount: ONLINE_MAX_RECHARGE_AMOUNT_FEN
})
const paymentVoucherFileKeys = ref<string[]>([])
const offlinePaymentMethodOptions = ref<EmployeeCollectionPaymentMethod[]>([])
const paymentMethodOptionsLoading = ref(false)
const otherVoucherUploading = ref(false)
const ocrLoading = ref(false)
const recognizedVoucherKey = ref('')
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
status: undefined,
recharge_source: undefined,
dateRange: [],
start_date: '',
end_date: ''
}
// 搜索表单
const searchForm = reactive<AgentRechargeQueryParams>({ ...initialSearchState })
// 店铺选项
const shopOptions = ref<any[]>([])
// 店铺级联选择相关
const shopCascadeOptions = ref<any[]>([])
const shopCascadeProps = {
lazy: true,
checkStrictly: true,
lazyLoad: async (node: any, resolve: any) => {
const { level, value } = node
const parentId = level === 0 ? undefined : value
try {
const res = await ShopService.getShopsCascade({ parent_id: parentId })
if (res.code === 0) {
const nodes = (res.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
resolve(nodes)
} else {
resolve([])
}
} catch (error) {
console.error('加载店铺级联数据失败:', error)
resolve([])
}
},
value: 'value',
label: 'label',
children: 'children'
}
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => {
const items: SearchFormItem[] = []
if (isPlatformAccount.value) {
items.push({
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)
}
})
}
items.push(
{
label: '充值状态',
prop: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已支付', value: 2 },
{ label: '已完成', value: 3 },
{ label: '已关闭', value: 4 },
{ label: '已退款', value: 5 },
{ label: '已驳回', value: 6 }
],
config: { clearable: true }
},
{
label: '充值来源',
prop: 'recharge_source',
type: 'select',
placeholder: '请选择充值来源',
options: [
{ label: '代理在线自充', value: 'agent_online' },
{ label: '平台线下代充', value: 'platform_offline' }
],
config: { clearable: true }
},
{
label: '起止时间',
prop: 'dateRange',
type: 'datetimerange',
config: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
}
}
)
return items
})
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值来源', prop: 'recharge_source' },
{ label: '充值金额', prop: 'amount' },
{ label: '状态', prop: 'status' },
{ label: '审批渠道', prop: 'approval_provider' },
{ label: '提交人', prop: 'submitter_name' },
{ label: '审批状态', prop: 'approval_status' },
{ label: '当前审批人摘要', prop: 'current_approver_summary' },
{ label: '业务处理状态', prop: 'processing_status_name' },
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付通道', prop: 'payment_channel' },
{ label: '交易流水号', prop: 'external_transaction_no' },
{ label: '收款方式', prop: 'offline_payment_method_name' },
{ label: '运营备注', prop: 'remark' },
{ label: '驳回原因', prop: 'rejection_reason' },
{ label: '创建时间', prop: 'created_at' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '完成时间', prop: 'completed_at' },
{ label: '更新时间', prop: 'updated_at' }
].filter(({ prop }) => shouldShowInternalColumn(prop))
const createFormRef = ref<FormInstance>()
const confirmPayFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
const otherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const OFFLINE_MIN_RECHARGE_AMOUNT = 0.01
const OFFLINE_MAX_RECHARGE_AMOUNT = 1_000_000
const minimumAmountYuan = computed(() =>
createMode.value === 'online'
? paymentMethodsBounds.min_amount / 100
: OFFLINE_MIN_RECHARGE_AMOUNT
)
const maximumAmountYuan = computed(() =>
createMode.value === 'online'
? paymentMethodsBounds.max_amount / 100
: OFFLINE_MAX_RECHARGE_AMOUNT
)
const availablePaymentMethodOptions = computed(() => {
if (createMode.value === 'offline') return [{ label: '线下转账', value: 'offline' }]
return onlinePaymentMethods.value.map((method) => ({
label: method === 'wechat' ? '微信支付' : '支付宝',
value: method
}))
})
const createSubmitDisabled = computed(
() =>
createLoading.value ||
voucherUploading.value ||
otherVoucherUploading.value ||
ocrLoading.value ||
paymentMethodsLoading.value ||
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
)
const onlinePaymentMethodLabel = computed(() => {
const method = onlineQrRecharge.value?.payment_method
if (method === 'wechat') return '微信支付'
if (method === 'alipay') return '支付宝'
return ''
})
const onlinePaymentStatusMessage = computed(() => {
const status = onlinePaymentStatus.value?.status ?? onlineQrRecharge.value?.status
if (status === 1) return `请使用${onlinePaymentMethodLabel.value || '对应支付方式'}扫码完成支付`
if (status === 2) return '支付已成功,钱包正在入账,请稍候'
if (status === 3) return '充值成功,钱包已到账'
if (status === 4) return '充值订单已关闭,请重新发起充值'
if (status === 5) return '充值订单已退款'
if (status === 6) return '充值订单已驳回'
return '正在获取支付状态'
})
const createRules = computed<FormRules>(() => {
const rules: FormRules = {
amount: [
{ required: true, message: '请输入充值金额', trigger: 'blur' },
{
validator: (_rule, value, callback) => {
if (value == null || value === '') {
callback()
return
}
if (Number(value) < minimumAmountYuan.value) {
callback(new Error(`充值金额最小为 ¥${minimumAmountYuan.value.toFixed(2)}`))
return
}
if (Number(value) > maximumAmountYuan.value) {
callback(
new Error(
`充值金额最大为 ¥${maximumAmountYuan.value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
)
)
return
}
callback()
},
trigger: 'blur'
}
],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
}
if (createMode.value === 'offline') {
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
rules.offline_payment_method_id = [
{ required: true, message: '请选择收款方式', trigger: 'change' }
]
rules.external_transaction_no = [
{ required: true, message: '请输入交易流水号', trigger: 'blur' }
]
}
if (createMode.value === 'online') delete rules.shop_id
return rules
})
const confirmPayRules = reactive<FormRules>({
operation_password: [
{ required: true, message: '请输入超级管理员统一设置的操作密码', trigger: 'blur' }
]
})
const rejectRules = reactive<FormRules>({
rejection_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const createForm = reactive<{
amount: number | null
payment_method: AgentRechargePaymentMethod | ''
shop_id: number | null
payment_voucher_key: string[]
offline_payment_method_id: number | null
external_transaction_no: string
other_voucher_key: string[]
remark: string
}>({
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_voucher_key: [],
offline_payment_method_id: null,
external_transaction_no: '',
other_voucher_key: [],
remark: ''
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
operation_password: ''
})
const rejectForm = reactive<RejectAgentRechargeRequest>({
rejection_reason: ''
})
const rechargeList = ref<AgentRecharge[]>([])
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取状态标签类型
const getStatusType = (
status: AgentRechargeStatus
): 'warning' | 'success' | 'info' | 'danger' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info' | 'danger'> = {
1: 'warning', // 待支付
2: 'success', // 已支付
3: 'success', // 已完成
4: 'info', // 已关闭
5: 'danger', // 已退款
6: 'danger' // 已驳回
}
return statusMap[status] || 'info'
}
// 获取支付方式文本
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
alipay: '支付宝在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
const getRechargeSourceText = (row: AgentRecharge): string => {
if (row.recharge_source_name) return row.recharge_source_name
if (row.recharge_source === 'agent_online') return '代理在线自充'
if (row.recharge_source === 'platform_offline') return '平台线下代充'
return '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() =>
[
{
prop: 'recharge_no',
label: '充值单号',
minWidth: 240,
formatter: (row: AgentRecharge) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.recharge_no
)
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'recharge_source',
label: '充值来源',
width: 140,
formatter: (row: AgentRecharge) => getRechargeSourceText(row)
},
{
prop: 'amount',
label: '充值金额',
width: 120,
formatter: (row: AgentRecharge) => formatCurrency(row.amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: AgentRecharge) => {
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || '-')
}
},
{
prop: 'approval_provider',
label: '审批渠道',
width: 110,
formatter: (row: AgentRecharge) => {
if (row.approval_provider === 'wecom' || row.approval_source === 'wecom') return '企微'
if (row.approval_source === 'legacy') return '历史审批'
return row.approval_provider || row.approval_source || '-'
}
},
{
prop: 'submitter_name',
label: '提交人',
width: 120,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.submitter_name || '-'
},
{
prop: 'approval_status',
label: '审批状态',
width: 120,
formatter: (row: AgentRecharge) => getApprovalStatusText(row)
},
{
prop: 'current_approver_summary',
label: '当前审批人摘要',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getCurrentApproverSummaryText(row)
},
{
prop: 'processing_status_name',
label: '业务处理状态',
width: 140,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => getProcessingStatusText(row)
},
{
prop: 'payment_method',
label: '支付方式',
width: 140,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
prop: 'payment_channel',
label: '支付通道',
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
prop: 'external_transaction_no',
label: '交易流水号',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.external_transaction_no || '-'
},
{
prop: 'offline_payment_method_name',
label: '收款方式',
width: 140,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) =>
row.payment_method === 'offline' ? row.offline_payment_method_name || '-' : '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.remark || '-'
},
{
prop: 'rejection_reason',
label: '驳回原因',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => formatRejectionReason(row.rejection_reason)
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.created_at)
},
{
prop: 'paid_at',
label: '支付时间',
width: 180,
formatter: (row: AgentRecharge) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'completed_at',
label: '完成时间',
width: 180,
formatter: (row: AgentRecharge) =>
row.completed_at ? formatDateTime(row.completed_at) : '-'
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.updated_at)
}
].filter(({ prop }) => shouldShowInternalColumn(prop))
)
onMounted(() => {
getTableData()
if (isPlatformAccount.value) loadShops()
document.addEventListener('visibilitychange', handleDocumentVisibilityChange)
})
onBeforeUnmount(() => {
stopPaymentStatusPolling()
document.removeEventListener('visibilitychange', handleDocumentVisibilityChange)
})
onDeactivated(() => {
stopPaymentStatusPolling()
})
let isFirstActivation = true
onActivated(() => {
if (!isFirstActivation) {
getTableData()
}
if (onlineQrDialogVisible.value && document.visibilityState === 'visible') {
startPaymentStatusPolling()
}
isFirstActivation = false
})
// 加载店铺列表 - 改用级联查询
const loadShops = async () => {
try {
// 加载顶级店铺用于级联选择
const res2 = await ShopService.getShopsCascade({ parent_id: undefined })
if (res2.code === 0) {
shopCascadeOptions.value = (res2.data || []).map((item: any) => ({
value: item.id,
label: item.shop_name,
leaf: !item.has_children
}))
}
} catch (error) {
console.error('Load shops failed:', error)
}
}
// 搜索店铺(用于搜索表单远程搜索)
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 handleShopChange = (value: any) => {
if (Array.isArray(value) && value.length > 0) {
createForm.shop_id = value[value.length - 1]
} else {
createForm.shop_id = null
}
}
// 获取充值订单列表
const getTableData = async () => {
if (!canViewRecharge.value) {
rechargeList.value = []
pagination.total = 0
return
}
loading.value = true
try {
const params: AgentRechargeQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
status: searchForm.status,
recharge_source: searchForm.recharge_source,
start_date: searchForm.start_date || undefined,
end_date: searchForm.end_date || undefined
}
const res = await AgentRechargeService.getAgentRecharges(params)
if (res.code === 0) {
rechargeList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.start_date = searchForm.dateRange[0]
searchForm.end_date = searchForm.dateRange[1]
} else {
searchForm.start_date = ''
searchForm.end_date = ''
}
pagination.page = 1
getTableData()
}
const exportQuery = computed(() => ({
shop_id: isPlatformAccount.value ? searchForm.shop_id : undefined,
status: searchForm.status,
recharge_source: searchForm.recharge_source,
start_date: searchForm.start_date || searchForm.dateRange?.[0],
end_date: searchForm.end_date || searchForm.dateRange?.[1]
}))
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建订单对话框
const showCreateDialog = async () => {
createMode.value = isAgentAccount.value ? 'online' : 'offline'
resetCreateForm()
if (createMode.value === 'online') {
await loadPaymentMethods()
} else {
// 重新加载店铺列表与收款方式字典,确保获取最新数据
await loadShops()
await loadPaymentMethodOptions()
}
createDialogVisible.value = true
}
const resetCreateForm = () => {
createForm.amount = minimumAmountYuan.value
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = []
createForm.offline_payment_method_id = null
createForm.external_transaction_no = ''
createForm.other_voucher_key = []
createForm.remark = ''
onlineRequestId.value = null
voucherUploading.value = false
otherVoucherUploading.value = false
ocrLoading.value = false
uploadRef.value?.clearFiles(false)
otherUploadRef.value?.clearFiles(false)
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
resetCreateForm()
}
// 加载代理在线充值可用支付方式(超管调用该接口返回 403仅通过系统配置查看允许范围
const loadPaymentMethods = async () => {
paymentMethodsLoading.value = true
onlinePaymentMethods.value = []
try {
const res = await AgentRechargeService.getSelfRechargePaymentMethods()
if (res.code === 0) {
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
paymentMethodsBounds.min_amount = ONLINE_MIN_RECHARGE_AMOUNT_FEN
paymentMethodsBounds.max_amount =
Number(res.data?.max_amount) || ONLINE_MAX_RECHARGE_AMOUNT_FEN
createForm.amount = minimumAmountYuan.value
} else {
ElMessage.warning(res.msg || '当前暂无可用的在线支付方式')
}
} catch (error) {
console.error('加载在线支付方式失败:', error)
ElMessage.warning('当前暂无可用的在线支付方式')
} finally {
paymentMethodsLoading.value = false
}
}
// 加载线下收款方式字典启用项
const loadPaymentMethodOptions = async () => {
paymentMethodOptionsLoading.value = true
offlinePaymentMethodOptions.value = []
try {
const res = await EmployeeCollectionService.getPaymentMethods({
page: 1,
page_size: 100,
enabled: true
})
if (res.code === 0) {
offlinePaymentMethodOptions.value =
normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
}
} catch (error) {
console.error('加载线下收款方式失败:', error)
} finally {
paymentMethodOptionsLoading.value = false
}
}
// 已上传的支付凭证对象键OCR 识别取第一个)
const offlineVoucherKeys = computed(() => toVoucherKeyList(createForm.payment_voucher_key))
// 凭证变更:校验字段,并对首次上传自动识别一次(已有流水号则跳过)
const handleVoucherChange = (keys: string[] | string) => {
createFormRef.value?.validateField('payment_voucher_key')
const list = toVoucherKeyList(keys)
const firstKey = list[0] || ''
if (!firstKey) {
recognizedVoucherKey.value = ''
return
}
if (firstKey === recognizedVoucherKey.value) return
recognizedVoucherKey.value = firstKey
if (createForm.external_transaction_no?.trim()) return
void handleRecognizeVoucher()
}
// 识别付款凭证中的交易流水号:只做预填,失败不阻断人工填写
const handleRecognizeVoucher = async () => {
if (ocrLoading.value) return
const voucherKeys = offlineVoucherKeys.value
if (!voucherKeys.length) {
ElMessage.warning('请先上传支付凭证')
return
}
ocrLoading.value = true
try {
const res = await AgentRechargeService.recognizePaymentVoucher({
payment_voucher_key: voucherKeys[0]
})
if (res.code !== 0) {
ElMessage.warning(res.msg || '识别失败,请手动填写交易流水号')
return
}
const transactionNo = res.data?.external_transaction_no
if (!transactionNo) {
ElMessage.warning('未识别到交易流水号,请手动填写')
return
}
createForm.external_transaction_no = transactionNo
createFormRef.value?.validateField('external_transaction_no')
ElMessage.success('已识别交易流水号,请对照凭证核对')
} catch (error) {
console.error('识别付款凭证失败:', error)
ElMessage.warning('识别失败,请手动填写交易流水号')
} finally {
ocrLoading.value = false
}
}
// 创建充值订单
const handleCreateRecharge = async () => {
if (createLoading.value) return
const formRef = createFormRef.value
if (!formRef) return
if (voucherUploading.value) {
ElMessage.warning('支付凭证上传中,请稍候')
return
}
if (otherVoucherUploading.value) {
ElMessage.warning('其他凭证上传中,请稍候')
return
}
createLoading.value = true
try {
const valid = await formRef.validate().catch(() => false)
if (!valid || createForm.amount == null || !createForm.payment_method) return
if (createMode.value === 'online') {
await handleOnlineRechargeCreate(createForm.amount, createForm.payment_method)
return
}
if (!createForm.shop_id) {
ElMessage.warning('请选择目标店铺')
return
}
if (!createForm.offline_payment_method_id) {
ElMessage.warning('请选择收款方式')
return
}
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
if (!hasVoucherKeys(voucherKeys)) {
ElMessage.warning('请上传支付凭证')
return
}
const otherVoucherKeys = toVoucherKeyList(createForm.other_voucher_key)
const data: CreateAgentRechargeRequest = {
amount: amountYuanToFen(createForm.amount),
payment_method: 'offline',
shop_id: createForm.shop_id,
offline_payment_method_id: createForm.offline_payment_method_id,
external_transaction_no: createForm.external_transaction_no.trim(),
payment_voucher_key: voucherKeys,
other_voucher_key: otherVoucherKeys.length ? otherVoucherKeys : undefined,
remark: createForm.remark || undefined
}
const res = await AgentRechargeService.createAgentRecharge(data)
if (res.code !== 0) {
ElMessage.error(res.msg || '充值订单创建失败')
return
}
ElMessage.success('充值订单创建成功')
createDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
const handleOnlineRechargeCreate = async (
amountYuan: number,
paymentMethod: AgentRechargePaymentMethod
) => {
if (paymentMethod === 'offline') return
const requestId = onlineRequestId.value || createOnlineRechargeRequestId()
onlineRequestId.value = requestId
const request: CreateAgentRechargeRequest = {
amount: amountYuanToFen(amountYuan),
payment_method: paymentMethod,
request_id: requestId
}
try {
const res = await AgentRechargeService.createAgentRecharge(request)
if (res.code !== 0) {
ElMessage.error(res.msg || '在线充值创建失败')
onlineRequestId.value = null
return
}
if (!res.data?.qr_content) {
ElMessage.error('支付接口未返回有效二维码,请刷新后重试')
return
}
onlineQrRecharge.value = res.data
onlinePaymentStatus.value = null
qrContent.value = res.data.qr_content
onlineQrDialogVisible.value = true
createDialogVisible.value = false
onlineRequestId.value = null
await getTableData()
} catch {
onlineRequestId.value = null
}
}
const stopPaymentStatusPolling = () => {
if (paymentStatusTimer.value) {
clearInterval(paymentStatusTimer.value)
paymentStatusTimer.value = null
}
}
const loadOnlinePaymentStatus = async () => {
const rechargeId = onlineQrRecharge.value?.id
if (!rechargeId || !onlineQrDialogVisible.value || document.visibilityState !== 'visible')
return
paymentStatusLoading.value = true
try {
const res = await AgentRechargeService.getPaymentStatus(rechargeId)
if (res.code !== 0) return
onlinePaymentStatus.value = res.data
if (onlineQrRecharge.value) {
onlineQrRecharge.value = {
...onlineQrRecharge.value,
status: res.data.status,
status_name: res.data.status_name || onlineQrRecharge.value.status_name,
paid_at: res.data.paid_at,
completed_at: res.data.completed_at
}
}
if (isOnlineRechargeTerminal(res.data.status)) {
stopPaymentStatusPolling()
if (res.data.status === 3) {
await Promise.all([getTableData(), refreshAgentWalletBalance()])
}
}
} catch (error) {
console.error('查询在线充值状态失败:', error)
} finally {
paymentStatusLoading.value = false
}
}
const startPaymentStatusPolling = () => {
stopPaymentStatusPolling()
void loadOnlinePaymentStatus()
paymentStatusTimer.value = setInterval(() => {
if (!onlineQrDialogVisible.value || document.visibilityState !== 'visible') {
stopPaymentStatusPolling()
return
}
void loadOnlinePaymentStatus()
}, 3000)
}
const handleDocumentVisibilityChange = () => {
if (document.visibilityState === 'visible' && onlineQrDialogVisible.value) {
startPaymentStatusPolling()
} else if (document.visibilityState === 'hidden') {
stopPaymentStatusPolling()
}
}
watch(onlineQrDialogVisible, (visible) => {
if (visible) {
startPaymentStatusPolling()
} else {
stopPaymentStatusPolling()
}
})
const handleQrDialogClosed = () => {
stopPaymentStatusPolling()
qrContent.value = ''
onlineQrRecharge.value = null
onlinePaymentStatus.value = null
onlineWalletBalance.value = null
paymentStatusLoading.value = false
}
const refreshAgentWalletBalance = async () => {
if (!isAgentAccount.value) return
try {
const res = await CommissionService.getShopFundSummary({ page: 1, page_size: 100 })
const currentShopId = Number(userStore.info.shop_id)
const currentShop =
res.code === 0 ? res.data.items?.find((item) => item.shop_id === currentShopId) : null
if (currentShop) onlineWalletBalance.value = currentShop.main_balance
} catch (error) {
console.error('刷新代理钱包余额失败:', error)
}
}
// 显示确认支付对话框
const handleShowConfirmPay = (row: AgentRecharge) => {
currentRecharge.value = row
confirmPayDialogVisible.value = true
}
// 确认支付对话框关闭后的清理
const handleConfirmPayDialogClosed = () => {
confirmPayFormRef.value?.resetFields()
confirmPayForm.operation_password = ''
currentRecharge.value = null
}
// 确认线下支付
const handleConfirmPay = async () => {
const formRef = confirmPayFormRef.value
const recharge = currentRecharge.value
if (!formRef || !recharge) return
await formRef.validate(async (valid) => {
if (valid) {
confirmPayLoading.value = true
try {
await AgentRechargeService.confirmOfflinePayment(recharge.id, {
operation_password: confirmPayForm.operation_password
})
ElMessage.success('确认支付成功')
confirmPayDialogVisible.value = false
formRef.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
confirmPayLoading.value = false
}
}
})
}
// 显示拒绝对话框
const handleShowReject = (row: AgentRecharge) => {
currentRecharge.value = row
rejectDialogVisible.value = true
}
// 拒绝对话框关闭后的清理
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.rejection_reason = ''
currentRecharge.value = null
}
// 拒绝代理充值订单
const handleRejectRecharge = async () => {
const formRef = rejectFormRef.value
const recharge = currentRecharge.value
if (!formRef || !recharge) return
await formRef.validate(async (valid) => {
if (valid) {
rejectLoading.value = true
try {
await AgentRechargeService.rejectAgentRecharge(recharge.id, {
rejection_reason: rejectForm.rejection_reason
})
ElMessage.success('拒绝成功')
rejectDialogVisible.value = false
await getTableData()
} catch (error) {
console.error(error)
} finally {
rejectLoading.value = false
}
}
})
}
// 补发历史线下代理充值审批
const handleTriggerApproval = (row: AgentRecharge) => {
if (triggerApprovalLoading.value) return
ElMessageBox.confirm(`确定要为充值单号 ${row.recharge_no} 补发企微审批吗?`, '补发审批', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
triggerApprovalLoading.value = true
try {
const res = await AgentRechargeService.triggerApproval(row.id)
if (res.code !== 0) {
ElMessage.error(res.msg || '补发审批失败')
return
}
ElMessage.success('补发审批成功')
await getTableData()
} catch (error) {
console.error(error)
ElMessage.error('补发审批失败')
} finally {
triggerApprovalLoading.value = false
}
})
.catch(() => {
// 用户取消
})
}
// 处理名称点击
const handleNameClick = (row: AgentRecharge) => {
if (hasAuth('agent_recharge:detail_page')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 查看详情
const handleViewDetail = (row: AgentRecharge) => {
router.push({
name: 'AgentRechargeDetailRoute',
params: { id: row.id }
})
}
// 获取操作按钮
const getActions = (row: AgentRecharge) => {
const actions = buildAgentRechargeActions(row, {
hasAuth,
onViewPaymentVoucher: handleViewPaymentVoucher,
onConfirmPayment: handleShowConfirmPay,
onReject: handleShowReject,
onTriggerApproval: handleTriggerApproval
})
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeEntry)) {
const resourceTarget = resolveAuditResourceTarget({
resourceType: 'agent_recharge',
internalId: row.id
})
if (resourceTarget)
actions.unshift({
label: '审计记录',
handler: () => openAuditInvestigation(resourceTarget),
type: 'primary'
})
}
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeFinanceEntry)) {
const financeTarget = resolveFinanceAuditTarget('recharge_id', row.id)
if (financeTarget)
actions.unshift({
label: '资金链路',
handler: () => openAuditInvestigation(financeTarget),
type: 'primary'
})
}
if (isPlatformAccount.value && hasAuth(AUDIT_PERMISSIONS.agentRechargeApprovalEntry)) {
const approvalTarget = resolveAuditResourceTarget({
resourceType: 'approval_instance',
internalId: row.approval_instance_id
})
if (approvalTarget)
actions.unshift({
label: '审批审计',
handler: () => openAuditInvestigation(approvalTarget),
type: 'primary'
})
}
const paymentVoucherIndex = actions.findIndex((action) => action.label === '支付凭证')
if (paymentVoucherIndex > 0) {
const [paymentVoucherAction] = actions.splice(paymentVoucherIndex, 1)
actions.unshift(paymentVoucherAction)
}
return actions
}
// 查看支付凭证
const handleViewPaymentVoucher = (row: AgentRecharge) => {
if (!hasVoucherKeys(row.payment_voucher_key)) return
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
}
</script>
<style scoped lang="scss">
.agent-recharge-page {
height: 100%;
}
.online-recharge-empty-hint {
margin-top: 8px;
font-size: 12px;
line-height: 1.5;
color: var(--el-color-danger);
}
.external-transaction-row {
display: flex;
gap: 8px;
width: 100%;
}
.external-transaction-tip {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.online-recharge-qr-dialog {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
&__code-surface {
display: flex;
padding: 20px;
background: #fff;
border-radius: 8px;
}
&__method {
font-weight: 600;
}
&__summary {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
width: 100%;
font-size: 14px;
color: var(--el-text-color-regular);
text-align: center;
}
&__hint {
color: var(--el-text-color-secondary);
}
&__loading {
font-size: 12px;
color: var(--el-color-primary);
}
}
</style>