1.新增时间筛选功能

This commit is contained in:
2026-06-14 13:08:32 +08:00
parent b20a1d276f
commit 0d19e4b6c4
29 changed files with 1011 additions and 88 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -2,6 +2,8 @@ import request from '@/utils/http'
export type DashboardQueryParams = {
dept_id?: number
/** 统计日期,格式 YYYY-MM-DD默认当日 */
date?: string
}
/**

View 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 }
)
}

View File

@@ -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",

View File

@@ -72,7 +72,8 @@
"placeholderNickname": "Please enter nickname",
"placeholderPhoneFuzzy": "Phone (fuzzy)",
"placeholderAll": "All",
"exactSearch": "Exact"
"exactSearch": "Exact",
"createTime": "Register Time"
},
"table": {
"username": "Username",

View File

@@ -1,6 +1,8 @@
{
"toolbar": {
"coinChangeSummary": "Coin Change Summary"
"coinChangeSummary": "Net Coin Change",
"coinInflow": "Inflow",
"coinOutflow": "Outflow"
},
"form": {
"dialogTitleAdd": "Add Wallet Record",

View File

@@ -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": "请输入手机号",

View File

@@ -72,7 +72,8 @@
"placeholderNickname": "请输入昵称",
"placeholderPhoneFuzzy": "手机号模糊查询",
"placeholderAll": "全部",
"exactSearch": "精确搜索"
"exactSearch": "精确搜索",
"createTime": "注册时间"
},
"table": {
"username": "用户名",

View File

@@ -1,6 +1,8 @@
{
"toolbar": {
"coinChangeSummary": "平台币变化统计"
"coinChangeSummary": "平台币变化",
"coinInflow": "流入",
"coinOutflow": "流出"
},
"form": {
"dialogTitleAdd": "新增玩家钱包流水",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
})

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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' }
}

View 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
}

View File

@@ -1,4 +1,5 @@
# 数据库配置
DB_TYPE=mysql
DB_HOST=127.0.0.1
DB_PORT=3306

View File

@@ -25,16 +25,13 @@ class DiceDashboardController extends BaseController
#[Permission('工作台数据统计', 'core:console:list')]
public function statistics(Request $request): Response
{
$thisWeekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
$thisWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday this week'));
$lastWeekStart = date('Y-m-d 00:00:00', strtotime('monday last week'));
$lastWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday last week'));
[$thisStart, $thisEnd, $lastStart, $lastEnd] = $this->resolveDateRanges($request);
$adminInfo = $this->adminInfo ?? null;
$filterDeptId = $request->input('dept_id');
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisStart, $thisEnd]);
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastStart, $lastEnd]);
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo, $filterDeptId);
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo, $filterDeptId);
$playerThis = $playerQueryThis->count();
@@ -42,35 +39,35 @@ class DiceDashboardController extends BaseController
$chargeQueryThis = DicePlayerWalletRecord::where('type', 0)
->where('coin', '>', 0)
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
->whereBetween('create_time', [$thisStart, $thisEnd]);
$chargeQueryLast = DicePlayerWalletRecord::where('type', 0)
->where('coin', '>', 0)
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
->whereBetween('create_time', [$lastStart, $lastEnd]);
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo, $filterDeptId);
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo, $filterDeptId);
$chargeThis = $chargeQueryThis->sum('coin');
$chargeLast = $chargeQueryLast->sum('coin');
$withdrawQueryThis = DicePlayerWalletRecord::where('type', 1)
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
->whereBetween('create_time', [$thisStart, $thisEnd]);
$withdrawQueryLast = DicePlayerWalletRecord::where('type', 1)
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
->whereBetween('create_time', [$lastStart, $lastEnd]);
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo, $filterDeptId);
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo, $filterDeptId);
$withdrawThis = $withdrawQueryThis->sum(Db::raw('ABS(coin)'));
$withdrawLast = $withdrawQueryLast->sum(Db::raw('ABS(coin)'));
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisStart, $thisEnd]);
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastStart, $lastEnd]);
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo, $filterDeptId);
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo, $filterDeptId);
$playThis = $playQueryThis->count();
$playLast = $playQueryLast->count();
$playerChange = $this->calcWeekChange($playerThis, $playerLast);
$chargeChange = $this->calcWeekChange((float) $chargeThis, (float) $chargeLast);
$withdrawChange = $this->calcWeekChange((float) $withdrawThis, (float) $withdrawLast);
$playChange = $this->calcWeekChange($playThis, $playLast);
$playerChange = $this->calcPeriodChange($playerThis, $playerLast);
$chargeChange = $this->calcPeriodChange((float) $chargeThis, (float) $chargeLast);
$withdrawChange = $this->calcPeriodChange((float) $withdrawThis, (float) $withdrawLast);
$playChange = $this->calcPeriodChange($playThis, $playLast);
return $this->success([
'player_count' => $playerThis,
@@ -81,6 +78,7 @@ class DiceDashboardController extends BaseController
'withdraw_amount_change' => $withdrawChange,
'play_count' => $playThis,
'play_count_change' => $playChange,
'date' => $this->resolveRequestDate($request),
]);
}
@@ -179,8 +177,9 @@ class DiceDashboardController extends BaseController
$q->field('id,username');
},
])
->where('type', 0)
->order('create_time', 'desc')
->where('type', 0);
$this->applyDashboardDateFilter($query, $request);
$query->order('create_time', 'desc')
->limit(50);
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
$list = $query->select();
@@ -204,8 +203,9 @@ class DiceDashboardController extends BaseController
public function newPlayerList(Request $request): Response
{
$adminInfo = $this->adminInfo ?? null;
$query = DicePlayer::field('username,coin,total_ticket_count,create_time')
->order('create_time', 'desc')
$query = DicePlayer::field('username,coin,total_ticket_count,create_time');
$this->applyDashboardDateFilter($query, $request);
$query->order('create_time', 'desc')
->limit(50);
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
$list = $query->select();
@@ -235,8 +235,9 @@ class DiceDashboardController extends BaseController
},
])
->where('status', 1)
->field('id,player_id,reward_tier,win_coin,create_time')
->order('create_time', 'desc')
->field('id,player_id,reward_tier,win_coin,create_time');
$this->applyDashboardDateFilter($query, $request);
$query->order('create_time', 'desc')
->limit(50);
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
$list = $query->select();
@@ -275,7 +276,7 @@ class DiceDashboardController extends BaseController
return $labels;
}
private function calcWeekChange($current, $last): float
private function calcPeriodChange($current, $last): float
{
if ($last == 0) {
return $current > 0 ? 100.0 : 0.0;
@@ -283,6 +284,56 @@ class DiceDashboardController extends BaseController
return round((($current - $last) / $last) * 100, 1);
}
/**
* 解析工作台统计区间:有 date 按单日对比昨日,无 date 按本周对比上周
*
* @return array{0: string, 1: string, 2: string, 3: string}
*/
private function resolveDateRanges(Request $request): array
{
$date = $this->resolveRequestDate($request);
if ($date === '') {
return [
date('Y-m-d 00:00:00', strtotime('monday this week')),
date('Y-m-d 23:59:59', strtotime('sunday this week')),
date('Y-m-d 00:00:00', strtotime('monday last week')),
date('Y-m-d 23:59:59', strtotime('sunday last week')),
];
}
$lastDate = date('Y-m-d', strtotime($date . ' -1 day'));
return [
$date . ' 00:00:00',
$date . ' 23:59:59',
$lastDate . ' 00:00:00',
$lastDate . ' 23:59:59',
];
}
private function resolveRequestDate(Request $request): string
{
$date = trim((string) $request->input('date', ''));
if ($date === '' || strtotime($date) === false) {
return '';
}
return date('Y-m-d', strtotime($date));
}
/**
* @param mixed $query
*/
private function applyDashboardDateFilter($query, Request $request, string $column = 'create_time'): void
{
$date = $this->resolveRequestDate($request);
if ($date === '') {
return;
}
$query->whereBetween($column, [$date . ' 00:00:00', $date . ' 23:59:59']);
}
/**
* 钱包流水 SQL 渠道条件;非超管无渠道时返回 __empty__
*/

