This commit is contained in:
李远 2026-03-16 17:03:22 +08:00
parent c8e9b79bce
commit 6d180b94a8
10 changed files with 624 additions and 638 deletions

View File

@ -6,12 +6,26 @@ export async function getMarketListApi(params: searchParams): Promise<any> {
return request.get<any>('agent/getAgentList', { ...params })
}
//获取代理点差配置
export async function getPointDiffConfigApi(params: pointDiffConfigParams): Promise<any> {
return request.get<any>('/agent/getSelfVarietyPointDiffConfig',{...params})
}
//获取所属上级列表
export async function getParAgentListApi(params: pointDiffConfigParams): Promise<any> {
return request.get<any>('/agent/getParAgentListByRoleId',{...params})
}
//获取代理点差列表
export async function getPointDiffConfigListApi(params: pointDiffConfigParams): Promise<any> {
return request.get<any>('/agent/getSelfVarietyPointDiffConfig',{...params})
}
//获取风控模板选列表
export async function getRiskTemplateListApi(): Promise<any> {
return request.get<any>('/agent/getRiskControlOptions')
}
//添加做市商
export async function addMarketApi(params:any): Promise<any> {
return request.post<any>('/agent/createAgent',{...params})
}
//编辑做市商
export async function editMarketApi(params:any): Promise<any> {
return request.post<any>('/agent/editAgent',{...params})
}

View File

@ -4,6 +4,6 @@ export interface searchParams {
}
//获取代理点差配置
export interface pointDiffConfigParams {
user_id?: number | null
user_id?: number | string
role_id?: number | string
}

View File

