feat: 迁移区服分析与游戏区服查询

This commit is contained in:
Yvan
2026-11-04 01:16:23 +08:00
parent fc27120950
commit 0bef29ae4f
27 changed files with 1113 additions and 6 deletions
+4
View File
@@ -0,0 +1,4 @@
import service from '@/utils/request'
export const listServerAnalysis = params => service({ url: '/sw/serverAnalysis/list', method: 'get', params })
export const listServerOptions = params => service({ url: '/sw/serverAnalysis/servers', method: 'get', params, donNotShowLoading: true })
+1
View File
@@ -61,6 +61,7 @@
"/src/view/sw/GameAnalytics.vue": "SwGameAnalytics",
"/src/view/sw/RechargeDetails.vue": "SwRechargeDetails",
"/src/view/sw/RegistrationDetails.vue": "SwRegistrationDetails",
"/src/view/sw/ServerAnalysis.vue": "ServerAnalysis",
"/src/view/sw/components/BusinessBindings.vue": "SwBusinessBindings",
"/src/view/sw/components/BusinessChannels.vue": "SwBusinessChannels",
"/src/view/system/security/forceChangePassword.vue": "ForceChangePassword",
+120
View File
@@ -0,0 +1,120 @@
<template>
<div>
<div class="kra-search-box">
<el-form :model="query" :inline="true" @submit.prevent="search">
<el-form-item label="所属平台"><el-select v-model="sources" multiple collapse-tags clearable placeholder="选择平台" style="width: 210px" @change="sourceChanged"><el-option v-for="(label, value) in platforms" :key="value" :label="label" :value="value" /></el-select></el-form-item>
<el-form-item label="游戏"><el-select v-model="games" multiple collapse-tags clearable filterable :reserve-keyword="false" :loading="gameLoading" :disabled="!sources.length" placeholder="请选择游戏" style="width: 200px" @change="gameChanged"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
<el-form-item label="区服"><el-select v-model="query.serverNum" clearable filterable :loading="serverLoading" :disabled="!sources.length || !games.length" placeholder="全部区服" style="width: 200px"><el-option label="全部区服" value="" /><el-option v-for="row in serverOptions" :key="row.serverNum" :label="row.serverName ? `${row.serverName} (${row.serverNum})` : row.serverNum" :value="row.serverNum" /></el-select></el-form-item>
<el-form-item label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
<el-form-item><el-button type="primary" icon="search" native-type="submit">搜索</el-button></el-form-item>
</el-form>
</div>
<div class="kra-table-box">
<div class="server-heading"><strong>区服数据</strong><el-text type="info">统计周期:{{ period }}</el-text></div>
<el-alert v-if="error || optionError" class="mb-4" type="error" :closable="false" :title="error || optionError" />
<el-alert v-else-if="!available" class="mb-4" type="warning" :closable="false" title="区服统计所需业务汇总尚未接入。" />
<el-table v-loading="loading" :data="rows" :row-key="row => `${row.sourceDb}:${row.gameId}:${row.serverNum}`" :default-sort="{ prop: 'totalPay', order: 'descending' }" show-summary :summary-method="summaries" @sort-change="sortChanged">
<el-table-column prop="sourceDbLabel" label="所属平台" min-width="100" />
<el-table-column prop="serverNum" label="区服编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="serverName" label="区服名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="openTime" label="开服日期" min-width="120" sortable="custom"><template #default="{ row }">{{ openDate(row.openTime) }}</template></el-table-column>
<el-table-column v-for="column in columns" :key="column.key" :prop="column.key" :label="column.label" :min-width="column.money ? 140 : 110" sortable="custom"><template #default="{ row }">{{ format(column, row[column.key]) }}</template></el-table-column>
</el-table>
<div class="kra-pagination"><el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { listLedgerGames } from '@/api/ledger'
import { listServerAnalysis, listServerOptions } from '@/api/serverAnalysis'
import { channelCount, channelMoney } from './channel-display'
defineOptions({ name: 'ServerAnalysis' })
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
const day = () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()) }
const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`
const end = day(), begin = day(); begin.setDate(begin.getDate() - 29)
const dates = ref([ymd(begin), ymd(end)])
const shortcuts = [
{ text: '今日', value: () => [day(), day()] },
{ text: '昨日', value: () => { const d = day(); d.setDate(d.getDate() - 1); return [d, new Date(d)] } },
{ text: '本周', value: () => { const e = day(), b = day(); b.setDate(b.getDate() - (b.getDay() || 7) + 1); return [b, e] } },
{ text: '本月', value: () => { const e = day(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } }
]
const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), serverOptions = ref([])
const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: 'totalPay', sortOrder: 'desc' })
const rows = ref([]), total = ref(0), summary = ref(null), result = ref(null), loading = ref(false), available = ref(true), error = ref(''), optionError = ref('')
const gameLoading = ref(false), serverLoading = ref(false)
let version = 0, gameVersion = 0, serverVersion = 0
const period = computed(() => result.value ? `${result.value.beginTime} 至 ${result.value.endTime}` : dates.value.join(' 至 '))
const columns = [{ key: 'newRegisterCount', label: '新增注册' }, { key: 'activeCount', label: '活跃玩家' }, { key: 'totalPay', label: '充值金额', money: true }, { key: 'payUserCount', label: '付费玩家' }, { key: 'arpu', label: 'ARPU', money: true }]
const format = (column, value) => value == null ? (error.value ? '未获取' : '—') : column.money ? channelMoney(value) : channelCount(value)
const openDate = value => {
if (value == null || value === '') return '—'
const n = Number(value), date = new Date(n > 1e12 ? n : n * 1000)
return Number.isNaN(date.getTime()) ? '—' : ymd(date)
}
const summaries = ({ columns: tableColumns }) => tableColumns.map((column, index) => {
if (index === 0) return '合计'
const field = columns.find(item => item.key === column.property)
return field ? format(field, summary.value?.[column.property]) : '—'
})
const clear = () => { ++version; rows.value = []; total.value = 0; summary.value = null; result.value = null; loading.value = false; error.value = ''; available.value = true }
const params = () => ({ ...query, sourceDb: sources.value.join(','), gameName: games.value.join(','), beginTime: dates.value[0], endTime: dates.value[1] })
const load = async () => {
if (!sources.value.length || !games.value.length) { clear(); return }
const current = ++version; loading.value = true; error.value = ''
try {
const res = await listServerAnalysis(params())
if (current !== version) return
if (res.code !== 0) { clear(); error.value = res.msg || '区服查询失败'; return }
result.value = res.data; rows.value = res.data.rows; total.value = res.data.total; summary.value = res.data.summary; available.value = res.data.available !== false
} catch { if (current === version) { clear(); error.value = '区服查询失败,请重试' } } finally { if (current === version) loading.value = false }
}
const search = () => {
query.pageNum = 1
if (!sources.value.length) ElMessage.warning('请选择平台')
else if (!games.value.length) ElMessage.warning('请选择游戏')
return load()
}
const sortChanged = ({ prop, order }) => { query.pageNum = 1; query.sortField = order ? prop : 'totalPay'; query.sortOrder = order === 'ascending' ? 'asc' : 'desc'; return load() }
const loadGames = async () => {
const current = ++gameVersion; gameLoading.value = true; optionError.value = ''
try {
const res = await listLedgerGames({ sourceDb: sources.value.join(','), limit: 10000 })
if (current !== gameVersion) return
if (res.code === 0) gameOptions.value = [...new Set(res.data.map(row => row.gameName).filter(Boolean))]
else optionError.value = res.msg || '游戏选项读取失败'
} catch { if (current === gameVersion) optionError.value = '游戏选项读取失败,请重试' } finally { if (current === gameVersion) gameLoading.value = false }
}
const loadServers = async () => {
const current = ++serverVersion; serverOptions.value = []
if (!games.value.length || !sources.value.length) { serverLoading.value = false; return }
serverLoading.value = true; optionError.value = ''
try {
const res = await listServerOptions({ sourceDb: sources.value.join(','), gameName: games.value.join(','), limit: 10000 })
if (current !== serverVersion) return
if (res.code === 0) { const seen = new Set(); serverOptions.value = res.data.filter(row => { if (!row.serverNum || seen.has(row.serverNum)) return false; seen.add(row.serverNum); return true }) }
else optionError.value = res.msg || '区服选项读取失败'
} catch { if (current === serverVersion) optionError.value = '区服选项读取失败,请重试' } finally { if (current === serverVersion) serverLoading.value = false }
}
const sourceChanged = async () => {
++gameVersion; ++serverVersion; gameLoading.value = false; serverLoading.value = false
games.value = []; gameOptions.value = []; query.serverNum = ''; serverOptions.value = []; optionError.value = ''; clear()
if (sources.value.length) await loadGames()
}
const gameChanged = () => { query.serverNum = ''; query.pageNum = 1; clear(); return loadServers() }
onMounted(async () => {
await loadGames()
if (!gameOptions.value.length) return
games.value = [gameOptions.value[0]]
await Promise.all([loadServers(), load()])
})
</script>
<style scoped>
.server-heading { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
</style>