This commit is contained in:
mazh 2026-04-25 20:13:33 +08:00
parent ea2361b232
commit 99e1f7e2c3
13 changed files with 1608 additions and 22 deletions

View File

@ -0,0 +1,6 @@
import { request } from '@/api';
// 获取充值订单列表
export const getRechargeOrderListApi = (params: any) => {
return request.get('/user/rechargeList', { ...params })
}

View File

@ -1,7 +1,327 @@
<script setup lang="ts"></script>
<template>
<h1>存款</h1>
<div class="rechange-page">
<!-- 页面标题 -->
<div class="page-title">存款</div>
<!-- 存款渠道列表每行提供一个存款按钮 -->
<RechangeTable :table-data="channelList" :columns="tableColumns" :highlight-current-row="false">
<template #action="{ row }">
<el-button type="primary" size="small" @click="handleOpenDialog(row)">
存款
</el-button>
</template>
</RechangeTable>
</div>
<!-- 存款弹窗改为和设计图一致的支付面板布局 -->
<RechangeDialog class="rechange-pay-dialog" :visible="dialogVisible" title=" " width="640px" :show-footer="false"
@cancel="handleCancel" @close="handleClose">
<div class="dialog-content">
<!-- 顶部标题展示当前点击的存款渠道名称 -->
<div class="dialog-page-title">{{ rechangeForm.channelName || '存款渠道名称' }}</div>
<!-- 存款面板当前为纯前端演示后续可在确认支付时接入真实接口 -->
<el-form ref="rechangeFormRef" :model="rechangeForm" :rules="rechangeFormRules" label-position="top"
class="pay-form">
<el-form-item prop="depositAccount">
<template #label>
<span class="required-label">存款账户</span>
</template>
<el-input v-model="rechangeForm.depositAccount" disabled />
</el-form-item>
<el-form-item prop="supportedCurrency">
<template #label>
<span class="required-label">支持币种</span>
</template>
<div class="currency-box">{{ rechangeForm.supportedCurrency }}</div>
</el-form-item>
<el-form-item prop="depositAmount">
<template #label>
<span class="required-label">存款金额</span>
</template>
<el-input v-model="rechangeForm.depositAmount" placeholder="请输入金额">
<template #prepend>{{ rechangeForm.amountCurrency }}</template>
</el-input>
<!-- 快捷金额模拟截图中的金额快捷按钮 -->
<div class="quick-amounts">
<el-button v-for="amount in rechangeQuickAmounts" :key="amount" plain size="small"
@click="handleQuickAmount(amount)">
{{ amount }}
</el-button>
</div>
</el-form-item>
</el-form>
<!-- 按钮区上一步用于关闭弹窗返回列表确认支付用于提交当前输入 -->
<div class="dialog-actions">
<el-button class="prev-btn" @click="handlePrevStep">上一步</el-button>
<el-button type="primary" class="confirm-btn" @click="handleSubmit">确认支付</el-button>
</div>
</div>
</RechangeDialog>
</template>
<style scoped lang="scss"></style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { nextTick, ref } from 'vue'
import RechangeTable from '@/components/common/Table.vue'
import RechangeDialog from '@/components/common/Dialog.vue'
import { rechangeQuickAmounts, rechangeTableColumns } from './options'
//
interface RechangeRow {
id: number
depositChannel: string
channelName: string
supportedCurrency: string
processingTime: string
singleAmount: string
depositAccount: string
amountCurrency: string
}
//
interface RechangeFormData {
id: number
channelName: string
depositAccount: string
supportedCurrency: string
amountCurrency: string
depositAmount: string
}
// mock
const channelList = ref<RechangeRow[]>([
{
id: 1,
depositChannel: 'BANK-CNY',
channelName: '存款渠道名称',
supportedCurrency: 'CNY',
processingTime: '5-10分钟',
singleAmount: '100 - 50,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USD',
},
{
id: 2,
depositChannel: 'BANK-USD',
channelName: '银行卡入金',
supportedCurrency: 'USD',
processingTime: '5-10分钟',
singleAmount: '100 - 50,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USD',
},
{
id: 3,
depositChannel: 'USDT-TRC20',
channelName: '数字货币入金',
supportedCurrency: 'USDT',
processingTime: '1-3分钟',
singleAmount: '50 - 100,000',
depositAccount: '只能在总钱包充值',
amountCurrency: 'USDT',
},
])
//
const tableColumns = ref(rechangeTableColumns)
//
const dialogVisible = ref(false)
const currentRowId = ref<number | null>(null)
const rechangeFormRef = ref<FormInstance>()
//
const createDefaultForm = (): RechangeFormData => ({
id: 0,
channelName: '',
depositAccount: '',
supportedCurrency: '',
amountCurrency: '',
depositAmount: '',
})
const rechangeForm = ref<RechangeFormData>(createDefaultForm())
//
const rechangeFormRules = ref<FormRules<RechangeFormData>>({
depositAmount: [
{ required: true, message: '请输入存款金额', trigger: ['blur', 'change'] },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '金额格式不正确最多保留2位小数', trigger: ['blur', 'change'] },
],
})
//
const resetDialogForm = () => {
rechangeForm.value = createDefaultForm()
currentRowId.value = null
rechangeFormRef.value?.clearValidate()
}
//
const handleOpenDialog = async (row: RechangeRow) => {
currentRowId.value = row.id
rechangeForm.value = {
id: row.id,
channelName: row.channelName,
depositAccount: row.depositAccount,
supportedCurrency: row.supportedCurrency,
amountCurrency: row.amountCurrency,
depositAmount: '',
}
dialogVisible.value = true
await nextTick()
rechangeFormRef.value?.clearValidate()
}
//
const handleQuickAmount = (amount: string) => {
rechangeForm.value.depositAmount = amount
}
//
const handlePrevStep = () => {
dialogVisible.value = false
resetDialogForm()
}
//
const handleSubmit = async () => {
try {
await rechangeFormRef.value?.validate()
ElMessage.success(`已进入支付确认流程渠道ID${currentRowId.value}`)
dialogVisible.value = false
resetDialogForm()
} catch (error) {
console.error('存款表单校验失败', error)
}
}
//
const handleCancel = () => {
dialogVisible.value = false
resetDialogForm()
}
//
const handleClose = () => {
dialogVisible.value = false
resetDialogForm()
}
</script>
<style scoped lang="scss">
.rechange-page {
height: 100%;
//
.page-title {
margin-bottom: 16px;
font-size: 28px;
font-weight: 600;
color: #303133;
}
}
.dialog-content {
padding: 6px 8px 0;
}
.dialog-page-title {
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.pay-form {
:deep(.el-form-item) {
margin-bottom: 28px;
}
:deep(.el-form-item__label) {
padding-bottom: 10px;
}
:deep(.el-input__wrapper),
:deep(.el-input-group__prepend) {
height: 46px;
font-size: 14px;
}
}
.required-label {
position: relative;
padding-left: 12px;
font-size: 16px;
color: #303133;
&::before {
position: absolute;
left: 0;
top: 1px;
color: #f56c6c;
content: '*';
}
}
.currency-box {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 116px;
height: 60px;
padding: 0 24px;
font-size: 18px;
color: #303133;
background: #fff;
border: 1px solid #dcdfe6;
box-sizing: border-box;
}
.quick-amounts {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
:deep(.el-button) {
min-width: 72px;
margin-left: 0;
color: #3b82f6;
border-color: #3b82f6;
}
}
.dialog-actions {
display: flex;
gap: 44px;
margin-top: 44px;
padding-left: 6px;
}
.prev-btn,
.confirm-btn {
min-width: 98px;
height: 44px;
}
.prev-btn {
color: #3b82f6;
border-color: #3b82f6;
}
:deep(.rechange-pay-dialog .el-dialog__header) {
display: none;
}
:deep(.rechange-pay-dialog .el-dialog__body) {
padding: 26px 26px 30px;
}
</style>

View File

@ -0,0 +1,12 @@
// 存款列表表格列配置:当前页面仍然保留列表 + 行内操作按钮的结构。
export const rechangeTableColumns = [
{ prop: 'depositChannel', label: '存款渠道', align: 'center' as const },
{ prop: 'channelName', label: '渠道名称', align: 'center' as const },
{ prop: 'supportedCurrency', label: '支持货币', align: 'center' as const },
{ prop: 'processingTime', label: '预计处理时间', align: 'center' as const },
{ prop: 'singleAmount', label: '单笔存款金额', align: 'center' as const },
{ label: '操作', slotName: 'action', align: 'center' as const, width: 120 },
]
// 存款弹窗快捷金额:点击后直接回填到金额输入框。
export const rechangeQuickAmounts = ['100.00', '500.00', '1000.00', '2000.00', '5000.00']

View File

@ -1,28 +1,36 @@
<template>
<div class="rechange-check">
<!-- 搜索区域复用项目内搜索组件 -->
<RechangeSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch" @reset="handleReset">
<template #button>
<!-- 当前阶段仅完成页面按钮先保留交互占位 -->
<el-button type="danger" plain @click="handleReject">驳回</el-button>
<el-button type="success" @click="handleApprove">通过</el-button>
<el-button type="primary" plain @click="handleExport">导出</el-button>
</template>
</RechangeSearch>
<!-- 列表区域当前使用页面内表格避免改动公共 Table 组件 -->
<div class="table-wrapper">
<el-table v-loading="loading" :data="tableData" border height="100%" row-key="id"
@selection-change="handleSelectionChange">
<!-- 勾选列用于后续审核通过/驳回 -->
<el-table-column type="selection" width="55" align="center" />
<!-- 序号列根据当前分页动态计算 -->
<el-table-column label="序号" width="70" align="center">
<template #default="scope">
{{ (currentPage - 1) * pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<!-- 业务字段列通过配置项统一渲染 -->
<el-table-column v-for="column in tableColumns" :key="column.prop" :prop="column.prop" :label="column.label"
:width="column.width" :align="column.align || 'center'" show-overflow-tooltip />
</el-table>
</div>
<!-- 分页区域复用项目内分页组件 -->
<RechangePagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
@ -35,6 +43,7 @@ import RechangeSearch from '@/components/common/Search.vue'
import RechangePagination from '@/components/common/Pagination.vue'
import { rechangeCheckSearch, rechangeCheckTableColumns } from './options'
//
interface TableRow {
id: number
account: string
@ -55,6 +64,7 @@ interface TableRow {
userType: number
}
// mock
const mockTableData: TableRow[] = [
{
id: 1,
@ -210,21 +220,25 @@ const mockTableData: TableRow[] = [
},
]
//
const searchOptions = ref(rechangeCheckSearch)
const searchForm = ref({
account: '',
userType: 1,
})
// loading
const tableColumns = computed(() => rechangeCheckTableColumns)
const tableData = ref<TableRow[]>([])
const selectedRows = ref<TableRow[]>([])
const loading = ref(false)
//
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// mock
const applyFilters = () => {
const keyword = searchForm.value.account.trim().toLowerCase()
const filtered = mockTableData.filter((item) => {
@ -239,6 +253,8 @@ const applyFilters = () => {
tableData.value = filtered.slice(start, end)
}
//
// mock
const getList = () => {
loading.value = true
selectedRows.value = []
@ -248,15 +264,18 @@ const getList = () => {
}, 120)
}
//
onMounted(() => {
getList()
})
//
const handleSearch = () => {
currentPage.value = 1
getList()
}
//
const handleReset = () => {
searchForm.value = {
account: '',
@ -266,10 +285,12 @@ const handleReset = () => {
getList()
}
//
const handleSelectionChange = (rows: TableRow[]) => {
selectedRows.value = rows
}
//
const handleApprove = () => {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择要通过的记录')
@ -278,6 +299,7 @@ const handleApprove = () => {
ElMessage.success('页面已完成,接口后续接入')
}
//
const handleReject = () => {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择要驳回的记录')
@ -286,10 +308,12 @@ const handleReject = () => {
ElMessage.success('页面已完成,接口后续接入')
}
//
const handleExport = () => {
ElMessage.success('页面已完成,接口后续接入')
}
//
const handlePaginationChange = (page: { currentPage: number; pageSize: number }) => {
currentPage.value = page.currentPage
pageSize.value = page.pageSize
@ -303,6 +327,7 @@ const handlePaginationChange = (page: { currentPage: number; pageSize: number })
padding-bottom: 70px;
box-sizing: border-box;
//
:deep(.search-container .el-form) {
flex-wrap: wrap;
align-items: center;
@ -317,6 +342,7 @@ const handlePaginationChange = (page: { currentPage: number; pageSize: number })
width: 100%;
}
//
.table-wrapper {
height: calc(100vh - 210px);
background: #fff;

View File

@ -1,5 +1,7 @@
// 搜索区域配置
export const rechangeCheckSearch = [
{
// 存款账户关键词
prop: 'account',
label: '存款账户',
type: 'input' as const,
@ -7,6 +9,7 @@ export const rechangeCheckSearch = [
width: '220px'
},
{
// 客户类型1=客户2=代理
prop: 'userType',
label: '客户类型',
type: 'select' as const,
@ -19,8 +22,7 @@ export const rechangeCheckSearch = [
}
]
// 表格列配置
export const rechangeCheckTableColumns = [
{ prop: 'account', label: '账户', align: 'center' as const, width: 140 },
{ prop: 'agentName', label: '所属代理', align: 'center' as const, width: 140 },

View File

@ -1,7 +1,334 @@
<script setup lang="ts"></script>
<template>
<h1>取款</h1>
<div class="withdraw-page">
<!-- 第一步输入页 -->
<div v-if="!isConfirmStep" class="withdraw-card">
<div class="page-title">{{ withdrawBaseInfo.channelName }}</div>
<!-- 输入表单当前先做纯前端演示后续可在此接入真实接口 -->
<el-form ref="withdrawFormRef" :model="withdrawForm" :rules="withdrawFormRules" label-position="top"
class="withdraw-form">
<el-form-item prop="withdrawAccount">
<template #label>
<span class="required-label">取款账户</span>
</template>
<el-input v-model="withdrawForm.withdrawAccount" disabled />
</el-form-item>
<el-form-item prop="currency">
<template #label>
<span class="required-label">支持币种</span>
</template>
<div class="currency-box">{{ withdrawForm.currency }}</div>
</el-form-item>
<el-form-item prop="amount">
<template #label>
<span class="required-label">取款金额</span>
</template>
<el-input v-model="withdrawForm.amount" placeholder="请输入金额">
<template #prepend>{{ withdrawForm.currency }}</template>
</el-input>
<!-- 快捷金额点击后直接回填金额输入框 -->
<div class="quick-amounts">
<el-button v-for="amount in withdrawQuickAmounts" :key="amount" plain size="small"
@click="handleQuickAmount(amount)">
{{ amount }}
</el-button>
</div>
</el-form-item>
<div class="action-row">
<el-button type="primary" class="submit-btn" @click="handleNextStep">
确认
</el-button>
</div>
</el-form>
</div>
<!-- 第二步收款信息页展示你图里的表单结构 -->
<div v-else class="withdraw-card receive-card">
<el-form ref="receiveFormRef" :model="receiveForm" :rules="receiveFormRules" label-position="top"
class="withdraw-form receive-form">
<!-- 收款信息字段由配置驱动渲染方便后续增删字段 -->
<el-form-item v-for="field in withdrawReceiveFields" :key="field.prop" :prop="field.prop">
<template #label>
<span class="required-label">{{ field.label }}</span>
</template>
<el-input v-model="receiveForm[field.prop as keyof WithdrawReceiveFormData]" />
</el-form-item>
<!-- 第二步底部按钮取消返回上一步确定执行最终提交占位 -->
<div class="action-row confirm-actions">
<el-button class="cancel-btn" @click="handleBackToEdit">取消</el-button>
<el-button type="primary" class="submit-btn" @click="handleMockSubmit">确定</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="scss"></style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { LocationQueryValue } from 'vue-router'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { withdrawBaseInfo, withdrawQuickAmounts, withdrawReceiveFields } from './options'
//
interface WithdrawFormData {
channelName: string
withdrawAccount: string
currency: string
amount: string
}
//
interface WithdrawReceiveFormData {
bankName: string
bankAddress: string
swift: string
receiverName: string
receiverAccount: string
currencyType: string
receiverAddress: string
remark: string
}
const route = useRoute()
const router = useRouter()
const withdrawFormRef = ref<FormInstance>()
const receiveFormRef = ref<FormInstance>()
// 使
const createDefaultForm = (): WithdrawFormData => ({
channelName: withdrawBaseInfo.channelName,
withdrawAccount: withdrawBaseInfo.withdrawAccount,
currency: withdrawBaseInfo.currency,
amount: '',
})
// query
const createDefaultReceiveForm = (): WithdrawReceiveFormData => ({
bankName: '',
bankAddress: '',
swift: '',
receiverName: '',
receiverAccount: '',
currencyType: '',
receiverAddress: '',
remark: '',
})
const withdrawForm = ref<WithdrawFormData>(createDefaultForm())
const receiveForm = ref<WithdrawReceiveFormData>(createDefaultReceiveForm())
//
const withdrawFormRules = ref<FormRules<WithdrawFormData>>({
amount: [
{ required: true, message: '请输入取款金额', trigger: ['blur', 'change'] },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '金额格式不正确最多保留2位小数', trigger: ['blur', 'change'] },
],
})
// 稿
const receiveFormRules = ref<FormRules<WithdrawReceiveFormData>>({
bankName: [{ required: true, message: '请输入收款银行名称', trigger: ['blur', 'change'] }],
bankAddress: [{ required: true, message: '请输入银行收款地址', trigger: ['blur', 'change'] }],
swift: [{ required: true, message: '请输入SWIFT', trigger: ['blur', 'change'] }],
receiverName: [{ required: true, message: '请输入收款人名称', trigger: ['blur', 'change'] }],
receiverAccount: [{ required: true, message: '请输入收款人账号', trigger: ['blur', 'change'] }],
currencyType: [{ required: true, message: '请输入货币类型', trigger: ['blur', 'change'] }],
receiverAddress: [{ required: true, message: '请输入收款人地址', trigger: ['blur', 'change'] }],
remark: [{ required: true, message: '请输入备注', trigger: ['blur', 'change'] }],
})
// query step
const isConfirmStep = computed(() => route.query.step === 'confirm')
// query string / string[]
const getQueryValue = (value: LocationQueryValue | LocationQueryValue[] | undefined, fallback = '') => {
if (Array.isArray(value)) {
return value[0] ?? fallback
}
return value ?? fallback
}
//
const syncFormFromRoute = () => {
withdrawForm.value = {
channelName: getQueryValue(route.query.channelName, withdrawBaseInfo.channelName),
withdrawAccount: getQueryValue(route.query.withdrawAccount, withdrawBaseInfo.withdrawAccount),
currency: getQueryValue(route.query.currency, withdrawBaseInfo.currency),
amount: getQueryValue(route.query.amount),
}
receiveForm.value = {
...createDefaultReceiveForm(),
currencyType: getQueryValue(route.query.currency, withdrawBaseInfo.currency),
}
}
watch(() => route.query, syncFormFromRoute, { immediate: true })
//
const handleQuickAmount = (amount: string) => {
withdrawForm.value.amount = amount
}
// query
const handleNextStep = async () => {
try {
await withdrawFormRef.value?.validate()
await router.push({
path: route.path,
query: {
step: 'confirm',
channelName: withdrawForm.value.channelName,
withdrawAccount: withdrawForm.value.withdrawAccount,
currency: withdrawForm.value.currency,
amount: withdrawForm.value.amount,
},
})
} catch (error) {
console.error('取款表单校验失败', error)
}
}
//
const handleBackToEdit = async () => {
await router.push({
path: route.path,
query: {
channelName: withdrawForm.value.channelName,
withdrawAccount: withdrawForm.value.withdrawAccount,
currency: withdrawForm.value.currency,
amount: withdrawForm.value.amount,
},
})
}
//
const handleMockSubmit = async () => {
try {
await receiveFormRef.value?.validate()
ElMessage.success(`已提交取款信息,金额:${withdrawForm.value.currency} ${withdrawForm.value.amount}`)
} catch (error) {
console.error('收款信息表单校验失败', error)
}
}
</script>
<style scoped lang="scss">
.withdraw-page {
min-height: 100%;
}
.withdraw-card {
width: 420px;
padding: 24px 24px 28px;
background: #fff;
border-radius: 8px;
box-sizing: border-box;
}
.page-title {
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.withdraw-form {
:deep(.el-form-item) {
margin-bottom: 22px;
}
:deep(.el-form-item__label) {
padding-bottom: 10px;
font-size: 16px;
color: #303133;
}
:deep(.el-input__wrapper),
:deep(.el-input-group__prepend) {
height: 46px;
font-size: 14px;
}
}
.required-label {
position: relative;
padding-left: 12px;
font-size: 16px;
color: #303133;
&::before {
position: absolute;
left: 0;
top: 1px;
color: #f56c6c;
content: '*';
}
}
.currency-box {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 92px;
height: 46px;
padding: 0 20px;
color: #303133;
background: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
}
.quick-amounts {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
:deep(.el-button) {
min-width: 66px;
margin-left: 0;
}
}
.action-row {
margin-top: 28px;
}
.submit-btn {
min-width: 96px;
height: 40px;
}
.receive-card {
width: 360px;
}
.receive-form {
:deep(.el-form-item) {
margin-bottom: 20px;
}
}
.confirm-actions {
display: flex;
gap: 18px;
}
.cancel-btn {
color: #3b82f6;
border-color: #3b82f6;
}
</style>

View File

@ -0,0 +1,45 @@
// 取款页面的基础信息:当前先用本地静态数据,后续可替换成接口返回值。
export const withdrawBaseInfo = {
channelName: '取款渠道名称',
withdrawAccount: '只能总钱包出账',
currency: 'USD',
}
// 快捷金额按钮:用于快速回填输入框。
export const withdrawQuickAmounts = ['100.00', '500.00', '1000.00', '2000.00', '5000.00']
// 第二步收款信息字段:统一由配置驱动,便于后续扩展或替换文案。
export const withdrawReceiveFields = [
{
label: '收款银行名称',
prop: 'bankName',
},
{
label: '银行收款地址',
prop: 'bankAddress',
},
{
label: 'SWIFT',
prop: 'swift',
},
{
label: '收款人名称',
prop: 'receiverName',
},
{
label: '收款人账号',
prop: 'receiverAccount',
},
{
label: '货币类型',
prop: 'currencyType',
},
{
label: '收款人地址',
prop: 'receiverAddress',
},
{
label: '备注',
prop: 'remark',
},
]

View File

@ -1,7 +1,353 @@
<script setup lang="ts"></script>
<template>
<h1>取款审核</h1>
<div class="withdraw-check">
<!-- 搜索区域复用项目内搜索组件 -->
<WithdrawSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch" @reset="handleReset">
<template #button>
<!-- 当前阶段仅完成页面按钮先保留交互占位 -->
<el-button type="success" @click="handleApprove">通过</el-button>
<el-button type="danger" plain @click="handleReject">驳回</el-button>
<el-button type="primary" plain @click="handleExport">导出</el-button>
</template>
</WithdrawSearch>
<!-- 列表区域当前使用页面内表格避免改动公共 Table 组件 -->
<div class="table-wrapper">
<el-table v-loading="loading" :data="tableData" border height="100%" row-key="id"
@selection-change="handleSelectionChange">
<!-- 勾选列用于后续审核通过/驳回 -->
<el-table-column type="selection" width="55" align="center" />
<!-- 序号列根据当前分页动态计算 -->
<el-table-column label="序号" width="70" align="center">
<template #default="scope">
{{ (currentPage - 1) * pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<!-- 业务字段列通过配置项统一渲染 -->
<el-table-column v-for="column in tableColumns" :key="column.prop" :prop="column.prop" :label="column.label"
:width="column.width" :align="column.align || 'center'" show-overflow-tooltip />
</el-table>
</div>
<!-- 分页区域复用项目内分页组件 -->
<WithdrawPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
</template>
<style scoped lang="scss"></style>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import WithdrawSearch from '@/components/common/Search.vue'
import WithdrawPagination from '@/components/common/Pagination.vue'
import { withdrawCheckSearch, withdrawCheckTableColumns } from './options'
//
interface TableRow {
id: number
account: string
agentName: string
walletAddress: string
walletType: string
holderName: string
bankName: string
bankCode: string
bankAddress: string
withdrawAmount: string
deductAmount: string
withdrawCurrency: string
arrivalAmount: string
arrivalCurrency: string
feeAmount: string
positionAmount: string
userType: number
}
// mock
const mockTableData: TableRow[] = [
{
id: 1,
account: 'U100101',
agentName: '一级代理A',
walletAddress: 'TRX8x2f...7Kp9',
walletType: 'TRC20',
holderName: '张三',
bankName: '中国银行',
bankCode: 'BKCHCNBJ',
bankAddress: '上海市浦东新区世纪大道100号',
withdrawAmount: '1000.00',
deductAmount: '20.00',
withdrawCurrency: 'USDT',
arrivalAmount: '980.00',
arrivalCurrency: 'USDT',
feeAmount: '20.00',
positionAmount: '1.20',
userType: 1,
},
{
id: 2,
account: 'U100102',
agentName: '一级代理B',
walletAddress: '0x5fa1...ac20',
walletType: 'ERC20',
holderName: '李四',
bankName: '工商银行',
bankCode: 'ICBKCNBJ',
bankAddress: '北京市朝阳区建国路88号',
withdrawAmount: '5000.00',
deductAmount: '50.00',
withdrawCurrency: 'USD',
arrivalAmount: '4950.00',
arrivalCurrency: 'USD',
feeAmount: '50.00',
positionAmount: '2.50',
userType: 1,
},
{
id: 3,
account: 'A200101',
agentName: '总代运营中心',
walletAddress: 'bc1q7m...91fa',
walletType: 'BTC',
holderName: '王五',
bankName: '建设银行',
bankCode: 'PCBCCNBJ',
bankAddress: '深圳市南山区科技园中一路1号',
withdrawAmount: '20000.00',
deductAmount: '120.00',
withdrawCurrency: 'CNY',
arrivalAmount: '19880.00',
arrivalCurrency: 'CNY',
feeAmount: '120.00',
positionAmount: '3.00',
userType: 2,
},
{
id: 4,
account: 'U100103',
agentName: '一级代理C',
walletAddress: 'TRX9m4k...6Ts1',
walletType: 'TRC20',
holderName: '赵六',
bankName: '农业银行',
bankCode: 'ABOCCNBJ',
bankAddress: '广州市天河区珠江新城华夏路18号',
withdrawAmount: '3000.00',
deductAmount: '15.00',
withdrawCurrency: 'USDT',
arrivalAmount: '2985.00',
arrivalCurrency: 'USDT',
feeAmount: '15.00',
positionAmount: '1.10',
userType: 1,
},
{
id: 5,
account: 'A200102',
agentName: '华东代理中心',
walletAddress: '0x9bd7...b3f2',
walletType: 'ERC20',
holderName: '孙七',
bankName: '招商银行',
bankCode: 'CMBCCNBS',
bankAddress: '杭州市西湖区文三路90号',
withdrawAmount: '8000.00',
deductAmount: '80.00',
withdrawCurrency: 'USD',
arrivalAmount: '7920.00',
arrivalCurrency: 'USD',
feeAmount: '80.00',
positionAmount: '2.20',
userType: 2,
},
{
id: 6,
account: 'U100104',
agentName: '一级代理D',
walletAddress: 'TRX6a1d...3Lm8',
walletType: 'TRC20',
holderName: '周八',
bankName: '交通银行',
bankCode: 'COMMCNSH',
bankAddress: '成都市高新区天府大道北段966号',
withdrawAmount: '1200.00',
deductAmount: '12.00',
withdrawCurrency: 'USDT',
arrivalAmount: '1188.00',
arrivalCurrency: 'USDT',
feeAmount: '12.00',
positionAmount: '0.80',
userType: 1,
},
{
id: 7,
account: 'U100105',
agentName: '一级代理E',
walletAddress: 'bc1qx8...0de4',
walletType: 'BTC',
holderName: '吴九',
bankName: '浦发银行',
bankCode: 'SPDBCNBS',
bankAddress: '苏州市工业园区星湖街328号',
withdrawAmount: '15000.00',
deductAmount: '105.00',
withdrawCurrency: 'CNY',
arrivalAmount: '14895.00',
arrivalCurrency: 'CNY',
feeAmount: '105.00',
positionAmount: '2.80',
userType: 1,
},
{
id: 8,
account: 'A200103',
agentName: '华南代理中心',
walletAddress: '0x8e2f...d1a0',
walletType: 'ERC20',
holderName: '郑十',
bankName: '民生银行',
bankCode: 'MSBCCNBJ',
bankAddress: '厦门市思明区湖滨南路258号',
withdrawAmount: '6000.00',
deductAmount: '36.00',
withdrawCurrency: 'USD',
arrivalAmount: '5964.00',
arrivalCurrency: 'USD',
feeAmount: '36.00',
positionAmount: '1.90',
userType: 2,
},
]
//
const searchOptions = ref(withdrawCheckSearch)
const searchForm = ref({
account: '',
userType: 1,
})
// loading
const tableColumns = computed(() => withdrawCheckTableColumns)
const tableData = ref<TableRow[]>([])
const selectedRows = ref<TableRow[]>([])
const loading = ref(false)
//
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
// mock
const applyFilters = () => {
const keyword = searchForm.value.account.trim().toLowerCase()
const filtered = mockTableData.filter((item) => {
const matchAccount = !keyword || item.account.toLowerCase().includes(keyword)
const matchUserType = !searchForm.value.userType || item.userType === searchForm.value.userType
return matchAccount && matchUserType
})
total.value = filtered.length
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
tableData.value = filtered.slice(start, end)
}
//
// mock
const getList = () => {
loading.value = true
selectedRows.value = []
window.setTimeout(() => {
applyFilters()
loading.value = false
}, 120)
}
//
onMounted(() => {
getList()
})
//
const handleSearch = () => {
currentPage.value = 1
getList()
}
//
const handleReset = () => {
searchForm.value = {
account: '',
userType: 1,
}
currentPage.value = 1
getList()
}
//
const handleSelectionChange = (rows: TableRow[]) => {
selectedRows.value = rows
}
//
const handleApprove = () => {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择要通过的记录')
return
}
ElMessage.success('页面已完成,接口后续接入')
}
//
const handleReject = () => {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择要驳回的记录')
return
}
ElMessage.success('页面已完成,接口后续接入')
}
//
const handleExport = () => {
ElMessage.success('页面已完成,接口后续接入')
}
//
const handlePaginationChange = (page: { currentPage: number; pageSize: number }) => {
currentPage.value = page.currentPage
pageSize.value = page.pageSize
applyFilters()
}
</script>
<style scoped lang="scss">
.withdraw-check {
height: 100%;
padding-bottom: 70px;
box-sizing: border-box;
//
:deep(.search-container .el-form) {
flex-wrap: wrap;
align-items: center;
}
:deep(.search-container .el-form-item) {
flex: 0 0 auto;
}
:deep(.search-container .el-input),
:deep(.search-container .el-select) {
width: 100%;
}
//
.table-wrapper {
height: calc(100vh - 210px);
background: #fff;
border-radius: 6px;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,42 @@
// 搜索区域配置
export const withdrawCheckSearch = [
{
// 取款账户关键词
prop: 'account',
label: '取款账户',
type: 'input' as const,
placeholder: '请输入取款账户',
width: '220px'
},
{
// 客户类型1=客户2=代理
prop: 'userType',
label: '客户类型',
type: 'select' as const,
placeholder: '请选择客户类型',
width: '180px',
options: [
{ label: '客户', value: 1 },
{ label: '代理', value: 2 }
]
}
]
// 表格列配置
export const withdrawCheckTableColumns = [
{ prop: 'account', label: '账户', align: 'center' as const, width: 140 },
{ prop: 'agentName', label: '所属代理', align: 'center' as const, width: 140 },
{ prop: 'walletAddress', label: '钱包地址', align: 'center' as const, width: 220 },
{ prop: 'walletType', label: '钱包类型', align: 'center' as const, width: 120 },
{ prop: 'holderName', label: '账户持有人姓名', align: 'center' as const, width: 160 },
{ prop: 'bankName', label: '银行名称', align: 'center' as const, width: 160 },
{ prop: 'bankCode', label: '银行识别代码', align: 'center' as const, width: 160 },
{ prop: 'bankAddress', label: '银行地址', align: 'center' as const, width: 200 },
{ prop: 'withdrawAmount', label: '取款金额', align: 'center' as const, width: 120 },
{ prop: 'deductAmount', label: '取款扣除金额', align: 'center' as const, width: 140 },
{ prop: 'withdrawCurrency', label: '取款币种', align: 'center' as const, width: 120 },
{ prop: 'arrivalAmount', label: '到账金额', align: 'center' as const, width: 120 },
{ prop: 'arrivalCurrency', label: '到账币种', align: 'center' as const, width: 120 },
{ prop: 'feeAmount', label: '手续费', align: 'center' as const, width: 100 },
{ prop: 'positionAmount', label: '头寸', align: 'center' as const, width: 100 }
]

View File

@ -1,7 +1,200 @@
<script setup lang="ts"></script>
<template>
<h1>充值订单</h1>
<div class="order-rechange-page">
<!-- 搜索区域按接口参数保留订单号用户名称状态开始/结束时间 -->
<OrderRechangeSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset" />
<!-- 列表区域改为真实接口数据保留原有表格结构 -->
<OrderRechangeTable :table-data="tableTree" :columns="tableColumnsOptions" :highlight-current-row="false" />
<!-- 分页区域使用接口返回的 total / current_page / per_page -->
<OrderRechangePagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
</template>
<style scoped lang="scss"></style>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import OrderRechangeSearch from '@/components/common/Search.vue'
import OrderRechangeTable from '@/components/common/Table.vue'
import OrderRechangePagination from '@/components/common/Pagination.vue'
import { getRechargeOrderListApi } from '@/api/modules/system/rechargeOrder'
import { search, tableColumns } from './options'
//
interface RechangeOrderItem {
id: number
orderNo: string
userName: string
currency: string
amount: string
status: string
statusText: string
rechangeTime: string
rechangeAccount: string
}
//
interface SearchFormData {
orderNo: string
userName: string
status: string
startTime: string
endTime: string
}
//
const searchOptions = ref(search)
// Search
const createDefaultSearchForm = (): SearchFormData => ({
orderNo: '',
userName: '',
status: '',
startTime: '',
endTime: '',
})
const searchForm = ref<SearchFormData>(createDefaultSearchForm())
//
const tableTree = ref<RechangeOrderItem[]>([])
const tableColumnsOptions = ref(tableColumns)
//
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 1/2/3/4
const getRechargeStatusText = (status: string | number | null | undefined) => {
const statusMap: Record<string, string> = {
'1': '未审核',
'2': '未付款',
'3': '已付款',
'4': '驳回',
}
return statusMap[String(status ?? '')] || '--'
}
// `YYYY-MM-DD HH:mm:ss`
const padZero = (value: number) => String(value).padStart(2, '0')
const formatDateTime = (date: Date) => {
const year = date.getFullYear()
const month = padZero(date.getMonth() + 1)
const day = padZero(date.getDate())
const hours = padZero(date.getHours())
const minutes = padZero(date.getMinutes())
const seconds = padZero(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const getTodayStartTime = () => {
const now = new Date()
now.setHours(0, 0, 0, 0)
return formatDateTime(now)
}
const getCurrentTime = () => formatDateTime(new Date())
//
const getRechargeOrderList = async () => {
const params = {
page: currentPage.value,
page_size: pageSize.value,
trade_id: searchForm.value.orderNo || null,
start_time: searchForm.value.startTime ? `${searchForm.value.startTime} 00:00:00` : getTodayStartTime(),
end_time: searchForm.value.endTime ? `${searchForm.value.endTime} 23:59:59` : getCurrentTime(),
user_name: searchForm.value.userName || null,
status: searchForm.value.status || null,
}
try {
const data: any = await getRechargeOrderListApi(params)
const list = Array.isArray(data?.data) ? data.data : []
tableTree.value = list.map((item: any, index: number) => ({
id: Number(item.id ?? index + 1),
// trade_id
orderNo: String(item.trade_id ?? '--'),
// user.username
userName: item.user?.username ?? '--',
// recharge_currency.symbol
currency: item.recharge_currency?.symbol ?? '--',
// recharge_amount
amount: String(item.recharge_amount ?? '--'),
status: String(item.status ?? ''),
statusText: getRechargeStatusText(item.status),
// created_at
rechangeTime: item.created_at ?? '--',
// user.username
rechangeAccount: item.user?.username ?? '--',
}))
total.value = Number(data?.total ?? list.length)
currentPage.value = Number(data?.current_page ?? currentPage.value)
pageSize.value = Number(data?.per_page ?? pageSize.value)
} catch (error) {
tableTree.value = []
total.value = 0
console.error('获取充值订单列表失败', error)
}
}
//
onMounted(() => {
getRechargeOrderList()
})
//
const handleSearch = () => {
currentPage.value = 1
getRechargeOrderList()
}
//
const handleReset = () => {
searchForm.value = createDefaultSearchForm()
currentPage.value = 1
pageSize.value = 10
getRechargeOrderList()
}
//
const handlePaginationChange = (page: { currentPage: number; pageSize: number }) => {
currentPage.value = page.currentPage
pageSize.value = page.pageSize
getRechargeOrderList()
}
</script>
<style scoped lang="scss">
.order-rechange-page {
min-height: 100%;
//
:deep(.search-container .el-form) {
display: flex;
flex-wrap: wrap;
align-items: center;
}
:deep(.search-container .el-form-item) {
flex: 0 0 auto;
margin-bottom: 8px;
}
:deep(.search-container .el-input),
:deep(.search-container .el-select),
:deep(.search-container .el-date-editor) {
width: 100%;
}
:deep(.table-container) {
height: calc(100vh - 220px);
}
}
</style>

View File

@ -0,0 +1,60 @@
// 充值订单搜索项:按接口参数保留订单号、用户名称、状态、结束时间、开始时间。
export const search = [
{
prop: 'orderNo',
label: '订单号',
type: 'input' as const,
placeholder: '请输入流水号',
width: '210px',
},
{
prop: 'userName',
label: '用户名称',
type: 'input' as const,
placeholder: '请输入用户名',
width: '190px',
},
{
prop: 'status',
label: '状态',
type: 'select' as const,
placeholder: '请选择状态',
width: '140px',
options: [
{ label: '未审核', value: '1' },
{ label: '未付款', value: '2' },
{ label: '已付款', value: '3' },
{ label: '驳回', value: '4' },
],
},
{
prop: 'startTime',
label: '开始时间',
type: 'date' as const,
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择开始时间',
width: '150px',
},
{
prop: 'endTime',
label: '结束时间',
type: 'date' as const,
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择结束时间',
width: '150px',
},
]
// 充值订单表格列:对应接口数据映射后的列表字段。
export const tableColumns = [
{ prop: 'orderNo', label: '订单号', align: 'center' as const },
{ prop: 'currency', label: '充值币种', align: 'center' as const },
{ prop: 'amount', label: '金额', align: 'center' as const },
{ prop: 'statusText', label: '状态', align: 'center' as const },
{ prop: 'rechangeTime', label: '充值时间', align: 'center' as const },
{ prop: 'rechangeAccount', label: '充值账号', align: 'center' as const },
]

View File

@ -1,7 +1,176 @@
<script setup lang="ts"></script>
<template>
<h1>提现</h1>
<div class="order-withdraw-page">
<!-- 搜索区域按设计图保留订单号开始时间结束时间并补一个导出按钮 -->
<OrderWithdrawSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset">
<template #button>
<el-button type="primary" @click="handleExport">
导出
</el-button>
</template>
</OrderWithdrawSearch>
<!-- 列表区域当前使用本地 mock 数据后续可直接替换为接口返回 -->
<OrderWithdrawTable :table-data="pagedTableData" :columns="tableColumnsOptions" :highlight-current-row="false" />
<!-- 分页区域保持和订单模块其他页面一致的交互结构 -->
<OrderWithdrawPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="filteredTableData.length"
@pagination-change="handlePaginationChange" />
</div>
</template>
<style scoped lang="scss"></style>
<script setup lang="ts">
import { computed, ref } from 'vue'
import OrderWithdrawSearch from '@/components/common/Search.vue'
import OrderWithdrawTable from '@/components/common/Table.vue'
import OrderWithdrawPagination from '@/components/common/Pagination.vue'
import { exportToExcel } from '@/utils/exportToExcel'
import { search, tableColumns } from './options'
//
interface WithdrawOrderItem {
id: number
orderNo: string
currency: string
amount: string
status: 'success' | 'pending' | 'failed'
statusText: string
withdrawTime: string
withdrawAccount: string
}
// 便
interface SearchFormData {
orderNo: string
startTime: string
endTime: string
}
//
const searchOptions = ref(search)
// Search
const createDefaultSearchForm = (): SearchFormData => ({
orderNo: '',
startTime: '',
endTime: '',
})
const searchForm = ref<SearchFormData>(createDefaultSearchForm())
// mock
const tableData = ref<WithdrawOrderItem[]>([
{
id: 1,
orderNo: '1111111',
currency: 'USD',
amount: 'USD 10.000000',
status: 'success',
statusText: '成功',
withdrawTime: '2026.01.30 10:05:15',
withdrawAccount: '123',
},
{
id: 2,
orderNo: 'WD202604250002',
currency: 'USDT',
amount: 'USDT 520.000000',
status: 'pending',
statusText: '待处理',
withdrawTime: '2026.02.18 08:20:11',
withdrawAccount: 'TRC20-9001',
},
{
id: 3,
orderNo: 'WD202604250003',
currency: 'CNY',
amount: 'CNY 1800.000000',
status: 'failed',
statusText: '失败',
withdrawTime: '2026.03.06 14:30:56',
withdrawAccount: 'BANK-3245',
},
])
//
const tableColumnsOptions = ref(tableColumns)
//
const currentPage = ref(1)
const pageSize = ref(10)
//
const filteredTableData = computed(() => {
return tableData.value.filter((item) => {
const matchOrderNo = !searchForm.value.orderNo || item.orderNo.includes(searchForm.value.orderNo)
const itemDate = item.withdrawTime.slice(0, 10).replace(/\./g, '-')
const matchStartTime = !searchForm.value.startTime || itemDate >= searchForm.value.startTime
const matchEndTime = !searchForm.value.endTime || itemDate <= searchForm.value.endTime
return matchOrderNo && matchStartTime && matchEndTime
})
})
//
const pagedTableData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredTableData.value.slice(start, end)
})
// 便
const handleSearch = () => {
currentPage.value = 1
}
//
const handleReset = () => {
searchForm.value = createDefaultSearchForm()
currentPage.value = 1
}
//
const handleExport = async () => {
await exportToExcel({
tableData: filteredTableData.value,
columns: tableColumnsOptions.value,
title: '导出提现订单',
defaultFileNamePrefix: '提现订单',
})
}
//
const handlePaginationChange = (page: { currentPage: number; pageSize: number }) => {
currentPage.value = page.currentPage
pageSize.value = page.pageSize
}
</script>
<style scoped lang="scss">
.order-withdraw-page {
min-height: 100%;
//
:deep(.search-container .el-form) {
display: flex;
flex-wrap: wrap;
align-items: center;
}
:deep(.search-container .el-form-item) {
flex: 0 0 auto;
margin-bottom: 8px;
}
:deep(.search-container .el-input),
:deep(.search-container .el-select),
:deep(.search-container .el-date-editor) {
width: 100%;
}
:deep(.table-container) {
height: calc(100vh - 220px);
}
}
</style>

View File

@ -0,0 +1,38 @@
// 提现订单搜索项:按截图保留订单号、开始时间、结束时间。
export const search = [
{
prop: 'orderNo',
label: '订单号',
type: 'input' as const,
placeholder: '请输入流水号',
width: '210px',
},
{
prop: 'startTime',
label: '开始时间',
type: 'date' as const,
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择开始时间',
width: '150px',
},
{
prop: 'endTime',
label: '结束时间',
type: 'date' as const,
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择结束时间',
width: '150px',
},
]
// 提现订单表格列:对应截图中的列表表头。
export const tableColumns = [
{ prop: 'orderNo', label: '订单号', align: 'center' as const },
{ prop: 'currency', label: '提现币种', align: 'center' as const },
{ prop: 'amount', label: '金额', align: 'center' as const },
{ prop: 'statusText', label: '状态', align: 'center' as const },
{ prop: 'withdrawTime', label: '提现时间', align: 'center' as const },
{ prop: 'withdrawAccount', label: '提现账号', align: 'center' as const },
]