@ -5,7 +5,7 @@
<slot />
<!-- 底部按钮插槽 -->
<template #footer>
<template #footer v-if="showFooter">
<slot name="footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
@ -21,6 +21,7 @@ interface Props {
width?: string | number
destroyOnClose?: boolean
type?: string
showFooter?: boolean
}
interface Emits {
@ -33,6 +34,7 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
width: '600px',
destroyOnClose: true,
showFooter: true,
})
const emit = defineEmits<Emits>()
@ -51,5 +53,4 @@ const handleClose = () => {
}
</script>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@ -30,7 +30,7 @@ const props = defineProps({
},
//
pageSizes: {
type: Array,
type: Array as PropType<number[]>,
default: () => [1, 10, 20, 50, 100] as unknown[]
},
//

View File

@ -1,7 +1,7 @@
<template>
<div class="table-container">
<el-table :data="tableData" :row-key="rowKey" :tree-props="treeProps" :border="border" :highlight-current-row="highlightCurrentRow"
v-bind="$attrs">
<el-table :data="tableData" :row-key="rowKey" :tree-props="treeProps" :border="border"
:highlight-current-row="highlightCurrentRow" v-bind="$attrs" @row-click="handleRowClick">
<!-- 动态表头列 -->
<template v-for="(column, index) in columns" :key="index">
<el-table-column v-if="!column.slotName" :prop="column.prop" :label="column.label" :width="column.width"
@ -44,6 +44,20 @@ const props = withDefaults(defineProps<Props>(), {
border: true,
highlightCurrentRow: true,
})
//
const emit = defineEmits<{
(e: 'row-click', row: any): void //
}>()
/**
* 处理行点击事件兼容原有逻辑
*/
const handleRowClick = (row: any) => {
// null
if (!row) return
emit('row-click', row)
}
</script>
<style scoped lang="scss">

View File

@ -7,21 +7,77 @@
<el-button type="primary" @click="handleAddMarket()">
添加
</el-button>
<el-button type="primary" @click="handleEditMarket()">
<el-button type="primary" :disabled="!selectedRow" @click="handleEditMarket()">
编辑
</el-button>
</template>
</MarketSearch>
<!-- 表格 -->
<MarketTable :table-data="marketTree" :columns="marketTableColumnsOptions" row-key="id">
<MarketTable :table-data="marketTree" :columns="marketTableColumnsOptions" row-key="id"
@row-click="handleRowClick">
<template #pointSpread="{ row }">
<el-button type="primary" size="small" @click="handleViewPointDiff(row)">
查看
</el-button>
</template>
</MarketTable>
<!-- 分页 -->
<MarketPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
<!-- dialog -->
<MarketDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" :fullscreen="dialogFullscreen"
:show-footer="dialogType !== 'view'" @confirm="handleSubmit" @cancel="handleCancel" @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>
<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>
<!-- 点差表格 -->
<MarketTable :table-data="marketPointTree" :columns="marketPointTableColumnsOptions" row-key="id">
<template #assignedPointDiff="{ row }">
<el-form :model="row" :rules="rules(marketForm, row)">
<el-form-item prop="assignedPointDifference">
<el-input v-model="row.assignedPointDifference" size="small" placeholder="请输入点差" />
</el-form-item>
</el-form>
</template>
</MarketTable>
</MarketDialog>
</template>
<script setup lang="ts">
@ -33,10 +89,12 @@ import MarketDialog from '@/components/common/Dialog.vue'
import MarketForm from '@/components/common/Form.vue'
//
import { marketSearch, marketTableColumns } from './options'
import { marketSearch, marketTableColumns, marketFormItem, marketPointTableColumns, countryOptions, viewMarketPointTableColumns } from './options'
import { rules } from './rules'
//
import { getMarketListApi } from '@/api/modules/account/market'
import { getMarketListApi, getPointDiffConfigListApi, getParAgentListApi, getRiskTemplateListApi, addMarketApi, editMarketApi } from '@/api/modules/account/market'
/**
* @description 定义搜索数据
@ -57,13 +115,68 @@ const marketTableColumnsOptions = ref(marketTableColumns)
* @description 定义分页数据
*/
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const currentPage = ref(1)
/**
* @description 定义dialog数据
*/
const dialogVisible = ref<boolean>(false)
const dialogTitle = ref<string>('')
const dialogType = ref<string>('add')
const dialogFullscreen = ref(false)
/**
* @description 定义dialog表单数据
*/
const marketForm = ref({
//
email: '',
phone: '',
countryCode: '+86',
accountName: '',
level: '',
parentAccountName: '',
riskControlTemplate: '',
positionRatio: '',
commissionRatio: '',
//
maxPositionRatio: '',
maxCommissionRatio: '',
})
const marketFormRules = rules(marketForm.value)
const marketFormRef = ref()
const marketFormItemOptions = ref(marketFormItem)
interface SelectOption {
label: string
value: string | number
row?: any //
}
const marketFormOptionsMap = ref<{
level: SelectOption[]
parentAccountName: SelectOption[]
riskControlTemplate: SelectOption[]
}>({
level: [
{ label: '平台', value: '1' },
{ label: '承包商', value: '2' },
{ label: '特殊做市', value: '3' },
{ label: '普通做市', value: '4' },
{ label: '一级代理', value: '5' },
],
parentAccountName: [],
riskControlTemplate: [],
})
const activeTab = ref('email')
/**
* @description 定义点差表格数据
*/
const marketPointTree = ref<any>([])
const marketPointTableColumnsOptions = computed(() => {
return dialogType.value !== 'view' ? marketPointTableColumns : viewMarketPointTableColumns;
});//
const selectedRow = ref<any>(null)
/**
* @description 数据处理方法
*/
@ -90,6 +203,65 @@ const mapMarketTree = (data: any) => {
}))
}
/**
* @description 统一处理做市商表单联动逻辑
* @param type 联动类型level(级别变更) / parentAccountName(上级账户变更)
* @param value 选中的值级别值/上级账户名
* @returns {Promise<void>}
*/
const handleMarketFormLinkage = async (type: 'level' | 'parentAccountName', value: string) => {
try {
// 1.
if (type === 'level') {
//
marketForm.value.parentAccountName = ''
marketFormOptionsMap.value.parentAccountName = []
marketPointTree.value = []
if (!value) return //
//
const data = await getParAgentListApi({ role_id: value })
console.log('datasssssssssssss', data);
//
marketFormOptionsMap.value.parentAccountName = data.map((item: any) => ({
label: item.username,
value: String(item.id), //
row: item
}))
}
// 2.
if (type === 'parentAccountName') {
//
marketPointTree.value = []
if (!value) return //
// value
const rulesData = marketFormOptionsMap.value.parentAccountName.find((item) => item.value === value)
if (!rulesData) return //
//
marketForm.value.maxPositionRatio = rulesData.row.wallet_capital.toucun_percent
marketForm.value.maxCommissionRatio = rulesData.row.wallet_capital.fee_handling_percent
//
const data = await getPointDiffConfigListApi({ user_id: value })
//
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.target_point_diff,
assignedPointDifference: '',
}))
}
} catch (error) {
console.error(`[${type}] 联动数据获取失败:`, error)
}
}
/**
* @description 统一数据获取
@ -97,18 +269,27 @@ const mapMarketTree = (data: any) => {
//
const getMarketList = async () => {
const params = {
page: page.value,
page: currentPage.value,
page_size: pageSize.value,
user_name: marketSearchForm.value.accountName
}
const data = await getMarketListApi(params)
console.log('data', data);
//
marketTree.value = mapMarketTree(data.data)
//
total.value = data.total
page.value = data.current_page
pageSize.value = data.per_page
currentPage.value = data.current_page
pageSize.value = Number(data.per_page)
}
//
const getRiskTemplateList = async () => {
const data = await getRiskTemplateListApi()
//
marketFormOptionsMap.value.riskControlTemplate = data.map((item: any) => ({
label: item.name,
value: item.id //
}))
}
/**
@ -116,8 +297,48 @@ const getMarketList = async () => {
*/
onMounted(() => {
getMarketList()
getRiskTemplateList()
})
/**
* @description 监听数据变化
*/
//
watch(
() => marketForm.value.level,
(newLevel) => {
handleMarketFormLinkage('level', newLevel)
},
{ immediate: false } //
)
//
watch(
() => marketForm.value.parentAccountName,
(newParentAccountName) => {
handleMarketFormLinkage('parentAccountName', newParentAccountName)
},
{ immediate: false }
)
/**
* @description 定义表单初始值方法
*/
// 1. 便
const initMarketForm = {
email: '',
phone: '',
countryCode: '+86',
accountName: '',
level: '',
parentAccountName: '',
riskControlTemplate: '',
positionRatio: '',
commissionRatio: '',
maxPositionRatio: '',
maxCommissionRatio: '',
};
/**
@ -133,23 +354,82 @@ const handleReset = async () => {
marketSearchForm.value = {
accountName: ''
}
page.value = 1
currentPage.value = 1
pageSize.value = 10
await getMarketList()
}
//
const handleAddMarket = async () => {
console.log('添加做市商');
dialogVisible.value = true
dialogTitle.value = '添加'
dialogType.value = 'add'
// 2.
marketForm.value = { ...initMarketForm }
dialogFullscreen.value = false;
}
//
const handleEditMarket = async () => {
console.log('编辑做市商');
if (!selectedRow.value) {
ElMessage.warning('请先选择要编辑的做市商!');
return;
}
const row = selectedRow.value;
console.log('row', row);
// ID
const getParentIdFromPath = (path: string, level: number) => {
const segments = path.split('/').filter(Boolean);
const levelNum = Number(level);
const parentIndex = levelNum - 2;
if (levelNum <= 1 || segments.length <= parentIndex) return '';
return segments[parentIndex];
};
const parentId = getParentIdFromPath(row.path, row.level);
console.log('解析后的上级ID:', parentId);
//
marketForm.value = {
...initMarketForm,
accountName: row.accountName || '',
level: String(row.level) || '',
parentAccountName: '',
riskControlTemplate: row.wallet_capital.risk_control_id || '',
positionRatio: row.positionRatio || '',
commissionRatio: row.commissionRatio || '',
};
try {
//
await handleMarketFormLinkage('level', marketForm.value.level);
if (parentId) {
marketForm.value.parentAccountName = parentId;
}
//
if (row.id) {
const data = await getPointDiffConfigListApi({ user_id: row.id });
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.point_diff,
assignedPointDifference: item.target_point_diff || '', //
}));
}
//
dialogVisible.value = true;
dialogTitle.value = '编辑';
dialogType.value = 'edit';
dialogFullscreen.value = false;
} catch (error) {
console.error('编辑数据加载失败:', error);
}
};
/**
@ -157,14 +437,118 @@ const handleEditMarket = async () => {
*/
//
const handleViewPointDiff = async (row: any) => {
console.log('查看点差', row);
try {
const data = await getPointDiffConfigListApi({ user_id: row.id });
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.point_diff,
assignedPointDifference: item.target_point_diff || ''
}));
//
dialogVisible.value = true;
dialogTitle.value = '查看点差';
dialogType.value = 'view';
dialogFullscreen.value = true;
} catch (error) {
console.error('加载点差数据失败:', error);
}
}
//
const handleRowClick = (row: any) => {
if (!row) {
selectedRow.value = null
return
}
selectedRow.value = row
}
/**
* @description 分页方法
*/
//
const handlePaginationChange = async (page: { currentPage: number, pageSize: number }) => {
currentPage.value = page.currentPage
await getMarketList()
}
/**
* @description dialog方法
*/
//
/**
* @description 提交之前构造数据
*/
// { "1":"42", "2":"38" }
const formatPointDiffToObject = () => {
if (!marketPointTree.value || !marketPointTree.value.length) return {};
//
const pointDiffObj: Record<string, string> = {};
// { : }
marketPointTree.value.forEach((item: { id: string | number; assignedPointDifference: string }) => {
//
pointDiffObj[String(item.id)] = item.assignedPointDifference || '0';
});
return pointDiffObj; //
};
const handleSubmit = async () => {
// console.log('', marketPointTree.value);
await marketFormRef.value?.validate()
const pointDiffObj = formatPointDiffToObject()
if (dialogType.value === 'add') {
const params = {
role_id: marketForm.value.level,
reg_type: activeTab.value === 'email' ? '1' : '2',
username: marketForm.value.accountName,
par_user_id: marketForm.value.parentAccountName,
mobile: activeTab.value === 'phone' ? marketForm.value.phone : '',
email: activeTab.value === 'email' ? marketForm.value.email : '',
password: '888888',
toucun_percent: marketForm.value.positionRatio,
fee_handling_percent: marketForm.value.commissionRatio,
point_diffs_json: JSON.stringify(pointDiffObj),
risk_control_id: marketForm.value.riskControlTemplate,
}
await addMarketApi(params)
// 5.
await getMarketList()
dialogVisible.value = false
marketForm.value = { ...initMarketForm }
} else if (dialogType.value === 'edit') {
const params = {
user_id: selectedRow.value.id,
toucun_percent: marketForm.value.positionRatio,
point_diffs_json: JSON.stringify(pointDiffObj),
risk_control_id: marketForm.value.riskControlTemplate,
fee_handling_percent: marketForm.value.commissionRatio,
}
// 4.
await editMarketApi(params)
}
}
//
const handleCancel = async () => {
dialogVisible.value = false
// dialog
}
//
const handleClose = async () => {
dialogVisible.value = false
// dialog
}
</script>
<style scoped lang="scss">
@ -177,4 +561,38 @@ const handleViewPointDiff = async (row: any) => {
}
}
}
.phone-email {
display: flex;
flex: 1;
.tab-btn {
display: flex;
margin-right: 10px;
.el-button {
height: 32px;
margin-left: 0;
}
}
.group {
display: flex;
flex: 1;
.el-form-item {
flex: 1;
}
.phone-item {
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
.el-select {
width: 110px;
}
}
}
}
</style>

