Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/views/profile/business/index.vue
#	src/views/profile/business/options.ts
This commit is contained in:
Songsl 2026-05-11 18:04:49 +08:00
commit 9ff372afa5
27 changed files with 1584 additions and 689 deletions

View File

@ -16,3 +16,13 @@ export const editChannel = (params: any) => {
export const delChannel = (params: any) => {
return request.post('/channel/delChannel', { ...params })
}
// 获取用户充值渠道列表
export const getRechargeChannelList = () => {
return request.get('/user/regchargeChannel')
}
// 用户充值
export const rechargeApi = (params: any) => {
return request.post('/user/recharge', { ...params })
}

View File

@ -61,3 +61,28 @@ export async function logoutApi(params: { device_no: string }) {
export async function sendCodeApi(params: SendCodeParams) {
return request.post<string>('/sendCode', params);
}
/**
*
* @param params ID/
* @returns Promise<boolean> true
*/
export async function updateCapitalApi(params: { target_user_id: number; amount: string }) {
return request.post<boolean>('/user/updateCapital', params);
}
/**
*
* @param params ID/ID/ID///
* @returns Promise<boolean> true
*/
export async function editUserApi(params: {
user_id: number;
risk_control_id?: string | number;
fee_setting_id?: string | number;
username?: string;
is_levelup?: number | null;
status?: number;
}) {
return request.post<boolean>('/user/editUser', params);
}

View File

@ -0,0 +1,8 @@
.el-drawer__body {
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
height: 100%;
max-height: 100%;
}

View File

@ -1,5 +1,6 @@
@use './user.scss';
@use './system.scss';
@use './element.scss';
* {
margin: 0;

View File

@ -3,60 +3,38 @@
<el-form :model="searchForm" inline @submit.prevent>
<!-- 动态生成的表单项 -->
<template v-for="item in searchOptions" :key="item.prop">
<el-form-item
v-auth="item.auth || ''"
:label="item.label"
:prop="item.prop"
v-if="!item.slot"
:style="{
minWidth: item.minWidth,
width: item.width,
flex: item.width ? '0 0 auto' : '',
}"
>
<el-form-item v-auth="item.auth || ''" :label="item.label" :prop="item.prop" v-if="!item.slot" :style="{
minWidth: item.minWidth,
width: item.width,
flex: item.width ? '0 0 auto' : '',
}">
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`"
:clearable="item.clearable ?? true"
:disabled="item.disabled"
@blur="handleBlur"
/>
<el-input v-if="item.type === 'input'" v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`" :clearable="item.clearable ?? true"
:disabled="item.disabled" @blur="handleBlur" />
<!-- 选择器 -->
<el-select
v-else-if="item.type === 'select'"
v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
:clearable="item.clearable ?? true"
:disabled="item.disabled"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
<el-select v-else-if="item.type === 'select'" v-model="searchForm[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`" :clearable="item.clearable ?? true"
:disabled="item.disabled">
<el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
<!-- 日期选择器单个/范围 -->
<el-date-picker
v-else-if="item.type === 'date'"
v-model="searchForm[item.prop]"
:type="item.dateType || 'date'"
:range-separator="item.rangeSeparator || '至'"
:start-placeholder="item.startPlaceholder || '开始日期'"
:end-placeholder="item.endPlaceholder || '结束日期'"
:value-format="item.valueFormat || 'YYYY-MM-DD'"
:clearable="item.clearable ?? true"
:disabled-date="item.disabledDate"
:locale="'zhCn'"
/>
<el-date-picker v-else-if="item.type === 'date'" v-model="searchForm[item.prop]"
:type="item.dateType || 'date'" :range-separator="item.rangeSeparator || '至'"
:placeholder="item.placeholder"
:start-placeholder="item.startPlaceholder || '开始日期'" :end-placeholder="item.endPlaceholder || '结束日期'"
:value-format="item.valueFormat || 'YYYY-MM-DD'" :clearable="item.clearable ?? true"
:disabled-date="item.disabledDate" :locale="'zhCn'" />
</el-form-item>
<!-- 自定义插槽项 -->
<el-form-item :label="item.label" :prop="item.prop" v-else>
<el-form-item v-else :label="item.label" :prop="item.prop" :style="{
minWidth: item.minWidth,
width: item.width,
flex: item.width ? '0 0 auto' : '',
}">
<slot :name="item.prop" :form="searchForm" />
</el-form-item>
</template>
@ -137,20 +115,34 @@ const handleBlur = () => {
.el-form {
display: flex;
flex-wrap: wrap;
width: 100%;
.el-form-item {
flex: 1 1 auto;
}
row-gap: 8px;
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 16px;
flex: 1 1 auto;
min-width: 200px;
}
:deep(.el-button) {
margin-left: 8px;
white-space: nowrap;
}
:deep(.el-form-item:last-child) {
display: flex;
width: fit-content;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 8px;
.el-form-item__content {
justify-content: flex-start;
row-gap: 4px;
}
}
}
</style>

View File

