1.新增时间筛选功能
This commit is contained in:
@@ -2,6 +2,8 @@ import request from '@/utils/http'
|
||||
|
||||
export type DashboardQueryParams = {
|
||||
dept_id?: number
|
||||
/** 统计日期,格式 YYYY-MM-DD,默认当日 */
|
||||
date?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
98
saiadmin-artd/src/composables/useDashboardScope.ts
Normal file
98
saiadmin-artd/src/composables/useDashboardScope.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
||||
import { getChannelDeptRequestParams, useInjectedChannelDept } from '@/composables/useChannelDeptScope'
|
||||
|
||||
export interface DashboardQueryParams {
|
||||
dept_id?: number
|
||||
date?: string
|
||||
}
|
||||
|
||||
export interface DashboardScopeContext {
|
||||
selectedDate: Ref<string | null>
|
||||
hasDateFilter: ComputedRef<boolean>
|
||||
queryParams: ComputedRef<DashboardQueryParams>
|
||||
}
|
||||
|
||||
export const DASHBOARD_SCOPE_KEY: InjectionKey<DashboardScopeContext> = Symbol('dashboardScope')
|
||||
|
||||
function formatDateYmd(date: Date): string {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
export function getTodayDateString(): string {
|
||||
return formatDateYmd(new Date())
|
||||
}
|
||||
|
||||
/** 工作台页面 provide 日期筛选上下文 */
|
||||
export function provideDashboardScope() {
|
||||
const selectedDate = ref<string | null>(getTodayDateString())
|
||||
const hasDateFilter = computed(() => !!selectedDate.value)
|
||||
const queryParams = computed<DashboardQueryParams>(() => {
|
||||
const params: DashboardQueryParams = {
|
||||
...getChannelDeptRequestParams()
|
||||
}
|
||||
if (selectedDate.value) {
|
||||
params.date = selectedDate.value
|
||||
}
|
||||
return params
|
||||
})
|
||||
const ctx: DashboardScopeContext = { selectedDate, hasDateFilter, queryParams }
|
||||
provide(DASHBOARD_SCOPE_KEY, ctx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function useDashboardScope(): DashboardScopeContext {
|
||||
const ctx = inject(DASHBOARD_SCOPE_KEY, null)
|
||||
if (ctx) {
|
||||
return ctx
|
||||
}
|
||||
const selectedDate = ref<string | null>(getTodayDateString())
|
||||
const hasDateFilter = computed(() => !!selectedDate.value)
|
||||
return {
|
||||
selectedDate,
|
||||
hasDateFilter,
|
||||
queryParams: computed<DashboardQueryParams>(() => {
|
||||
const params: DashboardQueryParams = {
|
||||
...getChannelDeptRequestParams()
|
||||
}
|
||||
if (selectedDate.value) {
|
||||
params.date = selectedDate.value
|
||||
}
|
||||
return params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 工作台:渠道或日期变化时重新拉数 */
|
||||
export function useDashboardReload(loadFn: () => void | Promise<void>) {
|
||||
const channel = useInjectedChannelDept()
|
||||
const { selectedDate } = useDashboardScope()
|
||||
|
||||
const run = () => {
|
||||
void loadFn()
|
||||
}
|
||||
|
||||
watch(selectedDate, run)
|
||||
|
||||
if (!channel) {
|
||||
onMounted(run)
|
||||
return
|
||||
}
|
||||
|
||||
watch(
|
||||
() => channel.selectedDeptId.value,
|
||||
(deptId) => {
|
||||
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||
run()
|
||||
return
|
||||
}
|
||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||
return
|
||||
}
|
||||
run()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
@@ -300,16 +300,33 @@
|
||||
"gohome": "Go Home"
|
||||
},
|
||||
"console": {
|
||||
"filter": {
|
||||
"date": "Statistics Date",
|
||||
"placeholder": "Select date",
|
||||
"today": "Today",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"card": {
|
||||
"playerRegister": "Player Register",
|
||||
"playerCharge": "Player Charge",
|
||||
"playerWithdraw": "Player Withdraw",
|
||||
"playerPlayCount": "Player Play Count",
|
||||
"vsLastWeek": "vs Last Week"
|
||||
"vsLastWeek": "vs Last Week",
|
||||
"vsYesterday": "vs Yesterday",
|
||||
"viewRechargeRecords": "View recharge records",
|
||||
"viewWithdrawRecords": "View withdraw records",
|
||||
"viewPlayRecords": "View play records",
|
||||
"viewRegisterRecords": "View registered players"
|
||||
},
|
||||
"nav": {
|
||||
"viewAllRecharge": "View all recharge",
|
||||
"viewAllPlay": "View all play records",
|
||||
"viewAllRegister": "View all registrations"
|
||||
},
|
||||
"newPlayer": {
|
||||
"title": "New Players",
|
||||
"subtitle": "Latest 50 new player records",
|
||||
"subtitleByDate": "New players on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"balance": "Balance",
|
||||
"ticket": "Tickets",
|
||||
@@ -318,6 +335,7 @@
|
||||
"playRecord": {
|
||||
"title": "Player Play Records",
|
||||
"subtitle": "Latest 50 play records",
|
||||
"subtitleByDate": "Play records on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"reward": "Reward Tier",
|
||||
"winCoin": "Win Amount",
|
||||
@@ -326,6 +344,7 @@
|
||||
"walletRecord": {
|
||||
"title": "Player Charge Records",
|
||||
"subtitle": "Latest 50 charge records",
|
||||
"subtitleByDate": "Charge records on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"chargeAmount": "Amount",
|
||||
"chargeTime": "Charge Time"
|
||||
@@ -450,6 +469,10 @@
|
||||
"max": "Max",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"quickDate": "Quick date",
|
||||
"quickToday": "Today",
|
||||
"quickYesterday": "Yesterday",
|
||||
"quickLast7Days": "Last 7 days",
|
||||
"placeholderUsername": "Username",
|
||||
"placeholderNickname": "Nickname",
|
||||
"placeholderPhone": "Phone",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"placeholderNickname": "Please enter nickname",
|
||||
"placeholderPhoneFuzzy": "Phone (fuzzy)",
|
||||
"placeholderAll": "All",
|
||||
"exactSearch": "Exact"
|
||||
"exactSearch": "Exact",
|
||||
"createTime": "Register Time"
|
||||
},
|
||||
"table": {
|
||||
"username": "Username",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"toolbar": {
|
||||
"coinChangeSummary": "Coin Change Summary"
|
||||
"coinChangeSummary": "Net Coin Change",
|
||||
"coinInflow": "Inflow",
|
||||
"coinOutflow": "Outflow"
|
||||
},
|
||||
"form": {
|
||||
"dialogTitleAdd": "Add Wallet Record",
|
||||
|
||||
@@ -300,16 +300,33 @@
|
||||
"gohome": "返回首页"
|
||||
},
|
||||
"console": {
|
||||
"filter": {
|
||||
"date": "统计日期",
|
||||
"placeholder": "选择日期",
|
||||
"today": "今日",
|
||||
"clear": "清空"
|
||||
},
|
||||
"card": {
|
||||
"playerRegister": "玩家注册",
|
||||
"playerCharge": "玩家充值",
|
||||
"playerWithdraw": "玩家提现",
|
||||
"playerPlayCount": "玩家游玩次数",
|
||||
"vsLastWeek": "较上周"
|
||||
"vsLastWeek": "较上周",
|
||||
"vsYesterday": "较昨日",
|
||||
"viewRechargeRecords": "查看充值记录",
|
||||
"viewWithdrawRecords": "查看提现记录",
|
||||
"viewPlayRecords": "查看游玩记录",
|
||||
"viewRegisterRecords": "查看注册玩家"
|
||||
},
|
||||
"nav": {
|
||||
"viewAllRecharge": "查看全部充值",
|
||||
"viewAllPlay": "查看全部游玩",
|
||||
"viewAllRegister": "查看全部注册"
|
||||
},
|
||||
"newPlayer": {
|
||||
"title": "新增玩家",
|
||||
"subtitle": "最新50条新增玩家记录",
|
||||
"subtitleByDate": "{date} 新增玩家记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"balance": "余额",
|
||||
"ticket": "抽奖券",
|
||||
@@ -318,6 +335,7 @@
|
||||
"playRecord": {
|
||||
"title": "玩家游玩记录",
|
||||
"subtitle": "最新50条游玩记录",
|
||||
"subtitleByDate": "{date} 游玩记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"reward": "中奖档位",
|
||||
"winCoin": "赢取平台币",
|
||||
@@ -326,6 +344,7 @@
|
||||
"walletRecord": {
|
||||
"title": "玩家充值记录",
|
||||
"subtitle": "最新50条充值记录",
|
||||
"subtitleByDate": "{date} 充值记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"chargeAmount": "充值金额",
|
||||
"chargeTime": "充值时间"
|
||||
@@ -446,6 +465,10 @@
|
||||
"max": "最大",
|
||||
"startTime": "开始时间",
|
||||
"endTime": "结束时间",
|
||||
"quickDate": "快捷日期",
|
||||
"quickToday": "今日",
|
||||
"quickYesterday": "昨日",
|
||||
"quickLast7Days": "近7天",
|
||||
"placeholderUsername": "请输入用户名",
|
||||
"placeholderNickname": "请输入昵称",
|
||||
"placeholderPhone": "请输入手机号",
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"placeholderNickname": "请输入昵称",
|
||||
"placeholderPhoneFuzzy": "手机号模糊查询",
|
||||
"placeholderAll": "全部",
|
||||
"exactSearch": "精确搜索"
|
||||
"exactSearch": "精确搜索",
|
||||
"createTime": "注册时间"
|
||||
},
|
||||
"table": {
|
||||
"username": "用户名",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"toolbar": {
|
||||
"coinChangeSummary": "平台币变化统计"
|
||||
"coinChangeSummary": "平台币净变化",
|
||||
"coinInflow": "流入",
|
||||
"coinOutflow": "流出"
|
||||
},
|
||||
"form": {
|
||||
"dialogTitleAdd": "新增玩家钱包流水",
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
<!-- 工作台页面:大富翁色子游戏数据统计 -->
|
||||
<template>
|
||||
<div>
|
||||
<div class="dashboard-filter-bar art-card flex flex-wrap items-center gap-3 px-5 py-3 mb-5 max-sm:mb-4">
|
||||
<span class="text-g-700 text-sm shrink-0">{{ $t('console.filter.date') }}</span>
|
||||
<ElDatePicker
|
||||
v-model="selectedDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
:placeholder="$t('console.filter.placeholder')"
|
||||
class="dashboard-date-picker"
|
||||
/>
|
||||
<ElButton type="primary" link @click="resetToToday">{{ $t('console.filter.today') }}</ElButton>
|
||||
<ElButton v-if="selectedDate" type="primary" link @click="clearDateFilter">
|
||||
{{ $t('console.filter.clear') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<CardList />
|
||||
|
||||
<template v-if="isStatisticsDashboard">
|
||||
@@ -40,10 +56,20 @@
|
||||
import PlayRecordList from './modules/play-record-list.vue'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { getTodayDateString, provideDashboardScope } from '@/composables/useDashboardScope'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { selectedDate } = provideDashboardScope()
|
||||
|
||||
const resetToToday = () => {
|
||||
selectedDate.value = getTodayDateString()
|
||||
}
|
||||
|
||||
const clearDateFilter = () => {
|
||||
selectedDate.value = null
|
||||
}
|
||||
|
||||
/** 统计页额外展示充值图表 */
|
||||
const isStatisticsDashboard = computed(
|
||||
@@ -53,3 +79,14 @@
|
||||
const { scrollToTop } = useCommon()
|
||||
scrollToTop()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-filter-bar {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.dashboard-date-picker {
|
||||
width: 180px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<ElRow :gutter="20" class="flex">
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<div
|
||||
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||
@click="goPlayerList"
|
||||
>
|
||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerRegister') }}</span>
|
||||
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.player_count" :duration="1300" />
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
||||
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||
<span
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.player_count_change)"
|
||||
@@ -13,6 +16,9 @@
|
||||
{{ formatChange(statData.player_count_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayerList">
|
||||
{{ $t('console.card.viewRegisterRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
@@ -21,7 +27,10 @@
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<div
|
||||
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||
@click="goWalletRecord(0)"
|
||||
>
|
||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerCharge') }}</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -30,7 +39,7 @@
|
||||
:decimals="2"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
||||
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||
<span
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.charge_amount_change)"
|
||||
@@ -38,6 +47,9 @@
|
||||
{{ formatChange(statData.charge_amount_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(0)">
|
||||
{{ $t('console.card.viewRechargeRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
@@ -46,7 +58,10 @@
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<div
|
||||
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||
@click="goWalletRecord(1)"
|
||||
>
|
||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerWithdraw') }}</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -55,7 +70,7 @@
|
||||
:decimals="2"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
||||
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||
<span
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.withdraw_amount_change)"
|
||||
@@ -63,6 +78,9 @@
|
||||
{{ formatChange(statData.withdraw_amount_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(1)">
|
||||
{{ $t('console.card.viewWithdrawRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
@@ -71,7 +89,10 @@
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :sm="12" :md="6" :lg="6">
|
||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
||||
<div
|
||||
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||
@click="goPlayRecord"
|
||||
>
|
||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerPlayCount') }}</span>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -79,7 +100,7 @@
|
||||
:duration="1300"
|
||||
/>
|
||||
<div class="flex-c mt-1">
|
||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
||||
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||
<span
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.play_count_change)"
|
||||
@@ -87,6 +108,9 @@
|
||||
{{ formatChange(statData.play_count_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayRecord">
|
||||
{{ $t('console.card.viewPlayRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
@@ -99,7 +123,21 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchStatistics } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
dashboardDateNavParams,
|
||||
openPlayRecord,
|
||||
openPlayerList,
|
||||
openWalletRecord
|
||||
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { queryParams, hasDateFilter, selectedDate } = useDashboardScope()
|
||||
|
||||
const compareLabel = computed(() =>
|
||||
hasDateFilter.value ? t('console.card.vsYesterday') : t('console.card.vsLastWeek')
|
||||
)
|
||||
|
||||
const statData = ref({
|
||||
player_count: 0,
|
||||
@@ -125,7 +163,7 @@
|
||||
}
|
||||
|
||||
const loadStatistics = () => {
|
||||
fetchStatistics(getChannelDeptRequestParams()).then((data: any) => {
|
||||
fetchStatistics(queryParams.value).then((data: any) => {
|
||||
statData.value = {
|
||||
player_count: data?.player_count ?? 0,
|
||||
player_count_change: data?.player_count_change ?? 0,
|
||||
@@ -139,5 +177,39 @@
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadStatistics)
|
||||
useDashboardReload(loadStatistics)
|
||||
|
||||
const goWalletRecord = (type: number) => {
|
||||
openWalletRecord({
|
||||
type,
|
||||
...dashboardDateNavParams(selectedDate.value)
|
||||
})
|
||||
}
|
||||
|
||||
const goPlayRecord = () => {
|
||||
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
|
||||
const goPlayerList = () => {
|
||||
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-stat-card--clickable {
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboard-stat-card--clickable:hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.dashboard-stat-link {
|
||||
align-self: flex-start;
|
||||
margin-top: 4px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header mb-4">
|
||||
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||
<div class="title">
|
||||
<h4>{{ $t('console.newPlayer.title') }}</h4>
|
||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.newPlayer.subtitle') }}</p>
|
||||
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||
</div>
|
||||
<ElButton type="primary" link @click="goPlayerList">
|
||||
{{ $t('console.nav.viewAllRegister') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
@@ -41,20 +44,37 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchNewPlayerList, type NewPlayerItem } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
dashboardDateNavParams,
|
||||
openPlayerList
|
||||
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||
const tableData = ref<NewPlayerItem[]>([])
|
||||
|
||||
const listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.newPlayer.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.newPlayer.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
const loadList = () => {
|
||||
fetchNewPlayerList(getChannelDeptRequestParams()).then((data) => {
|
||||
fetchNewPlayerList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadList)
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goPlayerList = () => {
|
||||
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header mb-4">
|
||||
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||
<div class="title">
|
||||
<h4>{{ $t('console.playRecord.title') }}</h4>
|
||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.playRecord.subtitle') }}</p>
|
||||
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||
</div>
|
||||
<ElButton type="primary" link @click="goPlayRecords">
|
||||
{{ $t('console.nav.viewAllPlay') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
@@ -51,20 +54,37 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchPlayRecordList, type PlayRecordItem } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
dashboardDateNavParams,
|
||||
openPlayRecord
|
||||
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||
const tableData = ref<PlayRecordItem[]>([])
|
||||
|
||||
const listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.playRecord.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.playRecord.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
const loadList = () => {
|
||||
fetchPlayRecordList(getChannelDeptRequestParams()).then((data) => {
|
||||
fetchPlayRecordList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadList)
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goPlayRecords = () => {
|
||||
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header mb-4">
|
||||
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||
<div class="title">
|
||||
<h4>{{ $t('console.walletRecord.title') }}</h4>
|
||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.walletRecord.subtitle') }}</p>
|
||||
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||
</div>
|
||||
<ElButton type="primary" link @click="goWalletRecords">
|
||||
{{ $t('console.nav.viewAllRecharge') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
@@ -30,20 +33,40 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchWalletRecordList, type WalletRecordItem } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
dashboardDateNavParams,
|
||||
openWalletRecord
|
||||
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||
const tableData = ref<WalletRecordItem[]>([])
|
||||
|
||||
const listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.walletRecord.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.walletRecord.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
const loadList = () => {
|
||||
fetchWalletRecordList(getChannelDeptRequestParams()).then((data) => {
|
||||
fetchWalletRecordList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadList)
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goWalletRecords = () => {
|
||||
openWalletRecord({
|
||||
type: 0,
|
||||
...dashboardDateNavParams(selectedDate.value)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,11 @@ export default {
|
||||
* @returns 数据列表
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage & { total_coin_change?: number }>({
|
||||
return request.get<Api.Common.ApiPage & {
|
||||
total_coin_change?: number
|
||||
total_coin_inflow?: number
|
||||
total_coin_outflow?: number
|
||||
}>({
|
||||
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/index',
|
||||
params
|
||||
})
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="quick-date-range-bar">
|
||||
<span class="quick-date-label">{{ $t('table.searchBar.quickDate') }}</span>
|
||||
<ElButton
|
||||
v-for="item in presetItems"
|
||||
:key="item.key"
|
||||
size="small"
|
||||
:type="modelValue === item.key ? 'primary' : 'default'"
|
||||
@click="selectPreset(item.key)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
type DatePresetKey,
|
||||
getCreateTimeRangeByPreset
|
||||
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||
|
||||
const modelValue = defineModel<DatePresetKey | null>({ default: null })
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [range: [string, string], preset: DatePresetKey]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const presetItems = computed(() => [
|
||||
{ key: 'today' as const, label: t('table.searchBar.quickToday') },
|
||||
{ key: 'yesterday' as const, label: t('table.searchBar.quickYesterday') },
|
||||
{ key: 'last7days' as const, label: t('table.searchBar.quickLast7Days') }
|
||||
])
|
||||
|
||||
const selectPreset = (preset: DatePresetKey) => {
|
||||
modelValue.value = preset
|
||||
emit('select', getCreateTimeRangeByPreset(preset), preset)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.quick-date-range-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-date-label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import {
|
||||
detectPresetFromRange,
|
||||
type DatePresetKey,
|
||||
hasRecordRouteFilter,
|
||||
parseRecordRouteQuery
|
||||
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||
|
||||
export interface RecordRouteInit {
|
||||
hasFilter: boolean
|
||||
create_time?: [string, string]
|
||||
type?: number
|
||||
activePreset: DatePresetKey | null
|
||||
}
|
||||
|
||||
export function getRecordRouteInit(query: LocationQuery, withType = false): RecordRouteInit {
|
||||
const filter = parseRecordRouteQuery(query)
|
||||
const create_time = filter.create_time
|
||||
const activePreset = filter.datePreset ?? detectPresetFromRange(create_time ?? null)
|
||||
return {
|
||||
hasFilter: hasRecordRouteFilter(query),
|
||||
create_time,
|
||||
type: withType ? filter.type : undefined,
|
||||
activePreset
|
||||
}
|
||||
}
|
||||
|
||||
/** 将路由 query 同步到搜索表单 */
|
||||
export function syncSearchFormFromRoute(
|
||||
query: LocationQuery,
|
||||
searchForm: Ref<Record<string, unknown>>,
|
||||
activeDatePreset: Ref<DatePresetKey | null>,
|
||||
options?: { withType?: boolean }
|
||||
): boolean {
|
||||
const init = getRecordRouteInit(query, options?.withType)
|
||||
if (init.create_time) {
|
||||
searchForm.value.create_time = init.create_time
|
||||
activeDatePreset.value = init.activePreset
|
||||
} else {
|
||||
searchForm.value.create_time = undefined
|
||||
activeDatePreset.value = null
|
||||
}
|
||||
if (options?.withType) {
|
||||
searchForm.value.type = init.type !== undefined ? init.type : undefined
|
||||
}
|
||||
return init.hasFilter
|
||||
}
|
||||
|
||||
export function buildRecordRouteQueryKey(query: LocationQuery, withType = false): string {
|
||||
const parts = [String(query.date ?? ''), String(query.datePreset ?? '')]
|
||||
if (withType) {
|
||||
parts.push(String(query.type ?? ''))
|
||||
}
|
||||
return parts.join('|')
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听路由 query 变化并触发查询(解决 KeepAlive 下跳转带参不刷新的问题)
|
||||
*/
|
||||
export function useRecordRouteSync(options: {
|
||||
searchForm: Ref<Record<string, unknown>>
|
||||
activeDatePreset: Ref<DatePresetKey | null>
|
||||
onSearch: (params: Record<string, unknown>) => void
|
||||
withType?: boolean
|
||||
skipImmediateTableLoad?: boolean
|
||||
}) {
|
||||
const route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query, options.withType)
|
||||
let lastQueryKey = buildRecordRouteQueryKey(route.query, options.withType)
|
||||
|
||||
const applyRoute = () => {
|
||||
const hasFilter = syncSearchFormFromRoute(
|
||||
route.query,
|
||||
options.searchForm,
|
||||
options.activeDatePreset,
|
||||
{ withType: options.withType }
|
||||
)
|
||||
if (hasFilter) {
|
||||
options.onSearch({ ...options.searchForm.value })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (routeInit.hasFilter) {
|
||||
applyRoute()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => buildRecordRouteQueryKey(route.query, options.withType),
|
||||
(newKey) => {
|
||||
if (newKey === lastQueryKey) return
|
||||
lastQueryKey = newKey
|
||||
applyRoute()
|
||||
}
|
||||
)
|
||||
|
||||
onActivated(() => {
|
||||
const key = buildRecordRouteQueryKey(route.query, options.withType)
|
||||
if (key !== lastQueryKey) {
|
||||
lastQueryKey = key
|
||||
applyRoute()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
routeInit,
|
||||
skipImmediateTableLoad: options.skipImmediateTableLoad ?? routeInit.hasFilter
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||
|
||||
@@ -138,13 +139,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/play_record/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||
import {
|
||||
detectPresetFromRange,
|
||||
type DatePresetKey
|
||||
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query)
|
||||
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
@@ -159,7 +171,7 @@
|
||||
reward_ui_text: undefined,
|
||||
reward_tier: undefined,
|
||||
direction: undefined,
|
||||
create_time: undefined
|
||||
create_time: routeInit.create_time
|
||||
})
|
||||
|
||||
const PLAY_RECORD_SEARCH_KEYS = [
|
||||
@@ -206,11 +218,29 @@
|
||||
getData()
|
||||
}
|
||||
|
||||
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||
searchForm.value.create_time = range
|
||||
activeDatePreset.value = preset
|
||||
handleSearch({ ...searchForm.value })
|
||||
}
|
||||
|
||||
const handleResetSearch = () => {
|
||||
activeDatePreset.value = null
|
||||
searchForm.value.create_time = undefined
|
||||
resetSearchParams()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => searchForm.value.create_time,
|
||||
(range) => {
|
||||
if (Array.isArray(range) && range.length === 2) {
|
||||
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||
} else {
|
||||
activeDatePreset.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const usernameFormatter = (row: Record<string, any>) =>
|
||||
row?.dicePlayer?.username ?? row?.player_id ?? '-'
|
||||
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
|
||||
@@ -257,6 +287,7 @@
|
||||
apiFn: listApi,
|
||||
apiParams: { limit: 100 },
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
// { type: 'selection' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80 },
|
||||
@@ -320,6 +351,12 @@
|
||||
handleSelectionChange
|
||||
// selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||
<!-- 搜索条件 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格操作 -->
|
||||
@@ -108,6 +109,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/player/index'
|
||||
@@ -116,9 +118,18 @@
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import WalletOperateDialog from './modules/WalletOperateDialog.vue'
|
||||
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||
import {
|
||||
detectPresetFromRange,
|
||||
type DatePresetKey
|
||||
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { copy } = useClipboard()
|
||||
const route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query)
|
||||
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
@@ -127,15 +138,64 @@
|
||||
phone: undefined,
|
||||
status: undefined,
|
||||
coin: undefined,
|
||||
lottery_config_id: undefined
|
||||
lottery_config_id: undefined,
|
||||
create_time: routeInit.create_time
|
||||
})
|
||||
|
||||
const PLAYER_SEARCH_KEYS = [
|
||||
'username',
|
||||
'name',
|
||||
'phone',
|
||||
'status',
|
||||
'coin',
|
||||
'lottery_config_id',
|
||||
'create_time_min',
|
||||
'create_time_max'
|
||||
] as const
|
||||
|
||||
const applySearchParams = (params: Record<string, unknown>) => {
|
||||
const p = { ...params }
|
||||
if (Array.isArray(p.create_time) && p.create_time.length === 2) {
|
||||
p.create_time_min = p.create_time[0]
|
||||
p.create_time_max = p.create_time[1]
|
||||
}
|
||||
delete p.create_time
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
PLAYER_SEARCH_KEYS.forEach((key) => {
|
||||
delete paramsRecord[key]
|
||||
})
|
||||
Object.assign(searchParams, p)
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
applySearchParams(params)
|
||||
getData()
|
||||
}
|
||||
|
||||
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||
searchForm.value.create_time = range
|
||||
activeDatePreset.value = preset
|
||||
handleSearch({ ...searchForm.value })
|
||||
}
|
||||
|
||||
const handleResetSearch = () => {
|
||||
activeDatePreset.value = null
|
||||
searchForm.value.create_time = undefined
|
||||
resetSearchParams()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => searchForm.value.create_time,
|
||||
(range) => {
|
||||
if (Array.isArray(range) && range.length === 2) {
|
||||
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||
} else {
|
||||
activeDatePreset.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 权重列显示为百分比
|
||||
const weightFormatter = (prop: string) => (row: any) => {
|
||||
const cellValue = row[prop]
|
||||
@@ -167,6 +227,8 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'username', label: 'page.table.username', align: 'center' },
|
||||
@@ -300,4 +362,10 @@
|
||||
ElMessage.warning(msg)
|
||||
}
|
||||
}
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -60,6 +60,19 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(8)">
|
||||
<el-form-item :label="$t('page.search.createTime')" prop="create_time">
|
||||
<el-date-picker
|
||||
v-model="formData.create_time"
|
||||
type="datetimerange"
|
||||
:range-separator="$t('table.searchBar.rangeSeparator')"
|
||||
:start-placeholder="$t('table.searchBar.startTime')"
|
||||
:end-placeholder="$t('table.searchBar.endTime')"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
@@ -11,6 +12,14 @@
|
||||
{{ $t('page.toolbar.coinChangeSummary') }}:<strong :class="coinSummaryClass">{{
|
||||
formatMoney2(totalCoinChange)
|
||||
}}</strong>
|
||||
<template v-if="totalCoinInflow !== null && totalCoinOutflow !== null">
|
||||
({{ $t('page.toolbar.coinInflow') }} <strong class="coin-summary-positive">{{
|
||||
formatMoney2(totalCoinInflow)
|
||||
}}</strong>
|
||||
/ {{ $t('page.toolbar.coinOutflow') }} <strong class="coin-summary-negative">{{
|
||||
formatMoney2(totalCoinOutflow)
|
||||
}}</strong>)
|
||||
</template>
|
||||
</span>
|
||||
<!-- <ElSpace wrap>-->
|
||||
<!-- <ElButton-->
|
||||
@@ -87,24 +96,52 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/player_wallet_record/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||
import {
|
||||
detectPresetFromRange,
|
||||
type DatePresetKey
|
||||
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||
|
||||
const route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query, true)
|
||||
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
type: undefined,
|
||||
type: undefined as number | undefined,
|
||||
username: undefined,
|
||||
coin_min: undefined,
|
||||
coin_max: undefined,
|
||||
create_time: undefined as [string, string] | undefined
|
||||
create_time: routeInit.create_time
|
||||
})
|
||||
if (routeInit.type !== undefined) {
|
||||
searchForm.value.type = routeInit.type
|
||||
}
|
||||
|
||||
/** 当前筛选条件下平台币变化合计 */
|
||||
/** 当前筛选条件下平台币净变化、流入、流出 */
|
||||
const totalCoinChange = ref<number | null>(null)
|
||||
const totalCoinInflow = ref<number | null>(null)
|
||||
const totalCoinOutflow = ref<number | null>(null)
|
||||
|
||||
const applyCoinSummary = (res: Record<string, unknown> | undefined) => {
|
||||
const summary = res?.total_coin_change
|
||||
totalCoinChange.value =
|
||||
summary !== undefined && summary !== null && summary !== '' ? Number(summary) : null
|
||||
const inflow = res?.total_coin_inflow
|
||||
totalCoinInflow.value =
|
||||
inflow !== undefined && inflow !== null && inflow !== '' ? Number(inflow) : null
|
||||
const outflow = res?.total_coin_outflow
|
||||
totalCoinOutflow.value =
|
||||
outflow !== undefined && outflow !== null && outflow !== '' ? Number(outflow) : null
|
||||
}
|
||||
|
||||
const coinSummaryClass = computed(() => {
|
||||
if (totalCoinChange.value === null) return ''
|
||||
@@ -128,9 +165,7 @@
|
||||
const reqId = ++summaryRequestSeq
|
||||
const res = await api.list(params)
|
||||
if (reqId === summaryRequestSeq) {
|
||||
const summary = (res as Record<string, unknown> | undefined)?.total_coin_change
|
||||
totalCoinChange.value =
|
||||
summary !== undefined && summary !== null && summary !== '' ? Number(summary) : null
|
||||
applyCoinSummary(res as Record<string, unknown> | undefined)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -155,6 +190,29 @@
|
||||
getData()
|
||||
}
|
||||
|
||||
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||
searchForm.value.create_time = range
|
||||
activeDatePreset.value = preset
|
||||
handleSearch({ ...searchForm.value })
|
||||
}
|
||||
|
||||
const handleResetSearch = () => {
|
||||
activeDatePreset.value = null
|
||||
searchForm.value.create_time = undefined
|
||||
resetSearchParams()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => searchForm.value.create_time,
|
||||
(range) => {
|
||||
if (Array.isArray(range) && range.length === 2) {
|
||||
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||
} else {
|
||||
activeDatePreset.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
// 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖
|
||||
const typeFormatter = (row: Record<string, unknown>) => {
|
||||
@@ -218,6 +276,7 @@
|
||||
apiFn: listApi,
|
||||
apiParams: { limit: 100 },
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection', align: 'center' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||
@@ -281,19 +340,22 @@
|
||||
},
|
||||
hooks: {
|
||||
onSuccess(_data, response) {
|
||||
const raw = response as unknown as Record<string, unknown>
|
||||
const summary = raw?.total_coin_change
|
||||
if (summary !== undefined && summary !== null && summary !== '') {
|
||||
totalCoinChange.value = Number(summary)
|
||||
}
|
||||
applyCoinSummary(response as unknown as Record<string, unknown>)
|
||||
}
|
||||
},
|
||||
transform: {
|
||||
responseAdapter(response) {
|
||||
const raw = (response ?? {}) as Record<string, unknown>
|
||||
const base = defaultResponseAdapter(response)
|
||||
const extra = base as Record<string, unknown>
|
||||
if (raw.total_coin_change !== undefined && raw.total_coin_change !== null) {
|
||||
;(base as Record<string, unknown>).total_coin_change = raw.total_coin_change
|
||||
extra.total_coin_change = raw.total_coin_change
|
||||
}
|
||||
if (raw.total_coin_inflow !== undefined && raw.total_coin_inflow !== null) {
|
||||
extra.total_coin_inflow = raw.total_coin_inflow
|
||||
}
|
||||
if (raw.total_coin_outflow !== undefined && raw.total_coin_outflow !== null) {
|
||||
extra.total_coin_outflow = raw.total_coin_outflow
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -311,6 +373,13 @@
|
||||
handleSelectionChange
|
||||
// selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch,
|
||||
withType: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { router } from '@/router'
|
||||
import type { DatePresetKey } from './dateRangePresets'
|
||||
|
||||
export const WALLET_RECORD_PATH = '/dice/player_wallet_record/index'
|
||||
export const PLAY_RECORD_PATH = '/dice/play_record/index'
|
||||
export const PLAYER_PATH = '/dice/player/index'
|
||||
|
||||
export interface WalletRecordNavOptions {
|
||||
type?: number
|
||||
date?: string | null
|
||||
datePreset?: DatePresetKey
|
||||
}
|
||||
|
||||
export interface PlayRecordNavOptions {
|
||||
date?: string | null
|
||||
datePreset?: DatePresetKey
|
||||
}
|
||||
|
||||
function buildDateQuery(opts: { date?: string | null; datePreset?: DatePresetKey }) {
|
||||
const query: Record<string, string> = {}
|
||||
if (opts.date) {
|
||||
query.date = opts.date
|
||||
} else if (opts.datePreset) {
|
||||
query.datePreset = opts.datePreset
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
export function buildWalletRecordRouteQuery(opts: WalletRecordNavOptions): Record<string, string> {
|
||||
const query = buildDateQuery(opts)
|
||||
if (opts.type !== undefined && opts.type !== null) {
|
||||
query.type = String(opts.type)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
export function buildPlayRecordRouteQuery(opts: PlayRecordNavOptions): Record<string, string> {
|
||||
return buildDateQuery(opts)
|
||||
}
|
||||
|
||||
export function openWalletRecord(opts: WalletRecordNavOptions) {
|
||||
const query = buildWalletRecordRouteQuery(opts)
|
||||
void router.push({ path: WALLET_RECORD_PATH, query }).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function openPlayRecord(opts: PlayRecordNavOptions) {
|
||||
const query = buildPlayRecordRouteQuery(opts)
|
||||
void router.push({ path: PLAY_RECORD_PATH, query }).catch(() => undefined)
|
||||
}
|
||||
|
||||
export function openPlayerList(opts: PlayRecordNavOptions) {
|
||||
const query = buildPlayRecordRouteQuery(opts)
|
||||
void router.push({ path: PLAYER_PATH, query }).catch(() => undefined)
|
||||
}
|
||||
|
||||
/** 工作台当前统计日期转跳转参数:有选中日用 date,清空周统计时用近7天 */
|
||||
export function dashboardDateNavParams(selectedDate: string | null): {
|
||||
date?: string
|
||||
datePreset?: DatePresetKey
|
||||
} {
|
||||
if (selectedDate) {
|
||||
return { date: selectedDate }
|
||||
}
|
||||
return { datePreset: 'last7days' }
|
||||
}
|
||||
101
saiadmin-artd/src/views/plugin/dice/utils/dateRangePresets.ts
Normal file
101
saiadmin-artd/src/views/plugin/dice/utils/dateRangePresets.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
|
||||
export type DatePresetKey = 'today' | 'yesterday' | 'last7days'
|
||||
|
||||
function pad2(n: number): string {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
export function formatDateYmd(date: Date): string {
|
||||
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
||||
}
|
||||
|
||||
function startOfDay(date: Date): Date {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
function endOfDay(date: Date): Date {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999)
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date): string {
|
||||
return `${formatDateYmd(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
|
||||
}
|
||||
|
||||
export function getCreateTimeRangeByPreset(preset: DatePresetKey): [string, string] {
|
||||
const now = new Date()
|
||||
if (preset === 'today') {
|
||||
return [formatDateTime(startOfDay(now)), formatDateTime(endOfDay(now))]
|
||||
}
|
||||
if (preset === 'yesterday') {
|
||||
const day = new Date(now)
|
||||
day.setDate(day.getDate() - 1)
|
||||
return [formatDateTime(startOfDay(day)), formatDateTime(endOfDay(day))]
|
||||
}
|
||||
const start = new Date(now)
|
||||
start.setDate(start.getDate() - 6)
|
||||
return [formatDateTime(startOfDay(start)), formatDateTime(endOfDay(now))]
|
||||
}
|
||||
|
||||
export function getCreateTimeRangeByDate(ymd: string): [string, string] | undefined {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) return undefined
|
||||
const parts = ymd.split('-').map((v) => Number(v))
|
||||
const date = new Date(parts[0], parts[1] - 1, parts[2])
|
||||
if (Number.isNaN(date.getTime())) return undefined
|
||||
return [formatDateTime(startOfDay(date)), formatDateTime(endOfDay(date))]
|
||||
}
|
||||
|
||||
export function detectPresetFromRange(range?: [string, string] | null): DatePresetKey | null {
|
||||
if (!range || range.length !== 2) return null
|
||||
const presets: DatePresetKey[] = ['today', 'yesterday', 'last7days']
|
||||
for (const preset of presets) {
|
||||
const expected = getCreateTimeRangeByPreset(preset)
|
||||
if (expected[0] === range[0] && expected[1] === range[1]) {
|
||||
return preset
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function readQueryString(query: LocationQuery, key: string): string {
|
||||
const raw = query[key]
|
||||
if (Array.isArray(raw)) return raw[0] ? String(raw[0]) : ''
|
||||
return raw != null ? String(raw) : ''
|
||||
}
|
||||
|
||||
export interface RecordRouteFilter {
|
||||
create_time?: [string, string]
|
||||
type?: number
|
||||
datePreset?: DatePresetKey
|
||||
}
|
||||
|
||||
export function parseRecordRouteQuery(query: LocationQuery): RecordRouteFilter {
|
||||
const result: RecordRouteFilter = {}
|
||||
const date = readQueryString(query, 'date')
|
||||
const datePreset = readQueryString(query, 'datePreset') as DatePresetKey
|
||||
|
||||
if (date) {
|
||||
const range = getCreateTimeRangeByDate(date)
|
||||
if (range) {
|
||||
result.create_time = range
|
||||
}
|
||||
} else if (datePreset === 'today' || datePreset === 'yesterday' || datePreset === 'last7days') {
|
||||
result.datePreset = datePreset
|
||||
result.create_time = getCreateTimeRangeByPreset(datePreset)
|
||||
}
|
||||
|
||||
const typeRaw = readQueryString(query, 'type')
|
||||
if (typeRaw !== '' && typeRaw !== undefined) {
|
||||
const typeNum = Number(typeRaw)
|
||||
if (Number.isFinite(typeNum)) {
|
||||
result.type = typeNum
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function hasRecordRouteFilter(query: LocationQuery): boolean {
|
||||
const filter = parseRecordRouteQuery(query)
|
||||
return filter.create_time != null || filter.type !== undefined
|
||||
}
|
||||
Reference in New Issue
Block a user