View File

@ -43,27 +43,21 @@ export const marketTableColumns = [
//做市商添加配置
export const marketFormItem = [
//注册方式
//邮箱或手机号
{ prop: 'phoneOrEmail', label: '', type: 'slot' as const, slotName: 'phoneOrEmail' },
//账户名称
{ label: '账户名称', prop: 'accountName', type: 'input', placeholder: '请输入账户名称', },
{ label: '账户名称', prop: 'accountName', type: 'input' as const, placeholder: '请输入账户名称', },
//级别
{ label: '级别', prop: 'level', type: 'select', placeholder: '请选择级别'},
{ label: '级别', prop: 'level', type: 'select' as const, placeholder: '请选择级别'},
//所属上级
{ label: '所属上级', prop: 'parentAccountName', type: 'select', placeholder: '请输入所属上级账户名称', },
{ label: '所属上级', prop: 'parentAccountName', type: 'select' as const, placeholder: '请选择所属上级账户名称', },
// 风控模板
{ label: '风控模板', prop: 'riskControlTemplate', type: 'select', placeholder: '请选择风控模板'},
{ label: '风控模板', prop: 'riskControlTemplate', type: 'select' as const, placeholder: '请选择风控模板'},
//头寸
{ label: '头寸', prop: 'position', type: 'input', placeholder: '请输入头寸', },
{ label: '头寸', prop: 'positionRatio', type: 'input' as const, placeholder: '请输入头寸', },
//手续费
{ label: '手续费', prop: 'commissionRatio', type: 'input', placeholder: '请输入手续费', },
{ label: '手续费', prop: 'commissionRatio', type: 'input' as const, placeholder: '请输入手续费', },
]
export const marketPointTableColumns = ([
{ prop: 'varietyName', label: '名称', align: 'center' as const },
{ prop: 'varietyCode', label: '编号', align: 'center' as const },
{ prop: 'pointDifference', label: '可分配点差', align: 'center' as const },
{ prop: 'assignedPointDifference', label: '已分配点差', align: 'center' as const, slotName:'assignedPointDiff' ,width:'160px',height:'600px'},
])
// 国家码选项
interface CountryItem {
@ -79,3 +73,18 @@ export const countryOptions = ref<CountryItem[]>([
{ name: '美国', code: '+1' },
{ name: '新加坡', code: '+65' }
])
export const marketPointTableColumns = ([
{ prop: 'varietyName', label: '名称', align: 'center' as const },
{ prop: 'varietyCode', label: '编号', align: 'center' as const },
{ prop: 'pointDifference', label: '可分配点差', align: 'center' as const },
{ prop: 'assignedPointDifference', label: '已分配点差', align: 'center' as const, slotName:'assignedPointDiff'},
])
// 查看点差表格列配置
export const viewMarketPointTableColumns = ([
{ prop: 'varietyName', label: '名称', align: 'center' as const },
{ prop: 'varietyCode', label: '编号', align: 'center' as const },
{ prop: 'pointDifference', label: '可分配点差', align: 'center' as const },
{ prop: 'assignedPointDifference', label: '已分配点差', align: 'center' as const},
])

View File

@ -1,95 +1,148 @@
// rules.ts
import type { FormRules } from 'element-plus'
// 接收上级上限参数
export const rules = (parentLimit: any = { position: 0, commissionRatio: 0 }): FormRules => {
export const rules = (marketForm: any, row?: any): FormRules => {
return {
//手机号
phone: [
{ required: true, message: '请输入手机号', trigger: ['blur'] },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: ['blur'] },
],
//邮箱
email: [
{ required: true, message: '请输入邮箱地址', trigger: ['blur'] },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur'] }
{ required: true, message: '请输入邮箱地址', trigger: ['blur', 'change'] },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
],
phone: [
{
validator: (rule, value, callback) => {
// 验证国家码必填
if (!marketForm.countryCode) {
return callback(new Error('请选择国家/地区码'));
}
// 验证手机号必填
if (!value) {
return callback(new Error('请输入手机号码'));
}
// 中国大陆手机号格式验证(仅+86时生效
if (marketForm.countryCode === '+86' && !/^1[3-9]\d{9}$/.test(value)) {
return callback(new Error('请输入正确的中国大陆手机号'));
}
callback();
},
trigger: ['blur', 'change']
}
],
accountName: [
{ required: true, message: '请输入账户名称', trigger: 'blur' },
{ min: 2, max: 50, message: '账户名称长度需在2-50个字符之间', trigger: 'blur' }
{ required: true, message: '请输入账号名称', trigger: 'blur' }
],
level: [
{ required: true, message: '请选择级别', trigger: 'change' }
{ required: true, message: '请选择级', trigger: 'change' }
],
parentAccountName: [
{ required: true, message: '请选择所属上级', trigger: 'change' }
{ required: true, message: '请选择上级账号', trigger: 'change' }
],
riskControlTemplate: [
{ required: true, message: '请选择风控模板', trigger: 'change' }
],
// 头寸:新增「不超过上级」校验
position: [
{ required: true, message: '请输入头寸', trigger: 'blur' },
{ pattern: /^-?\d+(\.\d+)?$/, message: '头寸需为数字(支持整数/小数/负数)', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value) return callback()
const inputVal = Number(value)
const maxVal = parentLimit.position
if (inputVal > maxVal) {
callback(new Error(`头寸不能大于上级的 ${maxVal}`))
} else {
callback()
}
},
trigger: 'blur'
}
],
// 手续费:新增「不超过上级」校验
commissionRatio: [
{ required: true, message: '请输入手续费', trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?$/, message: '手续费需为非负数字(支持整数/小数)', trigger: 'blur' },
{ min: 0, max: 100, message: '手续费范围需在0-100之间', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value) return callback()
const inputVal = Number(value)
const maxVal = parentLimit.commissionRatio
if (inputVal > maxVal) {
callback(new Error(`手续费不能大于上级的 ${maxVal}`))
} else {
callback()
}
},
trigger: 'blur'
}
],
// 新增:批量校验已分配点差
pointDiffList: [
{
validator: (rule, value, callback) => {
// value 是 marketPointTree 数组
if (!value || value.length === 0) {
return callback() // 没有点差数据时不校验
// 空值已经由 required 校验处理
if (value === '' || value === undefined || value === null) {
return callback();
}
// 遍历所有点差行,校验必填和数值合法性
for (const row of value) {
// 校验必填
if (row.assignedPointDifference === '' || row.assignedPointDifference === undefined) {
return callback(new Error(`品种【${row.varietyName || row.name}】的已分配点差不能为空`))
// 尝试转换为数字
const numValue = Number(value);
// 转换失败 → 不是有效数字
if (isNaN(numValue)) {
return callback(new Error('头寸必须为数字'));
}
// 校验数值格式(非负数字)
const numVal = Number(row.assignedPointDifference)
if (isNaN(numVal) || numVal < 0) {
return callback(new Error(`品种【${row.varietyName || row.name}】的已分配点差需为非负数字`))
// 非负数校验
if (numValue < 0) {
return callback(new Error('头寸不能为负数'));
}
// 可选:校验不超过上级点差
if (row.pointDifference && numVal > Number(row.pointDifference)) {
return callback(new Error(`品种【${row.varietyName || row.name}】的已分配点差不能超过可分配点差 ${row.pointDifference}`))
// 精度校验最多2位小数
if (!/^\d+(\.\d{1,2})?$/.test(value.toString())) {
return callback(new Error('头寸最多保留2位小数'));
}
// 不超过上级最大头寸校验
if (marketForm.maxPositionRatio && numValue > Number(marketForm.maxPositionRatio)) {
return callback(new Error(`头寸不能超过上级账户的最大头寸(${marketForm.maxPositionRatio})`));
}
callback();
},
trigger: ['blur', 'change']
}
],
commissionRatio: [
{ required: true, message: '请输入手续费比例', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === '' || value === undefined || value === null) {
return callback();
}
const numValue = Number(value);
if (isNaN(numValue)) {
return callback(new Error('手续费比例必须为数字'));
}
// // 范围校验0-100
// if (numValue < 0 || numValue > 100) {
// return callback(new Error('手续费比例需在0-100之间'));
// }
// 精度校验最多2位小数
if (!/^\d+(\.\d{1,2})?$/.test(value.toString())) {
return callback(new Error('手续费比例最多保留2位小数'));
}
// 不超过上级最大手续费比例校验
if (marketForm.maxCommissionRatio && numValue > Number(marketForm.maxCommissionRatio)) {
return callback(new Error(`手续费比例不能超过上级账户的最大比例(${marketForm.maxCommissionRatio})`));
}
callback();
},
trigger: ['blur', 'change']
}
],
assignedPointDifference: [
{ required: true, message: '请输入点差', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === '' || value === undefined || value === null) {
return callback()
}
const numValue = Number(value)
// 1. 基础数字校验
if (isNaN(numValue)) {
return callback(new Error('点差必须为数字'))
}
if (numValue < 0) {
return callback(new Error('点差不能为负数'))
}
if (!/^\d+(\.\d{1,2})?$/.test(value.toString())) {
return callback(new Error('点差最多保留2位小数'))
}
// 2. 核心校验:已分配点差 ≤ 可分配点差
if (row && row.pointDifference !== undefined) {
const maxPointDiff = Number(row.pointDifference)
if (numValue > maxPointDiff) {
return callback(new Error(`不能超过可分配点差(${maxPointDiff})`))
}
}
callback()
},
trigger: 'change' // 数据变化时触发
trigger: ['blur', 'change']
}
]
],
}
}

