This commit is contained in:
parent
019d09df3e
commit
21938f330f
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* 账户盈利统计 - 接口
|
||||||
|
*
|
||||||
|
* 后端约定:
|
||||||
|
* - GET /api/trade/profit/statistics 查询账户盈利统计列表
|
||||||
|
* - POST /api/trade/profit/export 导出账户盈利统计(后端返回流或 url)
|
||||||
|
*
|
||||||
|
* 入参:
|
||||||
|
* - mainAccountName 主账户名
|
||||||
|
* - subAccountName 子账户名
|
||||||
|
* - startDate 开始日期 (YYYY-MM-DD)
|
||||||
|
* - endDate 结束日期 (YYYY-MM-DD)
|
||||||
|
*
|
||||||
|
* 出参(列表项):
|
||||||
|
* - id 唯一标识
|
||||||
|
* - mainAccountName 主账户名
|
||||||
|
* - subAccountName 子账户名
|
||||||
|
* - profitAmount 盈利金额
|
||||||
|
* - orderCount 单数
|
||||||
|
* - lotCount 手数
|
||||||
|
* - winRate 赢率(0~1 或 0~100)
|
||||||
|
*
|
||||||
|
* 说明:项目内 request 是带 get/post 方法的对象(见 src/api/index.ts),
|
||||||
|
* 所以这里使用 request.get / request.post 调用方式。
|
||||||
|
*/
|
||||||
|
import { request } from '@/api'
|
||||||
|
|
||||||
|
export interface IProfitStatisticsQuery {
|
||||||
|
mainAccountName?: string
|
||||||
|
subAccountName?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProfitStatisticsItem {
|
||||||
|
id?: string | number
|
||||||
|
mainAccountName?: string
|
||||||
|
subAccountName?: string
|
||||||
|
profitAmount?: number | string
|
||||||
|
orderCount?: number | string
|
||||||
|
lotCount?: number | string
|
||||||
|
winRate?: number | string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询账户盈利统计列表 */
|
||||||
|
export const getProfitStatisticsList = (params: IProfitStatisticsQuery) => {
|
||||||
|
return request.get<{
|
||||||
|
list?: IProfitStatisticsItem[]
|
||||||
|
records?: IProfitStatisticsItem[]
|
||||||
|
data?: IProfitStatisticsItem[]
|
||||||
|
}>('/api/trade/profit/statistics', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出账户盈利统计(后端返回文件流 / 下载链接) */
|
||||||
|
export const exportProfitStatistics = (params: IProfitStatisticsQuery) => {
|
||||||
|
return request.post<Blob>('/api/trade/profit/export', params, {
|
||||||
|
responseType: 'blob',
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
@ -258,19 +258,34 @@ onMounted(() => {
|
||||||
router.replace({ path: targetTab.path, query: targetTab.query })
|
router.replace({ path: targetTab.path, query: targetTab.query })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始检查滚动状态
|
// 初始检查滚动状态
|
||||||
nextTick(checkScroll)
|
nextTick(checkScroll)
|
||||||
|
|
||||||
// 添加滚动监听
|
// 添加滚动监听
|
||||||
tabsContainerRef.value?.addEventListener('scroll', checkScroll)
|
tabsContainerRef.value?.addEventListener('scroll', checkScroll)
|
||||||
|
|
||||||
// 全局左键点击关闭右键菜单
|
// 全局左键点击关闭右键菜单
|
||||||
document.addEventListener('click', handleGlobalClick)
|
document.addEventListener('click', handleGlobalClick)
|
||||||
// 全局右键监听,如果右键在其他tab上,关闭当前右键菜单
|
// 全局右键监听,如果右键在其他tab上,关闭当前右键菜单
|
||||||
document.addEventListener('contextmenu', handleGlobalContextMenu)
|
document.addEventListener('contextmenu', handleGlobalContextMenu)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听动态路由恢复完成(router/index.ts 中通过 dynamicRoutesReady 标记),
|
||||||
|
// 在动态路由加载完毕后,如果当前路由与保存的激活 tab 不一致,则跳转过去
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
// 仅当保存的激活 tab 存在且与当前路由不一致时才跳转
|
||||||
|
if (tabsStore.activeTab && tabsStore.activeTab !== route.path) {
|
||||||
|
const targetTab = tabsStore.tabs.find(item => item.path === tabsStore.activeTab)
|
||||||
|
if (targetTab && targetTab.path !== route.path) {
|
||||||
|
router.replace({ path: targetTab.path, query: targetTab.query })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
tabsContainerRef.value?.removeEventListener('scroll', checkScroll)
|
tabsContainerRef.value?.removeEventListener('scroll', checkScroll)
|
||||||
document.removeEventListener('click', handleGlobalClick)
|
document.removeEventListener('click', handleGlobalClick)
|
||||||
|
|
|
||||||
|
|
@ -128,18 +128,21 @@ export const useTabsStore = defineStore('tabs', () => {
|
||||||
const existIndex = tabs.value.findIndex(item => item.path === tab.path)
|
const existIndex = tabs.value.findIndex(item => item.path === tab.path)
|
||||||
// 计算面包屑路径
|
// 计算面包屑路径
|
||||||
const breadcrumbs = findBreadcrumbPath(tab.path)
|
const breadcrumbs = findBreadcrumbPath(tab.path)
|
||||||
|
|
||||||
if (existIndex === -1) {
|
if (existIndex === -1) {
|
||||||
// 新标签页,添加到列表
|
// 新标签页,添加到列表
|
||||||
tabs.value.push({ ...tab, breadcrumbs })
|
tabs.value.push({ ...tab, breadcrumbs })
|
||||||
|
// 只有新增标签时才激活它
|
||||||
|
activeTab.value = tab.path
|
||||||
} else {
|
} else {
|
||||||
// 已存在的标签页,更新面包屑(确保数据是最新的)
|
// 已存在的标签页,更新面包屑(确保数据是最新的)
|
||||||
const existingTab = tabs.value[existIndex]
|
const existingTab = tabs.value[existIndex]
|
||||||
if (existingTab) {
|
if (existingTab) {
|
||||||
existingTab.breadcrumbs = breadcrumbs
|
existingTab.breadcrumbs = breadcrumbs
|
||||||
}
|
}
|
||||||
|
// 已存在的标签,不要覆盖当前激活状态,
|
||||||
|
// 避免在页面刷新/路由重新匹配时把激活的 tab 错误地切回当前路由
|
||||||
}
|
}
|
||||||
activeTab.value = tab.path
|
|
||||||
saveTabs()
|
saveTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
<template>
|
||||||
|
<div class="profit-statistics table_form-container">
|
||||||
|
<!-- 搜索 -->
|
||||||
|
<ProfitStatisticsSearch
|
||||||
|
:search-form="searchForm"
|
||||||
|
:button-loading="loading"
|
||||||
|
:search-options="searchOptions"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleReset"
|
||||||
|
>
|
||||||
|
<!-- 操作按钮:导出 -->
|
||||||
|
<template #button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="exportLoading"
|
||||||
|
:disabled="!tableData.length"
|
||||||
|
v-auth="'trade_profitStatistics_export'"
|
||||||
|
@click="handleExport"
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</ProfitStatisticsSearch>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<ProfitStatisticsTable
|
||||||
|
:table-data="tableData"
|
||||||
|
:columns="tableColumnsOptions"
|
||||||
|
:loading="loading"
|
||||||
|
row-key="id"
|
||||||
|
border
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<!-- 赢率:后端返回 0~1 的小数,这里转换为百分比 -->
|
||||||
|
<template #winRate="{ row }">
|
||||||
|
<span>{{ formatWinRate(row.winRate) }}</span>
|
||||||
|
</template>
|
||||||
|
</ProfitStatisticsTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import ProfitStatisticsSearch from '@/components/common/Search.vue'
|
||||||
|
import ProfitStatisticsTable from '@/components/common/Table.vue'
|
||||||
|
import { searchOptions, tableColumns } from './options'
|
||||||
|
import { rules as _rules } from './rules'
|
||||||
|
import { getProfitStatisticsList } from '@/api/modules/trade/profitStatistics'
|
||||||
|
import { exportToExcel } from '@/utils/exportToExcel'
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = ref({
|
||||||
|
mainAccountName: '',
|
||||||
|
subAccountName: '',
|
||||||
|
dateRange: [], // [startDate, endDate]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref<any[]>([])
|
||||||
|
const tableColumnsOptions = ref<any[]>(tableColumns)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const exportLoading = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索
|
||||||
|
*/
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const [startDate = '', endDate = ''] = (searchForm.value.dateRange as string[]) || []
|
||||||
|
const { data } = await getProfitStatisticsList({
|
||||||
|
mainAccountName: searchForm.value.mainAccountName || undefined,
|
||||||
|
subAccountName: searchForm.value.subAccountName || undefined,
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
})
|
||||||
|
tableData.value = Array.isArray(data) ? data : (data as any)?.list || (data as any)?.records || []
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || '查询失败')
|
||||||
|
tableData.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置
|
||||||
|
*/
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.value.mainAccountName = ''
|
||||||
|
searchForm.value.subAccountName = ''
|
||||||
|
searchForm.value.dateRange = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出
|
||||||
|
* 复用项目通用导出工具,导出当前查询结果。
|
||||||
|
*/
|
||||||
|
const handleExport = async () => {
|
||||||
|
if (!tableData.value.length) {
|
||||||
|
ElMessage.warning('当前没有可导出的数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
exportLoading.value = true
|
||||||
|
await exportToExcel({
|
||||||
|
tableData: tableData.value,
|
||||||
|
columns: tableColumnsOptions.value,
|
||||||
|
title: '账户盈利统计',
|
||||||
|
filename: `账户盈利统计_${new Date().getTime()}`,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || '导出失败')
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赢率格式化为百分比:0.62 -> 62.00%
|
||||||
|
*/
|
||||||
|
const formatWinRate = (val: any) => {
|
||||||
|
if (val === null || val === undefined || val === '') return '-'
|
||||||
|
const num = Number(val)
|
||||||
|
if (Number.isNaN(num)) return '-'
|
||||||
|
// 若后端已按 0~100 传值,则原样展示;否则按 0~1 处理
|
||||||
|
const rate = num > 1 ? num : num * 100
|
||||||
|
return `${rate.toFixed(2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始进入页面不自动查询,保持"用户主动查询"的交互
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.profit-statistics {
|
||||||
|
.cell-content {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* 账户盈利统计 - 搜索/表格列配置
|
||||||
|
*/
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索表单字段配置(传入 Search 通用组件自动渲染)
|
||||||
|
* - 主账户名 / 子账户名:input
|
||||||
|
* - 时间范围:daterange
|
||||||
|
*/
|
||||||
|
export const searchOptions: Ref<any[]> | any[] = [
|
||||||
|
// 主账户名
|
||||||
|
{
|
||||||
|
prop: 'mainAccountName',
|
||||||
|
label: '主账户名',
|
||||||
|
type: 'input' as const,
|
||||||
|
placeholder: '请输入主账户名',
|
||||||
|
clearable: true,
|
||||||
|
width: '200px',
|
||||||
|
},
|
||||||
|
// 子账户名
|
||||||
|
{
|
||||||
|
prop: 'subAccountName',
|
||||||
|
label: '子账户名',
|
||||||
|
type: 'input' as const,
|
||||||
|
placeholder: '请输入子账户名',
|
||||||
|
clearable: true,
|
||||||
|
width: '200px',
|
||||||
|
},
|
||||||
|
// 时间范围
|
||||||
|
{
|
||||||
|
prop: 'dateRange',
|
||||||
|
label: '时间日期',
|
||||||
|
type: 'date' as const,
|
||||||
|
dateType: 'daterange' as const,
|
||||||
|
rangeSeparator: '至',
|
||||||
|
startPlaceholder: '开始日期',
|
||||||
|
endPlaceholder: '结束日期',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
width: '320px',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格列配置(传入 Table 通用组件自动渲染)
|
||||||
|
*/
|
||||||
|
export const tableColumns = [
|
||||||
|
// 序号列
|
||||||
|
{ type: 'index', label: '序号', align: 'center' as const, width: 60 },
|
||||||
|
// 主账户名
|
||||||
|
{ prop: 'mainAccountName', label: '主账户名', align: 'center' as const, minWidth: 140 },
|
||||||
|
// 子账户名
|
||||||
|
{ prop: 'subAccountName', label: '子账户名', align: 'center' as const, minWidth: 140 },
|
||||||
|
// 盈利金额
|
||||||
|
{
|
||||||
|
prop: 'profitAmount',
|
||||||
|
label: '盈利金额',
|
||||||
|
align: 'right' as const,
|
||||||
|
minWidth: 140,
|
||||||
|
isNumber: true,
|
||||||
|
},
|
||||||
|
// 单数
|
||||||
|
{
|
||||||
|
prop: 'orderCount',
|
||||||
|
label: '单数',
|
||||||
|
align: 'center' as const,
|
||||||
|
minWidth: 100,
|
||||||
|
isNumber: true,
|
||||||
|
},
|
||||||
|
// 手数
|
||||||
|
{
|
||||||
|
prop: 'lotCount',
|
||||||
|
label: '手数',
|
||||||
|
align: 'center' as const,
|
||||||
|
minWidth: 100,
|
||||||
|
isNumber: true,
|
||||||
|
},
|
||||||
|
// 赢率(百分比展示)
|
||||||
|
{
|
||||||
|
prop: 'winRate',
|
||||||
|
label: '赢率',
|
||||||
|
align: 'center' as const,
|
||||||
|
minWidth: 120,
|
||||||
|
slotName: 'winRate',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* 账户盈利统计 - 表单校验
|
||||||
|
*
|
||||||
|
* 当前搜索字段均为非必填(用户可不指定主/子账户、不指定时间范围进行全量查询),
|
||||||
|
* 因此保留空对象占位,便于后续扩展。
|
||||||
|
*/
|
||||||
|
export const rules = {}
|
||||||
Loading…
Reference in New Issue