This commit is contained in:
李远 2026-03-19 17:27:43 +08:00
parent c9f943f8dd
commit 6484da509e
6 changed files with 492 additions and 5 deletions

View File

@ -95,7 +95,7 @@ const responseInterceptor = <T = unknown>(response: AxiosResponse<ApiResponse<T>
})
return data.data
} else {
if (data.code === 1 && data.msg === '用户不存在') {
if (data.code === 1) {
// 不弹错误提示,返回带特殊标识的结果
// 其他业务失败弹错误提示并拒绝Promise
ElNotification({

View File

@ -0,0 +1,14 @@
import { request } from '@/api';
//获取风险列表
export const getRiskListApi = () => {
return request.get('/riskControl/getRiskControlList')
}
//添加风险控制
export const addRiskApi = (params: any) => {
return request.post('/riskControl/createRiskControl', params)
}
//编辑风险控制
export const editRiskApi = (params: any) => {
return request.post('/riskControl/editRiskControlSetting', params)
}

View File

@ -7,7 +7,7 @@
<el-form-item :label="item.label" :prop="item.prop" :required="item.required" label-position="left"
:label-width="item.labelWidth || '100px'" :class="item.class">
<!-- 输入框 -->
<el-input v-if="item.type === 'input'" v-model="formData[item.prop]"
<el-input v-if="item.type === 'input'" v-model="formData[item.prop]"
:placeholder="item.placeholder" :disabled="item.disabled" clearable />
<!-- 下拉选择框 -->
@ -56,6 +56,12 @@
:end-placeholder="item.endPlaceholder || '结束时间'"
:value-format="item.valueFormat || 'YYYY-MM-DD'" :clearable="item.clearable ?? true"
:disabled-date="item.disabledDate" :locale="'zhCn'" />
<!-- 时间选择器单个/范围 -->
<el-time-picker v-else-if="item.type === 'time'" v-model="formData[item.prop]"
:range-separator="item.rangeSeparator || '至'"
:start-placeholder="item.startPlaceholder || '开始时间'"
:end-placeholder="item.endPlaceholder || '结束时间'"
:value-format="item.valueFormat || 'HH:mm:ss'" is-range />
<!-- 自定义插槽 -->
<slot v-else :name="item.slotName" :formData="formData" />
</el-form-item>
@ -70,7 +76,7 @@ import type { FormInstance, FormRules } from 'element-plus'
import type { Component } from 'vue'
interface FormItem {
type: 'input' | 'select' | 'number' | 'radio' | 'slot' | 'checkbox' | 'date' | 'switch' | 'edit'
type: 'input' | 'select' | 'number' | 'radio' | 'slot' | 'checkbox' | 'date' | 'switch' | 'edit' | 'time'
label: string
prop: string
placeholder?: string
@ -83,13 +89,13 @@ interface FormItem {
labelWidth?: string
class?: string
dateType?: 'date' | 'daterange' | 'monthrange' | 'yearrange' | 'datetimerange'
timeType?: 'time' | 'timerange'
rangeSeparator?: string
startPlaceholder?: string
endPlaceholder?: string
valueFormat?: string
clearable?: boolean
disabledDate?: (time: Date) => boolean
}
interface Props {

View File

@ -1,3 +1,362 @@
<template>
<h1>风控管理</h1>
<div class="risk">
<div class="bar">
<span>风控管理</span>
<el-button type="primary" @click="handleAddRisk">添加</el-button>
<el-button type="primary" @click="handleModifyRisk">修改</el-button>
<el-button type="danger" @click="handleRiskStatus">停用</el-button>
</div>
<!-- 表格 -->
<div class="risk-table-content">
<RiskTableSwitch :columns="tableColumnsSwitch" :table-data="tableTreeSwitch" class="table-switch"
@row-click="handleRowClick" row-key="id" highlight-current-row :current-row-key="selectedRowId">
<template #status="{ row }">
<span>{{ row.status === 1 ? '启用' : '停用' }}</span>
</template>
</RiskTableSwitch>
<RiskTableContent :columns="tableColumnsContent" :table-data="[currentContent]" class="table-content"
row-key="id">
<template #contentName="{ row }">
<div class="risk-content-group">
<span>单笔开仓最大数量:{{ row.maxOpenPosition }}</span>
<span>单品种最大持仓数量:{{ row.maxPositionPerSymbol }}</span>
<span>总品种最大持仓数量:{{ row.maxTotalPosition }}</span>
<span>强平比例:{{ row.strongCloseRatio }}</span>
<span>每次出金最大金额:{{ row.maxWithdrawalAmount }}</span>
<span>是否允许出金:{{ row.isWithdrawalAllowed }}</span>
<span>出金时间设置:{{ row.withdrawalTimeSetting }}</span>
<span>是否需要出金审核:{{ row.isWithdrawalAuditRequired }}</span>
<span>建仓多少秒不能平仓:{{ row.buildCloseTime }}</span>
<span>每次出金最小金额:{{ row.minWithdrawalAmount }}</span>
<span>滑点:{{ row.slippage }}</span>
<span>每次入金最小金额:{{ row.minDepositAmount }}</span>
</div>
</template>
</RiskTableContent>
</div>
<!-- dialog -->
<RiskDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" @confirm="handleSubmit"
@cancel="handleCancel" @close="handleClose">
<RiskForm :form-data="dialogForm" :form-rules="riskRules" :form-items="RiskFormItems"
:options-map="riskFormOptionsMap" :row-gutter="rowGutter" :col-span="colSpan" ref="riskFormRef" />
</RiskDialog>
</div>
</template>
<script setup lang="ts">
//
import RiskTableSwitch from '@/components/common/Table.vue'
import RiskTableContent from '@/components/common/Table.vue'
import RiskDialog from '@/components/common/Dialog.vue'
import RiskForm from '@/components/common/Form.vue'
//
import { tableColumnsSwitch, tableColumnsContent, formItem } from './options'
import { rules } from './rules'
//
import { getRiskListApi, addRiskApi, editRiskApi } from '@/api/modules/risk'
//
/**
* @description: 定义表格数据
*/
const tableTreeSwitch = ref([])
const tableTreeContent = ref([])
// ID
const selectedRowId = ref<number | null>(null)
//
const riskOriginData = ref<any[]>([])
//
const selectedRow = ref<any>({})
//
const currentContent = ref<any>({})
/**
* @description: 定义弹窗数据
*/
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogType = ref('')
/**
* @description: 定义弹窗表单数据
*/
const dialogForm = ref({
id: null,
name: '',
status: 1,
//
isDefault: 1,
maxOpenPosition: null,
maxPositionPerSymbol: null,
maxTotalPosition: null,
strongCloseRatio: null,
maxWithdrawalAmount: null,
isWithdrawalAllowed: 1,
withdrawalTimeSetting: '',
isWithdrawalAuditRequired: 1,
buildCloseTime: null,
minWithdrawalAmount: null,
slippage: null,
minDepositAmount: null,
})
const riskRules = ref(rules())
const riskFormRef = ref()
const RiskFormItems = ref(formItem)
const riskFormOptionsMap = ref({
//
status: [
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
//
isDefault: [
{ label: '是', value: 1 },
{ label: '否', value: 2 },
],
//
isWithdrawalAllowed: [
{ label: '是', value: 1 },
{ label: '否', value: 2 },
],
//
isWithdrawalAuditRequired: [
{ label: '是', value: 1 },
{ label: '否', value: 2 },
],
})
const rowGutter = ref(20)
const colSpan = ref(12)
/**
* @description: 定义公共数据处理方法
*/
/**
* @description: 定义请求数据方法
*/
const getRiskList = async () => {
const data: any = await getRiskListApi()
//
riskOriginData.value = data
//
tableTreeSwitch.value = data.map((item: any) => ({
id: item.id,
name: item.name,
status: item.status,
}))
tableTreeContent.value = data.map((item: any) => ({
id: item.id,
isDefault: item.is_default,
//
maxOpenPosition: item.max_opening_quantity_per_transaction,
//
maxPositionPerSymbol: item.max_holding_quantity_per_variety,
//
maxTotalPosition: item.max_holding_quantity_per_transaction,
//
strongCloseRatio: item.strong_to_average_ratio,
//
maxWithdrawalAmount: item.max_withdrawal_amount_per_transaction,
//
isWithdrawalAllowed: item.withdraw_status === 1 ? '是' : '否',
//
withdrawalTimeSetting: item.withdraw_time,
//
isWithdrawalAuditRequired: item.withdraw_is_check === 1 ? '是' : '否',
//
buildCloseTime: item.open_close_time,
//
minWithdrawalAmount: item.min_withdrawal_amount_per_transaction,
//
slippage: item.slippage,
//
minDepositAmount: item.min_recharge_amount_per_transaction,
}))
//
if (tableTreeSwitch.value.length > 0) {
handleRowClick(tableTreeSwitch.value[0])
}
}
/**
* @description: 页面加载完成需要请求的数据
*/
onMounted(() => {
getRiskList()
})
/**
* @description: 定义bar方法
*/
const handleAddRisk = () => {
dialogVisible.value = true
dialogTitle.value = '添加'
dialogType.value = 'add'
dialogForm.value = {
id: null,
name: '',
status: 1,
//
isDefault: 1,
maxOpenPosition: null,
maxPositionPerSymbol: null,
maxTotalPosition: null,
strongCloseRatio: null,
maxWithdrawalAmount: null,
isWithdrawalAllowed: 1,
withdrawalTimeSetting: '',
isWithdrawalAuditRequired: 1,
buildCloseTime: null,
minWithdrawalAmount: null,
slippage: null,
minDepositAmount: null,
}
}
const handleModifyRisk = () => {
dialogVisible.value = true
dialogTitle.value = '修改'
dialogType.value = 'edit'
dialogForm.value = {
name: selectedRow.value.name,
status: selectedRow.value.status,
//
isDefault: selectedRow.value.is_default,
maxOpenPosition: selectedRow.value.max_opening_quantity_per_transaction,
maxPositionPerSymbol: selectedRow.value.max_holding_quantity_per_variety,
maxTotalPosition: selectedRow.value.max_holding_quantity_per_transaction,
strongCloseRatio: selectedRow.value.strong_to_average_ratio,
maxWithdrawalAmount: selectedRow.value.max_withdrawal_amount_per_transaction,
isWithdrawalAllowed: selectedRow.value.withdraw_status,
withdrawalTimeSetting: selectedRow.value.withdraw_time.split('-'),
isWithdrawalAuditRequired: selectedRow.value.withdraw_is_check,
buildCloseTime: selectedRow.value.open_close_time,
minWithdrawalAmount: selectedRow.value.min_withdrawal_amount_per_transaction,
slippage: selectedRow.value.slippage,
minDepositAmount: selectedRow.value.min_recharge_amount_per_transaction,
} as any
}
const handleRiskStatus = async () => {
dialogTitle.value = '停用'
dialogType.value = 'status'
await editRiskApi({ ...selectedRow.value, status: 2 })
getRiskList()
}
/**
* @description: 定义表格方法
*/
const handleRowClick = (row: any) => {
//
const originRow = riskOriginData.value.find(item => item.id === row.id)
if (originRow) {
selectedRow.value = originRow //
} else {
selectedRow.value = row
}
selectedRowId.value = row.id
//
const targetItem = tableTreeContent.value.find((item: any) => item.id === row.id)
if (targetItem) {
currentContent.value = targetItem
}
}
/**
* @description: 定义弹窗方法
*/
const handleSubmit = async () => {
await riskFormRef.value?.validate()
const params = {
name: dialogForm.value.name,
status: dialogForm.value.status,
//
is_default: dialogForm.value.isDefault,
//
max_opening_quantity_per_transaction: dialogForm.value.maxOpenPosition,
//
max_holding_quantity_per_variety: dialogForm.value.maxPositionPerSymbol,
//
max_holding_quantity_per_transaction: dialogForm.value.maxTotalPosition,
//
strong_to_average_ratio: dialogForm.value.strongCloseRatio,
//
max_withdrawal_amount_per_transaction: dialogForm.value.maxWithdrawalAmount,
//
withdraw_status: dialogForm.value.isWithdrawalAllowed,
//
withdraw_time: Array.from(dialogForm.value.withdrawalTimeSetting || []).join('-'),
//
withdraw_is_check: dialogForm.value.isWithdrawalAuditRequired,
//
open_close_time: dialogForm.value.buildCloseTime,
//
min_withdrawal_amount_per_transaction: dialogForm.value.minWithdrawalAmount,
//
slippage: dialogForm.value.slippage,
//
min_recharge_amount_per_transaction: dialogForm.value.minDepositAmount,
}
//
if (dialogType.value === 'add') {
//
await addRiskApi(params)
getRiskList()
dialogVisible.value = false
dialogTitle.value = '添加'
dialogType.value = 'add'
}
else {
//
await editRiskApi({ ...params, id: selectedRow.value.id })
getRiskList()
dialogVisible.value = false
dialogTitle.value = '编辑'
dialogType.value = 'edit'
}
}
const handleCancel = () => {
dialogVisible.value = false
}
const handleClose = () => {
dialogVisible.value = false
}
</script>
<style scoped>
.risk {
.bar {
margin-bottom: 10px;
display: flex;
align-items: center;
span {
margin-right: 10px;
}
}
.risk-table-content {
display: flex;
gap: 10px;
.table-switch {
flex-basis: 20%;
min-width: 200px;
:deep(.el-table__row.current-row) {
background-color: #ecf5ff !important;
}
}
}
.table-content {
flex: 1;
.risk-content-group {
display: flex;
flex-direction: column;
gap: 5px;
}
}
}
</style>

48
src/views/risk/options.ts Normal file
View File

@ -0,0 +1,48 @@
export const tableColumnsSwitch = [
{
label: '名称',
prop: 'name',
align: 'center' as const,
},
{
label: '状态',
slotName: 'status',
align: 'center' as const,
},
]
export const tableColumnsContent = [
// 内容
{label: '内容', prop: 'content', align: 'center' as const,slotName: 'contentName'},
]
export const formItem = [
// 名称
{label: '名称', prop: 'name', type: 'input' as const, placeholder: '请输入名称',labelWidth: '150px'},
// 状态
{label: '状态', prop: 'status', type: 'switch' as const},
// 是否默认
{label: '是否默认', prop: 'isDefault', type: 'radio' as const,labelWidth: '150px'},
// 是否允许出金
{label: '是否允许出金', prop: 'isWithdrawalAllowed', type: 'radio' as const,labelWidth: '150px'},
// 是否需要出金审核
{label: '是否需要出金审核', prop: 'isWithdrawalAuditRequired', type: 'radio' as const ,labelWidth: '150px'},
// 单笔开仓最大数量
{label: '单笔开仓最大数量', prop: 'maxOpenPosition', type: 'input' as const, placeholder: '请输入单笔开仓最大数量',labelWidth: '150px'},
// 单品种最大持仓数量
{label: '单品种最大持仓数量', prop: 'maxPositionPerSymbol', type: 'input' as const, placeholder: '请输入单品种最大持仓数量',labelWidth: '150px'},
//总品种最大持仓数量
{label: '总品种最大持仓数量', prop: 'maxTotalPosition', type: 'input' as const, placeholder: '请输入总品种最大持仓数量',labelWidth: '150px'},
// 强平比例
{label: '强平比例', prop: 'strongCloseRatio', type: 'input' as const, placeholder: '请输入强平比例',labelWidth: '150px'},
// 每次出金最大金额
{label: '每次出金最大金额', prop: 'maxWithdrawalAmount', type: 'input' as const, placeholder: '请输入每次出金最大金额',labelWidth: '150px'},
// 出金时间设置
{ label: '出金时间设置', prop: 'withdrawalTimeSetting', type: 'time' as const, placeholder: '请选择出金时间', valueFormat: 'HH:mm',labelWidth: '150px'},
// 建仓多少秒不能平仓
{label: '建仓多少秒不能平仓', prop: 'buildCloseTime', type: 'input' as const, placeholder: '请输入建仓多少秒不能平仓',labelWidth: '150px'},
// 每次出金最小金额
{label: '每次出金最小金额', prop: 'minWithdrawalAmount', type: 'input' as const, placeholder: '请输入每次出金最小金额',labelWidth: '150px'},
//滑点
{label: '滑点', prop: 'slippage', type: 'input' as const, placeholder: '请输入滑点',labelWidth: '150px'},
//每次入金最小金额
{label: '每次入金最小金额', prop: 'minDepositAmount', type: 'input' as const, placeholder: '请输入每次入金最小金额',labelWidth: '150px'},
]

60
src/views/risk/rules.ts Normal file
View File

@ -0,0 +1,60 @@
import type { FormRules } from 'element-plus'
export const rules = (): FormRules => {
return {
name: [
{ required: true, message: '请输入名称', trigger: ['blur'] }
],
status: [
{ required: true, message: '请选择状态', trigger: ['change'] }
],
isDefault: [
{ required: true, message: '请选择是否默认', trigger: ['change'] }
],
isWithdrawalAllowed: [
{ required: true, message: '请选择是否允许出金', trigger: ['change'] }
],
isWithdrawalAuditRequired: [
{ required: true, message: '请选择是否需要出金审核', trigger: ['change'] }
],
maxOpenPosition: [
{ required: true, message: '请输入单笔开仓最大数量', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
maxPositionPerSymbol: [
{ required: true, message: '请输入单品种最大持仓数量', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
maxTotalPosition: [
{ required: true, message: '请输入总品种最大持仓数量', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
strongCloseRatio: [
{ required: true, message: '请输入强平比例', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
maxWithdrawalAmount: [
{ required: true, message: '请输入最大出金金额', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
withdrawalTimeSetting: [
{ required: true, message: '请选择出金时间', trigger: ['change'] }
],
buildCloseTime: [
{ required: true, message: '请输入建仓多少秒不能平仓', trigger: ['blur','change'] },
{ pattern: /^[0-9]+$/, message: '请输入整数' }
],
minWithdrawalAmount: [
{ required: true, message: '请输入出金最小金额', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
slippage: [
{ required: true, message: '请输入滑点', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
minDepositAmount: [
{ required: true, message: '请输入入金最小金额', trigger: ['blur','change'] },
{ pattern: /^[0-9]+(\.[0-9]+)?$/, message: '请输入有效的数字' }
],
}
}