This commit is contained in:
李远 2026-03-23 18:07:12 +08:00
parent 514c153745
commit fe641618e1
21 changed files with 1529 additions and 42 deletions

View File

@ -0,0 +1,6 @@
import { request } from '@/api';
//获取资金明细列表
export const getFundDetailListApi = (params: any) => {
return request.get('/user/FundFlowRecords', {...params})
}

View File

@ -0,0 +1,6 @@
import { request } from '@/api';
//获取历史交易列表
export const getHistoryTradeListApi = (params: any) => {
return request.get('orderRecords/getList', {...params})
}

View File

@ -38,6 +38,7 @@ body,
padding: 20px; padding: 20px;
overflow: auto; overflow: auto;
max-height: calc(1080px - 200px); max-height: calc(1080px - 200px);
height: 100%;
} }
} }
} }

View File

@ -78,7 +78,7 @@ import type { FormInstance, FormRules } from 'element-plus'
import type { Component } from 'vue' import type { Component } from 'vue'
interface FormItem { interface FormItem {
type: 'input' | 'select' | 'number' | 'radio' | 'slot' | 'checkbox' | 'date' | 'switch' | 'edit' | 'password' | 'time' type: 'input' | 'select' | 'number' | 'radio' | 'slot' | 'checkbox' | 'date' | 'switch' | 'edit' | 'password' | 'time' | 'upload'
label: string label: string
prop: string prop: string
placeholder?: string placeholder?: string

View File

@ -60,7 +60,7 @@ const handleChangePassword = () => {
const handleLogout = () => { const handleLogout = () => {
// 退 // 退
const deviceNo = getStorage('device_no') as string const deviceNo = getStorage('device_no') as string
userStore.logout({ device_no: deviceNo }) userStore.logout({ device_no: String(deviceNo) })
} }
// //

View File

@ -123,7 +123,19 @@ const staticMenuList = ref<ElMenuItem[]>(
label: '做市商管理', label: '做市商管理',
path: '/account/market-maker', // index="3-3" path: '/account/market-maker', // index="3-3"
icon: '' icon: ''
} },
{
id: 34,
label: '档案管理',
path: '/account/archives', // index="3-4"
icon: ''
},
{
id: 35,
label: '账号管理',
path: '/account/accountDetail', // index="3-5"
icon: ''
},
] ]
}, },
// //
@ -145,6 +157,24 @@ const staticMenuList = ref<ElMenuItem[]>(
path: '/trade/group', // index="4-2" path: '/trade/group', // index="4-2"
icon: '' icon: ''
}, },
{
id: 43,
label: '持仓统计',
path: '/trade/positionStat', // index="4-3"
icon: ''
},
{
id: 44,
label: '历史交易',
path: '/trade/historyTrade', // index="4-4"
icon: ''
},
{
id: 45,
label: '交易软件',
path: '/trade/tradeSoftware', // index="4-5"
icon: ''
},
// { // {
// id: 43, // id: 43,
// label: '', // label: '',
@ -197,6 +227,12 @@ const staticMenuList = ref<ElMenuItem[]>(
path: '/funding/transferDetail', // index="6-5" path: '/funding/transferDetail', // index="6-5"
icon: '' icon: ''
}, },
{
id: 66,
label: '资金明细',
path: '/funding/fundDetail', // index="6-6"
icon: ''
},
// { // {
// id: 66, // id: 66,
// label: '', // label: '',
@ -248,52 +284,62 @@ const staticMenuList = ref<ElMenuItem[]>(
// //
{ // {
id: 9, // id: 9,
label: '资金管理', // label: '',
path: 'funding', // index="funding" // path: 'funding', // index="funding"
icon: 'GoodsFilled', // <GoodsFilled /> // icon: 'GoodsFilled', // <GoodsFilled />
children: [ // children: [
/// // ///// /
// // //
//
// // ]
// // },
]
},
// //
{ // {
id: 10, // id: 10,
label: '交易统计', // label: '',
path: 'trade', // index="trade" // path: 'trade', // index="trade"
icon: 'ChartBarFilled', // <ChartBarFilled /> // icon: 'ChartBarFilled', // <ChartBarFilled />
children: [ // children: [
// // //
// // //
// // //
] // ]
}, // },
// //
{ // {
id: 11, // id: 11,
label: '账户管理', // label: '',
path: 'account', // index="account" // path: 'account', // index="account"
icon: 'UserFilled', // <UserFilled /> // icon: 'UserFilled', // <UserFilled />
children: [ // children: [
// // //
// // //
] // ]
}, // },
// //
{ {
id: 12, id: 12,
label: '个性化交易', label: '个性化交易',
path: 'customTrade', // index="customTrade" path: 'personalized', // index="personalized"
icon: 'SettingFilled', // <SettingFilled /> icon: 'SettingFilled', // <SettingFilled />
children: [ children: [
// // app
// //
{
id: 121,
label: '跟单记录',
path: '/personalized/followRecord', // index="12-1"
icon: ''
},
// //
{
id: 122,
label: '带单记录',
path: '/personalized/leadRecord', // index="12-2"
icon: ''
},
] ]
}, },
// //

View File

@ -33,6 +33,18 @@ const routes = [
name: 'ChangePassword', name: 'ChangePassword',
component: () => import('@/views/user/ChangePassword.vue'), component: () => import('@/views/user/ChangePassword.vue'),
meta: { requiresAuth: true, title: '修改密码' } meta: { requiresAuth: true, title: '修改密码' }
},
{
path: 'bind-email',
name: 'BindEmail',
component: () => import('@/views/user/bindEmail.vue'),
meta: { requiresAuth: true, title: '绑定邮箱' }
},
{
path: 'bind-phone',
name: 'BindPhone',
component: () => import('@/views/user/bindPhone.vue'),
meta: { requiresAuth: true, title: '绑定手机号' }
} }
] ]
}, },
@ -118,7 +130,23 @@ const routes = [
meta: { meta: {
title: '做市商管理', // 左侧菜单显示的标题 title: '做市商管理', // 左侧菜单显示的标题
} }
} },
{
path: 'archives',
name: 'Archives',
component: () => import('@/views/account/archives/index.vue'),
meta: {
title: '档案管理', // 左侧菜单显示的标题
}
},
{
path: 'accountDetail',
name: 'AccountDetail',
component: () => import('@/views/account/accountDetail/index.vue'),
meta: {
title: '账号管理', // 左侧菜单显示的标题
}
},
] ]
}, },
//交易管理 //交易管理
@ -152,7 +180,31 @@ const routes = [
// meta: { // meta: {
// title: '交易时间', // 左侧菜单显示的标题 // title: '交易时间', // 左侧菜单显示的标题
// } // }
// } // },
{
path: 'positionStat',
name: 'PositionStat',
component: () => import('@/views/trade/positionStat/index.vue'),
meta: {
title: '持仓统计', // 左侧菜单显示的标题
}
},
{
path: 'historyTrade',
name: 'HistoryTrade',
component: () => import('@/views/trade/historyTrade/index.vue'),
meta: {
title: '历史交易', // 左侧菜单显示的标题
}
},
{
path: 'tradeSoftware',
name: 'TradeSoftware',
component: () => import('@/views/trade/tradeSoftware/index.vue'),
meta: {
title: '交易软件', // 左侧菜单显示的标题
}
},
] ]
}, },
//公告管理 //公告管理
@ -236,6 +288,14 @@ const routes = [
title: '存款渠道列表', // 左侧菜单显示的标题 title: '存款渠道列表', // 左侧菜单显示的标题
} }
}, },
{
path: 'fundDetail',
name: 'FundDetail',
component: () => import('@/views/funding/fundDetail/index.vue'),
meta: {
title: '资金明细', // 左侧菜单显示的标题
}
},
] ]
}, },
{ {
@ -245,7 +305,35 @@ const routes = [
title: '风控管理', // 左侧菜单显示的标题 title: '风控管理', // 左侧菜单显示的标题
}, },
component: () => import('@/views/risk/index.vue'), component: () => import('@/views/risk/index.vue'),
} },
// 个性化交易
{
path: 'personalized',
name: 'Personalized',
meta: {
title: '个性化交易', // 左侧菜单显示的标题
},
children: [
// 跟单记录
{
path: 'followRecord',
name: 'FollowRecord',
component: () => import('@/views/personalized/followRecord/index.vue'),
meta: {
title: '跟单记录', // 左侧菜单显示的标题
}
},
// 带单记录
{
path: 'leadRecord',
name: 'LeadRecord',
component: () => import('@/views/personalized/leadRecord/index.vue'),
meta: {
title: '带单记录', // 左侧菜单显示的标题
}
},
]
},
] ]
}, },
{ {

View File

@ -0,0 +1,194 @@
<template>
<div class="account-detail">
<h4>账户信息</h4>
<el-card class="account-info">
<el-icon>
<UserFilled />
</el-icon>
<div class="item">
<div class="item-content">
<div class="item-label" v-if="!isChangeName">
<span>账号名称:</span>
<span>{{ accountName }}</span>
</div>
<div class="item-changeName" v-else>
<span>账号名称:</span>
<el-input v-model="newAccountName" placeholder="请输入新账号名称" size="small" ref="inputRef" @blur="handleSave" @keyup.enter="handleSave"/>
</div>
<el-button type="default" @click="changeName" size="small">修改名称</el-button>
</div>
<span>用户最后登录时间:2026.02.02 07:00:00</span>
</div>
</el-card>
<div class="security">
<h4>安全信息</h4>
<div class="card-group">
<el-card>
<span>邮箱</span>
<span>已绑定邮箱:{{ userInfo?.email || '未绑定' }}</span>
<el-button :type="accountStatus === true ? 'primary' : 'danger'" @click="handleEmail">{{ accountStatus === true ? '已验证' :
'未验证' }}</el-button>
</el-card>
<el-card>
<span>手机号</span>
<span>已绑定手机号:{{ userInfo?.phone || '未绑定' }}</span>
<el-button :type="phoneStatus === true ? 'primary' : 'danger'" @click="handlePhone">{{ phoneStatus === true ? '已绑定' :
'未绑定' }}</el-button>
</el-card>
<el-card>
<span>密码</span>
<span>************</span>
<el-button type="primary" @click="changePassword">修改</el-button>
</el-card>
<el-card>
<span>默认语言</span>
<span>选择您的语言偏好</span>
<LanguageSelect v-model="languageStore.lang" @change="languageStore.changeLang" />
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { UserFilled } from '@element-plus/icons-vue'
//
import LanguageSelect from '@/components/LanguageSelect/index.vue'
// Pinia
import { useLanguageStore } from '@/stores/modules/language'
import { getStorage, setStorage } from '@/utils/storage'
//
import { editAgentNameApi } from '@/api/modules/agent'
//
const router = useRouter()
const languageStore = useLanguageStore()
//
const accountStatus = ref(true)
//
const phoneStatus = ref(true)
/**
* @description 账户信息数据
*/
//
const userInfo = getStorage<any>('userInfo')
const accountName = ref(userInfo?.username || '')
const newAccountName = ref(accountName.value)
//
const isChangeName = ref(false)
//
const inputRef = ref<HTMLInputElement>()
/**
* @description 绑定邮箱
*/
const handleEmail = ()=>{
accountStatus.value === false ? router.push('/user/bind-email') : ''
}
/**
* @description 绑定手机号
*/
const handlePhone = ()=>{
phoneStatus.value === false ? router.push('/user/bind-phone') : ''
}
/**
* @description 修改密码
*/
const changePassword = () => {
router.push('/user/change-password')
}
//
const changeName = async () => {
isChangeName.value = true
// DOM
await nextTick()
inputRef.value?.focus()
}
//
const handleSave = async () => {
if (newAccountName.value.trim()) {
await editAgentNameApi({
account_id: userInfo?.id || '',
account_name: newAccountName.value
})
accountName.value = newAccountName.value.trim()
setStorage('userInfo', {
...userInfo,
username: accountName.value
})
} else {
newAccountName.value = accountName.value //
}
isChangeName.value = false
}
</script>
<style scoped lang="scss">
.account-detail {
h4 {
margin-bottom: 10px;
font-weight: 400;
}
.account-info {
box-shadow: none;
:deep(.el-card__body) {
display: flex;
padding: 10px;
.el-icon {
font-size: 48px;
}
}
.item {
.item-content {
display: flex;
align-items: center;
.item-changeName {
display: flex;
span {
width: 70px;
}
.el-input {
margin-right: 10px;
width: 140px;
}
}
}
}
}
.security {
margin-top: 20px;
.card-group {
display: flex;
flex-direction: column;
gap: 20px;
.el-card {
:deep(.el-card__body) {
padding: 20px 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,206 @@
<template>
<div class="archives">
<div class="bar">
<h4>档案管理</h4>
<span :class="['status', isActive === true ? 'active' : '']">未认证</span>
<span class="info">资料审核包含身份信息/联络信息/实名认证/合规文件需要管理员审核</span>
</div>
<div class="content">
<div class="tab">
<div class="tab-item">
<el-button :type="tabType === 'identity' ? 'primary' : 'default'" @click="tabType = 'identity'">身份信息</el-button>
</div>
<div class="tab-item">
<el-button :type="tabType === 'contact' ? 'primary' : 'default'" @click="tabType = 'contact'">联系信息</el-button>
</div>
</div>
<article>
<p class="warn">
<el-icon>
<WarnTriangleFilled />
</el-icon>
请填写正确的个人身份信息以确保所有功能正常使用
</p>
<ArchivesForm ref="archivesFormRef" :form-data="archivesForm" :form-rules="archivesFormRules"
:form-items="formOptions" :options-map="archivesFormOptionsMap" :row-gutter="rowGutter"
:col-span="colSpan">
<template #upload>
<div class="upload">
<div>
<p class="warn">请上传证件正面</p>
<el-upload class="avatar-uploader" action="" :show-file-list="true"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</div>
<div>
<p class="warn">请上传证件反面</p>
<el-upload class="avatar-uploader" action="" :show-file-list="true"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</div>
</div>
</template>
</ArchivesForm>
</article>
</div>
</div>
</template>
<script setup lang="ts">
import type { UploadProps } from 'element-plus'
import { WarnTriangleFilled, Plus } from '@element-plus/icons-vue'
//
import ArchivesForm from '@/components/common/Form.vue'
//
import { identityFormItems, contactFormItems } from './options'
import { ref } from 'vue'
//
const isActive = ref(false)
//
const tabType = ref('identity')
/**
* @description: 档案管理表单
*/
const archivesForm = ref({
nationality: 'CN',
idType: 'ID',
idNumber: '',
phone: '',
email: '',
address: '',
})
const archivesFormRules = ref()
const archivesFormOptionsMap = ref({
nationality: [
{ label: '中国', value: 'CN' },
{ label: '美国', value: 'US' },
{ label: '英国', value: 'GB' },
{ label: '法国', value: 'FR' },
],
idType: [
{ label: '身份证', value: 'ID' },
{ label: '护照', value: 'PASSPORT' },
{ label: '其他', value: 'OTHER' },
]
})
const formOptions = computed(() => tabType.value === 'identity' ? identityFormItems : contactFormItems)
const rowGutter = ref(20)
const colSpan = ref(12)
const imageUrl = ref('')
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
return true
}
</script>
<style scoped lang="scss">
.archives {
height: 100%;
.bar {
display: flex;
align-items: center;
margin-bottom: 20px;
.status {
background: #f56c6c;
border-radius: 5px;
padding: 4px;
margin: 0 10px;
}
.active {
background: var(--el-color-primary);
}
.info {
background: var(--el-color-primary-light-5);
padding: 4px;
border-radius: 5px;
}
}
.content {
display: flex;
gap: 20px;
height: calc(100% - 55px);
.tab {
display: flex;
flex-direction: column;
gap: 10px;
}
article {
flex: 1;
height: 100%;
border: 1px solid #e4e7ed;
border-radius: 5px;
padding: 20px;
.warn {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.el-icon {
font-size: 20px;
color: #f56c6c;
}
}
.el-form {
.upload {
display: flex;
gap: 20px;
.avatar-uploader {
width: 178px;
height: 178px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
:deep(.el-upload) {
width: 100%;
height: 100%;
.el-icon {
font-size: 20px;
}
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,14 @@
export const identityFormItems = [
// 国籍
{ prop: 'nationality', label: '国籍', type: 'select' as const ,width: "200px"},
// 证件类型
{ prop: 'idType', label: '证件类型', type: 'select' as const ,width: "200px"},
// 上传证件
{ prop: 'idCard', slotName: 'upload', label: '上传证件' ,type: 'upload' as const},
]
export const contactFormItems = [
// 联系电话
{ prop: 'phone', label: '联系电话', type: 'input' as const ,width: "200px"},
]

View File

@ -0,0 +1,128 @@
<template>
<div>
<!-- 搜索 -->
<FundDetailSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset">
<template #button="{ form }">
<el-button type="primary" @click="handleExport()">
导出
</el-button>
</template>
</FundDetailSearch>
<!-- 表格 -->
<FundDetailTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="id" />
<!-- 分页 -->
<FundDetailPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
</template>
<script setup lang="ts">
//
import FundDetailSearch from '@/components/common/Search.vue'
import FundDetailTable from '@/components/common/Table.vue'
import FundDetailPagination from '@/components/common/Pagination.vue'
//
import { search, tableColumns } from './options'
//
import { getFundDetailListApi } from '@/api/modules/funding/fundDetail'
//
import {exportToExcel} from '@/utils/exportToExcel'
/**
* @description: 定义搜索数据
*/
const searchOptions = ref(search)
const searchForm = ref({
username: '',
subAccountId: '',
startTime: '',
endTime: '',
})
/**
* @description: 定义表格数据
*/
const tableTree = ref([])
const tableColumnsOptions = ref(tableColumns)
/**
* @description: 定义分页数据
*/
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
/**
* @description: 定义公共请求数据方法
*/
//
const getFundDetailList = async () => {
const params = {
page: currentPage.value,
page_size: pageSize.value,
start_time: searchForm.value.startTime.length > 0 ? `${searchForm.value.startTime} 0:00:00` : '2026-01-01 0:00:00',
end_time: searchForm.value.endTime.length > 0 ? `${searchForm.value.endTime} 23:59:59` : '2026-03-15 23:59:59',
user_name: searchForm.value.username,
account_id: searchForm.value.subAccountId,
}
//
const data: any = await getFundDetailListApi(params)
console.log(data)
tableTree.value = data.data.map((item: any) => ({
//
username: item.user.username,
//
orderNo: item.trade_id,
//
type: item.enum_dic.value,
//
originalAmount: item.amount,
//
balanceChange: item.balance,
//
createTime: item.created_at,
//
walletType: item.wallet_type === "1" ? '主钱包' : '子钱包',
}))
//
total.value = data.total
currentPage.value = data.current_page
pageSize.value = Number(data.per_page)
}
/**
* @description: 页面加载完成需要请求的数据
*/
onMounted(() => {
getFundDetailList()
})
/**
* @description: 定义搜索方法
*/
const handleSearch = () => {
getFundDetailList()
}
const handleReset = () => {
searchForm.value = {
username: '',
subAccountId: '',
startTime: '',
endTime: '',
}
getFundDetailList()
}
const handleExport = () => {
exportToExcel({
tableData: tableTree.value,
columns: tableColumnsOptions.value,
title: '导出资金明细',
defaultFileNamePrefix: '资金明细',
})
}
/**
* @description: 定义分页方法
*/
const handlePaginationChange = (page: any) => {
currentPage.value = page.currentPage
getFundDetailList()
}
</script>

View File

@ -0,0 +1,55 @@
/**
* @description:
*/
export const search = [
//单号
// { prop: 'orderNo', label: '单号', type: 'input' as const, placeholder: '请输入流水单号', width: '140px' },
//用户名
{ prop: 'username', label: '用户名', type: 'input' as const, placeholder: '请输入用户名', width: '140px' },
// 子账号
{ prop: 'subAccountId', label: '子账号ID', type: 'input' as const, placeholder: '请输入子账号ID', width: '140px' },
//操作类型
// { prop: 'type', label: '操作类型', type: 'select' as const, placeholder: '请选择操作类型', width: '140px', options: [
// {label:"充值",value:"1"},
// {label:"提现",value:"2"},
// ] },
//钱包
// { prop: 'wallet', label: '钱包', type: 'select' as const, placeholder: '请选择钱包', width: '140px', options: [
// {label:"主钱包",value:"1"},
// {label:"子钱包",value:"2"},
// ] },
//开始时间
{
prop: 'startTime', label: '开始时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择开始时间', width: '150px'
},
//结束时间
{
prop: 'endTime', label: '结束时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择结束时间', width: '150px'
},
]
export const tableColumns = [
// 用户名
{ prop: 'username', label: '用户名' },
//单号
{ prop: 'orderNo', label: '单号' },
//操作类型
{ prop: 'type', label: '操作类型' },
//资金流向
// { prop: 'fundDirection', label: '资金流向' },
//原有金额
{ prop: 'originalAmount', label: '原有金额' },
//余额变动
{ prop: 'balanceChange', label: '余额变动' },
//现有余额
// { prop: 'currentBalance', label: '现有余额' },
//状态
// { prop: 'status', label: '状态' },
//创建时间
{ prop: 'createTime', label: '创建时间' },
//钱包类型
{ prop: 'walletType', label: '钱包类型' },
]

View File

@ -0,0 +1,5 @@
<template>
<div>
跟单记录
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
带单记录
</div>
</template>

View File

@ -0,0 +1,131 @@
<template>
<div class="history-trade">
<HistoryTradeSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset">
<template #button="{ form }">
<el-button type="primary" @click="handleExport()">
导出
</el-button>
</template>
</HistoryTradeSearch>
<!-- 表格 -->
<HistoryTradeTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="id" />
<!-- 分页 -->
<HistoryTradePagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total"
@pagination-change="handlePaginationChange" />
</div>
</template>
<script setup lang="ts">
//
import HistoryTradeSearch from '@/components/common/Search.vue'
import HistoryTradeTable from '@/components/common/Table.vue'
import HistoryTradePagination from '@/components/common/Pagination.vue'
//
import { search, tableColumns } from './options'
//
import { getHistoryTradeListApi } from '@/api/modules/trade/historyTrade'
//
//
import { exportToExcel } from '@/utils/exportToExcel'
/**
* @description: 定义搜索数据
*/
const searchOptions = ref(search)
const searchForm = ref({
username: '',
orderNo: '',
subAccountId: '',
tradeType: '',
startTime: '',
endTime: '',
})
/**
* @description: 定义表格数据
*/
const tableTree = ref([])
const tableColumnsOptions = ref(tableColumns)
/**
* @description: 定义分页数据
*/
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
/**
* @description: 定义公共请求数据方法
*/
const getHistoryTradeList = async () => {
const params = {
page: currentPage.value,
page_size: pageSize.value,
user_name: searchForm.value.username,
account_id: searchForm.value.subAccountId,
order_no: searchForm.value.orderNo,
type: searchForm.value.tradeType,
start_time: searchForm.value.startTime.length > 0 ? `${searchForm.value.startTime} 0:00:00` : '2026-01-01 0:00:00',
end_time: searchForm.value.endTime.length > 0 ? `${searchForm.value.endTime} 23:59:59` : '2026-03-01 23:59:59',
}
const data: any = await getHistoryTradeListApi(params)
tableTree.value = data.data.map((item: any) => ({
orderNo: item.order_id,
tradeType: item.type === '1' ? '买入' : '卖出',
amount: item.amount,
tradeQty: item.num,
tradeTime: item.created_at,
floatProfit: '后续添加'
}))
//
total.value = data.total
currentPage.value = data.current_page
pageSize.value = Number(data.per_page)
}
/**
* @description: 页面加载完成需要请求的数据
*/
onMounted(() => {
getHistoryTradeList()
})
/**
* @description: 定义搜索方法
*/
const handleSearch = () => {
getHistoryTradeList()
}
/**
* @description: 定义重置方法
*/
const handleReset = () => {
searchForm.value = {
username: '',
orderNo: '',
subAccountId: '',
tradeType: '',
startTime: '',
endTime: '',
}
getHistoryTradeList()
}
/**
* @description: 定义导出方法
*/
const handleExport = () => {
exportToExcel({
tableData: tableTree.value,
columns: tableColumnsOptions.value,
title: '导出交易记录',
defaultFileNamePrefix: '交易记录',
})
}
/**
* @description: 定义分页方法
*/
const handlePaginationChange = (page: any) => {
currentPage.value = page.currentPage
getHistoryTradeList()
}
</script>

View File

@ -0,0 +1,38 @@
export const search = [
// 用户名
{ prop: 'username', label: '用户名', type: 'input' as const, placeholder: '请输入用户名',width: '150px' },
// 订单号
{ prop: 'orderNo', label: '订单号', type: 'input' as const, placeholder: '请输入订单号',width: '150px' },
// 交易账户ID
{ prop: 'subAccountId', label: '交易账户ID', type: 'input' as const, placeholder: '请输入交易账户ID',width: '150px' },
// 交易类型
{ prop: 'tradeType', label: '交易类型', type: 'select' as const, placeholder: '请选择交易类型',width: '140px', options: [
{ label: '买入', value: '1' },
{ label: '卖出', value: '2' },
] },
//开始时间
{
prop: 'startTime', label: '开始时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择开始时间', width: '150px'
},
//结束时间
{
prop: 'endTime', label: '结束时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择结束时间', width: '150px'
},
]
export const tableColumns = [
// 订单号
{ prop: 'orderNo', label: '订单号' },
// 操作类型
{ prop: 'tradeType', label: '操作类型' },
// 金额
{ prop: 'amount', label: '金额' },
// 交易数量
{ prop: 'tradeQty', label: '交易数量' },
// 交易时间
{ prop: 'tradeTime', label: '交易时间' },
// 浮动盈亏
{ prop: 'floatProfit', label: '浮动盈亏' },
]

View File

@ -0,0 +1,88 @@
<template>
<div class="position-stat">
<!-- 搜索 -->
<PositionStatSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset">
<template #button="{ form }">
<el-button type="primary" @click="handleExport()">
导出
</el-button>
</template>
</PositionStatSearch>
<!-- 表格 -->
<PositionStatTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="id" />
<!-- 分页 -->
<PositionStatPagination :current-page="currentPage" :page-size="pageSize" :total="total" @change="handleChange" />
</div>
</template>
<script setup lang="ts">
//
import PositionStatSearch from '@/components/common/Search.vue'
import PositionStatTable from '@/components/common/Table.vue'
import PositionStatPagination from '@/components/common/Pagination.vue'
//
import { search, tableColumns } from './options'
//
//
import {exportToExcel} from '@/utils/exportToExcel'
/**
* @description: 定义搜索数据
*/
const searchOptions = ref(search)
const searchForm = ref({
orderNo: '',
tradeType: '',
account: '',
startTime: '',
endTime: '',
})
/**
* @description: 定义表格数据
*/
const tableTree = ref([])
const tableColumnsOptions = ref(tableColumns)
/**
* @description: 定义分页数据
*/
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
/**
* @description: 定义公共数据处理方法
*/
/**
* @description: 定义公共请求数据方法
*/
/**
* @description: 页面加载完成需要请求的数据
*/
/**
* @description: 定义搜索方法
*/
const handleSearch = async () => {
console.log("搜索");
}
const handleReset = () => {
console.log('重置搜索')
}
const handleExport = () => {
exportToExcel({
tableData: tableTree.value,
columns: tableColumnsOptions.value,
title: '导出资金明细',
defaultFileNamePrefix: '资金明细',
})
}
/**
* @description: 定义分页方法
*/
const handleChange = (page: {currentPage: number, size: number}) => {
currentPage.value = page.currentPage
}
</script>

View File

@ -0,0 +1,36 @@
export const search = [
// 订单号
{ prop: 'orderNo', label: '订单号', type: 'input' as const, placeholder: '请输入订单号',width: '200px' },
// 交易类型
{ prop: 'tradeType', label: '交易类型', type: 'select' as const, placeholder: '请选择交易类型',width: '150px', options: [
{ label: '买入', value: '1' },
{ label: '卖出', value: '2' },
] },
//账号
{ prop: 'account', label: '账号', type: 'select' as const, placeholder: '请选择账号',width: '150px' },
//开始时间
{
prop: 'startTime', label: '开始时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择开始时间', width: '150px'
},
//结束时间
{
prop: 'endTime', label: '结束时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
valueFormat: 'YYYY-MM-DD', placeholder: '请选择结束时间', width: '150px'
},
]
export const tableColumns = [
// 订单号
{ prop: 'orderNo', label: '订单号' },
// 操作类型
{ prop: 'tradeType', label: '操作类型' },
// 金额
{ prop: 'amount', label: '金额' },
// 交易数量
{ prop: 'tradeQty', label: '交易数量' },
// 交易时间
{ prop: 'tradeTime', label: '交易时间' },
// 浮动盈亏
{ prop: 'floatProfit', label: '浮动盈亏' },
]

View File

@ -0,0 +1,113 @@
<template>
<div class="trade-software">
<el-card>
<div class="left">
<el-icon>
<Monitor />
</el-icon>
<span class="title">PC端</span>
<el-icon class="download">
<Bottom />
</el-icon>
</div>
<div class="right">
<span class="text">系统要求:适用于windows 2008/7/8/10操作系统网络速度为56kbps或更高</span>
</div>
</el-card>
<el-card>
<div class="left">
<el-icon>
<svg t="1774244659320" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="2548" width="200" height="200">
<path
d="M839.562 344.72c-30.082 0-54.476 24.771-54.476 55.326l0 216.209c0 30.558 24.393 55.328 54.476 55.328 30.087 0 54.476-24.771 54.476-55.328l0-216.209c0.001-30.555-24.387-55.326-54.476-55.326zM182.482 344.72c-30.085 0-54.476 24.771-54.476 55.326l0 216.209c0 30.558 24.391 55.328 54.476 55.328s54.476-24.771 54.476-55.328l0-216.209c0-30.555-24.391-55.326-54.476-55.326zM269.74 346.395l0 395.014c0 23.462 19.02 42.481 42.483 42.481l48.533 0 0 120.293c0 30.558 24.391 55.328 54.476 55.328 30.087 0 54.478-24.771 54.478-55.328l0-120.294 84.786 0 0 120.293c0 30.558 24.393 55.328 54.476 55.328 30.087 0 54.481-24.771 54.481-55.328l0-120.293 48.528 0c23.467 0 42.485-19.018 42.485-42.481l0-395.013-484.725 0zM625.92 141.706l44.777-64.999c2.657-3.861 1.997-8.932-1.478-11.326-3.475-2.392-8.447-1.204-11.104 2.659l-46.512 67.518c-30.65-12.075-64.707-18.805-100.581-18.805-35.874 0-69.929 6.729-100.579 18.805l-46.512-67.518c-2.659-3.863-7.631-5.05-11.106-2.659-3.475 2.395-4.134 7.466-1.476 11.326l44.777 64.999c-71.188 33.117-121.113 96.042-127.467 169.487l484.727 0c-6.355-73.445-56.28-136.37-127.466-169.487zM408.772 244.144c-14.811 0-26.819-12.005-26.819-26.819 0-14.811 12.007-26.819 26.819-26.819 14.813 0 26.819 12.007 26.819 26.819 0 14.813-12.005 26.819-26.819 26.819zM616.625 244.144c-14.811 0-26.816-12.005-26.816-26.819 0-14.811 12.004-26.819 26.816-26.819 14.816 0 26.821 12.007 26.821 26.819 0 14.813-12.004 26.819-26.821 26.819z"
fill="#272636" p-id="2549"></path>
</svg>
</el-icon>
<span class="title">Android APK</span>
<el-icon class="download">
<Bottom />
</el-icon>
</div>
<div class="right">
<span class="text">下载说明:下载后可以通过手机安装APK</span>
</div>
</el-card>
<el-card>
<div class="left">
<el-icon>
<svg t="1774244690948" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3750" width="200" height="200">
<path
d="M836.608 675.84q41.984 59.392 96.256 76.8-23.552 72.704-74.752 151.552-78.848 118.784-155.648 118.784-28.672 0-84.992-19.456-52.224-19.456-91.136-19.456t-86.016 20.48q-49.152 19.456-80.896 19.456-92.16 0-181.248-156.672-89.088-154.624-89.088-304.128 0-138.24 67.584-226.304 69.632-88.064 172.032-88.064 22.528 0 50.176 5.632t57.344 20.992q31.744 17.408 52.224 24.064t31.744 6.656q13.312 0 40.96-6.144t55.296-22.528q29.696-16.384 51.2-24.576t44.032-8.192q71.68 0 129.024 38.912 30.72 20.48 62.464 60.416-47.104 40.96-68.608 71.68-39.936 57.344-39.936 124.928 0 74.752 41.984 135.168z m-205.824-478.208q-35.84 33.792-65.536 44.032-10.24 3.072-26.112 5.632t-36.352 4.608q1.024-90.112 47.104-155.648T701.44 6.144q2.048 10.24 3.072 14.336v11.264q0 36.864-17.408 82.944-18.432 45.056-56.32 82.944z"
fill="#272636" p-id="3751"></path>
</svg>
</el-icon>
<span class="title">IOS</span>
<el-icon class="download">
<Bottom />
</el-icon>
</div>
<div class="right">
<span class="text">下载说明:iPhone和iPad用户可以通过 Apple App Store搜索InTrade安装</span>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { Monitor, Bottom } from '@element-plus/icons-vue'
</script>
<style scoped lang="scss">
.trade-software {
display: flex;
flex-direction: column;
gap: 20px;
.el-card {
:deep(.el-card__body) {
display: flex;
justify-content: space-between;
}
.left {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
.title {
font-weight: 500;
}
}
.right {
.text {
font-size: 14px;
}
}
.left,
.right {
.el-icon{
font-size: 30px;
}
.download {
font-size: 20px;
cursor: pointer;
&:before {
content: "";
display: block;
height: 2px;
width: 16px;
background: var(--el-text-color-primary);
position: absolute;
bottom: 0;
}
}
}
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="user-card">
<UserHeader />
<UserText :user-text="userText" @back="handleBack" />
<el-form class="user-form" ref="bindEmailFormRef" :model="bindEmailForm" :rules="bindEmailRules">
<el-form-item prop="email">
<el-input v-model="bindEmailForm.email" placeholder="请输入邮箱" type="email" clearable
size="large" />
</el-form-item>
<UserCaptcha v-model="bindEmailForm.code" @sendCode="handleSendCode" :show-send-button="showSendButton"
:send-state="sendState" />
<el-form-item class="form-submit">
<el-button type="primary" @click="handleSubmit">确认绑定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
// alert('')
// type
import type { FormInstance, FormRules } from 'element-plus'
//
import UserHeader from './components/Header.vue'
import UserText from './components/Text.vue'
import UserCaptcha from './components/Captcha.vue'
//
import { emailRules, codeRules } from './config/rules'
import { useUserStore } from '@/stores/modules/user'
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
const userStore = useUserStore()
const router = useRouter()
//userText
const userText = ref({
backText: '返回',
account: ''
})
// bindEmail
const bindEmailFormRef = ref<FormInstance>()
const bindEmailForm = ref({
email: '',
code: '',
})
const bindEmailRules = ref<FormRules>({
email: emailRules().email,
code: codeRules().code,
})
//
const showSendButton = ref<boolean>(true)
const sendState = ref<object>({
isCounting: false,
countdown: 0,
})
//
const emailCaptchaState = initCaptchaState('email');
//
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
//
// sendState
const updateSendState = (type: string) => {
sendState.value = {
isCounting: emailIsCounting,
countdown: emailCountdown
}
}
// activeTabsendState
const initSendState = () => {
const currentType = 'email'
updateSendState(currentType)
}
onMounted(() => {
//
initSendState()
})
//
const sendCodeApi = async () => {
//
const sendParams = {
email: bindEmailForm.value.email,
reg_type: 1,
type:'resetPass_code'
}
return userStore.sendCode(sendParams)
}
//
const handleSendCode = async () => {
const currentType = 'email'
//
const type: string = currentType || 'email'
const receiver = bindEmailForm.value.email;
const currentCaptchaState = emailCaptchaState;
//
await sendCaptcha(
type,
receiver,
currentCaptchaState, //
() => sendCodeApi() // Promise
);
//
updateSendState(type)
}
//
onUnmounted(() => {
clearCaptchaTimer(emailCaptchaState);
});
/**
* @description: 返回
*/
const handleBack = () => {
router.back()
}
//
const handleSubmit = async () => {
if (!bindEmailFormRef.value) return;
await bindEmailFormRef.value.validate();
}
</script>
<style scoped lang="scss">
.user-card{
.user-text{
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<div class="user-card">
<UserHeader />
<UserText :user-text="userText" @back="handleBack" />
<el-form class="user-form" ref="bindPhoneFormRef" :model="bindPhoneForm" :rules="bindPhoneRules">
<el-form-item class="phone-item" prop="phone">
<el-select v-model="bindPhoneForm.countryCode" :teleported="false" value-key="code" placeholder="国家"
size="large">
<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="bindPhoneForm.phone" placeholder="请输入手机号" clearable size="large" />
</el-form-item>
<UserCaptcha v-model="bindPhoneForm.code" @sendCode="handleSendCode" :show-send-button="showSendButton"
:send-state="sendState" />
<el-form-item class="form-submit">
<el-button type="primary" @click="handleSubmit">确认绑定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
// alert('')
// type
import type { FormInstance, FormRules } from 'element-plus'
//
import UserHeader from './components/Header.vue'
import UserText from './components/Text.vue'
import UserCaptcha from './components/Captcha.vue'
//
import { emailRules, codeRules } from './config/rules'
import { useUserStore } from '@/stores/modules/user'
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
import { countryOptions } from '@/views/user/config/mock'
const userStore = useUserStore()
const router = useRouter()
//userText
const userText = ref({
backText: '返回',
account: ''
})
// bindPhone
const bindPhoneFormRef = ref<FormInstance>()
const bindPhoneForm = ref({
phone: '',
code: '',
countryCode: '+86',
})
const bindPhoneRules = ref<FormRules>({
phone: emailRules().phone,
code: codeRules().code,
})
//
const showSendButton = ref<boolean>(true)
const sendState = ref<object>({
isCounting: false,
countdown: 0,
})
//
const phoneCaptchaState = initCaptchaState('phone');
//
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
//
// sendState
const updateSendState = (type: string) => {
sendState.value = {
isCounting: phoneIsCounting,
countdown: phoneCountdown
}
}
// activeTabsendState
const initSendState = () => {
const currentType = 'phone'
updateSendState(currentType)
}
onMounted(() => {
//
initSendState()
})
//
const sendCodeApi = async () => {
//
const sendParams = {
mobile: bindPhoneForm.value.phone,
reg_type: 2,
type: 'resetPass_code'
}
return userStore.sendCode(sendParams)
}
//
const handleSendCode = async () => {
const currentType = 'phone'
//
const type: string = currentType || 'phone'
const receiver = bindPhoneForm.value.phone;
const currentCaptchaState = phoneCaptchaState;
//
await sendCaptcha(
type,
receiver,
currentCaptchaState, //
() => sendCodeApi() // Promise
);
//
updateSendState(type)
}
//
onUnmounted(() => {
clearCaptchaTimer(phoneCaptchaState);
});
/**
* @description: 返回
*/
const handleBack = () => {
router.back()
}
//
const handleSubmit = async () => {
if (!bindPhoneFormRef.value) return;
await bindPhoneFormRef.value.validate();
}
</script>
<style scoped lang="scss">
.user-card{
.user-text{
margin-bottom: 0;
}
}
.user-form {
.phone-item {
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
.el-select {
width: 120px;
margin-right: 10px;
.country-option {
width: 120px;
display: flex;
justify-content: space-between;
}
:deep(.el-select-dropdown__item) {
padding: 0 16px 0 16px;
}
}
}
.forgot-password {
font-size: 14px;
color: #409eff;
text-decoration: none;
}
.form-submit {
margin-top: 18px;
:deep(.el-form-item__content) {
width: 100%;
}
}
}
</style>