[update]新增customerReport页面
This commit is contained in:
@@ -35,3 +35,18 @@ export function dailyReport(params: { start: string; end: string; type: 'daily'
|
|||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function customerReport(params: {
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
username: string
|
||||||
|
lose_rebate: string
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
}) {
|
||||||
|
return createAxios({
|
||||||
|
url: url + 'customerReport',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
[adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'],
|
[adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'],
|
||||||
[adminBaseRoutePath + '/user/moneyLog/annualReport']: ['./backend/${lang}/user/moneyLog.ts'],
|
[adminBaseRoutePath + '/user/moneyLog/annualReport']: ['./backend/${lang}/user/moneyLog.ts'],
|
||||||
[adminBaseRoutePath + '/user/moneyLog/dailyReport']: ['./backend/${lang}/user/moneyLog.ts'],
|
[adminBaseRoutePath + '/user/moneyLog/dailyReport']: ['./backend/${lang}/user/moneyLog.ts'],
|
||||||
|
[adminBaseRoutePath + '/user/moneyLog/customerReport']: ['./backend/${lang}/user/moneyLog.ts'],
|
||||||
[adminBaseRoutePath + '/user/submittedReward']: ['./backend/${lang}/user/submittedReward.ts'],
|
[adminBaseRoutePath + '/user/submittedReward']: ['./backend/${lang}/user/submittedReward.ts'],
|
||||||
[adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'],
|
[adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'],
|
||||||
[adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'],
|
[adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'],
|
||||||
|
|||||||
@@ -79,4 +79,31 @@ export default {
|
|||||||
count: 'COUNT',
|
count: 'COUNT',
|
||||||
total: 'TOTAL',
|
total: 'TOTAL',
|
||||||
},
|
},
|
||||||
|
customerReport: {
|
||||||
|
title: 'CUSTOMER REPORT',
|
||||||
|
noData: 'No customer report data',
|
||||||
|
startDate: 'Start Date',
|
||||||
|
endDate: 'End Date',
|
||||||
|
fullUsername: 'Full Username',
|
||||||
|
loseRebate: 'LOSE REBATE',
|
||||||
|
options: '(Options)',
|
||||||
|
rebatePlaceholder: '(Options) percentage %',
|
||||||
|
option: 'option',
|
||||||
|
search: 'Search',
|
||||||
|
clear: 'Clear',
|
||||||
|
dateOfData: 'Date of data',
|
||||||
|
allTime: 'All Time',
|
||||||
|
registerDate: 'REGISTER DATE',
|
||||||
|
username: 'USERNAME',
|
||||||
|
deposit: 'DEPOSIT',
|
||||||
|
withdraw: 'WITHDRAW',
|
||||||
|
winLose: 'WIN / LOSE',
|
||||||
|
referral: 'REFERRAL (Downline)',
|
||||||
|
reward: 'REWARD ({game})',
|
||||||
|
count: 'Count',
|
||||||
|
amount: 'Amount',
|
||||||
|
totalRecords: 'Total {total} records',
|
||||||
|
show: 'Show',
|
||||||
|
entries: 'entries',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,4 +78,31 @@ export default {
|
|||||||
count: '笔数',
|
count: '笔数',
|
||||||
total: '总额',
|
total: '总额',
|
||||||
},
|
},
|
||||||
|
customerReport: {
|
||||||
|
title: '客户报表',
|
||||||
|
noData: '暂无客户报表数据',
|
||||||
|
startDate: '开始日期',
|
||||||
|
endDate: '结束日期',
|
||||||
|
fullUsername: '完整用户名',
|
||||||
|
loseRebate: '亏损返利',
|
||||||
|
options: '(可选)',
|
||||||
|
rebatePlaceholder: '(可选)百分比 %',
|
||||||
|
option: '可选',
|
||||||
|
search: '搜索',
|
||||||
|
clear: '清空',
|
||||||
|
dateOfData: '数据日期',
|
||||||
|
allTime: '全部时间',
|
||||||
|
registerDate: '注册日期',
|
||||||
|
username: '用户名',
|
||||||
|
deposit: '存款',
|
||||||
|
withdraw: '提款',
|
||||||
|
winLose: '输赢',
|
||||||
|
referral: '推荐(下线)',
|
||||||
|
reward: '奖励({game})',
|
||||||
|
count: '笔数',
|
||||||
|
amount: '金额',
|
||||||
|
totalRecords: '共 {total} 条记录',
|
||||||
|
show: '每页显示',
|
||||||
|
entries: '条',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default {
|
|||||||
Report: 'Report',
|
Report: 'Report',
|
||||||
'Annual Report': 'Annual Report',
|
'Annual Report': 'Annual Report',
|
||||||
'Daily Report': 'Daily Report',
|
'Daily Report': 'Daily Report',
|
||||||
|
'Customer Report': 'Customer Report',
|
||||||
'银行账户管理': 'Bank Account Management',
|
'银行账户管理': 'Bank Account Management',
|
||||||
'Bank Account Management': 'Bank Account Management',
|
'Bank Account Management': 'Bank Account Management',
|
||||||
'权限管理': 'Permission Management',
|
'权限管理': 'Permission Management',
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default {
|
|||||||
Report: '报表',
|
Report: '报表',
|
||||||
'Annual Report': '年度报表',
|
'Annual Report': '年度报表',
|
||||||
'Daily Report': '日报表',
|
'Daily Report': '日报表',
|
||||||
|
'Customer Report': '客户报表',
|
||||||
'银行账户管理': '银行账户管理',
|
'银行账户管理': '银行账户管理',
|
||||||
'Bank Account Management': '银行账户管理',
|
'Bank Account Management': '银行账户管理',
|
||||||
'权限管理': '权限管理',
|
'权限管理': '权限管理',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
<el-menu-item @click="onClickMenuItem(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
|
||||||
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
|
||||||
<span>{{ menuTitle(menu) }}</span>
|
<span>{{ menuTitle(menu) }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
@@ -22,8 +22,9 @@
|
|||||||
import { ElNotification } from 'element-plus'
|
import { ElNotification } from 'element-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { adminBaseRoutePath } from '/@/router/static/adminBase'
|
||||||
import { useConfig } from '/@/stores/config'
|
import { useConfig } from '/@/stores/config'
|
||||||
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
|
import { getFirstRoute, getMenuKey, onClickMenu, routePush } from '/@/utils/router'
|
||||||
|
|
||||||
const { t, te } = useI18n()
|
const { t, te } = useI18n()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
@@ -50,6 +51,25 @@ const menuTitle = (menu: RouteRecordRaw) => {
|
|||||||
return te(title) ? t(title) : title
|
return te(title) ? t(title) : title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reportMenuPaths: Record<string, string> = {
|
||||||
|
'Annual Report': '/user/moneyLog/annualReport',
|
||||||
|
'年度报表': '/user/moneyLog/annualReport',
|
||||||
|
'Daily Report': '/user/moneyLog/dailyReport',
|
||||||
|
'日报表': '/user/moneyLog/dailyReport',
|
||||||
|
'Customer Report': '/user/moneyLog/customerReport',
|
||||||
|
'客户报表': '/user/moneyLog/customerReport',
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickMenuItem = (menu: RouteRecordRaw) => {
|
||||||
|
const title = typeof menu.meta?.title === 'string' ? menu.meta.title : ''
|
||||||
|
const reportPath = reportMenuPaths[title]
|
||||||
|
if (reportPath) {
|
||||||
|
routePush(adminBaseRoutePath + reportPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onClickMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sub-menu-item 被点击 - 用于单栏布局和双栏布局
|
* sub-menu-item 被点击 - 用于单栏布局和双栏布局
|
||||||
* 顶栏菜单:点击时打开第一个菜单
|
* 顶栏菜单:点击时打开第一个菜单
|
||||||
|
|||||||
@@ -132,6 +132,24 @@ export const handleAdminRoute = (routes: any) => {
|
|||||||
navTabs.fillAuthNode(handleAuthNode(routes, menuAdminBaseRoute))
|
navTabs.fillAuthNode(handleAuthNode(routes, menuAdminBaseRoute))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reportRouteName = (route: any) => {
|
||||||
|
if (typeof route.component !== 'string') return ''
|
||||||
|
const match = route.component.match(/\/user\/moneyLog\/(annualReport|dailyReport|customerReport)\.vue$/)
|
||||||
|
return match?.[1] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeRoutePath = (route: any) => {
|
||||||
|
const reportName = reportRouteName(route)
|
||||||
|
if (!reportName) return route.path
|
||||||
|
return String(route.path).replace(/[^/]+$/, reportName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeRouteName = (route: any) => {
|
||||||
|
const reportName = reportRouteName(route)
|
||||||
|
if (!reportName) return route.name
|
||||||
|
return String(route.name).replace(/[^/]+$/, reportName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取菜单的paths
|
* 获取菜单的paths
|
||||||
*/
|
*/
|
||||||
@@ -179,14 +197,16 @@ const handleMenuRule = (routes: any, pathPrefix = '/', type = ['menu', 'menu_dir
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const currentPath = ['link', 'iframe'].includes(routes[key].menu_type) ? routes[key].url : pathPrefix + routes[key].path
|
const currentPath = ['link', 'iframe'].includes(routes[key].menu_type)
|
||||||
|
? routes[key].url
|
||||||
|
: pathPrefix + normalizeRoutePath(routes[key])
|
||||||
let children: RouteRecordRaw[] = []
|
let children: RouteRecordRaw[] = []
|
||||||
if (routes[key].children && routes[key].children.length > 0) {
|
if (routes[key].children && routes[key].children.length > 0) {
|
||||||
children = handleMenuRule(routes[key].children, pathPrefix, type)
|
children = handleMenuRule(routes[key].children, pathPrefix, type)
|
||||||
}
|
}
|
||||||
menuRule.push({
|
menuRule.push({
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
name: routes[key].name,
|
name: normalizeRouteName(routes[key]),
|
||||||
component: routes[key].component,
|
component: routes[key].component,
|
||||||
meta: {
|
meta: {
|
||||||
id: routes[key].id,
|
id: routes[key].id,
|
||||||
@@ -262,7 +282,8 @@ export const addRouteItem = (viewsComponent: Record<string, any>, route: any, pa
|
|||||||
path = (isAdminApp() ? adminBaseRoute.path : memberCenterBaseRoute.path) + '/iframe/' + encodeURIComponent(route.url)
|
path = (isAdminApp() ? adminBaseRoute.path : memberCenterBaseRoute.path) + '/iframe/' + encodeURIComponent(route.url)
|
||||||
component = () => import('/@/layouts/common/router-view/iframe.vue')
|
component = () => import('/@/layouts/common/router-view/iframe.vue')
|
||||||
} else {
|
} else {
|
||||||
path = parentName ? route.path : '/' + route.path
|
const routePath = normalizeRoutePath(route)
|
||||||
|
path = parentName ? routePath : '/' + routePath
|
||||||
component = viewsComponent[route.component]
|
component = viewsComponent[route.component]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +301,7 @@ export const addRouteItem = (viewsComponent: Record<string, any>, route: any, pa
|
|||||||
|
|
||||||
const routeBaseInfo: RouteRecordRaw = {
|
const routeBaseInfo: RouteRecordRaw = {
|
||||||
path: path,
|
path: path,
|
||||||
name: route.name,
|
name: normalizeRouteName(route),
|
||||||
component: component,
|
component: component,
|
||||||
meta: {
|
meta: {
|
||||||
title: route.title,
|
title: route.title,
|
||||||
|
|||||||
465
web/src/views/backend/user/moneyLog/customerReport.vue
Normal file
465
web/src/views/backend/user/moneyLog/customerReport.vue
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
<template>
|
||||||
|
<div class="default-main customer-report">
|
||||||
|
<section class="report-filter">
|
||||||
|
<header>{{ t('user.moneyLog.customerReport.title') }}</header>
|
||||||
|
<div class="filter-content">
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>{{ t('user.moneyLog.customerReport.startDate') }}:</label>
|
||||||
|
<el-date-picker v-model="filters.start" type="date" value-format="YYYY-MM-DD" clearable />
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>{{ t('user.moneyLog.customerReport.endDate') }}:</label>
|
||||||
|
<el-date-picker v-model="filters.end" type="date" value-format="YYYY-MM-DD" clearable />
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>{{ t('user.moneyLog.customerReport.fullUsername') }}:</label>
|
||||||
|
<el-input v-model="filters.username" :placeholder="t('user.moneyLog.customerReport.options')" clearable />
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<label>{{ t('user.moneyLog.customerReport.loseRebate') }}:</label>
|
||||||
|
<el-input
|
||||||
|
v-model="filters.loseRebate"
|
||||||
|
:placeholder="t('user.moneyLog.customerReport.rebatePlaceholder')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<el-button type="primary" @click="search">{{ t('user.moneyLog.customerReport.search') }}</el-button>
|
||||||
|
<el-button @click="clear">{{ t('user.moneyLog.customerReport.clear') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="date-summary">
|
||||||
|
<strong>{{ t('user.moneyLog.customerReport.dateOfData') }}:</strong>
|
||||||
|
{{ dateSummary }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="report-table-wrap">
|
||||||
|
<table class="report-table" :style="{ minWidth: `${tableMinWidth}px` }">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" class="date-column">{{ t('user.moneyLog.customerReport.registerDate') }} ↓</th>
|
||||||
|
<th rowspan="2" class="username-column">{{ t('user.moneyLog.customerReport.username') }}</th>
|
||||||
|
<th colspan="2">{{ t('user.moneyLog.customerReport.deposit') }}</th>
|
||||||
|
<th colspan="2">{{ t('user.moneyLog.customerReport.withdraw') }}</th>
|
||||||
|
<th rowspan="2">{{ t('user.moneyLog.customerReport.winLose') }} ↕</th>
|
||||||
|
<th rowspan="2">
|
||||||
|
{{ t('user.moneyLog.customerReport.loseRebate') }}
|
||||||
|
<small>({{ t('user.moneyLog.customerReport.option') }})</small>
|
||||||
|
</th>
|
||||||
|
<th rowspan="2">{{ t('user.moneyLog.customerReport.referral') }} ↕</th>
|
||||||
|
<th v-for="game in games" :key="game.id" colspan="2" class="reward-heading">
|
||||||
|
{{ t('user.moneyLog.customerReport.reward', { game: game.name }) }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<template v-for="group in subHeaderGroups" :key="group">
|
||||||
|
<th>{{ t('user.moneyLog.customerReport.count') }} ↕</th>
|
||||||
|
<th>{{ t('user.moneyLog.customerReport.amount') }} ↕</th>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in rows" :key="`${row.registerDate}-${row.username}`">
|
||||||
|
<td>{{ row.registerDate }}</td>
|
||||||
|
<td class="username-cell"><span class="user-icon">♟</span>{{ row.username }}</td>
|
||||||
|
<td class="deposit-value">{{ row.depositCount }}</td>
|
||||||
|
<td class="deposit-value">{{ money(row.depositAmount, true) }}</td>
|
||||||
|
<td class="withdraw-value">{{ row.withdrawCount }}</td>
|
||||||
|
<td class="withdraw-value">{{ money(row.withdrawAmount, true) }}</td>
|
||||||
|
<td :class="row.winLose >= 0 ? 'positive-value' : 'negative-value'">{{ signedMoney(row.winLose) }}</td>
|
||||||
|
<td>{{ money(row.loseRebate) }}</td>
|
||||||
|
<td>{{ row.referral }}</td>
|
||||||
|
<template v-for="reward in row.rewards" :key="reward.gameId">
|
||||||
|
<td class="reward-cell">{{ reward.count }}</td>
|
||||||
|
<td class="reward-cell">{{ money(reward.amount) }}</td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="rows.length === 0">
|
||||||
|
<td class="empty-row" :colspan="columnCount">{{ t('user.moneyLog.customerReport.noData') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-footer">
|
||||||
|
<div>{{ t('user.moneyLog.customerReport.totalRecords', { total }) }}</div>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
small
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:page-count="lastPage"
|
||||||
|
@current-change="loadReport"
|
||||||
|
/>
|
||||||
|
<div class="page-size">
|
||||||
|
<span>{{ t('user.moneyLog.customerReport.show') }}</span>
|
||||||
|
<el-select v-model="pageSize" @change="changePageSize">
|
||||||
|
<el-option v-for="size in pageSizes" :key="size" :label="size" :value="size" />
|
||||||
|
</el-select>
|
||||||
|
<span>{{ t('user.moneyLog.customerReport.entries') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { customerReport } from '/@/api/backend/user/moneyLog'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'user/moneyLog/customerReport',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface CustomerReportRow {
|
||||||
|
registerDate: string
|
||||||
|
username: string
|
||||||
|
depositCount: number
|
||||||
|
depositAmount: number
|
||||||
|
withdrawCount: number
|
||||||
|
withdrawAmount: number
|
||||||
|
winLose: number
|
||||||
|
loseRebate: number
|
||||||
|
referral: number
|
||||||
|
rewards: { gameId: string; count: number; amount: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomerReportSourceRow {
|
||||||
|
register_date?: string
|
||||||
|
username?: string
|
||||||
|
deposit_count?: number | string
|
||||||
|
deposit_total?: number | string
|
||||||
|
withdraw_count?: number | string
|
||||||
|
withdraw_total?: number | string
|
||||||
|
win_lose?: number | string
|
||||||
|
lose_rebate?: number | string
|
||||||
|
referral_count?: number | string
|
||||||
|
[key: string]: number | string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const games = ref<{ id: string; name: string }[]>([])
|
||||||
|
const subHeaderGroups = computed(() => ['deposit', 'withdraw', ...games.value.map((game) => game.id)])
|
||||||
|
const columnCount = computed(() => 9 + games.value.length * 2)
|
||||||
|
const tableMinWidth = computed(() => 1580 + games.value.length * 250)
|
||||||
|
const pageSizes = [100, 500, 1000, 5000]
|
||||||
|
const rows = ref<CustomerReportRow[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const lastPage = ref(1)
|
||||||
|
const pageSize = ref(100)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
|
username: '',
|
||||||
|
loseRebate: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const appliedFilters = reactive({ ...filters })
|
||||||
|
|
||||||
|
const dateSummary = computed(() => {
|
||||||
|
if (appliedFilters.start && appliedFilters.end) return `${appliedFilters.start} to ${appliedFilters.end}`
|
||||||
|
if (appliedFilters.start) return `${appliedFilters.start} -`
|
||||||
|
if (appliedFilters.end) return `- ${appliedFilters.end}`
|
||||||
|
return t('user.moneyLog.customerReport.allTime')
|
||||||
|
})
|
||||||
|
|
||||||
|
const toNumber = (value: unknown) => {
|
||||||
|
const number = Number(value)
|
||||||
|
return Number.isFinite(number) ? number : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeResponse = (responseData: any) => {
|
||||||
|
const source = responseData?.data ?? responseData ?? {}
|
||||||
|
const gameMap = source.games && typeof source.games === 'object' ? source.games : {}
|
||||||
|
const normalizedGames = Object.entries(gameMap).map(([id, name]) => ({
|
||||||
|
id,
|
||||||
|
name: String(name),
|
||||||
|
}))
|
||||||
|
const list = Array.isArray(source.list) ? source.list : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
games: normalizedGames,
|
||||||
|
rows: list.map((row: CustomerReportSourceRow) => ({
|
||||||
|
registerDate: String(row.register_date ?? ''),
|
||||||
|
username: String(row.username ?? ''),
|
||||||
|
depositCount: toNumber(row.deposit_count),
|
||||||
|
depositAmount: toNumber(row.deposit_total),
|
||||||
|
withdrawCount: toNumber(row.withdraw_count),
|
||||||
|
withdrawAmount: toNumber(row.withdraw_total),
|
||||||
|
winLose: toNumber(row.win_lose),
|
||||||
|
loseRebate: toNumber(row.lose_rebate),
|
||||||
|
referral: toNumber(row.referral_count),
|
||||||
|
rewards: normalizedGames.map((game) => ({
|
||||||
|
gameId: game.id,
|
||||||
|
count: toNumber(row[`game_${game.id}_count`]),
|
||||||
|
amount: toNumber(row[`game_${game.id}_total`]),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
total: toNumber(source.count ?? source.total ?? list.length),
|
||||||
|
currentPage: toNumber(source.current_page) || currentPage.value,
|
||||||
|
lastPage: toNumber(source.last_page) || 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadReport = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await customerReport({
|
||||||
|
start: appliedFilters.start,
|
||||||
|
end: appliedFilters.end,
|
||||||
|
username: appliedFilters.username,
|
||||||
|
lose_rebate: appliedFilters.loseRebate,
|
||||||
|
page: currentPage.value,
|
||||||
|
limit: pageSize.value,
|
||||||
|
})
|
||||||
|
const normalized = normalizeResponse(response.data)
|
||||||
|
games.value = normalized.games
|
||||||
|
rows.value = normalized.rows
|
||||||
|
total.value = normalized.total
|
||||||
|
currentPage.value = normalized.currentPage
|
||||||
|
lastPage.value = normalized.lastPage
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
Object.assign(appliedFilters, filters)
|
||||||
|
currentPage.value = 1
|
||||||
|
await loadReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
Object.assign(filters, {
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
|
username: '',
|
||||||
|
loseRebate: '',
|
||||||
|
})
|
||||||
|
await search()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changePageSize = async () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
await loadReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
const money = (value: number, currency = false) => {
|
||||||
|
const formatted = new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(Number(value) || 0)
|
||||||
|
return currency ? `AUD${formatted}` : formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedMoney = (value: number) => {
|
||||||
|
const number = Number(value) || 0
|
||||||
|
if (number === 0) return money(0)
|
||||||
|
return `${number > 0 ? '+' : '-'}${money(Math.abs(number))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadReport)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.customer-report {
|
||||||
|
--report-dark: #333;
|
||||||
|
--report-border: #d8d8d8;
|
||||||
|
--report-reward: #e5e5e5;
|
||||||
|
--report-surface: var(--el-bg-color);
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-filter {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid var(--report-border);
|
||||||
|
background: var(--report-surface);
|
||||||
|
|
||||||
|
header {
|
||||||
|
min-height: 37px;
|
||||||
|
padding: 9px 8px;
|
||||||
|
background: var(--report-dark);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 335px 335px;
|
||||||
|
gap: 6px 18px;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 13px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 115px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-date-editor),
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
grid-column: 2;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 160px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-summary {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 2500px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
height: 38px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--report-border);
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
border-color: #eee;
|
||||||
|
background: var(--report-dark);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
background: var(--report-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-column {
|
||||||
|
width: 225px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-column {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-heading {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-cell {
|
||||||
|
background: var(--report-reward);
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deposit-value,
|
||||||
|
.positive-value {
|
||||||
|
color: #35ad78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-value,
|
||||||
|
.negative-value {
|
||||||
|
color: #e05a55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-row {
|
||||||
|
height: 150px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root html.dark {
|
||||||
|
.customer-report {
|
||||||
|
--report-dark: #2b2b2b;
|
||||||
|
--report-border: var(--el-border-color);
|
||||||
|
--report-reward: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.customer-report {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user