From 5f6e89345722f5f21d00ca94952510f5d411a0dc Mon Sep 17 00:00:00 2001
From: Yvan <8574526@qq,com>
Date: Thu, 5 Nov 2026 12:39:50 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B6=88=E9=99=A4=E4=B8=9A=E5=8A=A1?=
=?UTF-8?q?=E9=87=8D=E5=A4=8Dloading=E5=B9=B6=E5=8F=96=E6=B6=88=E5=B7=B2?=
=?UTF-8?q?=E5=85=B3=E9=97=AD=E6=8A=BD=E5=B1=89=E7=9A=84=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/src/api/activation.js | 4 +-
web/src/api/alert.js | 6 +-
web/src/api/analytics.js | 2 +-
web/src/api/business.js | 8 +-
web/src/api/businessPortrait.js | 6 +-
web/src/api/channel.js | 8 +-
web/src/api/fileUploadAndDownload.js | 3 +-
web/src/api/ledger.js | 2 +-
web/src/api/otherSettings.js | 2 +-
web/src/api/performance.js | 8 +-
web/src/api/ranking.js | 2 +-
web/src/api/serverAnalysis.js | 2 +-
web/src/api/workbench.js | 2 +-
web/src/utils/businessRequest.js | 5 ++
web/src/utils/latestRequest.js | 21 +++++
web/src/utils/latestRequest.test.js | 77 +++++++++++++++++++
web/src/utils/request.js | 8 +-
web/src/view/sw/AlertRules.vue | 11 ++-
web/src/view/sw/ChannelActivationList.vue | 20 +++--
.../view/sw/components/AddChannelDrawer.vue | 20 +++--
.../view/sw/components/AlertHandleDrawer.vue | 23 +++---
.../view/sw/components/BusinessBindings.vue | 11 ++-
.../view/sw/components/BusinessChannels.vue | 11 ++-
.../sw/components/BusinessProfileDrawer.vue | 37 +++++----
.../view/sw/components/ChannelGameDrawer.vue | 23 +++---
.../sw/components/ChannelProfileDrawer.vue | 36 +++++----
web/src/view/sw/components/GoalDrawer.vue | 12 ++-
.../sw/components/PerformanceDetailDrawer.vue | 12 ++-
28 files changed, 270 insertions(+), 112 deletions(-)
create mode 100644 web/src/utils/businessRequest.js
create mode 100644 web/src/utils/latestRequest.js
create mode 100644 web/src/utils/latestRequest.test.js
diff --git a/web/src/api/activation.js b/web/src/api/activation.js
index 37b0c71..fb40a5a 100644
--- a/web/src/api/activation.js
+++ b/web/src/api/activation.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listChannelActivations = (params) => service({ url: '/sw/promoteActivePool/bindList', method: 'get', params })
-export const getActivationSpend = (id) => service({ url: '/sw/promoteActivePool/bindSpendDetail', method: 'get', params: { id } })
+export const getActivationSpend = (id, options = {}) => service({ ...options, url: '/sw/promoteActivePool/bindSpendDetail', method: 'get', params: { id } })
diff --git a/web/src/api/alert.js b/web/src/api/alert.js
index 30a0486..ac4c64d 100644
--- a/web/src/api/alert.js
+++ b/web/src/api/alert.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listAlertRules = (params) => service({ url: '/sw/alertRule/list', method: 'get', params })
export const getAlertRuleTypes = () => service({ url: '/sw/alertRule/ruleTypes', method: 'get' })
@@ -7,7 +7,7 @@ export const updateAlertRule = (data) => service({ url: '/sw/alertRule/edit', me
export const deleteAlertRule = (id) => service({ url: `/sw/alertRule/${id}`, method: 'delete' })
export const toggleAlertRule = (id, enabled) => service({ url: `/sw/alertRule/toggle/${id}`, method: 'put', params: { enabled } })
-export const listAlertRecords = (params) => service({ url: '/sw/alertRecord/list', method: 'get', params })
-export const getAlertRecord = (id) => service({ url: `/sw/alertRecord/detail/${id}`, method: 'get' })
+export const listAlertRecords = (params, options = {}) => service({ ...options, url: '/sw/alertRecord/list', method: 'get', params })
+export const getAlertRecord = (id, options = {}) => service({ ...options, url: `/sw/alertRecord/detail/${id}`, method: 'get' })
export const processAlertRecord = (action, data) => service({ url: `/sw/alertRecord/${action}`, method: 'post', data })
export const addAlertNote = (data) => service({ url: '/sw/alertRecord/addNote', method: 'post', data })
diff --git a/web/src/api/analytics.js b/web/src/api/analytics.js
index 14a7a31..ca49dce 100644
--- a/web/src/api/analytics.js
+++ b/web/src/api/analytics.js
@@ -1,3 +1,3 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listAnalytics = (kind, params) => service({ url: `/sw/${kind === 'channel' ? 'channelData' : 'gameData'}/list`, method: 'get', params })
export const exportChannelAnalytics = params => service({ url: '/sw/channelData/list', method: 'get', params: { ...params, export: true }, responseType: 'blob' })
diff --git a/web/src/api/business.js b/web/src/api/business.js
index 7619318..82fc74e 100644
--- a/web/src/api/business.js
+++ b/web/src/api/business.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const getUserPreference = () => service({ url: '/sw/user/preference', method: 'get' })
export const updateUserPreference = (data) => service({ url: '/sw/user/preference', method: 'put', data })
@@ -8,8 +8,8 @@ export const getBusiness = (id) => service({ url: `/sw/business/${id}`, method:
export const createBusiness = (data) => service({ url: '/sw/business', method: 'post', data })
export const updateBusiness = (data) => service({ url: '/sw/business', method: 'put', data })
-export const listBusinessBindings = (params) => service({ url: '/sw/business/bindPlatform', method: 'get', params })
-export const listUserBindings = (id) => service({ url: `/sw/business/${id}/bindPlatform`, method: 'get' })
+export const listBusinessBindings = (params, options = {}) => service({ ...options, url: '/sw/business/bindPlatform', method: 'get', params })
+export const listUserBindings = (id, options = {}) => service({ ...options, url: `/sw/business/${id}/bindPlatform`, method: 'get' })
export const bindPlatform = (data) => service({ url: '/sw/business/bindPlatform', method: 'post', data })
export const unbindPlatform = (data) => service({ url: '/sw/business/bindPlatform', method: 'delete', data })
-export const listBusinessChannels = (id, params) => service({ url: `/sw/business/${id}/promotes`, method: 'get', params })
+export const listBusinessChannels = (id, params, options = {}) => service({ ...options, url: `/sw/business/${id}/promotes`, method: 'get', params })
diff --git a/web/src/api/businessPortrait.js b/web/src/api/businessPortrait.js
index 87ecd53..5fb64d3 100644
--- a/web/src/api/businessPortrait.js
+++ b/web/src/api/businessPortrait.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
-export const getBusinessPortrait = (userId, params) => service({ url: `/sw/business/${userId}/portrait`, method: 'get', params })
-export const getBindingPortrait = params => service({ url: '/sw/business/bindPlatform/portrait', method: 'get', params })
+export const getBusinessPortrait = (userId, params, options = {}) => service({ ...options, url: `/sw/business/${userId}/portrait`, method: 'get', params })
+export const getBindingPortrait = (params, options = {}) => service({ ...options, url: '/sw/business/bindPlatform/portrait', method: 'get', params })
diff --git a/web/src/api/channel.js b/web/src/api/channel.js
index e7cdb60..7c07021 100644
--- a/web/src/api/channel.js
+++ b/web/src/api/channel.js
@@ -1,7 +1,7 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listChannels = params => service({ url: '/sw/promote/list', method: 'get', params })
-export const channelBindOptions = sourceDb => service({ url: '/sw/promote/bindOptions', method: 'get', params: { sourceDb } })
+export const channelBindOptions = (sourceDb, options = {}) => service({ ...options, url: '/sw/promote/bindOptions', method: 'get', params: { sourceDb } })
export const createChannel = data => service({ url: '/sw/promote', method: 'post', data })
-export const channelGames = params => service({ url: '/sw/promote/game-distribution', method: 'get', params })
-export const channelPortrait = params => service({ url: '/sw/promote/portrait', method: 'get', params })
+export const channelGames = (params, options = {}) => service({ ...options, url: '/sw/promote/game-distribution', method: 'get', params })
+export const channelPortrait = (params, options = {}) => service({ ...options, url: '/sw/promote/portrait', method: 'get', params })
diff --git a/web/src/api/fileUploadAndDownload.js b/web/src/api/fileUploadAndDownload.js
index 0f260b6..f079b2c 100644
--- a/web/src/api/fileUploadAndDownload.js
+++ b/web/src/api/fileUploadAndDownload.js
@@ -58,8 +58,9 @@ export const importURL = (data) => {
// 上传文件 暂时用于头像上传
-export const uploadFile = (data) => {
+export const uploadFile = (data, options = {}) => {
return service({
+ ...options,
url: "/fileUploadAndDownload/upload",
method: "post",
data,
diff --git a/web/src/api/ledger.js b/web/src/api/ledger.js
index 8394b9d..b06908d 100644
--- a/web/src/api/ledger.js
+++ b/web/src/api/ledger.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listLedger = (kind, params) => service({ url: `/sw/${kind}/list`, method: 'get', params })
export const exportLedger = (kind, data) => {
diff --git a/web/src/api/otherSettings.js b/web/src/api/otherSettings.js
index c758159..c7bf92c 100644
--- a/web/src/api/otherSettings.js
+++ b/web/src/api/otherSettings.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const getOtherSettings = () => service({ url: '/sw/settings/effectiveness', method: 'get', donNotShowLoading: true })
export const saveOtherSettings = data => service({ url: '/sw/settings/effectiveness', method: 'put', data, donNotShowLoading: true })
diff --git a/web/src/api/performance.js b/web/src/api/performance.js
index 4d96314..9e40af6 100644
--- a/web/src/api/performance.js
+++ b/web/src/api/performance.js
@@ -1,12 +1,12 @@
-import service from '@/utils/request'
-const get = (url, params) => service({ url, params, method: 'get', donNotShowLoading: true })
+import service from '@/utils/businessRequest'
+const get = (url, params, options = {}) => service({ ...options, url, params, method: 'get', donNotShowLoading: true })
export const getTeams = () => get('/sw/groups')
export const saveTeams = data => service({ url: '/sw/groups', method: 'put', data })
export const updateMember = data => service({ url: '/sw/business/profile', method: 'put', data })
export const getManagement = params => get('/sw/business/management', params)
export const getPerformance = params => get('/sw/performance', params)
-export const getPerformanceDetails = params => get('/sw/performance/details', params)
-export const getGoals = params => get('/sw/goals', params)
+export const getPerformanceDetails = (params, options) => get('/sw/performance/details', params, options)
+export const getGoals = (params, options) => get('/sw/goals', params, options)
export const saveGoal = data => service({ url: '/sw/goals', method: 'put', data })
export const getMyGoal = () => get('/sw/goals/mine')
export const getDashboardOverview = params => get('/sw/workbench/overview', params)
diff --git a/web/src/api/ranking.js b/web/src/api/ranking.js
index c60d1f8..fb674fb 100644
--- a/web/src/api/ranking.js
+++ b/web/src/api/ranking.js
@@ -1,3 +1,3 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const listRankings = (dimension, params) => service({ url: `/sw/rank/${dimension}`, method: 'get', params, timeout: 30000, donNotShowLoading: true })
diff --git a/web/src/api/serverAnalysis.js b/web/src/api/serverAnalysis.js
index bf30b08..c63036e 100644
--- a/web/src/api/serverAnalysis.js
+++ b/web/src/api/serverAnalysis.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
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 })
diff --git a/web/src/api/workbench.js b/web/src/api/workbench.js
index 1861a17..ae70e0c 100644
--- a/web/src/api/workbench.js
+++ b/web/src/api/workbench.js
@@ -1,4 +1,4 @@
-import service from '@/utils/request'
+import service from '@/utils/businessRequest'
export const getWorkbenchSummary = params => service({ url: '/sw/workbench/summary', method: 'get', params, donNotShowLoading: true })
export const getWorkbenchRanks = (endpoint, params) => service({ url: `/sw/workbench/${endpoint}`, method: 'get', params, donNotShowLoading: true })
diff --git a/web/src/utils/businessRequest.js b/web/src/utils/businessRequest.js
new file mode 100644
index 0000000..f33ca66
--- /dev/null
+++ b/web/src/utils/businessRequest.js
@@ -0,0 +1,5 @@
+import service from './request'
+
+// Business views own their table, drawer and submit-button loading states.
+// Keep the framework request default for screens that rely on its page mask.
+export default config => service({ donNotShowLoading: true, ...config })
diff --git a/web/src/utils/latestRequest.js b/web/src/utils/latestRequest.js
new file mode 100644
index 0000000..691b173
--- /dev/null
+++ b/web/src/utils/latestRequest.js
@@ -0,0 +1,21 @@
+// A query owns one AbortController. Replacing or closing it invalidates both
+// its network request and late callbacks from adapters that ignore cancellation.
+export const createLatestRequest = () => {
+ let current
+ return {
+ start() {
+ current?.abort()
+ const controller = new AbortController()
+ current = controller
+ return {
+ signal: controller.signal,
+ isCurrent: () => current === controller && !controller.signal.aborted
+ }
+ },
+ cancel() {
+ const previous = current
+ current = undefined
+ previous?.abort()
+ }
+ }
+}
diff --git a/web/src/utils/latestRequest.test.js b/web/src/utils/latestRequest.test.js
new file mode 100644
index 0000000..9a7143b
--- /dev/null
+++ b/web/src/utils/latestRequest.test.js
@@ -0,0 +1,77 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import http from 'node:http'
+import axios from 'axios'
+import { createLatestRequest } from './latestRequest.js'
+
+test('replacing and closing a query aborts it and invalidates late callbacks', () => {
+ const scope = createLatestRequest()
+ const first = scope.start()
+ let aborted = 0
+ first.signal.addEventListener('abort', () => aborted++)
+ const second = scope.start()
+ assert.equal(aborted, 1)
+ assert.equal(first.isCurrent(), false)
+ assert.equal(second.isCurrent(), true)
+ scope.cancel()
+ scope.cancel()
+ assert.equal(second.signal.aborted, true)
+ assert.equal(second.isCurrent(), false)
+ const reopened = scope.start()
+ assert.equal(reopened.signal.aborted, false)
+ assert.equal(reopened.isCurrent(), true)
+})
+
+test('a late response or finally callback cannot overwrite the newly opened query', async () => {
+ const scope = createLatestRequest()
+ let resolveOld, resolveNew
+ const oldResponse = new Promise(resolve => { resolveOld = resolve })
+ const newResponse = new Promise(resolve => { resolveNew = resolve })
+ let data = null, loading = false
+ const load = async response => {
+ const request = scope.start()
+ loading = true
+ try {
+ const result = await response
+ if (request.isCurrent()) data = result
+ } finally { if (request.isCurrent()) loading = false }
+ }
+ const old = load(oldResponse)
+ scope.cancel()
+ const current = load(newResponse)
+ resolveOld('old channel')
+ await old
+ assert.equal(data, null)
+ assert.equal(loading, true)
+ resolveNew('new channel')
+ await current
+ assert.equal(data, 'new channel')
+ assert.equal(loading, false)
+})
+
+test('axios cancellation releases the slow HTTP request without cancelling another query', async t => {
+ const pending = new Map()
+ let ready
+ const bothConnected = new Promise(resolve => { ready = resolve })
+ const server = http.createServer((req, res) => {
+ pending.set(req.url, res)
+ if (pending.size === 2) ready()
+ })
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
+ t.after(() => { server.closeAllConnections(); server.close() })
+ const firstScope = createLatestRequest(), secondScope = createLatestRequest()
+ const first = firstScope.start(), second = secondScope.start()
+ const base = `http://127.0.0.1:${server.address().port}`
+ const cancelled = axios.get(`${base}/slow`, { signal: first.signal })
+ const cancellation = assert.rejects(cancelled, error => axios.isCancel(error) && !error.response)
+ const completed = axios.get(`${base}/other`, { signal: second.signal })
+ await bothConnected
+ const disconnected = new Promise(resolve => pending.get('/slow').on('close', resolve))
+ firstScope.cancel()
+ await cancellation
+ await disconnected
+ assert.equal(second.isCurrent(), true)
+ assert.equal(second.signal.aborted, false)
+ pending.get('/other').end('other query finished')
+ assert.equal((await completed).data, 'other query finished')
+})
diff --git a/web/src/utils/request.js b/web/src/utils/request.js
index cc0a625..ece979e 100644
--- a/web/src/utils/request.js
+++ b/web/src/utils/request.js
@@ -144,12 +144,14 @@ service.interceptors.request.use(
closeLoading(error.config?.loadingOption)
}
+ if (axios.isCancel(error)) return Promise.reject(error)
+
emitter.emit('show-error', {
code: 'request',
message: error.message || '请求发送失败'
})
- return error
+ return Promise.reject(error)
}
)
@@ -219,6 +221,10 @@ service.interceptors.response.use(
closeLoading(error.config?.loadingOption)
}
+ // Closing a query is intentional. It must neither open a network error
+ // dialog nor reset loading owned by another request that is still running.
+ if (axios.isCancel(error)) return Promise.reject(error)
+
if (!error.response) {
resetLoading()
emitter.emit('show-error', {
diff --git a/web/src/view/sw/AlertRules.vue b/web/src/view/sw/AlertRules.vue
index dbb0632..7244a3a 100644
--- a/web/src/view/sw/AlertRules.vue
+++ b/web/src/view/sw/AlertRules.vue
@@ -17,7 +17,7 @@