View File

@@ -187,6 +187,8 @@ class DicePlayerController extends BaseController
['status', ''],
['coin', ''],
['lottery_config_id', ''],
['create_time_min', ''],
['create_time_max', ''],
]);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));

View File

@@ -48,10 +48,18 @@ class DicePlayerWalletRecordController extends BaseController
['create_time_max', ''],
]);
$deptId = $request->input('dept_id');
$totalCoinChange = $this->logic->sumCoinBySearch($where, $this->adminInfo ?? null, $deptId);
$query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $deptId);
$sumQuery = clone $query;
$totalCoinChange = round((float) $sumQuery->sum('coin'), 2);
$inflowQuery = clone $query;
$totalCoinInflow = round((float) $inflowQuery->where('coin', '>', 0)->sum('coin'), 2);
$outflowQuery = clone $query;
$totalCoinOutflow = round((float) $outflowQuery->where('coin', '<', 0)->sum('coin'), 2);
$query->with([
'dicePlayer',
'operator',
@@ -59,6 +67,8 @@ class DicePlayerWalletRecordController extends BaseController
$data = $this->logic->getList($query);
$data['total_coin_change'] = $totalCoinChange;
$data['total_coin_inflow'] = $totalCoinInflow;
$data['total_coin_outflow'] = $totalCoinOutflow;
return $this->success($data);
}

View File

@@ -6,7 +6,6 @@
// +----------------------------------------------------------------------
namespace app\dice\logic\player_wallet_record;
use app\dice\helper\AdminScopeHelper;
use app\dice\basic\DiceBaseLogic;
use plugin\saiadmin\exception\ApiException;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
@@ -28,18 +27,6 @@ class DicePlayerWalletRecordLogic extends DiceBaseLogic
$this->setOrderType('DESC');
}
/**
* 按与列表相同的筛选条件汇总平台币变化(不含 with / 分页 / 排序)
*/
public function sumCoinBySearch(array $where, ?array $adminInfo, $requestDeptId = null): float
{
$query = $this->search($where);
AdminScopeHelper::applyAdminScope($query, $adminInfo, $requestDeptId);
$table = $this->model->getTable();
$sum = $query->sum($table . '.coin');
return round((float) $sum, 2);
}
/**
* 添加数据(补全抽奖次数字段默认值)
*/

View File

@@ -224,6 +224,26 @@ class DicePlayer extends DiceModel
}
}
/**
* 注册时间起始 搜索
*/
public function searchCreateTimeMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('create_time', '>=', $value);
}
}
/**
* 注册时间结束 搜索
*/
public function searchCreateTimeMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('create_time', '<=', $value);
}
}
/**
* 关联彩金池配置
*/