部门交易金额代码框架

This commit is contained in:
zhonghua.li
2026-04-30 08:15:53 +08:00
parent 46e98e664d
commit ed97d027f6
2 changed files with 294 additions and 0 deletions

View File

@@ -83,3 +83,17 @@ export function listYestodayBuyPositiveAndDailySellZero(startDate, endDate, tena
}
})
}
/** 按日期范围统计本人及递归下级交易金额(不含 report_sum */
export function statsUserTradeWithTeamByDateRangeAndTenant(startDate, endDate, tenantId) {
return request({
url: '/lbDailyUserTradeReport/stats/user-trade-with-team/by-date-range-and-tenant',
method: 'get',
params: {
startDate,
endDate,
tenantId
},
timeout: 120000
})
}

View File

@@ -46,6 +46,7 @@
<el-button type="warning" icon="el-icon-data-analysis" :loading="reportSumLoading" @click="openReportSumDialog">汇总数据</el-button>
<el-button type="danger" icon="el-icon-delete" :loading="deleteByDateLoading" @click="openDeleteByDateDialog">删除报表数据</el-button>
<el-button type="success" icon="el-icon-download" :loading="exportLoading" @click="openExportDialog">报表导出</el-button>
<el-button type="info" icon="el-icon-s-grid" :loading="deptStatsLoading" @click="openDeptStatsDialog">统计部门金额</el-button>
</div>
<div class="action-tip">
说明服务费=当日买入×0.01差额=当日卖出当日买入应收应付=差额服务费>0 应收当前人<0 应付当前人
@@ -215,6 +216,99 @@
</div>
</el-dialog>
<el-dialog
title="统计部门金额"
:visible.sync="deptStatsDialogVisible"
width="96%"
top="4vh"
custom-class="dept-stats-dialog"
@close="resetDeptStatsDialog"
>
<el-form :inline="true" label-width="88px" class="dept-stats-filter">
<el-form-item label="开始日期">
<el-date-picker
v-model="deptStatsForm.startDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="开始日期"
style="width: 160px"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="deptStatsForm.endDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="结束日期"
style="width: 160px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :loading="deptStatsLoading" @click="fetchDeptStatsTree">查询</el-button>
</el-form-item>
</el-form>
<p v-if="deptStatsSummaryText" class="dept-stats-summary">{{ deptStatsSummaryText }}</p>
<div class="dept-stats-table-wrap">
<el-table
v-loading="deptStatsLoading"
:data="deptStatsTreeData"
row-key="userId"
border
stripe
height="520"
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
style="width: 100%"
>
<el-table-column label="姓名 / 用户ID" prop="name" min-width="220" fixed show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.name || '-' }}</span>
<span class="dept-stats-user-id">{{ scope.row.userId }}</span>
</template>
</el-table-column>
<el-table-column label="本人昨日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.ownYestodayBuyAmt) }}</template>
</el-table-column>
<el-table-column label="团队昨日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.teamYestodayBuyAmt) }}</template>
</el-table-column>
<el-table-column label="合计昨日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.totalYestodayBuyAmt) }}</template>
</el-table-column>
<el-table-column label="本人当日卖出" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.ownDailySellAmt) }}</template>
</el-table-column>
<el-table-column label="团队当日卖出" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.teamDailySellAmt) }}</template>
</el-table-column>
<el-table-column label="合计当日卖出" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.totalDailySellAmt) }}</template>
</el-table-column>
<el-table-column label="本人当日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.ownDailyBuyAmt) }}</template>
</el-table-column>
<el-table-column label="团队当日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.teamDailyBuyAmt) }}</template>
</el-table-column>
<el-table-column label="合计当日买入" min-width="118" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.totalDailyBuyAmt) }}</template>
</el-table-column>
<el-table-column label="本人差额" min-width="100" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.ownDiffAmt) }}</template>
</el-table-column>
<el-table-column label="团队差额" min-width="100" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.teamDiffAmt) }}</template>
</el-table-column>
<el-table-column label="合计差额" min-width="100" align="right">
<template slot-scope="s">{{ formatDeptAmt(s.row.totalDiffAmt) }}</template>
</el-table-column>
</el-table>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="deptStatsDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
<el-dialog title="报表导出" :visible.sync="exportDialogVisible" width="480px" @close="resetExportForm">
<el-form ref="exportFormRef" :model="exportForm" :rules="exportRules" label-width="120px">
<el-form-item label="报表日期" prop="reportDate">
@@ -244,8 +338,10 @@ import {
exportLbDailyUserTradeReportByDateAndTenant,
getLbDailyUserTradeReportList,
listYestodayBuyPositiveAndDailySellZero,
statsUserTradeWithTeamByDateRangeAndTenant,
updateLbDailyUserTradeReport
} from '@/api/lb-daily-user-trade-report'
import { getLbDepartmentUserList } from '@/api/lb-department-user'
import { getBusinessHeaders } from '@/utils/business-headers'
function formatDateTimeForQuery(date) {
@@ -273,6 +369,69 @@ function getLocalTodayDateString() {
return `${year}-${month}-${day}`
}
function compareDeptAmtDesc(a, b) {
const na = Number(a)
const nb = Number(b)
if (!Number.isFinite(na) && !Number.isFinite(nb)) return 0
if (!Number.isFinite(na)) return 1
if (!Number.isFinite(nb)) return -1
return nb - na
}
/** 将统计扁平行按 lbDepartmentUser 的 parentId 转为 el-table 树形数据 */
function buildDeptTradeStatsTree(flatRows, departmentRows) {
if (!Array.isArray(flatRows) || flatRows.length === 0) return []
const userIds = new Set()
flatRows.forEach((r) => {
if (r && r.userId != null && String(r.userId).trim() !== '') userIds.add(String(r.userId).trim())
})
const parentByUserId = {}
if (Array.isArray(departmentRows)) {
departmentRows.forEach((d) => {
if (!d || d.userId == null || String(d.userId).trim() === '') return
const uid = String(d.userId).trim()
if (!userIds.has(uid)) return
const rawParent = d.parentId == null ? '' : String(d.parentId).trim()
if (!(uid in parentByUserId)) parentByUserId[uid] = rawParent
})
}
const statsByUserId = {}
flatRows.forEach((r) => {
if (!r || r.userId == null) return
const uid = String(r.userId).trim()
statsByUserId[uid] = { ...r, userId: uid }
})
const childrenByParent = {}
userIds.forEach((uid) => {
childrenByParent[uid] = []
})
userIds.forEach((uid) => {
const p = parentByUserId[uid] != null ? String(parentByUserId[uid]).trim() : ''
if (p && userIds.has(p)) {
childrenByParent[p].push(uid)
}
})
Object.keys(childrenByParent).forEach((pid) => {
childrenByParent[pid].sort((x, y) =>
compareDeptAmtDesc(statsByUserId[y].totalDiffAmt, statsByUserId[x].totalDiffAmt))
})
function buildNode(uid) {
const row = statsByUserId[uid]
const childIds = childrenByParent[uid] || []
const kids = childIds.map(buildNode)
const node = { ...row }
if (kids.length) node.children = kids
return node
}
const roots = []
userIds.forEach((uid) => {
const p = parentByUserId[uid] != null ? String(parentByUserId[uid]).trim() : ''
if (!p || !userIds.has(p)) roots.push(uid)
})
roots.sort((a, b) => compareDeptAmtDesc(statsByUserId[b].totalDiffAmt, statsByUserId[a].totalDiffAmt))
return roots.map(buildNode)
}
export default {
name: 'LbDailyReport',
data() {
@@ -283,6 +442,14 @@ export default {
reportSumLoading: false,
exportLoading: false,
checkUnsoldLoading: false,
deptStatsLoading: false,
deptStatsDialogVisible: false,
deptStatsForm: {
startDate: '',
endDate: ''
},
deptStatsTreeData: [],
deptStatsSummaryText: '',
list: [],
total: 0,
dialogVisible: false,
@@ -496,6 +663,93 @@ export default {
if (this.$refs.exportFormRef) this.$refs.exportFormRef.clearValidate()
})
},
openDeptStatsDialog() {
const sd = this.extractDateOnly(this.queryParams.reportStartTime)
const ed = this.extractDateOnly(this.queryParams.reportEndTime)
this.deptStatsForm = {
startDate: sd || '',
endDate: ed || ''
}
this.deptStatsTreeData = []
this.deptStatsSummaryText = ''
this.deptStatsDialogVisible = true
this.$nextTick(() => {
const tenantId = this.getTenantIdFromBusinessHeaders()
if (tenantId && this.deptStatsForm.startDate && this.deptStatsForm.endDate) {
this.fetchDeptStatsTree()
}
})
},
resetDeptStatsDialog() {
this.deptStatsForm = { startDate: '', endDate: '' }
this.deptStatsTreeData = []
this.deptStatsSummaryText = ''
},
fetchAllLbDepartmentUsers() {
const pageSize = 500
let all = []
let current = 1
let total = Number.POSITIVE_INFINITY
const step = () =>
getLbDepartmentUserList({ current, size: pageSize }).then((res) => {
const ok = res && (res.code === 20000 || res.code === 200 || res.success === true)
const chunk = ok && Array.isArray(res.data) ? res.data : []
if (typeof res.total === 'number') total = res.total
else if (res.page && typeof res.page.total === 'number') total = res.page.total
all = all.concat(chunk)
current += 1
if (chunk.length === 0 || chunk.length < pageSize || all.length >= total) return all
return step()
})
return step()
},
fetchDeptStatsTree() {
const tenantId = this.getTenantIdFromBusinessHeaders()
if (!tenantId) {
this.$message.error('未获取到租户ID请先登录或切换到正确租户后重试')
return
}
const { startDate, endDate } = this.deptStatsForm
if (!startDate || !endDate) {
this.$message.warning('请选择开始日期和结束日期')
return
}
this.deptStatsLoading = true
this.deptStatsTreeData = []
this.deptStatsSummaryText = ''
Promise.all([
statsUserTradeWithTeamByDateRangeAndTenant(startDate, endDate, tenantId),
this.fetchAllLbDepartmentUsers()
])
.then(([statsRes, deptList]) => {
const ok = statsRes && (statsRes.code === 20000 || statsRes.code === 200 || statsRes.success === true)
if (!ok) {
this.$message.error((statsRes && statsRes.message) || '统计失败')
return
}
const rows = Array.isArray(statsRes.data) ? statsRes.data : []
const total = typeof statsRes.total === 'number' ? statsRes.total : rows.length
this.deptStatsTreeData = buildDeptTradeStatsTree(rows, deptList)
const rangeLabel = `${startDate} ~ ${endDate}`
this.deptStatsSummaryText =
`统计区间:${rangeLabel};租户:${tenantId};共 ${total} 人(树按部门上下级展示,同级按合计差额降序)`
if (!rows.length) {
this.$message.success((statsRes && statsRes.message) || '暂无数据')
}
})
.catch(() => {
this.deptStatsTreeData = []
})
.finally(() => {
this.deptStatsLoading = false
})
},
formatDeptAmt(val) {
if (val === null || val === undefined || val === '') return '-'
const n = Number(val)
if (!Number.isFinite(n)) return String(val)
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
},
submitDeleteByDate() {
this.$refs.deleteByDateFormRef.validate((valid) => {
if (!valid) return
@@ -732,3 +986,29 @@ export default {
}
}
</style>
<style lang="scss">
/* el-dialog 挂载到 body需非 scoped */
.dept-stats-dialog .dept-stats-filter {
margin-bottom: 8px;
}
.dept-stats-dialog .dept-stats-summary {
margin: 0 0 12px;
color: #606266;
font-size: 13px;
line-height: 20px;
}
.dept-stats-dialog .dept-stats-table-wrap {
width: 100%;
overflow-x: auto;
}
.dept-stats-dialog .dept-stats-user-id {
display: block;
margin-top: 2px;
color: #909399;
font-size: 12px;
}
</style>