This commit is contained in:
2026-06-12 16:30:06 +08:00
parent 7b3f40069f
commit 5e4329dc5b
8 changed files with 209 additions and 85 deletions

View File

@ -63,8 +63,13 @@
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<!-- 勾选列 -->
<el-table-column v-if="showSelection" type="selection" width="55" align="center" />
<!-- 勾选列/单选列 -->
<el-table-column v-if="showSelection && !singleSelect" type="selection" width="55" align="center" />
<el-table-column v-if="showSelection && singleSelect" width="55" align="center" label="">
<template #default="{ row }">
<el-radio v-model="selectedRadioRow" :label="getRowKeyValue(row)" @change="handleRadioChange(row)">&nbsp;</el-radio>
</template>
</el-table-column>
<!-- 动态表头列 -->
<template v-for="(column, index) in finalColumns" :key="index">
@ -139,6 +144,7 @@
:label="column.label"
:width="column.width"
:align="column.align || 'left'"
:fixed="column.fixed"
>
<template #default="{ row }">
<slot :name="column.slotName" :row="row" />
@ -180,7 +186,7 @@ interface TableColumn {
isNumber?: boolean
options?: { label: string; value: string | number }[]
visible?: boolean
fixed?: boolean
fixed?: boolean | 'left' | 'right'
sortOrder?: number
//
filters?: { text: string; value: any }[]
@ -201,6 +207,7 @@ interface Props {
highlightCurrentRow?: boolean
loading?: boolean
showSelection?: boolean
singleSelect?: boolean //
showColumnSetting?: boolean
columnSettingStorageKey?: string
enableClientFilter?: boolean //
@ -211,11 +218,25 @@ const props = withDefaults(defineProps<Props>(), {
highlightCurrentRow: true,
loading: false,
showSelection: false,
singleSelect: false,
showColumnSetting: true, //
columnSettingStorageKey: '',
enableClientFilter: true, //
})
//
const selectedRadioRow = ref<any>(null)
// safe
const getRowKeyValue = (row: any) => {
return props.rowKey ? row[props.rowKey] : row.id
}
const handleRadioChange = (row: any) => {
selectedRadioRow.value = getRowKeyValue(row)
emit('selection-change', [row])
}
//
const filteredTableData = computed(() => {
if (!props.enableClientFilter || Object.keys(filterStates).length === 0) {
@ -293,10 +314,18 @@ const initColumns = () => {
//
localColumns.value = cachedConfig.map((cached: any) => {
// prop
const originalCol = props.columns?.find((c) => c.prop === cached.prop)
if (originalCol) {
return { ...originalCol, ...cached }
}
// prop slotName
if (cached.slotName) {
const slotCol = props.columns?.find((c) => c.slotName === cached.slotName)
if (slotCol) {
return { ...slotCol, ...cached }
}
}
return null
}).filter(Boolean)
} catch {
@ -375,6 +404,26 @@ onMounted(() => {
}, 100)
})
// tableData
watch(
() => props.tableData,
() => {
if (props.singleSelect) {
selectedRadioRow.value = null
}
}
)
// tableData
watch(
() => props.tableData,
() => {
if (props.singleSelect) {
selectedRadioRow.value = null
}
}
)
//
const handleSelectionChange = (rows: any[]) => {
emit('selection-change', rows)
@ -409,6 +458,15 @@ const handleRowContextmenu = (row: any, column: any, event: MouseEvent) => {
emit('row-contextmenu', row, column, event)
}
// tableData
watch(
() => props.tableData,
() => {
if (props.singleSelect) {
selectedRadioRow.value = null
}
}
)
// tableData
watch(
() => props.tableData,

View File

@ -101,9 +101,11 @@
<Loading />
</el-icon>
</div>
<div v-show="!qrcodeLoading">
<img v-if="qrcodeImageUrl" :src="qrcodeImageUrl" alt="支付二维码" class="qrcode-image" />
<div v-else ref="qrcodeRef" class="qrcode"></div>
<div v-show="!qrcodeLoading" class="qrcode-list">
<div v-for="(url, index) in paymentUrls" :key="index" class="qrcode-item">
<div class="qrcode-label">支付链接 {{ index + 1 }}</div>
<img :src="qrcodeImageUrls[index]" alt="支付二维码" class="qrcode-image" />
</div>
</div>
</div>
<div class="qrcode-tip">请使用{{ getMethodName(selectedMethod) }}扫描上方二维码完成支付</div>
@ -277,6 +279,12 @@ const qrcodeLoading = ref(false)
// URL
const qrcodeImageUrl = ref('')
// URL
const qrcodeImageUrls = ref<string[]>([])
//
const paymentUrls = ref<string[]>([])
//
const generateQRCode = async (url: string) => {
await nextTick()
@ -296,6 +304,23 @@ const generateQRCode = async (url: string) => {
}
}
//
const generateQRCodes = async (urls: string[]) => {
//
for (let i = 0; i < urls.length; i++) {
try {
const dataUrl = await QRCode.toDataURL(urls[i] as any, {
width: 200,
margin: 2,
})
qrcodeImageUrls.value[i] = dataUrl as any
} catch (error) {
console.error('生成二维码失败:', error)
}
}
qrcodeLoading.value = false
}
//
const dialogVisible = ref(false)
const rechangeFormRef = ref<FormInstance>()
@ -458,10 +483,13 @@ const handleCreateOrder = async () => {
})
// URL
const payUrl = res.payment_urls[1]
if (payUrl) {
const payUrls = res.payment_urls || []
if (payUrls.length > 0) {
paymentUrls.value = payUrls
if (isMobileDevice.value) {
//
// safe.paylinktrust.com
const safeUrl = payUrls.find((url:string)=> url.includes('safe.paylinktrust.com'))
const payUrl = safeUrl || payUrls[0]
ElMessage.success('正在跳转支付页面...')
setTimeout(() => {
window.location.href = payUrl
@ -469,9 +497,9 @@ const handleCreateOrder = async () => {
handleCancel()
}, 500)
} else {
//
//
ElMessage.success('正在生成支付二维码...')
await generateQRCode(payUrl)
await generateQRCodes(payUrls)
}
} else {
ElMessage.error('获取支付链接失败')
@ -543,6 +571,8 @@ const handleCancel = () => {
rechangeFormRef.value?.resetFields()
rechangeForm.value = createDefaultForm()
qrcodeImageUrl.value = ''
qrcodeImageUrls.value = []
paymentUrls.value = []
qrcodeLoading.value = false
payStep.value = 1
selectedMethod.value = ''
@ -800,6 +830,26 @@ onMounted(() => {
display: block;
}
.qrcode-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 24px;
}
.qrcode-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.qrcode-label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.qrcode {
width: 200px;
height: 200px;

View File

@ -9,13 +9,6 @@
@reset="handleReset"
>
<template #button>
<!-- 审核动作先保留按钮交互后续可直接接接口 -->
<el-button type="danger" plain @click="handleReject" v-auth="'user_reviewRechargeOrder'"
>驳回</el-button
>
<el-button type="success" @click="handleApprove" v-auth="'user_reviewRechargeOrder'"
>通过</el-button
>
<el-button type="primary" plain @click="handleExport">导出</el-button>
</template>
</RechangeSearch>
@ -27,9 +20,21 @@
:columns="tableColumns"
row-key="id"
:loading="loading"
:show-selection="true"
:show-selection="false"
:single-select="false"
@selection-change="handleSelectionChange"
/>
>
<!-- 操作列驳回/通过按钮 -->
<template #action="{ row }">
<el-button type="danger" text size="small" @click="handleReject(row)" v-auth="'user_reviewRechargeOrder'">
驳回
</el-button>
<span style="color: #dcdfe6; margin: 0 2px;">|</span>
<el-button type="success" text size="small" @click="handleApprove(row)" v-auth="'user_reviewRechargeOrder'">
通过
</el-button>
</template>
</Table>
<!-- 分页区域使用接口返回的 total / current_page / per_page -->
<RechangePagination
@ -198,10 +203,10 @@ console.log('rows:', rows);
// }
}
//
const handleApprove = async () => {
console.log('selectedRows.value:', selectedRows.value);
if (!selectedRows.value.length) {
//
const handleApprove = async (row?: any) => {
const targetRow = row || selectedRows.value[0]
if (!targetRow) {
ElMessage.warning('请先选择要通过的记录')
return
}
@ -217,21 +222,22 @@ const handleApprove = async () => {
startLoading()
try {
await reviewRechargeOrder({ id: selectedRows.value[0]?.id, status: '3' })
await reviewRechargeOrder({ id: targetRow.id, status: '3' })
await getList()
} finally {
stopLoading()
}
}
//
const handleReject = async () => {
if (!selectedRows.value.length) {
//
const handleReject = async (row?: any) => {
const targetRow = row || selectedRows.value[0]
if (!targetRow) {
ElMessage.warning('请先选择要驳回的记录')
return
}
try {
await ElMessageBox.confirm(`是否确认驳回该审核?订单号:${selectedRows.value[0]?.orderNo}`, '审核确认', {
await ElMessageBox.confirm(`是否确认驳回该审核?订单号:${targetRow.orderNo}`, '审核确认', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
@ -243,7 +249,7 @@ const handleReject = async () => {
//:1,2,3,4
startLoading()
try {
await reviewRechargeOrder({ id: selectedRows.value[0]?.id, status: '4' })
await reviewRechargeOrder({ id: targetRow.id, status: '4' })
await getList()
} finally {
stopLoading()

View File

@ -47,4 +47,5 @@ export const rechangeCheckTableColumns = [
{ prop: 'rate', label: '汇率', align: 'center' as const, width: 100 },
{ prop: 'statusText', label: '状态', align: 'center' as const, width: 100 },
{ prop: 'rechangeTime', label: '充值时间', align: 'center' as const },
{ label: '操作', align: 'center' as const, width: 180, fixed: 'right' as const, slotName: 'action' },
]

View File

@ -248,26 +248,26 @@ const availableBalance = computed(() => {
const totalWalletBalance = computed(() => {
const amount = parseFloat(walletInfo.value.amount || '0')
const freeze = parseFloat(walletInfo.value.freeze || '0')
console.log('walletInfo.value:', amount , freeze);
console.log('walletInfo.value:', amount, freeze);
// + + + +
// const walletSum =
// parseFloat(walletInfo.value.commission_fee_handling || '0') +
// parseFloat(walletInfo.value.commission_point_diff || '0') +
// parseFloat(walletInfo.value.service || '0') +
// parseFloat(walletInfo.value.risk || '0') +
// parseFloat(walletInfo.value.closing_profit || '0')
const walletSum =
parseFloat(walletInfo.value.commission_fee_handling || '0') +
parseFloat(walletInfo.value.commission_point_diff || '0') +
parseFloat(walletInfo.value.service || '0') +
parseFloat(walletInfo.value.risk || '0') +
parseFloat(Math.min(0, +walletInfo.value.closing_profit)+'')
// if (roleId >= 5) {
// // roleId >= 5
// // availableBalance = amount - freeze - walletSum
if (roleId >= 5) {
// roleId >= 5
// availableBalance = amount - freeze - walletSum
// const availableBal = Math.max(0, amount - walletSum)
// return walletSum + availableBal
// }
return floorMoney(amount - freeze)
}
return floorMoney(walletSum)
})
const floorMoney = (num:number) => {
const floorMoney = (num: number) => {
const cents = Math.floor(num * 100);
return cents / 100;
}
@ -295,10 +295,10 @@ const feeFieldMap: Record<string, keyof WalletInfo | 'available'> = {
//
const feeOrder = computed(() => {
const orders: Record<number, string[]> = {
1: ['手续费', '点差', '服务费', '风险金', '盈亏', '可用余额'], //
2: ['手续费', '点差', '服务费', '可用余额'], //
3: ['手续费', '点差', '服务费', '盈亏', '可用余额'], //
4: ['手续费', '点差', '服务费', '盈亏', '可用余额'], //
1: ['手续费', '点差', '服务费', '风险金', '盈亏'], //
2: ['手续费', '点差', '服务费'], //
3: ['手续费', '点差', '服务费', '盈亏'], //
4: ['手续费', '点差', '服务费', '盈亏'], //
5: ['手续费', '点差', '可用余额'], //
6: ['手续费', '点差', '可用余额'], //
}
@ -413,7 +413,7 @@ const initData = async () => {
if (res) {
walletInfo.value = {
commission_fee_handling: res.commission_fee_handling || '0',
commission_point_diff: ''+floorMoney(res.commission_point_diff - res.freeze||0) || '0',
commission_point_diff: '' + floorMoney(res.commission_point_diff - res.freeze || 0) || '0',
service: res.service || '0',
risk: res.risk || '0',
closing_profit: res.closing_profit || '0',

View File

@ -1,12 +1,7 @@
<template>
<div class="withdraw-check table_form-container">
<!-- 搜索区域复用项目内搜索组件 -->
<WithdrawSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch" @reset="handleReset">
<template #button>
<el-button type="success" :loading="approveLoading" @click="handleApprove">通过</el-button>
<el-button type="danger" plain :loading="rejectLoading" @click="handleReject">驳回</el-button>
</template>
</WithdrawSearch>
<WithdrawSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch" @reset="handleReset" />
<!-- 列表区域使用公共 Table 组件 -->
<Table
@ -17,7 +12,18 @@
:loading="loading"
:show-selection="true"
@selection-change="handleSelectionChange"
/>
>
<!-- 操作列驳回/通过按钮 -->
<template #action="{ row }">
<el-button type="success" text size="small" @click="handleApprove(row)" v-auth="'user_reviewWithdrawOrder'">
通过
</el-button>
<span style="color: #dcdfe6; margin: 0 2px;">|</span>
<el-button type="danger" text size="small" @click="handleReject(row)" v-auth="'user_reviewWithdrawOrder'">
驳回
</el-button>
</template>
</Table>
<!-- 分页区域复用项目内分页组件 -->
<WithdrawPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@ -216,16 +222,17 @@ const handleSelectionChange = (rows: TableRow[]) => {
selectedRows.value = rows
}
//
const handleApprove = async () => {
if (!selectedRows.value.length) {
//
const handleApprove = async (row?: TableRow) => {
const rowsToProcess = row ? [row] : selectedRows.value
if (!rowsToProcess.length) {
ElMessage.warning('请先选择要通过的记录')
return
}
try {
await ElMessageBox.confirm(
`确定要通过 ${selectedRows.value.length} 条提现记录吗?`,
`确定要通过 ${rowsToProcess.length} 条提现记录吗?`,
'审核确认',
{
confirmButtonText: '确定',
@ -236,9 +243,9 @@ const handleApprove = async () => {
approveLoading.value = true
for (const row of selectedRows.value) {
for (const item of rowsToProcess) {
await withdrawReviewOrder({
id: row.id,
id: item.id,
status: 2
})
}
@ -254,16 +261,17 @@ const handleApprove = async () => {
}
}
//
const handleReject = async () => {
if (!selectedRows.value.length) {
//
const handleReject = async (row?: TableRow) => {
const rowsToProcess = row ? [row] : selectedRows.value
if (!rowsToProcess.length) {
ElMessage.warning('请先选择要驳回的记录')
return
}
try {
await ElMessageBox.confirm(
`确定要驳回 ${selectedRows.value.length} 条提现记录吗?`,
`确定要驳回 ${rowsToProcess.length} 条提现记录吗?`,
'审核确认',
{
confirmButtonText: '确定',
@ -274,9 +282,9 @@ const handleReject = async () => {
rejectLoading.value = true
for (const row of selectedRows.value) {
for (const item of rowsToProcess) {
await withdrawReviewOrder({
id: row.id,
id: item.id,
status: 4
})
}

View File

@ -38,5 +38,6 @@ export const withdrawCheckTableColumns = [
{ prop: 'withdraw_point_diff', label: '点差', align: 'center' as const, width: 100, isNumber: true },
{ prop: 'withdraw_risk_fee', label: '风控费', align: 'center' as const, isNumber: true },
{ prop: 'withdraw_address', label: '提现地址', align: 'center' as const},
{ prop: 'created_at', label: '申请时间', align: 'center' as const}
{ prop: 'created_at', label: '申请时间', align: 'center' as const},
{ label: '操作', align: 'center' as const, width: 140, fixed: 'right' as const, slotName: 'action' },
]

View File

@ -69,14 +69,14 @@
<div class="balance-section">
<div class="balance-label">总金额</div>
<div class="balance-value">
<AnimatedNumber :value="walletCapital.amount_total || 0" :decimals="5" />
<AnimatedNumber :value="walletCapital.amount_total || 0" :decimals="2" />
</div>
</div>
<div class="balance-detail">
<div class="detail-item">
<span class="label">可用资金</span>
<span class="value">
<AnimatedNumber :value="walletCapital.amount || 0" :decimals="5" />
<AnimatedNumber :value="walletCapital.amount || 0" :decimals="2" />
</span>
</div>
<template v-if="isAgent">
@ -152,13 +152,13 @@
<div class="stat-item">
<span class="stat-label">净资产</span>
<span class="stat-value">
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="5" />
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="2" />
</span>
</div>
<div class="stat-item">
<span class="stat-label">余额</span>
<span class="stat-value">
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="5" />
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="2" />
</span>
</div>
<div class="stat-item">