Files
one-pipe-system/src/views/finance/agent-recharge/index.vue
luo 17d2eeebc5
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 6m55s
fix: 回调配置, 调整信用位置, 套餐
2026-07-29 18:20:42 +08:00

1362 lines
42 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="false"
@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="2"
:actionsWidth="200"
@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="500px"
@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="支付凭证"
@uploading-change="voucherUploading = $event"
@change="createFormRef?.validateField('payment_voucher_key')"
/>
</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">
<QrcodeVue
v-if="qrContent"
:value="qrContent"
:size="240"
level="H"
render-as="canvas"
/>
<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, ShopService } from '@/api/modules'
import {
ElMessage,
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,
RejectAgentRechargeRequest
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { normalizeApiError } from '@/utils/business/apiError'
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 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 {
amountYuanToFen,
createOnlineRechargeRequestId,
isOnlineRechargeTerminal,
shouldRetryOnlineCreate
} 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 canViewRecharge = computed(() => [1, 2, 3].includes(Number(userStore.info.user_type)))
const canCreateRecharge = computed(() => isAgentAccount.value || isPlatformAccount.value)
const createMode = ref<'online' | 'offline'>('offline')
const createDialogTitle = computed(() =>
createMode.value === 'online' ? '代理钱包在线扫码充值' : '创建平台线下代充'
)
const createButtonLabel = computed(() =>
createMode.value === 'online' ? '立即充值' : '创建充值订单'
)
const loading = ref(false)
const createLoading = ref(false)
const voucherUploading = ref(false)
const confirmPayLoading = ref(false)
const rejectLoading = 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 onlineCreateRetried = ref(false)
const paymentStatusTimer = ref<ReturnType<typeof setInterval> | null>(null)
const paymentMethodsBounds = reactive({
min_amount: 10000,
max_amount: 100000000
})
const paymentVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
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: 'date',
config: {
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
)
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: 'remark' },
{ label: '驳回原因', prop: 'rejection_reason' },
{ label: '创建时间', prop: 'created_at' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '完成时间', prop: 'completed_at' },
{ label: '更新时间', prop: 'updated_at' }
]
const createFormRef = ref<FormInstance>()
const confirmPayFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const uploadRef = 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(
() =>
voucherUploading.value ||
paymentMethodsLoading.value ||
(createMode.value === 'online' && onlinePaymentMethods.value.length === 0)
)
const onlinePaymentStatusMessage = computed(() => {
const status = onlinePaymentStatus.value?.status ?? onlineQrRecharge.value?.status
if (status === 1) return '请使用对应支付方式扫码完成支付'
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' }]
}
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[]
remark: string
}>({
amount: OFFLINE_MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_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: 120,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
prop: 'payment_channel',
label: '支付通道',
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
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)
}
])
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()
}
createDialogVisible.value = true
}
const resetCreateForm = () => {
createForm.amount = minimumAmountYuan.value
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = []
createForm.remark = ''
onlineRequestId.value = null
onlineCreateRetried.value = false
voucherUploading.value = false
uploadRef.value?.clearFiles(false)
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
resetCreateForm()
}
// 加载代理在线充值可用支付方式
const loadPaymentMethods = async () => {
paymentMethodsLoading.value = true
onlinePaymentMethods.value = []
try {
const res = await AgentRechargeService.getPaymentMethods()
if (res.code === 0) {
onlinePaymentMethods.value = Array.isArray(res.data?.methods) ? res.data.methods : []
paymentMethodsBounds.min_amount = Number(res.data?.min_amount) || 10000
paymentMethodsBounds.max_amount = Number(res.data?.max_amount) || 100000000
createForm.amount = minimumAmountYuan.value
} else {
ElMessage.warning(res.msg || '当前暂无可用在线支付方式')
}
} catch (error) {
console.error('加载在线支付方式失败:', error)
} finally {
paymentMethodsLoading.value = false
}
}
// 创建充值订单
const handleCreateRecharge = async () => {
const formRef = createFormRef.value
if (!formRef) return
if (voucherUploading.value) {
ElMessage.warning('支付凭证上传中,请稍候')
return
}
const valid = await formRef.validate().catch(() => false)
if (!valid || createForm.amount == null || !createForm.payment_method) return
createLoading.value = true
try {
if (createMode.value === 'online') {
await handleOnlineRechargeCreate(createForm.amount, createForm.payment_method)
return
}
if (!createForm.shop_id) {
ElMessage.warning('请选择目标店铺')
return
}
const voucherKeys = toVoucherKeyList(createForm.payment_voucher_key)
if (!hasVoucherKeys(voucherKeys)) {
ElMessage.warning('请上传支付凭证')
return
}
const data: CreateAgentRechargeRequest = {
amount: amountYuanToFen(createForm.amount),
payment_method: 'offline',
shop_id: createForm.shop_id,
payment_voucher_key: voucherKeys,
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
onlineCreateRetried.value = false
const request: CreateAgentRechargeRequest = {
amount: amountYuanToFen(amountYuan),
payment_method: paymentMethod,
request_id: requestId
}
let attempt = 0
while (attempt < 2) {
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()
return
} catch (error) {
if (attempt === 0 && shouldRetryOnlineCreate(error)) {
attempt += 1
onlineCreateRetried.value = true
continue
}
const normalized = normalizeApiError(error)
if (normalized.kind === 'validation' || normalized.kind === 'conflict') {
ElMessage.error(normalized.message)
} else {
ElMessage.warning('在线充值请求结果未知,请勿重复提交;可关闭后刷新列表确认')
}
onlineRequestId.value = null
return
}
}
}
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 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) => {
return buildAgentRechargeActions(row, {
hasAuth,
onViewPaymentVoucher: handleViewPaymentVoucher,
onConfirmPayment: handleShowConfirmPay,
onReject: handleShowReject
})
}
// 查看支付凭证
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;
color: var(--el-color-danger);
font-size: 12px;
line-height: 1.5;
}
.online-recharge-qr-dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
&__summary {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
color: var(--el-text-color-regular);
font-size: 14px;
text-align: center;
}
&__hint {
color: var(--el-text-color-secondary);
}
&__loading {
color: var(--el-color-primary);
font-size: 12px;
}
}
</style>