Files
one-pipe-system/src/views/finance/agent-recharge/index.vue
sexygoat 14832f5c6f
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m30s
fix: 拖拽上传
2026-05-18 13:11:40 +08:00

888 lines
25 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"
label-width="100"
@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')"
>创建充值订单</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="创建充值订单"
width="500px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="充值金额" prop="amount">
<ElInputNumber
v-model="createForm.amount"
:min="0.01"
:precision="2"
:step="0.01"
style="width: 100%"
placeholder="请输入充值金额(元)"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
最小: ¥0.01最大: 不限制
</div>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
>
<ElOption label="线下转账" value="offline" />
</ElSelect>
</ElFormItem>
<ElFormItem 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"
>
<ElUpload
ref="uploadRef"
class="payment-voucher-upload"
drag
:auto-upload="false"
:on-change="handleVoucherFileChange"
:on-remove="handleRemoveVoucher"
accept="image/*"
list-type="picture"
:limit="1"
>
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
<div class="payment-voucher-upload__text">
将支付凭证拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div>
</template>
</ElUpload>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleCreateRecharge" :loading="createLoading">
确认创建
</ElButton>
</div>
</template>
</ElDialog>
<!-- 确认线下支付对话框 -->
<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>
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-key="paymentVoucherFileKey"
@close="paymentVoucherFileKey = ''"
/>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AgentRechargeService, ShopService, StorageService } from '@/api/modules'
import {
ElMessage,
ElTag,
ElTreeSelect,
ElButton,
ElCascader,
ElInputNumber,
ElSelect,
ElOption,
ElUpload
} from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type {
AgentRecharge,
AgentRechargeQueryParams,
AgentRechargeStatus,
AgentRechargePaymentMethod,
CreateAgentRechargeRequest,
ConfirmOfflinePaymentRequest,
ShopResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { useAuth } from '@/composables/useAuth'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'AgentRechargeList' })
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const loading = ref(false)
const createLoading = ref(false)
const confirmPayLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const paymentVoucherFileKey = ref('')
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
status: undefined,
dateRange: [],
start_date: '',
end_date: ''
}
// 搜索表单
const searchForm = reactive<AgentRechargeQueryParams>({ ...initialSearchState })
// 店铺选项
const shopOptions = ref<any[]>([])
const shopTreeData = ref<ShopResponse[]>([])
// 店铺级联选择相关
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: 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: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已取消', value: 3 }
],
config: {
clearable: true
}
},
{
label: '开始至结束',
prop: 'dateRange',
type: 'date',
config: {
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值金额', prop: 'amount' },
{ label: '状态', prop: 'status' },
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付通道', prop: 'payment_channel' },
{ label: '创建时间', prop: 'created_at' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '完成时间', prop: 'completed_at' }
]
const createFormRef = ref<FormInstance>()
const confirmPayFormRef = ref<FormInstance>()
const uploadRef = ref()
const MIN_RECHARGE_AMOUNT = 0.01
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) < MIN_RECHARGE_AMOUNT) {
callback(new Error(`充值金额最小为 ¥${MIN_RECHARGE_AMOUNT.toFixed(2)}`))
return
}
callback()
},
trigger: 'blur'
}
],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
}
if (createForm.payment_method === 'offline') {
rules.payment_voucher_key = [{ required: true, message: '请上传支付凭证', trigger: 'change' }]
}
return rules
})
const confirmPayRules = reactive<FormRules>({
operation_password: [
{ required: true, message: '请输入超级管理员统一设置的操作密码', trigger: 'blur' }
]
})
const createForm = reactive<{
amount: number
payment_method: string
shop_id: number | null
payment_voucher_key?: string
}>({
amount: MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_voucher_key: undefined
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
operation_password: ''
})
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' // 已退款
}
return statusMap[status] || 'info'
}
// 获取状态文本(优先使用 status_name否则使用本地映射
const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => {
if (statusName) return statusName
const statusMap: Record<AgentRechargeStatus, string> = {
1: '待支付',
2: '已支付',
3: '已完成',
4: '已关闭',
5: '已退款'
}
return statusMap[status] || '-'
}
// 获取支付方式文本
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
// 动态列配置
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: '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) }, () =>
getStatusText(row.status, row.status_name)
)
}
},
{
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: '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) : '-')
}
])
onMounted(() => {
getTableData()
loadShops()
})
let isFirstActivation = true
onActivated(() => {
if (!isFirstActivation) {
getTableData()
}
isFirstActivation = false
})
// 构建树形数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
// 加载店铺列表 - 改用级联查询
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 () => {
loading.value = true
try {
const params: AgentRechargeQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id,
status: searchForm.status,
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 handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建订单对话框
const showCreateDialog = async () => {
// 重新加载店铺列表,确保获取最新数据
await loadShops()
createForm.payment_method = ''
createDialogVisible.value = true
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
createForm.amount = MIN_RECHARGE_AMOUNT
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = undefined
uploadRef.value?.clearFiles()
}
// 支付凭证文件变化
const handleVoucherFileChange = async (file: any) => {
const isImage = file.raw.type.startsWith('image/')
const isLt5M = file.raw.size / 1024 / 1024 < 5
if (!isImage) {
ElMessage.error('只能上传图片文件')
uploadRef.value?.clearFiles()
return
}
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB')
uploadRef.value?.clearFiles()
return
}
try {
const uploadUrlRes = await StorageService.getUploadUrl({
file_name: file.name,
content_type: file.raw.type,
purpose: 'attachment'
})
if (uploadUrlRes.code !== 0) {
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
uploadRef.value?.clearFiles()
return
}
const { upload_url, file_key } = uploadUrlRes.data
await StorageService.uploadFile(upload_url, file.raw, file.raw.type)
createForm.payment_voucher_key = file_key
ElMessage.success('上传成功')
} catch (error: any) {
console.error('上传失败:', error)
createForm.payment_voucher_key = undefined
uploadRef.value?.clearFiles()
}
}
// 删除支付凭证
const handleRemoveVoucher = () => {
createForm.payment_voucher_key = undefined
}
// 创建充值订单
const handleCreateRecharge = async () => {
const formRef = createFormRef.value
if (!formRef) return
await formRef.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
const data: CreateAgentRechargeRequest = {
amount: Math.round(createForm.amount * 100), // 元转分
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
shop_id: createForm.shop_id!
}
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
}
await AgentRechargeService.createAgentRecharge(data)
ElMessage.success('充值订单创建成功')
createDialogVisible.value = false
formRef.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
})
}
// 显示确认支付对话框
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 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: any[] = []
// 线下支付且有凭证可以查看
if (
row.payment_method === 'offline' &&
row.payment_voucher_key &&
hasAuth('agent_recharge:view_payment_voucher')
) {
actions.push({
label: '查看支付凭证',
handler: () => handleViewPaymentVoucher(row),
type: 'primary'
})
}
// 待支付且线下转账的订单可以确认支付
if (
row.status === 1 &&
row.payment_method === 'offline' &&
hasAuth('agent_recharge:confirm_payment')
) {
actions.push({
label: '确认支付',
handler: () => handleShowConfirmPay(row),
type: 'primary'
})
}
return actions
}
// 查看支付凭证
const handleViewPaymentVoucher = (row: AgentRecharge) => {
if (!row.payment_voucher_key) return
paymentVoucherFileKey.value = row.payment_voucher_key
}
</script>
<style scoped lang="scss">
.agent-recharge-page {
height: 100%;
}
.payment-voucher-upload {
width: 100%;
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
width: 100%;
padding: 24px 16px;
border-radius: 8px;
}
&__icon {
margin-bottom: 12px;
font-size: 28px;
color: var(--el-color-primary);
}
&__text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
color: var(--el-color-primary);
font-style: normal;
}
}
}
</style>