fix: 消除游戏下拉重复请求并优化申请范围查询
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<el-form-item v-if="spend" label="角色名"><el-input v-model="query.gamePlayerName" clearable placeholder="角色名" /></el-form-item>
|
||||
<el-form-item label="渠道名称"><el-input v-model="query.promoteAccount" clearable placeholder="渠道名称或账号(模糊匹配)" /></el-form-item>
|
||||
<el-form-item v-if="spend" label="订单号"><el-input v-model="query.payOrderNumber" clearable placeholder="订单号" /></el-form-item>
|
||||
<el-form-item label="游戏"><el-select v-model="gameNames" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gamesLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 200px" @visible-change="open => open && loadGames('')"><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="gameNames" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gamesLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 200px"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select></el-form-item>
|
||||
<el-form-item v-if="spend" label="方式"><el-select v-model="query.payWay" clearable placeholder="全部方式" style="width: 140px"><el-option v-for="(label, value) in payWays" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
|
||||
<el-form-item v-if="spend" label="状态"><el-select v-model="query.payStatus" clearable placeholder="全部状态" style="width: 140px"><el-option v-for="(label, value) in statuses" :key="value" :label="label" :value="Number(value)" /></el-select></el-form-item>
|
||||
<el-form-item label="日期"><el-date-picker v-model="dates" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" :clearable="false" :shortcuts="shortcuts" style="width: 300px" /></el-form-item>
|
||||
@@ -50,7 +50,9 @@ import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useBtnAuth } from '@/utils/btnAuth'
|
||||
import { listLedger, exportLedger, listLedgerGames } from '@/api/ledger'
|
||||
import { channelMoney } from '../channel-display'
|
||||
import { createGameOptionsLoader } from '../game-options-loader'
|
||||
import { compareAmounts } from '../table-display'
|
||||
const fetchGameOptions = createGameOptionsLoader(listLedgerGames)
|
||||
const props = defineProps({ kind: { type: String, required: true } })
|
||||
const spend = computed(() => props.kind === 'spend')
|
||||
const user = useUserStore(), buttons = useBtnAuth()
|
||||
@@ -100,7 +102,7 @@ const loadGames = async (keyword = '') => {
|
||||
const version = ++gameVersion; if (!sources.value.length) return
|
||||
gamesLoading.value = true
|
||||
try {
|
||||
const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: 50 })
|
||||
const res = await fetchGameOptions({ sourceDb: sources.value.join(','), keyword, limit: 50 })
|
||||
if (version !== gameVersion) return
|
||||
if (res.code === 0) gameOptions.value = [...new Set([...gameNames.value, ...res.data.map(row => row.gameName).filter(Boolean)])]
|
||||
else { gameOptions.value = []; ElMessage.error(res.msg || '游戏选项读取失败') }
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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-if="isChannel" v-model="games" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @visible-change="open => open && loadGames('')"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select>
|
||||
<el-select v-if="isChannel" v-model="games" multiple collapse-tags clearable filterable remote reserve-keyword :remote-method="loadGames" :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px"><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select>
|
||||
<el-select v-else v-model="selectedGame" clearable filterable :loading="gameLoading" :disabled="!sources.length" placeholder="全部游戏" style="width: 190px" @change="gameChanged"><el-option label="全部游戏" value="" /><el-option v-for="name in gameOptions" :key="name" :label="name" :value="name" /></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isChannel" label="渠道类型"><el-select v-model="query.promoteType" clearable placeholder="全部渠道类型" style="width: 150px"><el-option label="公会" :value="1" /><el-option label="个人" :value="0" /></el-select></el-form-item>
|
||||
@@ -39,8 +39,10 @@ import { ElMessage } from 'element-plus'
|
||||
import { listAnalytics } from '@/api/analytics'
|
||||
import { listLedgerGames } from '@/api/ledger'
|
||||
import { channelCount, channelMoney } from '../channel-display'
|
||||
import { createGameOptionsLoader } from '../game-options-loader'
|
||||
import { metricTips } from '../table-display'
|
||||
import { analysisGameSelection } from '../analysis-game-selection'
|
||||
const fetchGameOptions = createGameOptionsLoader(listLedgerGames)
|
||||
const props = defineProps({ kind: { type: String, required: true } })
|
||||
const isChannel = computed(() => props.kind === 'channel')
|
||||
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
|
||||
@@ -87,7 +89,7 @@ const loadGames = async (keyword = '') => {
|
||||
const current = ++gameVersion; if (!sources.value.length) return
|
||||
gameLoading.value = true
|
||||
try {
|
||||
const res = await listLedgerGames({ sourceDb: sources.value.join(','), keyword, limit: isChannel.value ? 50 : 10000 })
|
||||
const res = await fetchGameOptions({ sourceDb: sources.value.join(','), keyword, limit: isChannel.value ? 50 : 10000 })
|
||||
if (current !== gameVersion) return
|
||||
if (res.code === 0) gameOptions.value = [...new Set(res.data.map(row => row.gameName).filter(Boolean))]
|
||||
else { gameOptions.value = []; ElMessage.error(res.msg || '游戏选项读取失败') }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// One loader per page: share identical requests and briefly reuse successful
|
||||
// options when reopening the selector. Errors must remain retryable.
|
||||
export const createGameOptionsLoader = (fetchOptions, now = Date.now) => {
|
||||
const pending = new Map(), cache = new Map()
|
||||
return params => {
|
||||
const key = JSON.stringify([params.sourceDb, params.keyword, params.limit])
|
||||
const saved = cache.get(key)
|
||||
if (saved && now() - saved.time < 30000) return Promise.resolve(saved.result)
|
||||
if (pending.has(key)) return pending.get(key)
|
||||
const request = Promise.resolve().then(() => fetchOptions(params)).then(result => {
|
||||
if (result.code === 0) {
|
||||
cache.delete(key)
|
||||
cache.set(key, { result, time: now() })
|
||||
if (cache.size > 20) cache.delete(cache.keys().next().value)
|
||||
}
|
||||
return result
|
||||
}).finally(() => pending.delete(key))
|
||||
pending.set(key, request)
|
||||
return request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createGameOptionsLoader } from './game-options-loader.js'
|
||||
|
||||
const params = { sourceDb: 'lx,hw,yq', keyword: '', limit: 50 }
|
||||
|
||||
test('concurrent opening events share one request and reopening reuses the result briefly', async () => {
|
||||
let finish, calls = 0, time = 0
|
||||
const load = createGameOptionsLoader(() => { calls++; return new Promise(resolve => { finish = resolve }) }, () => time)
|
||||
const requests = [load(params), load(params), load(params)]
|
||||
await Promise.resolve()
|
||||
assert.equal(calls, 1)
|
||||
const result = { code: 0, data: [{ gameName: '游戏甲' }] }
|
||||
finish(result)
|
||||
assert.deepEqual(await Promise.all(requests), [result, result, result])
|
||||
assert.equal(await load(params), result)
|
||||
assert.equal(calls, 1)
|
||||
time = 30001
|
||||
const refreshed = load(params)
|
||||
await Promise.resolve()
|
||||
assert.equal(calls, 2)
|
||||
finish(result)
|
||||
await refreshed
|
||||
})
|
||||
|
||||
test('platform, search text and limit use separate results', async () => {
|
||||
let calls = 0
|
||||
const load = createGameOptionsLoader(async query => { calls++; return { code: 0, data: [query] } })
|
||||
await Promise.all([load(params), load({ ...params, sourceDb: 'lx' }), load({ ...params, keyword: '传奇' }), load({ ...params, limit: 10000 })])
|
||||
assert.equal(calls, 4)
|
||||
assert.deepEqual((await load(params)).data, [params])
|
||||
assert.equal(calls, 4)
|
||||
})
|
||||
|
||||
test('network and API failures can be retried immediately', async () => {
|
||||
let calls = 0
|
||||
const load = createGameOptionsLoader(async () => {
|
||||
calls++
|
||||
if (calls === 1) throw new Error('network unavailable')
|
||||
return { code: calls === 2 ? 7 : 0, data: [] }
|
||||
})
|
||||
await assert.rejects(load(params), /network unavailable/)
|
||||
assert.equal((await load(params)).code, 7)
|
||||
assert.equal((await load(params)).code, 0)
|
||||
assert.equal(calls, 3)
|
||||
})
|
||||
Reference in New Issue
Block a user