@ -142,7 +142,7 @@ defineExpose({
<style scoped lang="scss">
.table-container {
width: 100%;
height: calc(100vh - 180px);
overflow: hidden;
height: 100%;
// overflow: hidden;
}
</style>

View File

@ -21,8 +21,8 @@
<!-- 用户信息下拉菜单 -->
<el-dropdown @command="handleCommand" class="user-dropdown">
<div class="user-info">
<el-avatar :size="32" :src="userAvatar" class="user-avatar">
<el-icon><User /></el-icon>
<el-avatar :size="32" class="user-avatar">
<el-icon :size="20"><User /></el-icon>
</el-avatar>
<span class="username">{{ activeUserName }}</span>
<el-icon class="dropdown-icon"><ArrowDown /></el-icon>
@ -49,6 +49,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
Bell,
Sunny,
@ -69,10 +70,8 @@ const router = useRouter()
const userStore = useUserStore()
const languageStore = useLanguageStore()
//
const userInfo = getStorage<any>('userInfo')
const activeUserName = ref(userInfo?.username || 'Admin')
const userAvatar = ref(userInfo?.avatar || '')
// - 使 userStore
const activeUserName = computed(() => userStore.userInfo?.username || '-')
const unreadCount = ref(3) //
//

View File

@ -165,6 +165,19 @@ const switchAccount = async () => {
clearUserInfo();
router.push('/user/login');
};
/**
*
* @param data
*/
const updateUserInfo = (data: Partial<UserInfo>) => {
if (userInfo.value) {
// 合并更新用户信息
userInfo.value = { ...userInfo.value, ...data };
// 更新本地存储
setStorage('userInfo', userInfo.value);
}
};
return {
// 状态
token,
@ -182,6 +195,7 @@ return {
forgotPassword,
changePassword,
sendCode,
switchAccount
switchAccount,
updateUserInfo
};
});

View File

@ -1,40 +1,20 @@
<template>
<div>
<!-- 搜索 -->
<CustomerListSearch
:search-form="searchForm"
:search-options="searchOptions"
@search="handleSearch"
@reset="handleReset"
></CustomerListSearch>
<CustomerListSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset"></CustomerListSearch>
<!-- 表格 -->
<CustomerListTable
:table-data="tableTree"
:columns="tableColumnsOptions"
row-key="userId"
:tree-props="treeProps"
:default-sort="{ prop: 'createTime', order: 'descending' }"
@expand-change="handleGetSubList"
>
<CustomerListTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="userId" :tree-props="treeProps"
:default-sort="{ prop: 'createTime', order: 'descending' }" @expand-change="handleGetSubList">
<template #marginRatio="{ row }">
<el-button
v-if="row.role_id === 5"
type="primary"
size="small"
@click="handleMarginRatio(row)"
>
<el-button v-if="row.role_id === 5" type="primary" size="small" @click="handleMarginRatio(row)">
修改
</el-button>
</template>
<template #commissionRatio="{ row }">
{{ row.commissionRatio }}
<el-button
v-if="row.role_id === 5"
type="primary"
size="small"
@click="handleCommissionRatio(row)"
>
<el-button v-if="row.role_id === 5" type="primary" size="small" @click="handleCommissionRatio(row)">
修改
</el-button>
</template>
@ -43,62 +23,97 @@
<el-button link type="primary" size="small" @click="openDetailDrawer(row)" v-if="row.userId">
查看详情
</el-button>
<el-divider direction="vertical" />
<el-button link type="primary" size="small" @click="handleEditCustomer(row)">
编辑
</el-button>
<el-divider direction="vertical" />
<el-button link type="warning" size="small" @click="handleAdjustAmount(row)">
调整金额
</el-button>
</template>
</CustomerListTable>
<!-- 分页 -->
<CustomerListPagination
v-model="currentPage"
v-model:pageSizeValue="pageSize"
:total="total"
@pagination-change="handlePaginationChange"
/>
<CustomerListPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
<!-- dialog -->
<CustomerListDialog
:visible="dialogVisible"
:title="dialogTitle"
:type="dialogType"
:fullscreen="dialogFullscreen"
:show-footer="dialogType !== 'view'"
@confirm="handleSubmit"
@cancel="handleCancel"
@close="handleClose"
>
<CustomerListTable
:table-data="marginRatioTree"
:columns="marginRatioTableColumnsOptions"
row-key="id"
v-if="dialogType === 'editMarginRatio'"
style="height: 600px"
>
<CustomerListDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" :fullscreen="dialogFullscreen"
:show-footer="dialogType !== 'view'" @confirm="handleSubmit" @cancel="handleCancel" @close="handleClose">
<CustomerListTable :table-data="marginRatioTree" :columns="marginRatioTableColumnsOptions" row-key="id"
v-if="dialogType === 'editMarginRatio'" style="height: 600px">
<template #assignedPointDiff="{ row }">
<el-form
:model="row"
:rules="marginRatioRules(row)"
:ref="(el) => setFormRef(el, row.id)"
>
<el-form :model="row" :rules="marginRatioRules(row)" :ref="(el) => setFormRef(el, row.id)">
<el-form-item style="margin-bottom: 0" prop="assignedPointDifference">
<el-input v-model="row.assignedPointDifference" placeholder="请输入点差" />
</el-form-item>
</el-form>
</template>
</CustomerListTable>
<CustomerListForm
:form-data="commissionRatioForm"
:form-rules="commissionRatioFormRules"
:form-items="commissionRatioFormItemOptions"
ref="commissionRatioFormRef"
v-if="dialogType === 'editCommissionRatio'"
></CustomerListForm>
<CustomerListForm :form-data="commissionRatioForm" :form-rules="commissionRatioFormRules"
:form-items="commissionRatioFormItemOptions" ref="commissionRatioFormRef"
v-if="dialogType === 'editCommissionRatio'"></CustomerListForm>
</CustomerListDialog>
<!-- 调整金额弹窗 -->
<CustomerListDialog :visible="adjustAmountVisible" title="调整金额" type="adjust" @confirm="handleAdjustAmountSubmit"
@cancel="handleAdjustAmountCancel" @close="handleAdjustAmountClose">
<el-form :model="adjustAmountForm" :rules="adjustAmountRules" ref="adjustAmountFormRef" label-width="120px">
<el-form-item label="账号名" prop="username">
<el-input v-model="adjustAmountForm.username" disabled />
</el-form-item>
<el-form-item label="币种" prop="currency">
<el-input v-model="adjustAmountForm.currency" disabled />
</el-form-item>
<el-form-item label="当前余额" prop="currentBalance">
<el-input v-model="adjustAmountForm.currentBalance" disabled />
</el-form-item>
<el-form-item label="调整金额" prop="adjustAmount">
<el-input v-model="adjustAmountForm.adjustAmount" placeholder="请输入调整金额(可为负数)"
@input="handleAdjustAmountInput" />
</el-form-item>
<el-form-item label="调整后余额" prop="afterBalance">
<el-input v-model="adjustAmountForm.afterBalance" disabled />
</el-form-item>
</el-form>
</CustomerListDialog>
<!-- 修改客户弹窗 -->
<CustomerListDialog :visible="editCustomerVisible" title="修改客户" type="editCustomer"
@confirm="handleEditCustomerSubmit" @cancel="handleEditCustomerCancel" @close="handleEditCustomerClose">
<el-form :model="editCustomerForm" :rules="editCustomerRules" ref="editCustomerFormRef" label-width="140px">
<el-form-item label="手续费模板" prop="feeSettingId">
<el-select v-model="editCustomerForm.feeSettingId" placeholder="请选择手续费模板" style="width: 100%">
<el-option v-for="item in feeTemplateOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="风控模板" prop="riskControlId">
<el-select v-model="editCustomerForm.riskControlId" placeholder="请选择风控模板" style="width: 100%">
<el-option v-for="item in riskTemplateOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="客户名称" prop="username">
<el-input v-model="editCustomerForm.username" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="是否升级成代理" prop="isLevelUp">
<el-radio-group v-model="editCustomerForm.isLevelUp" :disabled="editCustomerForm.roleId === 5">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择状态" prop="status">
<el-radio-group v-model="editCustomerForm.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="2">封禁</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</CustomerListDialog>
<!-- ===================== 新增客户详情抽屉 ===================== -->
<CustomerDetailDrawer
v-model:drawer-visible="detailDrawerVisible"
:query-id="detailQueryId"
:drawer-title="detailDrawerTitle"
/>
<CustomerDetailDrawer v-model:drawer-visible="detailDrawerVisible" :query-id="detailQueryId"
:drawer-title="detailDrawerTitle" />
</div>
</template>
@ -127,6 +142,11 @@ import {
getSelfVarietyPointDiffConfigApi,
editCustomerApi,
} from '@/api/modules/agent/customerList'
import { updateCapitalApi, editUserApi } from '@/api/modules/user'
import {
getFeeTemplateOptionsApi,
getRiskTemplateOptionsApi,
} from '@/api/modules/account/client'
//
import { getStorage } from '@/utils/storage'
@ -231,6 +251,61 @@ const commissionRatioFormRules = {
const commissionRatioFormItemOptions = ref(commissionRatioFormItem)
const selectId = ref('')
/**
* @description 定义修改客户弹窗数据
*/
const editCustomerVisible = ref(false)
const editCustomerFormRef = ref()
const editCustomerUserId = ref<number>(0)
const editCustomerForm = ref({
feeSettingId: '',
riskControlId: '',
username: '',
isLevelUp: 2,
status: 1,
roleId: 0,
})
const editCustomerRules = {
feeSettingId: [{ required: true, message: '请选择手续费模板', trigger: 'change' }],
riskControlId: [{ required: true, message: '请选择风控模板', trigger: 'change' }],
isLevelUp: [{ required: true, message: '请选择是否升级成代理', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}
const feeTemplateOptions = ref<any[]>([])
const riskTemplateOptions = ref<any[]>([])
const templateOptionsFetched = ref(false)
/**
* @description 定义调整金额弹窗数据
*/
const adjustAmountVisible = ref(false)
const adjustAmountFormRef = ref()
const adjustUserId = ref<number>(0)
const adjustAmountForm = ref({
username: '',
currency: '',
currentBalance: '',
adjustAmount: '',
afterBalance: '',
})
const adjustAmountRules = {
adjustAmount: [
{ required: true, message: '请输入调整金额', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value === '' || value === null || value === undefined) {
callback(new Error('请输入调整金额'))
} else if (!/^-?\d+(\.\d{1,5})?$/.test(value)) {
callback(new Error('请输入有效的金额格式,最多五位小数'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
/**
* @description: 角色映射方法
*/
@ -277,10 +352,10 @@ const getCustomerList = async (id: string) => {
children:
item.role_id === 5
? [
{
hasChildren: true,
},
]
{
hasChildren: true,
},
]
: undefined,
}))
console.log('tableTree', tableTree.value)
@ -399,10 +474,10 @@ const handleGetSubList = async (row: any) => {
children:
item.role_id === 5
? [
{
hasChildren: true,
},
]
{
hasChildren: true,
},
]
: undefined,
}))
}
@ -478,6 +553,134 @@ const handleClose = () => {
dialogVisible.value = false
}
/**
* @description 调整金额相关方法
*/
//
const handleAdjustAmount = (row: any) => {
adjustAmountVisible.value = true
adjustUserId.value = row.userId
// rowbalance
// 使使
const balance = row.balance || row.wallet_capital?.amount || '0'
adjustAmountForm.value = {
username: row.accountName,
currency: 'USDT', //
currentBalance: balance,
adjustAmount: '',
afterBalance: balance,
}
}
//
const handleAdjustAmountInput = () => {
const currentBalance = parseFloat(adjustAmountForm.value.currentBalance) || 0
const adjustAmount = parseFloat(adjustAmountForm.value.adjustAmount) || 0
const afterBalance = currentBalance + adjustAmount
adjustAmountForm.value.afterBalance = afterBalance.toFixed(5)
}
//
const handleAdjustAmountSubmit = async () => {
try {
await adjustAmountFormRef.value?.validate()
await updateCapitalApi({
target_user_id: adjustUserId.value,
amount: adjustAmountForm.value.adjustAmount,
})
await getCustomerList(userId.value)
adjustAmountVisible.value = false
} catch (err) {
console.error('调整金额表单验证失败', err)
}
}
//
const handleAdjustAmountCancel = () => {
adjustAmountVisible.value = false
}
//
const handleAdjustAmountClose = () => {
adjustAmountVisible.value = false
}
/**
* @description 修改客户相关方法
*/
//
const getTemplateOptions = async () => {
if (templateOptionsFetched.value) return
try {
const [feeTemplateRes, riskTemplateRes] = await Promise.all([
getFeeTemplateOptionsApi(),
getRiskTemplateOptionsApi(),
])
feeTemplateOptions.value = Object.entries(feeTemplateRes as Record<string, string>).map(
([label, value]) => ({
label,
value,
}),
)
riskTemplateOptions.value = Object.entries(riskTemplateRes as Record<string, string>).map(
([label, value]) => ({
label,
value,
}),
)
templateOptionsFetched.value = true
} catch (error) {
console.error('获取模板选项失败:', error)
}
}
//
const handleEditCustomer = async (row: any) => {
await getTemplateOptions()
editCustomerVisible.value = true
editCustomerUserId.value = row.userId
editCustomerForm.value = {
feeSettingId: row.feeSettingId || '',
riskControlId: row.riskControlId || '',
username: row.accountName || '',
isLevelUp: (row.role_id === 5) ? 1 : 2,
status: row.status === '正常' ? 1 : 2,
roleId: row.role_id,
}
}
//
const handleEditCustomerSubmit = async () => {
try {
await editCustomerFormRef.value?.validate()
await editUserApi({
user_id: editCustomerUserId.value,
fee_setting_id: editCustomerForm.value.feeSettingId,
risk_control_id: editCustomerForm.value.riskControlId,
username: editCustomerForm.value.username,
is_levelup: editCustomerForm.value.isLevelUp,
status: editCustomerForm.value.status,
})
await getCustomerList(userId.value)
editCustomerVisible.value = false
} catch (err) {
console.error('修改客户表单验证失败', err)
}
}
//
const handleEditCustomerCancel = () => {
editCustomerVisible.value = false
}
//
const handleEditCustomerClose = () => {
editCustomerVisible.value = false
}
</script>
<style scoped lang="scss">
@ -485,5 +688,4 @@ const handleClose = () => {
text-align: center;
color: #909399;
}
</style>

View File

@ -3,12 +3,12 @@ export const search = [
{ prop: 'accountName', label: '账号名称', type: 'input' as const, placeholder: '请输入账号名称', width: '150px' },
// 开始时间
{
prop: "startTime", label: '注册开始时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
prop: "startTime", label: '注册开始时间', type: 'date' as const, dateType: 'date' as const,placeholder: '请选择注册开始时间', width: '270px', // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
},
// 结束时间
{
prop: "endTime", label: '注册结束时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
prop: "endTime", label: '注册结束时间', type: 'date' as const, dateType: 'date' as const,placeholder: '请选择注册结束时间', width: '270px', // 时间范围选择器
valueFormat: 'YYYY-MM-DD',
},
// 状态
@ -17,23 +17,23 @@ export const search = [
export const tableColumns = [
// 账号名称
{ label: '账号名称', prop: 'accountName'},
{ label: '账号名称', prop: 'accountName', },
// 账号id
{ prop: 'userId', label: '账号id' },
// 状态
{ prop: 'status', label: '状态' },
// 账号角色
{ prop: 'role', label: '账号角色' },
{ prop: 'role', label: '账号角色', width: 100 },
// 账号数量
{ prop: 'accountQty', label: '账号数量' },
{ prop: 'accountQty', label: '账号数量', width: 100 },
// 点差分配比例
{ prop: 'marginRatio', label: '点差分配比例', slotName: 'marginRatio' },
{ prop: 'marginRatio', label: '点差分配比例', slotName: 'marginRatio' , width: 140},
// 手续费分成比例
{ prop: 'commissionRatio', label: '手续费分成比例', slotName: 'commissionRatio' },
{ prop: 'commissionRatio', label: '手续费分成比例', slotName: 'commissionRatio' , width: 140},
// 注册时间
{ prop: 'createTime', label: '注册时间', sortable: true },
{ prop: 'createTime', label: '注册时间', sortable: true , minWidth: 180},
// 操作
{ label: '操作', slotName: 'action' }
{ label: '操作', slotName: 'action', width: 200}
]
// 点差表格配置

View File

@ -3,7 +3,7 @@
<!-- 页面标题 -->
<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)">
@ -13,218 +13,318 @@
</RechangeTable>
</div>
<!-- 存款弹窗改为和设计图一致的支付面板布局 -->
<RechangeDialog class="rechange-pay-dialog" :visible="dialogVisible" title=" " width="640px" :show-footer="false"
<!-- 存款弹窗 -->
<RechangeDialog class="rechange-pay-dialog" :visible="dialogVisible" title="存款" width="600px" :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>
<el-form ref="rechangeFormRef" :model="rechangeForm" :rules="rechangeFormRules" label-position="top">
<!-- 渠道信息 -->
<div class="channel-card">
<div class="channel-info">
<div class="channel-name">{{ rechangeForm.channelName }}</div>
<div class="channel-meta">
<span></span>
</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 class="channel-meta">支持货币: {{ rechangeForm.currencySymbol }}</div>
</div>
<!-- 银行卡信息channel_code = BANK 时显示 -->
<template v-if="rechangeForm.channelCode === 'BANK'">
<div class="info-section">
<div class="section-title">收款信息</div>
<div class="info-grid">
<div class="info-item">
<span class="label">账户持有人姓名</span>
<span class="value">{{ rechangeForm.beneficiaryName }}</span>
</div>
<div class="info-item">
<span class="label">银行名称</span>
<span class="value">{{ rechangeForm.beneficiaryBankName }}</span>
</div>
<div class="info-item">
<span class="label">银行识别代码</span>
<span class="value">{{ rechangeForm.swiftBicCode }}</span>
</div>
<div class="info-item">
<span class="label">收款人银行账号</span>
<span class="value">{{ rechangeForm.beneficiaryBankAccount }}</span>
</div>
<div class="info-item full-width">
<span class="label">银行地址</span>
<span class="value">{{ rechangeForm.beneficiaryBankAddress }}</span>
</div>
</div>
</div>
</template>
<!-- 钱包地址信息其他渠道显示 -->
<template v-else>
<div class="info-section">
<div class="section-title">收款信息</div>
<div class="info-item full-width">
<span class="label">钱包收款地址</span>
<span class="value address-value">{{ rechangeForm.address }}</span>
</div>
</div>
</template>
<!-- 充值金额 -->
<el-form-item label="充值金额" prop="depositAmount">
<el-input v-model="rechangeForm.depositAmount" placeholder="请输入充值金额" size="large" @input="handleAmountInput">
<template #suffix>
<span class="currency-suffix">{{ rechangeForm.currencySymbol }}</span>
</template>
</el-input>
</el-form-item>
<!-- 上传凭证 -->
<el-form-item label="上传凭证" prop="voucherUrl">
<el-upload class="voucher-uploader" action="" :show-file-list="false"
:http-request="handleUpload" :before-upload="beforeAvatarUpload" :loading="uploadLoading">
<img v-if="voucherUrl" :src="voucherUrl" class="voucher-image" />
<div v-else class="upload-placeholder">
<el-icon class="upload-icon">
<Plus />
</el-icon>
<span class="upload-text">点击上传凭证</span>
</div>
</el-upload>
</el-form-item>
</el-form>
<!-- 按钮区 -->
<div class="dialog-actions">
<el-button size="large" @click="handleCancel">取消</el-button>
<el-button type="primary" size="large" :loading="submitLoading" @click="handleSubmit">
确认充值
</el-button>
</div>
</RechangeDialog>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
defineOptions({
name: 'Rechange'
name: 'Rechange'
})
import type { FormInstance, FormRules } from 'element-plus'
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
import { nextTick, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import RechangeTable from '@/components/common/Table.vue'
import RechangeDialog from '@/components/common/Dialog.vue'
import { rechangeQuickAmounts, rechangeTableColumns } from './options'
import { rechangeTableColumns } from './options'
import { uploadImageApi } from '@/api/modules/upload'
import { getRechargeChannelList, rechargeApi } from '@/api/modules/capital/rechangeChanl'
//
interface RechangeRow {
//
interface ChannelCurrency {
id: number
depositChannel: string
channelName: string
supportedCurrency: string
processingTime: string
singleAmount: string
depositAccount: string
amountCurrency: string
symbol: string
rate: string
}
//
interface RechangeFormData {
interface ChannelRow {
id: number
channelName: string
depositAccount: string
supportedCurrency: string
amountCurrency: string
depositAmount: string
name: string
channel_fee: string
type: number
beneficiary_name: string | null
beneficiary_bank_name: string | null
swift_bic_code: string | null
beneficiary_bank_address: string | null
beneficiary_bank_account: string | null
channel_code: string
currency: ChannelCurrency
address?: 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 submitLoading = ref(false)
//
const voucherUrl = ref('')
const uploadLoading = ref(false)
//
const channelList = ref<ChannelRow[]>([])
//
interface RechangeFormData {
id: number
channelName: string
channelCode: string
currencySymbol: string
channelFee: string
depositAmount: string
voucherUrl: string
beneficiaryName: string
beneficiaryBankName: string
swiftBicCode: string
beneficiaryBankAddress: string
beneficiaryBankAccount: string
address: string
}
//
const createDefaultForm = (): RechangeFormData => ({
id: 0,
channelName: '',
depositAccount: '',
supportedCurrency: '',
amountCurrency: '',
channelCode: '',
currencySymbol: '',
channelFee: '0',
depositAmount: '',
voucherUrl: '',
beneficiaryName: '',
beneficiaryBankName: '',
swiftBicCode: '',
beneficiaryBankAddress: '',
beneficiaryBankAccount: '',
address: '',
})
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'] },
{ required: true, message: '请输入充值金额', trigger: ['blur', 'change'] },
{ pattern: /^\d+$/, message: '请输入整数金额', trigger: ['blur', 'change'] },
],
voucherUrl: [
{ required: true, message: '请上传充值凭证', trigger: ['change'] },
],
})
//
const resetDialogForm = () => {
rechangeForm.value = createDefaultForm()
currentRowId.value = null
rechangeFormRef.value?.clearValidate()
//
const fetchChannelList = async () => {
try {
const res = await getRechargeChannelList() as ChannelRow[]
channelList.value = res || []
} catch (error) {
console.error('获取充值渠道列表失败', error)
}
}
//
const handleOpenDialog = async (row: RechangeRow) => {
currentRowId.value = row.id
//
const handleOpenDialog = async (row: ChannelRow) => {
rechangeForm.value = {
id: row.id,
channelName: row.channelName,
depositAccount: row.depositAccount,
supportedCurrency: row.supportedCurrency,
amountCurrency: row.amountCurrency,
channelName: row.name,
channelCode: row.channel_code,
currencySymbol: row.currency.symbol,
channelFee: row.channel_fee || '0',
depositAmount: '',
voucherUrl: '',
beneficiaryName: row.beneficiary_name || '',
beneficiaryBankName: row.beneficiary_bank_name || '',
swiftBicCode: row.swift_bic_code || '',
beneficiaryBankAddress: row.beneficiary_bank_address || '',
beneficiaryBankAccount: row.beneficiary_bank_account || '',
address: (row as any).address || '',
}
voucherUrl.value = ''
dialogVisible.value = true
await nextTick()
rechangeFormRef.value?.clearValidate()
}
//
const handleQuickAmount = (amount: string) => {
rechangeForm.value.depositAmount = amount
//
const handleAvatarSuccess: UploadProps['onSuccess'] = (response: any) => {
if (response.code === 0) {
voucherUrl.value = response.url
rechangeForm.value.voucherUrl = response.url
ElMessage.success('上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
}
uploadLoading.value = false
}
//
const handlePrevStep = () => {
dialogVisible.value = false
resetDialogForm()
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('图片格式必须为 JPG 或 PNG!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
uploadLoading.value = true
return true
}
//
//
const handleUpload = async (options: any) => {
const { file, onSuccess, onError } = options
try {
const res: any = await uploadImageApi(file)
voucherUrl.value = res.url
rechangeForm.value.voucherUrl = res.url
onSuccess(res)
//
nextTick(() => {
rechangeFormRef.value?.clearValidate('voucherUrl')
})
} catch (error) {
onError(error)
uploadLoading.value = false
}
}
//
const handleAmountInput = (value: string) => {
const filtered = value.replace(/\D/g, '')
rechangeForm.value.depositAmount = filtered
}
//
const handleSubmit = async () => {
try {
await rechangeFormRef.value?.validate()
ElMessage.success(`已进入支付确认流程渠道ID${currentRowId.value}`)
submitLoading.value = true
await rechargeApi({
channel_id: rechangeForm.value.id,
amount: rechangeForm.value.depositAmount,
img: rechangeForm.value.voucherUrl,
})
ElMessage.success('充值提交成功')
dialogVisible.value = false
resetDialogForm()
} catch (error) {
console.error('存款表单校验失败', error)
fetchChannelList()
} catch (error: any) {
ElMessage.error(error?.msg || '充值提交失败')
} finally {
submitLoading.value = false
}
}
//
const handleCancel = () => {
dialogVisible.value = false
resetDialogForm()
rechangeFormRef.value?.resetFields()
rechangeForm.value = createDefaultForm()
}
//
const handleClose = () => {
dialogVisible.value = false
resetDialogForm()
rechangeFormRef.value?.resetFields()
rechangeForm.value = createDefaultForm()
}
//
onMounted(() => {
fetchChannelList()
})
</script>
<style scoped lang="scss">
.rechange-page {
height: 100%;
//
max-height: 100%;
height: 100%;
.page-title {
margin-bottom: 16px;
font-size: 28px;
@ -233,99 +333,172 @@ const handleClose = () => {
}
}
.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 {
.channel-card {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
color: #fff;
:deep(.el-button) {
min-width: 72px;
margin-left: 0;
color: #3b82f6;
border-color: #3b82f6;
.channel-info {
.channel-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.channel-meta {
display: flex;
gap: 16px;
font-size: 14px;
opacity: 0.9;
}
}
.channel-fee {
font-size: 16px;
font-weight: 600;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
}
}
.info-section {
background: #f5f7fa;
border-radius: 10px;
padding: 16px;
margin-bottom: 20px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
&.full-width {
grid-column: span 2;
}
.label {
font-size: 13px;
color: #909399;
}
.value {
font-size: 14px;
color: #303133;
word-break: break-all;
&.address-value {
font-family: monospace;
background: #fff;
padding: 8px;
border-radius: 4px;
}
}
}
}
}
.currency-suffix {
font-size: 16px;
font-weight: 500;
color: #606266;
}
//
.voucher-uploader {
width: 100%;
height: 160px;
border: 1px dashed var(--el-border-color);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
:deep(.el-upload) {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.voucher-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #909399;
.upload-icon {
font-size: 32px;
}
.upload-text {
font-size: 14px;
}
}
}
//
.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;
gap: 12px;
margin-top: 24px;
.el-button {
flex: 1;
height: 44px;
font-size: 16px;
border-radius: 8px;
}
}
// Element Plus Dialog
:deep(.rechange-pay-dialog .el-dialog__header) {
display: none;
padding: 20px 24px;
border-bottom: 1px solid #e4e7ed;
}
:deep(.rechange-pay-dialog .el-dialog__body) {
padding: 26px 26px 30px;
padding: 24px;
}
:deep(.rechange-pay-dialog .el-dialog) {
border-radius: 12px;
overflow: hidden;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
</style>

View File

@ -1,12 +1,11 @@
// 存款列表表格列配置:当前页面仍然保留列表 + 行内操作按钮的结构。
// 存款列表表格列配置
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 },
{ prop: 'name', label: '渠道名称', align: 'center' as const },
{
prop: 'currency.symbol',
label: '支持货币',
align: 'center' as const
},
{ prop: 'channel_fee', 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

@ -92,7 +92,9 @@ const formData = ref({
beneficiary_name: '',
beneficiary_bank_name: '',
swift_bic_code: '',
beneficiary_bank_account: '',
beneficiary_bank_address: '',
address: '',
channel_code: '',
status: 1,
})
@ -128,7 +130,9 @@ const resetFormData = () => {
beneficiary_name: '',
beneficiary_bank_name: '',
swift_bic_code: '',
bank_account: '',
beneficiary_bank_address: '',
address: '',
channel_code: '',
}
}
@ -195,3 +199,4 @@ onMounted(() => {
}
}
</style>

View File

@ -52,12 +52,24 @@ export const formItemsData = [
type: 'input',
placeholder: '请输入银行识别代码',
},
{
label: '银行账号',
prop: 'beneficiary_bank_account',
type: 'input',
placeholder: '请输入银行账号',
},
{
label: '银行地址',
prop: 'beneficiary_bank_address',
type: 'input',
placeholder: '请输入银行地址',
},
{
label: 'U钱包收款地址',
prop: 'address',
type: 'input',
placeholder: '请输入U钱包收款地址',
},
{ label: '渠道编码', prop: 'channel_code', type: 'input', placeholder: '请输入渠道编码' },
{ label: '状态', prop: 'status', type: 'radio' },
]
@ -73,7 +85,9 @@ export const formRulesData = {
beneficiary_name: [{ required: true, message: '请输入账户持有人姓名', trigger: ['change'] }],
beneficiary_bank_name: [{ required: true, message: '请输入银行名称', trigger: ['change'] }],
swift_bic_code: [{ required: true, message: '请输入银行识别代码', trigger: ['change'] }],
beneficiary_bank_account: [{ required: true, message: '请输入银行账号', trigger: ['change'] }],
beneficiary_bank_address: [{ required: true, message: '请输入银行地址', trigger: ['change'] }],
address: [{ required: true, message: '请输入U钱包收款地址', trigger: ['change'] }],
channel_code: [{ required: true, message: '请输入渠道编码', trigger: ['change'] }],
}

View File

@ -85,37 +85,28 @@
</TransferForm>
<div class="transfer-type verify" v-if="transferType === 'external'">
<div class="type-title">验证方式</div>
<div class="tab">
<el-button
:type="verifyType === 'email' ? 'primary' : 'default'"
@click="handleVerifyTabClick('email')"
>邮箱验证</el-button
>
<el-button
:type="verifyType === 'phone' ? 'primary' : 'default'"
@click="handleVerifyTabClick('phone')"
>手机号验证</el-button
>
</div>
<div class="username">{{ verifyType === 'email' ? userInfo.email : userInfo.phone }}</div>
<el-form
class="verify-form"
:model="transferForm"
:rules="transferFormRules"
:model="verifyForm"
:rules="verifyFormRules"
ref="verifyFormRef"
>
<el-form-item prop="code">
<el-input
v-model="transferForm.code"
style="max-width: 300px"
placeholder="请输入验证码"
>
</el-input>
<el-button type="primary" @click="handleSendCode" :disabled="sendState.isCounting">{{
sendState.isCounting ? `${sendState.countdown}s后重新获取` : '发送验证码'
}}</el-button>
<el-form-item :label="t('personalCenter.verifyMethod')" prop="verifyType">
<el-radio-group v-model="verifyForm.verifyType" @change="onVerifyTypeChange">
<el-radio label="email" :disabled="!userInfo.email">邮箱验证</el-radio>
<el-radio label="phone" :disabled="!userInfo.mobile">手机号验证</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('personalCenter.verifyAccount')">
<span class="verify-account">{{ verifyForm.verifyType === 'email' ? userInfo.email : userInfo.mobile }}</span>
</el-form-item>
<UserCaptcha
prop="code"
v-model="verifyForm.code"
@sendCode="handleSendCode"
:show-send-button="true"
:send-state="sendState"
/>
</el-form>
</div>
<el-button type="primary" @click="handleSubmit" v-auth="'user_transfer'">提交转账</el-button>
@ -129,10 +120,13 @@ defineOptions({
})
//
import TransferForm from '@/components/common/Form.vue'
import UserCaptcha from '@/views/user/components/Captcha.vue'
import { useI18n } from 'vue-i18n'
//
import { internalFormItem, externalFormItem } from './options'
import { rules } from './rules'
import { codeRules } from '@/views/user/config/rules'
//
import { getTransferWalletOptionsApi, transferApi } from '@/api/modules/funding/transfer'
@ -142,7 +136,10 @@ import { getStorage } from '@/utils/storage'
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
console.log('userStore:', userStore);
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
const { t } = useI18n()
/**
* @description 转账类型
* @default internal
@ -160,6 +157,13 @@ const createDefaultTransferForm = () => ({
//
MaxOutAmount: '',
type: '',
})
/**
* @description 创建默认验证码表单数据
*/
const createDefaultVerifyForm = () => ({
verifyType: 'email',
code: '',
})
@ -167,11 +171,25 @@ const createDefaultTransferForm = () => ({
* @description 转账表单数据
*/
const transferForm = ref(createDefaultTransferForm())
/**
* @description 验证码表单数据
*/
const verifyForm = ref(createDefaultVerifyForm())
/**
* @description 主转账表单和验证码表单引用
*/
const mainTransferFormRef = ref()
const verifyFormRef = ref()
/**
* @description 验证码表单校验规则
*/
const verifyFormRules = ref({
verifyType: [{ required: true, message: t('personalCenter.pleaseSelectVerifyMethod'), trigger: 'change' }],
code: codeRules().code,
})
const transferFormItemOptions = computed(() =>
transferType.value === 'internal' ? internalFormItem : externalFormItem,
)
@ -202,10 +220,9 @@ const transferFormOptionsMap = ref({
*/
//userInfo
const userInfo = getStorage<any>('userInfo')
const verifyType = ref('email')
//
const sendState = ref<any>({
//
const sendState = ref({
isCounting: false,
countdown: 0,
})
@ -220,78 +237,85 @@ const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaS
//
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
// sendState
const updateSendState = (type: string) => {
if (type === 'email') {
sendState.value = {
isCounting: emailIsCounting,
countdown: emailCountdown,
}
} else {
sendState.value = {
isCounting: phoneIsCounting,
countdown: phoneCountdown,
}
}
}
// activeTabsendState
const initSendState = () => {
const currentType = verifyType.value === 'email' ? 'email' : 'phone'
updateSendState(currentType)
}
onMounted(() => {
//
initSendState()
})
// verifyTypesendState
watch(
verifyType,
(newTab, oldTab) => {
verifyType.value = newTab
console.log('verifyType.value', verifyType.value)
// taboldTaboldTabundefined
if (oldTab === undefined) return
if (newTab === 'email' || newTab === 'phone') {
// /
updateSendState(newTab)
} else if (newTab === 'code') {
//
const currentType: string = getStorage('forgotType', 'session') || 'email'
updateSendState(currentType)
}
},
{ immediate: false }, //
//
const currentIsCounting = computed(() =>
verifyForm.value.verifyType === 'email' ? emailIsCounting.value : phoneIsCounting.value
)
const currentCountdown = computed(() =>
verifyForm.value.verifyType === 'email' ? emailCountdown.value : phoneCountdown.value
)
//
const sendCodeApi = async () => {
//
const sendParams = {
email: userInfo.email,
mobile: userInfo.mobile,
reg_type: verifyType.value === 'email' ? 1 : 2,
type: 'resetPass_code',
// sendState
const updateSendState = () => {
const type = verifyForm.value.verifyType
//
sendState.value = {
isCounting: type === 'email' ? emailIsCounting.value : phoneIsCounting.value,
countdown: type === 'email' ? emailCountdown.value : phoneCountdown.value,
}
return userStore.sendCode(sendParams)
}
// sendState
watch(
emailIsCounting,
() => updateSendState()
)
watch(
emailCountdown,
() => updateSendState()
)
watch(
phoneIsCounting,
() => updateSendState()
)
watch(
phoneCountdown,
() => updateSendState()
)
//
const onVerifyTypeChange = () => {
verifyForm.value.code = ''
nextTick(() => {
verifyFormRef.value?.clearValidate(['code'])
})
}
onMounted(() => {
//
})
//
const sendCodeApi = async (receiver: string) => {
//
const type = verifyForm.value.verifyType
const params: any = {
reg_type: userStore.userInfo!.reg_type,
type: 'transfer_code',
}
if (type === 'email') {
params.email = receiver
} else {
params.mobile = receiver
//
const match = receiver.match(/^\+(\d{1,4})/)
params.mobile_area = match ? '+' + match[1] : '+86'
}
return userStore.sendCode(params)
}
//
const handleSendCode = async () => {
const currentType = verifyType.value
// /
const type: string = currentType || 'email'
const type = verifyForm.value.verifyType
if (type === 'phone') {
// 1
if (!userInfo.phone) {
if (!userInfo.mobile) {
ElMessage.error('请先绑定手机号')
return
}
//
await sendCaptcha(type, userInfo.phone, phoneCaptchaState, () => sendCodeApi())
updateSendState(type)
await sendCaptcha(type, userInfo.mobile, phoneCaptchaState, sendCodeApi)
} else {
// 2
if (!userInfo.email) {
@ -300,8 +324,7 @@ const handleSendCode = async () => {
}
//
await sendCaptcha(type, userInfo.email, emailCaptchaState, () => sendCodeApi())
updateSendState(type)
await sendCaptcha(type, userInfo.email, emailCaptchaState, sendCodeApi)
}
}
@ -357,6 +380,9 @@ const resetTransferForm = async () => {
MaxOutAmount: getDefaultMaxOutAmount(),
})
//
Object.assign(verifyForm.value, createDefaultVerifyForm())
await nextTick()
verifyFormRef.value?.clearValidate?.()
}
@ -376,16 +402,12 @@ const handleTransferTabClick = async (type: string) => {
* @description 切换验证方式
* @param type 验证方式
*/
const handleVerifyTabClick = (type: string) => {
verifyType.value = type
}
const handleVerifyParams = (type: string) => {
if (type === 'external') {
if (verifyType.value === 'email') {
if (verifyForm.value.verifyType === 'email') {
return userInfo.email
} else {
return userInfo.phone
return userInfo.mobile
}
} else {
return transferForm.value.transferInAccount
@ -418,8 +440,8 @@ const handleSubmit = async () => {
} else {
await transferApi({
...params,
verify_type: verifyType.value === 'email' ? '1' : '2',
verify_code: transferForm.value.code,
verify_type: verifyForm.value.verifyType === 'email' ? '1' : '2',
verify_code: verifyForm.value.code,
})
}
@ -503,17 +525,26 @@ const handleSubmit = async () => {
}
.verify {
.tab,
.username {
margin-bottom: 10px;
:deep(.el-radio-group) {
display: flex;
gap: 20px;
}
.verify-account {
font-weight: 500;
color: #333;
}
}
.verify-form {
.el-form-item {
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
max-width: 500px;
:deep(.el-form-item) {
margin-bottom: 20px;
}
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
}
}

View File

@ -194,7 +194,7 @@
>
<el-button
:type="dialogType === 'leverage' ? 'primary' : 'default'"
@click="dialogType = 'leverage'"
@click="handleChangeToLeverage"
>修改杠杆</el-button
>
</div>
@ -249,7 +249,7 @@ const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogType = ref('add')
const accountList = ref([])
const accountList = ref<any[]>([])
const getAccoundList = async () => {
const res = await getAllAccount({ username: '' })
console.log(res)
@ -432,6 +432,18 @@ const handleSetting = (id: string) => {
dialogType.value = 'setting'
}
/**
* @description: 修改杠杆
*/
const handleChangeToLeverage = () => {
//
const currentAccount = accountList.value.find((item: any) => item.account_id === account_id)
if (currentAccount?.wallets_trade?.lever) {
subAccountForm.value.leverage = currentAccount.wallets_trade.lever
}
dialogType.value = 'leverage'
}
/**
* @description: 复制推广二维码和推广链接方法
*/

View File

@ -35,18 +35,27 @@ import { search, tradeOrderTableColumns, positionOrderTableColumns } from './opt
import { getCustomTradeOrderListApi, getSubAccountListApi } from '@/api/modules/agent/customerTradeOrder'
//
import { getStorage } from '@/utils/storage'
//
import dayjs from 'dayjs'
//
/**
* @description: 定义搜索数据
*/
//
const dateRange = {
start: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
end: dayjs().format('YYYY-MM-DD'),
}
const searchForm = ref({
accountName: '',
orderId: '2',
tradeType: '',
startTime: '',
endTime: '',
start_time: dateRange.start,
end_time: dateRange.end,
startTime: dateRange.start,
endTime: dateRange.end,
subAccountName: '',
})
const searchOptions = ref(search)
@ -78,8 +87,8 @@ const getCustomerTradeOrderList = async () => {
account_id: searchForm.value.subAccountName,
order_no: '',
type: searchForm.value.tradeType,
start_time: '',
end_time: '',
start_time: dateRange.start,
end_time: dateRange.end,
}
console.log(params)
const data: any = await getCustomTradeOrderListApi(params)
@ -94,6 +103,7 @@ const getCustomerTradeOrderList = async () => {
quantity: item.num,
openPrice: item.build_price,
closePrice: item.close_price,
closeProfitLoss: item.profit_loss,
amount: item.amount,
createdTime: item.created_at,
}))
@ -147,8 +157,10 @@ const handleReset = () => {
accountName: '',
orderId: '',
tradeType: '',
startTime: '',
endTime: '',
startTime: dateRange.start,
endTime: dateRange.end,
start_time: dateRange.start,
end_time: dateRange.end,
subAccountName: '',
}
getCustomerTradeOrderList()

View File

@ -110,7 +110,7 @@ export const positionOrderTableColumns = [
//平仓价格
{ label: '平仓价格', prop: 'closePrice' },
//金额
{ label: '金额', prop: 'amount' },
{ label: '平仓盈亏', prop: 'closeProfitLoss', align: 'center' as const, width: '150px' },
//创建时间
{ label: '创建时间', prop: 'createdTime' },
]

View File

@ -1,99 +1,107 @@
<template>
<div>
<!-- 搜索 -->
<CustomerListSearch
:search-form="searchForm"
:search-options="searchOptions"
@search="handleSearch"
@reset="handleReset"
></CustomerListSearch>
<CustomerListSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset"></CustomerListSearch>
<!-- 表格 -->
<CustomerListTable
:table-data="tableTree"
:columns="tableColumnsOptions"
row-key="userId"
:default-sort="{ prop: 'createTime', order: 'descending' }"
@expand-change="handleGetSubList"
>
<CustomerListTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="userId"
:default-sort="{ prop: 'createTime', order: 'descending' }" @expand-change="handleGetSubList">
<template #marginRatio="{ row }">
<el-button
v-if="row.role_id === 5"
type="primary"
size="small"
v-auth="'agent_getSelfVarietyPointDiffConfig'"
@click="handleMarginRatio(row)"
>
<el-button v-if="row.role_id === 5" type="primary" size="small" v-auth="'agent_getSelfVarietyPointDiffConfig'"
@click="handleMarginRatio(row)">
修改
</el-button>
</template>
<template #commissionRatio="{ row }">
{{ row.commissionRatio }}
<el-button
v-if="row.role_id === 5"
type="primary"
size="small"
@click="handleCommissionRatio(row)"
v-auth="'agent_editAgent'"
>
<el-button v-if="row.role_id === 5" type="primary" size="small" @click="handleCommissionRatio(row)"
v-auth="'agent_editAgent'">
修改
</el-button>
</template>
<template #action="{ row }">
<el-button link type="primary" size="small" @click="handleDetail(row)" v-if="row.userId">
<!-- <el-button link type="primary" size="small" @click="handleDetail(row)" v-if="row.userId">
查看详情
</el-button> -->
<!-- <el-divider direction="vertical" /> -->
<el-button link type="primary" size="small" @click="handleEditCustomer(row)">
编辑
</el-button>
<el-divider direction="vertical" />
<el-button link type="warning" size="small" @click="handleAdjustAmount(row)">
调整金额
</el-button>
</template>
</CustomerListTable>
<!-- 分页 -->
<CustomerListPagination
v-model="currentPage"
v-model:pageSizeValue="pageSize"
:total="total"
@pagination-change="handlePaginationChange"
/>
<CustomerListPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
<!-- dialog -->
<CustomerListDialog
:visible="dialogVisible"
:title="dialogTitle"
:type="dialogType"
:fullscreen="dialogFullscreen"
:show-footer="dialogType !== 'view'"
@confirm="handleSubmit"
@cancel="handleCancel"
@close="handleClose"
>
<CustomerListTable
:table-data="marginRatioTree"
:columns="marginRatioTableColumnsOptions"
row-key="id"
v-if="dialogType === 'editMarginRatio'"
style="height: 500px"
>
<CustomerListDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" :fullscreen="dialogFullscreen"
:show-footer="dialogType !== 'view'" @confirm="handleSubmit" @cancel="handleCancel" @close="handleClose">
<CustomerListTable :table-data="marginRatioTree" :columns="marginRatioTableColumnsOptions" row-key="id"
v-if="dialogType === 'editMarginRatio'" style="height: 500px">
<template #assignedPointDiff="{ row }">
<el-form
:model="row"
:rules="marginRatioRules(row)"
:ref="(el) => setFormRef(el, row.id)"
>
<el-form :model="row" :rules="marginRatioRules(row)" :ref="(el) => setFormRef(el, row.id)">
<el-form-item prop="assignedPointDifference">
<el-input
v-model="row.assignedPointDifference"
size="small"
placeholder="请输入点差"
/>
<el-input v-model="row.assignedPointDifference" size="small" placeholder="请输入点差" />
</el-form-item>
</el-form>
</template>
</CustomerListTable>
<CustomerListForm
:form-data="commissionRatioForm"
:form-rules="commissionRatioFormRules"
:form-items="commissionRatioFormItemOptions"
ref="commissionRatioFormRef"
v-if="dialogType === 'editCommissionRatio'"
></CustomerListForm>
<CustomerListForm :form-data="commissionRatioForm" :form-rules="commissionRatioFormRules"
:form-items="commissionRatioFormItemOptions" ref="commissionRatioFormRef"
v-if="dialogType === 'editCommissionRatio'"></CustomerListForm>
</CustomerListDialog>
<!-- 调整金额弹窗 -->
<CustomerListDialog :visible="adjustAmountVisible" title="调整金额" type="adjust" @confirm="handleAdjustAmountSubmit"
@cancel="handleAdjustAmountCancel" @close="handleAdjustAmountClose">
<el-form :model="adjustAmountForm" :rules="adjustAmountRules" ref="adjustAmountFormRef" label-width="120px">
<el-form-item label="账号名" prop="username">
<el-input v-model="adjustAmountForm.username" disabled />
</el-form-item>
<el-form-item label="币种" prop="currency">
<el-input v-model="adjustAmountForm.currency" disabled />
</el-form-item>
<el-form-item label="当前余额" prop="currentBalance">
<el-input v-model="adjustAmountForm.currentBalance" disabled />
</el-form-item>
<el-form-item label="调整金额" prop="adjustAmount">
<el-input v-model="adjustAmountForm.adjustAmount" placeholder="请输入调整金额(可为负数)"
@input="handleAdjustAmountInput" />
</el-form-item>
<el-form-item label="调整后余额" prop="afterBalance">
<el-input v-model="adjustAmountForm.afterBalance" disabled />
</el-form-item>
</el-form>
</CustomerListDialog>
<!-- 修改客户弹窗 -->
<CustomerListDialog :visible="editCustomerVisible" title="修改客户" type="editCustomer"
@confirm="handleEditCustomerSubmit" @cancel="handleEditCustomerCancel" @close="handleEditCustomerClose">
<el-form :model="editCustomerForm" :rules="editCustomerRules" ref="editCustomerFormRef" label-width="140px">
<el-form-item label="手续费模板" prop="feeSettingId">
<el-select v-model="editCustomerForm.feeSettingId" placeholder="请选择手续费模板" style="width: 100%">
<el-option v-for="item in feeTemplateOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="风控模板" prop="riskControlId">
<el-select v-model="editCustomerForm.riskControlId" placeholder="请选择风控模板" style="width: 100%">
<el-option v-for="item in riskTemplateOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="客户名称" prop="username">
<el-input v-model="editCustomerForm.username" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="选择状态" prop="status">
<el-radio-group v-model="editCustomerForm.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="2">封禁</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</CustomerListDialog>
<!-- dialog表格 -->
</div>
</template>
@ -113,6 +121,8 @@ import { search, tableColumns, marginRatioTableColumns, commissionRatioFormItem
//
import { getSelfVarietyPointDiffConfigApi, editCustomerApi } from '@/api/modules/agent/customerList'
import { updateCapitalApi, editUserApi } from '@/api/modules/user'
import { getFeeTemplateOptionsApi, getRiskTemplateOptionsApi } from '@/api/modules/account/client'
import { getAgentListApi } from '@/api/modules/profile/agent.ts'
//
@ -212,6 +222,59 @@ const commissionRatioFormRules = {
const commissionRatioFormItemOptions = ref(commissionRatioFormItem)
const selectId = ref('')
/**
* @description 定义修改客户弹窗数据
*/
const editCustomerVisible = ref(false)
const editCustomerFormRef = ref()
const editCustomerUserId = ref<number>(0)
const editCustomerForm = ref({
feeSettingId: '',
riskControlId: '',
username: '',
status: 1,
roleId: 0,
})
const editCustomerRules = {
feeSettingId: [{ required: true, message: '请选择手续费模板', trigger: 'change' }],
riskControlId: [{ required: true, message: '请选择风控模板', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}
const feeTemplateOptions = ref<any[]>([])
const riskTemplateOptions = ref<any[]>([])
const templateOptionsFetched = ref(false)
/**
* @description 定义调整金额弹窗数据
*/
const adjustAmountVisible = ref(false)
const adjustAmountFormRef = ref()
const adjustUserId = ref<number>(0)
const adjustAmountForm = ref({
username: '',
currency: '',
currentBalance: '',
adjustAmount: '',
afterBalance: '',
})
const adjustAmountRules = {
adjustAmount: [
{ required: true, message: '请输入调整金额', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value === '' || value === null || value === undefined) {
callback(new Error('请输入调整金额'))
} else if (!/^-?\d+(\.\d{1,5})?$/.test(value)) {
callback(new Error('请输入有效的金额格式,最多五位小数'))
} else {
callback()
}
},
trigger: 'blur',
},
],
}
/**
* @description: 角色映射方法
*/
@ -355,10 +418,10 @@ const handleGetSubList = async (row: any) => {
children:
item.role_id === 5
? [
{
hasChildren: true,
},
]
{
hasChildren: true,
},
]
: undefined,
}))
}
@ -435,6 +498,131 @@ const handleClose = () => {
/**
* @description: 定义弹窗表格方法
*/
/**
* @description 调整金额相关方法
*/
//
const handleAdjustAmount = (row: any) => {
adjustAmountVisible.value = true
adjustUserId.value = row.userId
const balance = row.balance || row.wallet_capital?.amount || '0'
adjustAmountForm.value = {
username: row.accountName,
currency: 'USDT',
currentBalance: balance,
adjustAmount: '',
afterBalance: balance,
}
}
//
const handleAdjustAmountInput = () => {
const currentBalance = parseFloat(adjustAmountForm.value.currentBalance) || 0
const adjustAmount = parseFloat(adjustAmountForm.value.adjustAmount) || 0
const afterBalance = currentBalance + adjustAmount
adjustAmountForm.value.afterBalance = afterBalance.toFixed(5)
}
//
const handleAdjustAmountSubmit = async () => {
try {
await adjustAmountFormRef.value?.validate()
await updateCapitalApi({
target_user_id: adjustUserId.value,
amount: adjustAmountForm.value.adjustAmount,
})
await getCustomerList()
adjustAmountVisible.value = false
} catch (err) {
console.error('调整金额表单验证失败', err)
}
}
//
const handleAdjustAmountCancel = () => {
adjustAmountVisible.value = false
}
//
const handleAdjustAmountClose = () => {
adjustAmountVisible.value = false
}
/**
* @description 修改客户相关方法
*/
//
const getTemplateOptions = async () => {
if (templateOptionsFetched.value) return
try {
const [feeTemplateRes, riskTemplateRes] = await Promise.all([
getFeeTemplateOptionsApi(),
getRiskTemplateOptionsApi(),
])
feeTemplateOptions.value = Object.entries(feeTemplateRes as Record<string, string>).map(
([label, value]) => ({
label,
value,
}),
)
riskTemplateOptions.value = Object.entries(riskTemplateRes as Record<string, string>).map(
([label, value]) => ({
label,
value,
}),
)
templateOptionsFetched.value = true
} catch (error) {
console.error('获取模板选项失败:', error)
}
}
//
const handleEditCustomer = async (row: any) => {
await getTemplateOptions()
editCustomerVisible.value = true
editCustomerUserId.value = row.userId
editCustomerForm.value = {
feeSettingId: row.feeSettingId || '',
riskControlId: row.riskControlId || '',
username: row.accountName || '',
status: row.status === '正常' ? 1 : 2,
roleId: row.role_id,
}
}
//
const handleEditCustomerSubmit = async () => {
try {
await editCustomerFormRef.value?.validate()
await editUserApi({
user_id: editCustomerUserId.value,
fee_setting_id: editCustomerForm.value.feeSettingId,
risk_control_id: editCustomerForm.value.riskControlId,
username: editCustomerForm.value.username,
is_levelup: null,
status: editCustomerForm.value.status,
})
await getCustomerList()
editCustomerVisible.value = false
} catch (err) {
console.error('修改客户表单验证失败', err)
}
}
//
const handleEditCustomerCancel = () => {
editCustomerVisible.value = false
}
//
const handleEditCustomerClose = () => {
editCustomerVisible.value = false
}
</script>
<style scoped lang="scss">

View File

@ -46,7 +46,7 @@ export const tableColumns = [
// 注册时间
{ prop: 'createTime', label: '注册时间', sortable: true },
// 操作
// { label: '操作', slotName: 'action' }
{ label: '操作', slotName: 'action', width: '150px'}
]
// 点差表格配置

View File

@ -93,81 +93,69 @@
@close="handleClose"
>
<!-- 表单 -->
<MarketForm
:form-data="marketForm"
:form-rules="marketFormRules"
:form-items="marketFormItemOptions"
:options-map="marketFormOptionsMap"
ref="marketFormRef"
v-if="dialogType !== 'view'"
>
<template #phoneOrEmail="{ formData }" v-if="dialogType === 'add'">
<div class="phone-email">
<div class="tab-btn">
<el-button
:type="activeTab === 'email' ? 'primary' : 'default'"
size="small"
@click="activeTab = 'email'"
>
邮箱
</el-button>
<el-button
:type="activeTab === 'phone' ? 'primary' : 'default'"
size="small"
@click="activeTab = 'phone'"
>
手机号
</el-button>
<div v-if="dialogType !== 'view'" class="drawer-form">
<MarketForm :form-data="marketForm" :form-rules="marketFormRules" :form-items="marketFormItemOptions"
:options-map="marketFormOptionsMap" ref="marketFormRef">
<template #phoneOrEmail="{ formData }" v-if="dialogType === 'add'">
<div class="phone-email">
<div class="tab-btn">
<el-button :type="activeTab === 'email' ? 'primary' : 'default'" size="small"
@click="activeTab = 'email'">
邮箱
</el-button>
<el-button :type="activeTab === 'phone' ? 'primary' : 'default'" size="small"
@click="activeTab = 'phone'">
手机号
</el-button>
</div>
<div class="group">
<el-form-item prop="email" v-if="activeTab === 'email'">
<el-input v-model="marketForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item prop="phone" class="phone-item" v-if="activeTab === 'phone'">
<el-select v-model="marketForm.countryCode" placeholder="请选择国家区号">
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
<div class="country-option">
<span class="country-name">{{ item.name }}</span>
<span class="country-code">{{ item.code }}</span>
</div>
</el-option>
</el-select>
<el-input v-model="marketForm.phone" placeholder="请输入手机号" />
</el-form-item>
</div>
</div>
<div class="group">
<el-form-item prop="email" v-if="activeTab === 'email'">
<el-input v-model="marketForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item prop="phone" class="phone-item" v-if="activeTab === 'phone'">
<el-select v-model="marketForm.countryCode" placeholder="请选择国家区号">
<el-option
v-for="item in countryOptions"
:key="item.code"
:label="item.code"
:value="item.code"
>
<div class="country-option">
<span class="country-name">{{ item.name }}</span>
<span class="country-code">{{ item.code }}</span>
</div>
</el-option>
</el-select>
<el-input v-model="marketForm.phone" placeholder="请输入手机号" />
</el-form-item>
</div>
</div>
</template>
</MarketForm>
</template>
</MarketForm>
</div>
<!-- 点差表格 -->
<MarketTable
:table-data="marketPointTree"
:columns="marketPointTableColumnsOptions"
row-key="id"
style="height: 300px"
>
<template #assignedPointDiff="{ row }">
<el-form :model="row" :rules="rules(marketForm, row)">
<el-form-item prop="pointDifference">
<el-input v-model="row.pointDifference" size="small" placeholder="请输入点差" />
</el-form-item>
</el-form>
</template>
</MarketTable>
</MarketDialog>
<div class="drawer-table">
<div class="table-header" v-if="dialogType !== 'view'">点差配置</div>
<div class="table-wrapper">
<MarketTable :table-data="marketPointTree" :columns="marketPointTableColumnsOptions" row-key="id">
<template #assignedPointDiff="{ row }">
<el-form :model="row" :rules="rules(marketForm, row)">
<el-form-item prop="pointDifference">
<el-input v-model="row.pointDifference" size="small" placeholder="请输入点差" />
</el-form-item>
</el-form>
</template>
</MarketTable>
</div>
</div>
<MarketDialog
:visible="rightClickDialogVisible"
:title="rightClickDialogTitleEnum[rightClickDialogType]"
@confirm="handleRightClickDialogSubmit"
@cancel="handleRightClickDialogCancel"
@close="handleRightClickDialogClose"
>
<!-- 底部按钮 -->
<template #footer v-if="dialogType !== 'view'">
<div class="drawer-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</template>
</el-drawer>
<MarketDialog :visible="rightClickDialogVisible" :title="rightClickDialogTitleEnum[rightClickDialogType]"
@confirm="handleRightClickDialogSubmit" @cancel="handleRightClickDialogCancel" @close="handleRightClickDialogClose">
<!-- 'account' | 'acceptOrders' | 'transfer' | 'deposit' | 'withdraw' -->
<MarketForm
:form-data="rightClickMarketForm"
@ -178,6 +166,7 @@
</template>
<script setup lang="ts">
defineOptions({
name: 'Business',
})
@ -247,7 +236,6 @@ const currentPage = ref(1)
const dialogVisible = ref<boolean>(false)
const dialogTitle = ref<string>('')
const dialogType = ref<string>('add')
const dialogFullscreen = ref(false)
/**
* @description 定义dialog表单数据
@ -269,7 +257,14 @@ const marketForm = ref({
})
const marketFormRules = rules(marketForm.value)
const marketFormRef = ref()
const marketFormItemOptions = ref(marketFormItem)
const marketFormItemOptions = computed(() => {
return marketFormItem.map((item) => {
if (item.prop === 'parentAccountName' || item.prop === 'level') {
return { ...item, disabled: dialogType.value === 'edit' }
}
return item
})
})
interface SelectOption {
label: string
value: string | number
@ -497,8 +492,8 @@ const handleMarketFormLinkage = async (type: 'level' | 'parentAccountName', valu
}))
}
// 2.
if (type === 'parentAccountName') {
// 2.
if (type === 'parentAccountName' && dialogType.value === 'add') {
//
marketPointTree.value = []
@ -521,7 +516,8 @@ const handleMarketFormLinkage = async (type: 'level' | 'parentAccountName', valu
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.target_point_diff,
point_diff: item.target_point_diff, // target_point_diff
pointDifference: 0, // 0
assignedPointDifference: '',
}))
}
@ -632,7 +628,6 @@ const handleAddMarket = async () => {
dialogType.value = 'add'
// 2.
marketForm.value = { ...initMarketForm }
dialogFullscreen.value = false
}
//
@ -666,6 +661,9 @@ const handleEditMarket = async () => {
console.log(marketForm.value)
try {
// watch
dialogType.value = 'edit'
//
await handleMarketFormLinkage('level', marketForm.value.level)
if (parentId) {
@ -679,16 +677,15 @@ const handleEditMarket = async () => {
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.point_diff,
assignedPointDifference: item.target_point_diff || '', //
point_diff: item.point_diff, // point_diff
pointDifference: item.target_point_diff,
assignedPointDifference: item.target_point_diff || '',
}))
}
//
//
dialogVisible.value = true
dialogTitle.value = '编辑'
dialogType.value = 'edit'
dialogFullscreen.value = false
} catch (error) {
console.error('编辑数据加载失败:', error)
}
@ -700,7 +697,7 @@ const handleEditMarket = async () => {
//
const handleViewPointDiff = async (row: any) => {
try {
const data = await getPointDiffConfigListApi({ user_id: row.role_id })
const data = await getPointDiffConfigListApi({ user_id: row.id })
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
@ -709,11 +706,10 @@ const handleViewPointDiff = async (row: any) => {
assignedPointDifference: item.target_point_diff || '',
}))
//
//
dialogVisible.value = true
dialogTitle.value = '查看点差'
dialogType.value = 'view'
dialogFullscreen.value = true
} catch (error) {
console.error('加载点差数据失败:', error)
}
@ -820,13 +816,7 @@ const handleSubmit = async () => {
//
const handleCancel = async () => {
dialogVisible.value = false
// dialog
}
//
const handleClose = async () => {
dialogVisible.value = false
// dialog
// drawer
}
</script>
@ -834,6 +824,7 @@ const handleClose = async () => {
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
.market {
.search-container {
:deep(.el-form) {
@ -877,7 +868,49 @@ const handleClose = async () => {
}
}
}
.table-container {
max-width: 100%;
}
//
.drawer-form {
flex-shrink: 0;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
}
.drawer-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
max-height: 100%;
.table-header {
flex-shrink: 0;
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.table-wrapper {
flex: 1;
min-height: 0;
}
}
.drawer-footer {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
</style>

View File

@ -57,45 +57,35 @@ export const marketTableColumns = [
//做市商添加配置
export const marketFormItem = [
//邮箱或手机号
{ prop: 'phoneOrEmail', label: '', type: 'slot' as const, slotName: 'phoneOrEmail' },
//账户名称
{ label: '账户名称', prop: 'accountName', type: 'input' as const, placeholder: '请输入账户名称' },
//级别
{ label: '级别', prop: 'level', type: 'select' as const, placeholder: '请选择级别' },
//所属上级
{
label: '所属上级',
prop: 'parentAccountName',
type: 'select' as const,
placeholder: '请选择所属上级账户名称',
},
// 风控模板
{
label: '风控模板',
prop: 'riskControlTemplate',
type: 'select' as const,
placeholder: '请选择风控模板',
},
//头寸
{ label: '头寸', prop: 'positionRatio', type: 'input' as const, placeholder: '请输入头寸' },
//手续费
{ label: '手续费', prop: 'commissionRatio', type: 'input' as const, placeholder: '请输入手续费' },
//邮箱或手机号
{ prop: 'phoneOrEmail', label: '', type: 'slot' as const, slotName: 'phoneOrEmail' },
//账户名称
{ label: '账户名称', prop: 'accountName', type: 'input' as const, placeholder: '请输入账户名称', },
//级别
{ label: '级别', prop: 'level', type: 'select' as const, placeholder: '请选择级别' },
//所属上级
{ label: '所属上级', prop: 'parentAccountName', type: 'select' as const, placeholder: '请选择所属上级账户名称', },
// 风控模板
{ label: '风控模板', prop: 'riskControlTemplate', type: 'select' as const, placeholder: '请选择风控模板' },
//头寸
{ label: '头寸', prop: 'positionRatio', type: 'input' as const, placeholder: '请输入头寸', },
//手续费
{ label: '手续费', prop: 'commissionRatio', type: 'input' as const, placeholder: '请输入手续费', },
]
// 国家码选项
interface CountryItem {
name: string
code: string
name: string
code: string
}
export const countryOptions = ref<CountryItem[]>([
{ name: '中国', code: '+86' },
{ name: '中国台湾', code: '+886' },
{ name: '中国香港', code: '+852' },
{ name: '中国澳门', code: '+853' },
{ name: '韩国', code: '+82' },
{ name: '美国', code: '+1' },
{ name: '新加坡', code: '+65' },
{ name: '中国', code: '+86' },
{ name: '中国台湾', code: '+886' },
{ name: '中国香港', code: '+852' },
{ name: '中国澳门', code: '+853' },
{ name: '韩国', code: '+82' },
{ name: '美国', code: '+1' },
{ name: '新加坡', code: '+65' }
])
//编辑点差表格列配置

View File

@ -377,6 +377,8 @@ const handleEditConfirm = async () => {
const params: EditUsernameParams = { username: editForm.value };
await editUsernameApi(params);
userInfo.username = editForm.value;
//
userStore.updateUserInfo({ username: editForm.value });
}
if (editField.value === 'mobile') {
const mobile = editForm.countryCode + editForm.value;

View File

@ -23,6 +23,9 @@
<el-button type="primary" size="small" @click="handleChange(row)" v-anth="'user_editUser'">
修改
</el-button>
<el-button type="warning" size="small" @click="handleAdjustAmount(row)">
调整金额
</el-button>
</template>
</ClientTable>
<!-- 分页组件 -->
@ -92,6 +95,38 @@
</template>
</ClientForm>
</ClientDialog>
<!-- 调整金额弹窗 -->
<ClientDialog
:visible="adjustAmountVisible"
title="调整金额"
type="adjust"
@confirm="handleAdjustAmountSubmit"
@cancel="handleAdjustAmountCancel"
@close="handleAdjustAmountClose"
>
<el-form :model="adjustAmountForm" :rules="adjustAmountRules" ref="adjustAmountFormRef" label-width="120px">
<el-form-item label="账号名" prop="username">
<el-input v-model="adjustAmountForm.username" disabled />
</el-form-item>
<el-form-item label="币种" prop="currency">
<el-input v-model="adjustAmountForm.currency" disabled />
</el-form-item>
<el-form-item label="当前余额" prop="currentBalance">
<el-input v-model="adjustAmountForm.currentBalance" disabled />
</el-form-item>
<el-form-item label="调整金额" prop="adjustAmount">
<el-input
v-model="adjustAmountForm.adjustAmount"
placeholder="请输入调整金额(可为负数)"
@input="handleAdjustAmountInput"
/>
</el-form-item>
<el-form-item label="调整后余额" prop="afterBalance">
<el-input v-model="adjustAmountForm.afterBalance" disabled />
</el-form-item>
</el-form>
</ClientDialog>
</template>
<script lang="ts" setup>
@ -114,6 +149,7 @@ import {
createClientApi,
editClientApi,
} from '@/api/modules/account/client'
import { updateCapitalApi } from '@/api/modules/user'
//
import {
@ -154,6 +190,37 @@ const pageSize = ref(10)
const dialogTitle = ref('')
const dialogVisible = ref(false)
const dialogType = ref('add')
/**
* @description 定义调整金额弹窗数据
*/
const adjustAmountVisible = ref(false)
const adjustAmountFormRef = ref()
const adjustUserId = ref<number>(0)
const adjustAmountForm = ref({
username: '',
currency: '',
currentBalance: '',
adjustAmount: '',
afterBalance: '',
})
const adjustAmountRules = {
adjustAmount: [
{ required: true, message: '请输入调整金额', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (value === '' || value === null || value === undefined) {
callback(new Error('请输入调整金额'))
} else if (!/^-?\d+(\.\d{1,5})?$/.test(value)) {
callback(new Error('请输入有效的金额格式,最多五位小数'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
/**
* @description 定义dialog中form的数据
*/
@ -403,6 +470,55 @@ const handleClose = () => {
//
dialogVisible.value = false
}
/**
* @description 调整金额相关方法
*/
//
const handleAdjustAmount = (row: any) => {
adjustAmountVisible.value = true
adjustUserId.value = row.id
adjustAmountForm.value = {
username: row.username,
currency: 'USDT', //
currentBalance: row.wallet_capital?.amount || '0',
adjustAmount: '',
afterBalance: row.wallet_capital?.amount || '0',
}
}
//
const handleAdjustAmountInput = () => {
const currentBalance = parseFloat(adjustAmountForm.value.currentBalance) || 0
const adjustAmount = parseFloat(adjustAmountForm.value.adjustAmount) || 0
const afterBalance = currentBalance + adjustAmount
adjustAmountForm.value.afterBalance = afterBalance.toFixed(5)
}
//
const handleAdjustAmountSubmit = async () => {
try {
await adjustAmountFormRef.value?.validate()
await updateCapitalApi({
target_user_id: adjustUserId.value,
amount: adjustAmountForm.value.adjustAmount,
})
await getClientList()
adjustAmountVisible.value = false
} catch (err) {
console.error('调整金额表单验证失败', err)
}
}
//
const handleAdjustAmountCancel = () => {
adjustAmountVisible.value = false
}
//
const handleAdjustAmountClose = () => {
adjustAmountVisible.value = false
}
</script>
<style scoped lang="scss">

View File

@ -33,9 +33,11 @@
:options-map="roleFormOptionsMap" :dialog-type="dialogType">
<template #permissions="{ formData }">
<el-tree ref="permissionTreeRef" :data="permissionTreeData" :props="treeProps" show-checkbox node-key="id"
:default-checked-keys="formData.permissions" :default-expand-all="true" class="permission-tree" />
:default-expand-all="true" :check-strictly="false"
class="permission-tree" />
</template>
</RoleForm>
<template #footer>
<div style="flex: auto">
<el-button @click="handleCancel">取消</el-button>
@ -226,6 +228,39 @@ const transformPermissionsToTree = (permissions: any[]) => {
return Array.from(moduleMap.values()).sort((a, b) => a.id - b.id)
}
/**
* @description: 过滤掉父节点ID只保留子节点ID用于显示选中状态
*/
const filterParentNodes = (permissionIds: number[], treeData: any[]): number[] => {
// ID
const parentNodeIds = treeData.map(node => node.id)
// IDID
return permissionIds.filter(id => !parentNodeIds.includes(id))
}
/**
* @description: 根据选中的子节点计算父节点ID半选中的父节点
*/
const getParentNodeIds = (checkedKeys: number[], treeData: any[]): number[] => {
const parentIds: number[] = []
treeData.forEach(parent => {
// ID
const childIds = parent.children?.map((child: any) => child.id) || []
//
const hasCheckedChild = childIds.some((id: number) => checkedKeys.includes(id))
//
const allChildrenChecked = childIds.length > 0 && childIds.every((id: number) => checkedKeys.includes(id))
//
if (hasCheckedChild && !allChildrenChecked) {
parentIds.push(parent.id)
}
})
return parentIds
}
//
const handlePermission = async (row: any) => {
dialogVisible.value = true
@ -237,8 +272,18 @@ const handlePermission = async (row: any) => {
const data = await getRolePermissionApi({ id: row.id })
allPermissions.value = data.all_permission_info
permissionTreeData.value = transformPermissionsToTree(data.all_permission_info)
roleForm.value.permissions = data.permission_ids || []
// IDID
const childPermissionIds = filterParentNodes(data.permission_ids || [], permissionTreeData.value)
roleForm.value.permissions = childPermissionIds
console.log('树形权限数据', permissionTreeData.value)
console.log('过滤后的权限ID仅子节点:', childPermissionIds)
// DOM
await nextTick()
// 使 setCheckedKeys
permissionTreeRef.value?.setCheckedKeys(childPermissionIds, false)
} catch (err) {
console.error('获取角色权限失败', err)
}
@ -262,14 +307,20 @@ const handleSubmit = async () => {
await editRoleApi(roleForm.value)
await fetchRoleList()
} else {
// Tree
// Tree
const checkedKeys = permissionTreeRef.value?.getCheckedKeys() || []
const halfCheckedKeys = permissionTreeRef.value?.getHalfCheckedKeys() || []
console.log('设置角色权限', checkedKeys.join(','))
// ID
const allSelectedKeys = [...checkedKeys, ...halfCheckedKeys]
console.log('设置角色权限 - 完全选中:', checkedKeys)
console.log('设置角色权限 - 半选中:', halfCheckedKeys)
console.log('设置角色权限 - 合并后:', allSelectedKeys)
await setRolePermissionApi({
id: permissionIds.value,
permission_ids: checkedKeys.join(','),
permission_ids: allSelectedKeys.join(','),
})
await fetchRoleList()
}

View File

@ -4,7 +4,7 @@
<UserText :user-text="userText" @back="handleBack" />
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
<el-form class="user-form" ref="loginFormRef" :model="loginForm" :rules="loginRules">
<el-form class="user-form" ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin">
<Transition name="form-switch" mode="out-in">
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email">
<el-input v-model="loginForm.email" placeholder="请输入邮箱" clearable size="large" />
@ -35,7 +35,7 @@
</div>
</el-option>
</el-select>
<el-input v-model="loginForm.phone" placeholder="请输入手机号" clearable size="large" />
<el-input v-model="loginForm.phone" placeholder="请输入手机号" clearable size="large" @keyup.enter.prevent="handleLogin"/>
</el-form-item>
<el-form-item v-else-if="activeTab === 'password'" key="form-password-item" prop="password">
@ -137,6 +137,8 @@ function generateTimestampWithRandomNum() {
//
const handleLogin = async () => {
console.log('触发登录');
if (!loginFormRef.value) return
try {

View File

@ -3,12 +3,13 @@
<el-input v-for="(item, index) in inputList" :key="index" v-model="inputList[index]" class="captcha-input-item"
:maxlength="1" @input="handleInput(index, $event)" @focus="handleFocus(index)" @keyup.delete="handleDelete(index)"
ref="inputRefs" type="text" />
<el-button type="primary" class="form-btn" v-if="showSendButton" @click="sendCode" :disabled="sendState.isCounting">{{
sendState.isCounting ? `${sendState.countdown}s后重新获取` : '发送验证码' }}</el-button>
<el-button type="primary" class="form-btn" v-if="showSendButton" @click="sendCode" :disabled="isCounting">{{
isCounting ? `${countdown}s后重新获取` : '发送验证码' }}</el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, toRefs, reactive, computed } from 'vue'
defineOptions({
name: 'Captcha'
@ -39,6 +40,21 @@ const props = defineProps({
default: 'code' // ruleskey
},
})
//
const isCounting = ref(false)
const countdown = ref(0)
// sendState
watch(
() => props.sendState,
(newVal) => {
isCounting.value = newVal.isCounting
countdown.value = newVal.countdown
},
{ deep: true, immediate: true }
)
const { showSendButton } = toRefs(props)
const emit = defineEmits(['update:modelValue', 'change', 'finish', 'sendCode'])