View File

@ -1,523 +0,0 @@
<template>
<div class="market">
<MarketSearch :search-form="marketSearchForm" :search-options="marketSearchOptions" @search="handleSearch"
@reset="handleReset">
<template #button="{ form }">
<el-button type="primary" @click="handleAddAgent()">
添加代理
</el-button>
<el-button type="primary" @click="handleEditAgent(tableRow)" :disabled="!tableRow">
编辑
</el-button>
</template>
</MarketSearch>
<MarketTable :table-data="marketTree" :columns="marketTableColumnsOptions" row-key="id"
@row-click="handleRowClick">
<template #pointSpread="{ row }">
<el-button type="primary" size="small" @click="handleViewPointDiff(row)">
查看
</el-button>
</template>
</MarketTable>
<MarketPagination v-model="marketSearchForm.page" v-model:page-size-value="marketSearchForm.pageSize"
:total="total" @pagination-change="handlePaginationChange" />
<MarketDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType"
:class="{ 'showForm-dialog': dialogType !== 'view' }" @confirm="handleSubmit" @cancel="handleCancel"
@close="handleClose" :fullscreen="dialogFullscreen">
<MarketForm :form-data="marketForm" :form-rules="marketFormRules" :form-items="marketFormItemOptions"
:options-map="marketFormOptionsMap" ref="marketFormRef" @form-item-change="handleFormItemChange"
v-if="dialogType !== 'view'">
</MarketForm>
<MarketTable :table-data="marketPointTree" :columns="marketPointTableColumnsOptions" row-key="id">
<template #assignedPointDiff="{ row }">
<el-form-item prop="assignedPointDifference">
<el-input v-if="dialogType !== 'view'" v-model.number="row.assignedPointDifference" size="small"
placeholder="请输入点差" />
</el-form-item>
</template>
</MarketTable>
</MarketDialog>
</div>
</template>
<script lang="ts" setup>
//
import MarketSearch from '@/components/common/Search.vue'
import MarketTable from '@/components/common/Table.vue'
import MarketPagination from '@/components/common/Pagination.vue'
import MarketDialog from '@/components/common/Dialog.vue'
import MarketForm from '@/components/common/Form.vue'
//
import { getMarketListApi, getPointDiffConfigApi, getParAgentListApi } from '@/api/modules/account/market'
//
import { marketSearch, marketTableColumns, marketFormItem, marketPointTableColumns, countryOptions } from './options'
import { rules } from './rules'
/**
* @description 搜索配置
*/
const marketSearchForm = ref<any>({
accountName: '',
page: 1,
pageSize: 10,
})
const marketSearchOptions = ref<any>(marketSearch)
/**
* @description 表格配置
*/
const marketTableColumnsOptions = ref<any>(marketTableColumns)
//
const tableRow = ref<any>(null)
const tableRowId = ref<number | null>(null)
const tableRowRoleId = ref<number | null>(null)
const marketTree = ref<any>([])
/**
* @description 分页配置
*/
const total = ref<number>(0)
/**
* @description dialog配置
*/
const dialogVisible = ref<boolean>(false)
const dialogTitle = ref<string>('')
const dialogType = ref<string>('add')
const dialogFullscreen = ref(false)
const activeTab = ref('email')
/**
* @description 点差表格配置
*/
const marketPointTree = ref<any>([])
const marketPointTableColumnsOptions = ref<any>(marketPointTableColumns)
/**
* @description 表单数据
*/
const marketForm = ref<any>({
accountName: '',
level: '',
parentAccountName: '',
position: '',
commissionRatio: '',
assignedPointDifference: '',
pointDiffList: [] //
})
//
const walletCapital = ref<any>({
position: 0,
commissionRatio: 0,
})
const marketFormOptionsMap = ref<any>({
level: [
{ label: '平台', value: '1' },
{ label: '承包商', value: '2' },
{ label: '特殊做市', value: '3' },
{ label: '普通做市', value: '4' },
{ label: '一级代理', value: '5' },
],
parentAccountName: [] //
})
const showForm = ref<boolean>(true)
const marketFormRef = ref<any>()
const marketFormItemOptions = ref<any>(marketFormItem)
const marketFormRules = ref<any>(rules(walletCapital.value))
const pointDiffData = ref<any>({})
/**
* 级别映射工具函数全局复用
* @param roleId 级别数字1/2/3/4/5
* @returns 对应的文字标签
*/
const getLevelLabel = (roleId: number | string): string => {
const levelMap: Record<string | number, string> = {
'1': '平台',
'2': '承包商',
'3': '特殊做市',
'4': '普通做市',
'5': '一级代理'
}
return levelMap[roleId] || '未知级别'
}
/**
* @description 点击表格行时存储选中行的ID和数据
*/
const handleRowClick = (row: any) => {
tableRow.value = row
tableRowId.value = row.id
tableRowRoleId.value = row.role_id
console.log('点击行数据', row)
}
/**
* @description 监听表单字段变化获取所属上级列表 / 点差数据
*/
const handleFormItemChange = (field: string, value: any) => {
// level
if (field === 'level') {
const levelRoleMap: Record<string, number> = {
'1': 1, //
'2': 2, //
'3': 3, //
'4': 4, //
'5': 5 //
}
const roleId = levelRoleMap[value]
if (roleId) {
fetchParAgentList(roleId)
} else {
marketFormOptionsMap.value.parentAccountName = []
//
marketForm.value.parentAccountName = ''
marketPointTree.value = []
}
}
// parentAccountName ID
if (field === 'parentAccountName' && value) {
// value id fetchParAgentList value: item.id
fetchPointDiffConfigByParentId(value)
//
const parentAgent = marketFormOptionsMap.value.parentAccountName.find(
(item: any) => item.value === value
)
walletCapital.value.position = parentAgent.position
walletCapital.value.commissionRatio = parentAgent.commissionRatio
} else if (field === 'parentAccountName' && !value) {
//
marketPointTree.value = []
}
}
/**
* @description 统一获取数据
*/
//
const fetchMarketList = async () => {
const params = {
page: marketSearchForm.value.page,
page_size: marketSearchForm.value.pageSize,
user_name: marketSearchForm.value.accountName
}
const data = await getMarketListApi(params)
//
marketTree.value = data.data.map((item: any) => ({
...item,
agentRelation: item.path_name || '--', //
accountName: item.username || '', //
level: getLevelLabel(item.role_id), // role_id 1-> 2->
basicAccount: item.wallet_capital?.amount || '0.00000', //
margin: item.wallet_capital?.bail || '0.00000', //
stopOrderMargin: item.wallet_capital?.bail_stop_order || '0.00000', //
positionTransferMargin: item.wallet_capital?.bail_change_order || '0.00000', //
pointSpreadCommission: item.wallet_capital?.commission_point_diff || '0.00000', //
commissionRatio: item.wallet_capital?.toucun_percent || '0.00000', //
commissionReturn: item.wallet_capital?.commission_fee_handling || '0.00000', //
closeProfitLoss: item.wallet_capital?.closing_profit || '0.00000', //
riskMargin: item.wallet_capital?.risk || '0.00000', //
serviceFee: item.wallet_capital?.service || '0.00000' //
}))
//
total.value = Number(data.total)
marketSearchForm.value.page = Number(data.current_page)
marketSearchForm.value.pageSize = Number(data.per_page)
}
//
const fetchPointDiffConfig = async () => {
const params = {
user_id: tableRowId.value
}
try {
const data = await getPointDiffConfigApi(params)
//
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.point_diff,
assignedPointDifference: item.target_point_diff
}))
} catch (err) {
console.error('获取代理点差配置失败', err)
}
}
//
const fetchParAgentList = async (roleId?: number) => {
const params = { role_id: roleId }
try {
const data = await getParAgentListApi(params)
marketFormOptionsMap.value.parentAccountName = data.map((item: any) => ({
label: item.accountName || item.username, //
value: item.id, // IDrow.parent_id
position: item.wallet_capital.toucun_percent,
commissionRatio: item.wallet_capital.toucun_percent
}))
} catch (err) {
console.error('获取所属上级列表失败', err)
marketFormOptionsMap.value.parentAccountName = []
}
}
// ID
const fetchPointDiffConfigByParentId = async (parentId: number) => {
if (!parentId) {
marketPointTree.value = []
return
}
const params = { user_id: parentId }
try {
const data = await getPointDiffConfigApi(params)
marketPointTree.value = data.map((item: any) => ({
...item,
varietyName: item.name,
varietyCode: item.code,
pointDifference: item.target_point_diff, //
assignedPointDifference: '' //
}))
} catch (err) {
console.error('获取上级点差配置失败', err)
marketPointTree.value = []
}
}
/**
* @description 组件挂载时获取数据
*/
onMounted(() => {
fetchMarketList()
})
/**
* @description 搜索
*/
const handleSearch = async () => {
await fetchMarketList()
}
const handleReset = () => {
marketSearchForm.value.accountName = ''
handleSearch()
}
/**
* @description 处理弹窗类型
*/
const handleDialogType = (type: string) => {
if (type === "view") {
dialogFullscreen.value = true
showForm.value = false
} else {
dialogFullscreen.value = false
showForm.value = true
}
}
/**
* @description 新增做市商
*/
const handleAddAgent = () => {
dialogVisible.value = true
dialogTitle.value = '新增做市商'
dialogType.value = 'add'
handleDialogType(dialogType.value)
//
marketForm.value = {
accountName: '',
level: '',
parentAccountName: '',
position: '',
commissionRatio: '',
}
//
marketFormOptionsMap.value.parentAccountName = []
marketPointTree.value = []
}
/**
* @description 编辑做市商
*/
const handleEditAgent = (row: any) => {
if (!row) return
dialogVisible.value = true
dialogTitle.value = '编辑做市商'
dialogType.value = 'edit'
handleDialogType(dialogType.value)
// 1. value
const levelValueMap: Record<string | number, string> = {
'1': '1', '2': '2', '3': '3', '4': '4', '5': '5'
}
const levelValue = levelValueMap[row.role_id] || ''
// 2. path IDpath : "/1/30/33" ID
const pathSegments = row.path.split('/').filter(Boolean) // ['1', '30', '33']
const parentId = pathSegments.length >= 2 ? Number(pathSegments[pathSegments.length - 2]) : null
// 3.
marketForm.value = {
...row,
level: levelValue,
parentAccountName: parentId // ID
}
// 4.
const levelRoleMap: Record<string, number> = {
'1': 1, '2': 2, '3': 3, '4': 4, '5': 5
}
const roleId = levelRoleMap[levelValue]
if (roleId) {
fetchParAgentList(roleId)
}
// 5.
if (parentId) {
fetchPointDiffConfigByParentId(parentId)
} else {
marketPointTree.value = []
}
}
/**
* @description 查看点差
*/
const handleViewPointDiff = (row: any) => {
tableRowId.value = row.id
dialogVisible.value = true
dialogTitle.value = '查看点差'
dialogType.value = 'view'
handleDialogType(dialogType.value)
fetchPointDiffConfig()
}
/**
* @description 分页变更
*/
const handlePaginationChange = (params: any) => {
marketSearchForm.value.page = params.currentPage
marketSearchForm.value.pageSize = params.pageSize
handleSearch()
}
/**
* @description 提交表单新增/编辑做市商
*/
const handleSubmit = async () => {
// idkeyconst pointDiffList = []
marketPointTree.value.forEach((row: any) => {
if (row.assignedPointDifference || row.assignedPointDifference === 0) {
pointDiffData.value[row.id] = row.assignedPointDifference
}
console.log('pointDiffData', pointDiffData.value)
})
await marketFormRef.value?.validate()
if (dialogType.value === 'add') {
//
const params = {
username: '',
}
} else if (dialogType.value === 'edit') {
//
} else {
//
}
}
/**
* @description 取消操作
*/
const handleCancel = () => {
dialogVisible.value = false
dialogType.value = 'add'
}
/**
* @description 关闭弹窗
*/
const handleClose = () => {
dialogVisible.value = false
dialogType.value = 'add'
}
</script>
<style scoped lang="scss">
.el-form {
.el-form-item {
flex: none !important;
}
}
.market {
.search-container {
:deep(.el-form) {
.el-form-item {
flex: none;
}
}
}
:deep(.el-dialog) {
.el-dialog__footer {
display: none;
}
}
:deep(.showForm-dialog) {
.el-dialog__footer {
display: block;
}
}
.phone-email {
display: flex;
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
.tab-btn {
display: flex;
margin-right: 10px;
.el-button {
margin-left: 0;
background-color: transparent;
border: 1px solid #DCDFE6;
color: #303133;
height: 32px;
}
.active {
background-color: #409EFF;
color: #fff;
}
}
.email-item {
.el-input {
width: 100%;
}
}
.phone-item {
.el-select {
width: 120px;
margin-right: 10px;
}
}
}
}
</style>

View File

@ -41,7 +41,7 @@
</el-checkbox>
<!-- 添加子权限 change 事件监听 -->
<el-checkbox-group v-model="formData.permissions" @change="handlePermissionChange">
<el-checkbox v-for="perm in group" :key="perm.id" :label="perm.id">
<el-checkbox v-for="perm in group" :key="perm.id" :value="perm.id">
{{ perm.title }} ({{ perm.name }})
</el-checkbox>
</el-checkbox-group>