vue2-client 1.18.38 → 1.18.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs//350/257/267/346/261/202/345/267/245/345/205/267/344/275/277/347/224/250/350/257/264/346/230/216.md +353 -0
- package/package.json +1 -1
- package/src/base-client/components/common/XDatePicker/demo.vue +31 -0
- package/src/base-client/components/common/XDatePicker/index.vue +61 -0
- package/src/base-client/components/common/XForm/XForm.vue +5 -3
- package/src/base-client/components/common/XForm/XFormItem.vue +1 -1
- package/src/base-client/components/common/XFormTable/demo.vue +123 -125
- package/src/base-client/components/common/XReportGrid/XReportTrGroup.vue +1 -1
- package/src/base-client/components/common/XTable/XTableWrapper.vue +18 -1
- package/src/composables/demo/UseRequestDemo.vue +175 -0
- package/src/composables/index.js +6 -0
- package/src/composables/useGlobalLoading.js +206 -0
- package/src/composables/usePost.js +221 -0
- package/src/services/api/restTools.js +34 -46
- package/src/utils/request.js +103 -0
- package/vue.config.js +5 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import Vue from 'vue'
|
|
2
|
+
import { post } from '@vue2-client/services/api/restTools'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} RequestResult
|
|
6
|
+
* @property {boolean} success - 请求是否成功
|
|
7
|
+
* @property {any} data - 响应数据
|
|
8
|
+
* @property {Error|null} error - 错误信息
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} UsePostReturn
|
|
13
|
+
* @property {boolean} loading - 加载状态
|
|
14
|
+
* @property {Error|null} error - 错误信息
|
|
15
|
+
* @property {any} data - 响应数据
|
|
16
|
+
* @property {(params?: object, config?: object) => Promise<RequestResult>} execute - 执行请求
|
|
17
|
+
* @property {() => void} reset - 重置状态
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Vue Composition API 风格的 POST 请求 Hook
|
|
22
|
+
* 提供响应式的 loading 状态管理,返回统一的结果结构
|
|
23
|
+
*
|
|
24
|
+
* @param {string} url - 请求地址
|
|
25
|
+
* @param {object} defaultConfig - 默认配置
|
|
26
|
+
* @param {boolean|string} defaultConfig.globalLoading - 是否显示全局 Loading
|
|
27
|
+
* @param {boolean} defaultConfig.dedupe - 是否启用请求去重(POST 默认开启,设为 false 可关闭)
|
|
28
|
+
* @returns {UsePostReturn}
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* // 基础用法
|
|
32
|
+
* const { loading, execute } = usePost('/api/save')
|
|
33
|
+
* const { success, data, error } = await execute(formData)
|
|
34
|
+
* if (success) {
|
|
35
|
+
* message.success('保存成功')
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // 在模板中绑定 loading 状态
|
|
40
|
+
* // <a-button :loading="loading" @click="handleSave">保存</a-button>
|
|
41
|
+
* const { loading, execute } = usePost('/api/save', { globalLoading: '保存中...' })
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // 获取响应式数据
|
|
45
|
+
* const { loading, data, error, execute } = usePost('/api/query')
|
|
46
|
+
* await execute({ id: 1 })
|
|
47
|
+
* // data 会自动更新,可直接在模板中使用
|
|
48
|
+
*/
|
|
49
|
+
export function usePost (url, defaultConfig = {}) {
|
|
50
|
+
// 响应式状态
|
|
51
|
+
const state = Vue.observable({
|
|
52
|
+
loading: false,
|
|
53
|
+
error: null,
|
|
54
|
+
data: null
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 执行请求
|
|
59
|
+
* @param {object} params - 请求参数
|
|
60
|
+
* @param {object} config - 本次请求的配置(覆盖默认配置)
|
|
61
|
+
* @returns {Promise<RequestResult>} 统一的结果结构
|
|
62
|
+
*/
|
|
63
|
+
async function execute (params = {}, config = {}) {
|
|
64
|
+
state.loading = true
|
|
65
|
+
state.error = null
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await post(url, params, { ...defaultConfig, ...config })
|
|
69
|
+
state.data = result
|
|
70
|
+
return { success: true, data: result, error: null }
|
|
71
|
+
} catch (err) {
|
|
72
|
+
state.error = err
|
|
73
|
+
return { success: false, data: null, error: err }
|
|
74
|
+
} finally {
|
|
75
|
+
state.loading = false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 重置状态
|
|
81
|
+
*/
|
|
82
|
+
function reset () {
|
|
83
|
+
state.loading = false
|
|
84
|
+
state.error = null
|
|
85
|
+
state.data = null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 返回响应式属性的 getter,确保在模板中能正确响应
|
|
89
|
+
return {
|
|
90
|
+
get loading () { return state.loading },
|
|
91
|
+
get error () { return state.error },
|
|
92
|
+
get data () { return state.data },
|
|
93
|
+
execute,
|
|
94
|
+
reset
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default usePost
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {Object} UseRunLogicReturn
|
|
102
|
+
* @property {boolean} loading - 加载状态
|
|
103
|
+
* @property {Error|null} error - 错误信息
|
|
104
|
+
* @property {any} data - 响应数据
|
|
105
|
+
* @property {(params?: object, overrideOptions?: object) => Promise<RequestResult>} execute - 执行请求
|
|
106
|
+
* @property {() => void} reset - 重置状态
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Vue Composition API 风格的 runLogic Hook
|
|
111
|
+
* 用于调用后端业务逻辑,提供响应式的 loading 状态管理
|
|
112
|
+
*
|
|
113
|
+
* @param {string} logicName - 业务逻辑名称
|
|
114
|
+
* @param {object} options - 配置选项
|
|
115
|
+
* @param {string} options.serviceName - 服务名称,默认为 VUE_APP_SYSTEM_NAME
|
|
116
|
+
* @param {boolean} options.isDev - 是否为开发环境
|
|
117
|
+
* @param {boolean|string} options.globalLoading - 是否显示全局 Loading
|
|
118
|
+
* @param {boolean} options.dedupe - 是否启用请求去重(POST 默认开启,设为 false 可关闭)
|
|
119
|
+
* @returns {UseRunLogicReturn}
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // 基础用法
|
|
123
|
+
* const { loading, execute, data } = useRunLogic('getUserInfo')
|
|
124
|
+
* const { success, data: result } = await execute({ userId: 1 })
|
|
125
|
+
* if (success) {
|
|
126
|
+
* console.log(result)
|
|
127
|
+
* }
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* // 带全局 Loading
|
|
131
|
+
* const { loading, execute } = useRunLogic('saveOrder', {
|
|
132
|
+
* globalLoading: '保存中...',
|
|
133
|
+
* dedupe: true
|
|
134
|
+
* })
|
|
135
|
+
* // 模板: <a-button :loading="loading" @click="handleSave">保存</a-button>
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // 指定服务名称
|
|
139
|
+
* const { execute } = useRunLogic('queryData', {
|
|
140
|
+
* serviceName: 'af-custom',
|
|
141
|
+
* isDev: false
|
|
142
|
+
* })
|
|
143
|
+
*/
|
|
144
|
+
export function useRunLogic (logicName, options = {}) {
|
|
145
|
+
const {
|
|
146
|
+
serviceName = process.env.VUE_APP_SYSTEM_NAME,
|
|
147
|
+
isDev = false,
|
|
148
|
+
globalLoading = false,
|
|
149
|
+
dedupe = false
|
|
150
|
+
} = options
|
|
151
|
+
|
|
152
|
+
// 响应式状态
|
|
153
|
+
const state = Vue.observable({
|
|
154
|
+
loading: false,
|
|
155
|
+
error: null,
|
|
156
|
+
data: null
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 构建请求 URL
|
|
161
|
+
* @param {string} svcName - 服务名称
|
|
162
|
+
* @param {boolean} dev - 是否开发环境
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
function buildUrl (svcName, dev) {
|
|
166
|
+
const apiPre = dev ? '/devApi/' : '/api/'
|
|
167
|
+
return `${apiPre}${svcName}/logic/${logicName}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 执行业务逻辑
|
|
172
|
+
* @param {object} params - 请求参数
|
|
173
|
+
* @param {object} overrideOptions - 本次请求的配置(覆盖默认配置)
|
|
174
|
+
* @returns {Promise<RequestResult>} 统一的结果结构
|
|
175
|
+
*/
|
|
176
|
+
async function execute (params = {}, overrideOptions = {}) {
|
|
177
|
+
state.loading = true
|
|
178
|
+
state.error = null
|
|
179
|
+
|
|
180
|
+
// 合并配置
|
|
181
|
+
const finalServiceName = overrideOptions.serviceName || serviceName
|
|
182
|
+
const finalIsDev = overrideOptions.isDev !== undefined ? overrideOptions.isDev : isDev
|
|
183
|
+
|
|
184
|
+
// 构建请求 URL
|
|
185
|
+
const url = buildUrl(finalServiceName, finalIsDev)
|
|
186
|
+
|
|
187
|
+
// 构建请求配置
|
|
188
|
+
const requestConfig = {
|
|
189
|
+
globalLoading: overrideOptions.globalLoading !== undefined ? overrideOptions.globalLoading : globalLoading,
|
|
190
|
+
dedupe: overrideOptions.dedupe !== undefined ? overrideOptions.dedupe : dedupe
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await post(url, params, requestConfig)
|
|
195
|
+
state.data = result
|
|
196
|
+
return { success: true, data: result, error: null }
|
|
197
|
+
} catch (err) {
|
|
198
|
+
state.error = err
|
|
199
|
+
return { success: false, data: null, error: err }
|
|
200
|
+
} finally {
|
|
201
|
+
state.loading = false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 重置状态
|
|
207
|
+
*/
|
|
208
|
+
function reset () {
|
|
209
|
+
state.loading = false
|
|
210
|
+
state.error = null
|
|
211
|
+
state.data = null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
get loading () { return state.loading },
|
|
216
|
+
get error () { return state.error },
|
|
217
|
+
get data () { return state.data },
|
|
218
|
+
execute,
|
|
219
|
+
reset
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSystemVersion, METHOD, request } from '@vue2-client/utils/request'
|
|
2
2
|
import { ACCESS_TOKEN, V4_ACCESS_TOKEN } from '@vue2-client/store/mutation-types'
|
|
3
|
+
import { showGlobalLoading, hideGlobalLoading } from '@vue2-client/composables/useGlobalLoading'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GET请求
|
|
@@ -15,21 +16,31 @@ function get (url, parameter) {
|
|
|
15
16
|
* POST请求
|
|
16
17
|
* @param url 请求地址
|
|
17
18
|
* @param parameter 请求参数
|
|
18
|
-
* @param config
|
|
19
|
+
* @param config 配置项
|
|
20
|
+
* @param {boolean|string} config.globalLoading - 是否显示全局 Loading,可传入字符串作为提示文字
|
|
21
|
+
* @param {boolean} config.dedupe - 是否启用请求去重(POST 默认开启,设为 false 可关闭)
|
|
19
22
|
* @returns {Promise<AxiosResponse<T>>}
|
|
20
23
|
*/
|
|
21
24
|
function post (url, parameter, config = {}) {
|
|
22
|
-
|
|
25
|
+
const { globalLoading, ...axiosConfig } = config
|
|
26
|
+
|
|
27
|
+
// 全局 Loading - 请求开始时显示
|
|
28
|
+
if (globalLoading) {
|
|
29
|
+
showGlobalLoading(globalLoading)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 发起请求
|
|
33
|
+
return request(url, METHOD.POST, parameter, axiosConfig)
|
|
34
|
+
.finally(() => {
|
|
35
|
+
// 请求结束时隐藏全局 Loading
|
|
36
|
+
if (globalLoading) {
|
|
37
|
+
hideGlobalLoading()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
|
-
* POST
|
|
27
|
-
* @param url 请求地址
|
|
28
|
-
* @param parameter 请求参数
|
|
29
|
-
* @param serviceName
|
|
30
|
-
* @param isDev
|
|
31
|
-
* @param config
|
|
32
|
-
* @returns {Promise<AxiosResponse<T>>}
|
|
43
|
+
* POST请求(按服务名)
|
|
33
44
|
*/
|
|
34
45
|
function postByServiceName (url, parameter, serviceName = process.env.VUE_APP_SYSTEM_NAME, isDev, config = {}) {
|
|
35
46
|
if (url.startsWith('api/') || url.startsWith('/api/')) {
|
|
@@ -76,11 +87,14 @@ function put (url, parameter, config = {}) {
|
|
|
76
87
|
* @param {Function} onError - 错误处理回调
|
|
77
88
|
* @returns {Function} - 用于取消请求的函数
|
|
78
89
|
*/
|
|
79
|
-
function startEventStreamPOST (url, params, headers = {}, onDataUpdate, onError, serviceName = process.env.VUE_APP_SYSTEM_NAME
|
|
90
|
+
function startEventStreamPOST (url, params, headers = {}, onDataUpdate, onError, serviceName = process.env.VUE_APP_SYSTEM_NAME) {
|
|
80
91
|
return startEventStream(url, params, headers, onDataUpdate, onError, serviceName, 'POST')
|
|
81
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* EventSource 请求
|
|
96
|
+
*/
|
|
82
97
|
function startEventStream (url, params, headers = {}, onDataUpdate, onError, serviceName = process.env.VUE_APP_SYSTEM_NAME, method = 'GET') {
|
|
83
|
-
// let isStopped = false
|
|
84
98
|
let isStopped = false
|
|
85
99
|
let requestCount = 0
|
|
86
100
|
|
|
@@ -91,25 +105,21 @@ function startEventStream (url, params, headers = {}, onDataUpdate, onError, ser
|
|
|
91
105
|
url = '/api/' + serviceName + url
|
|
92
106
|
}
|
|
93
107
|
|
|
94
|
-
console.log(`[EventStream] 开始新的请求: ${url}`, {
|
|
95
|
-
params,
|
|
96
|
-
headers,
|
|
97
|
-
method
|
|
98
|
-
})
|
|
108
|
+
console.log(`[EventStream] 开始新的请求: ${url}`, { params, headers, method })
|
|
99
109
|
|
|
100
110
|
const controller = new AbortController()
|
|
101
111
|
const { signal } = controller
|
|
102
112
|
const token = localStorage.getItem(ACCESS_TOKEN)
|
|
103
|
-
|
|
113
|
+
|
|
104
114
|
if (token) {
|
|
105
115
|
const compatible = getSystemVersion()
|
|
106
116
|
if (compatible === 'V4') {
|
|
107
|
-
// V4 环境则添加 V4请求头
|
|
108
117
|
headers[V4_ACCESS_TOKEN] = token
|
|
109
118
|
} else {
|
|
110
119
|
headers[ACCESS_TOKEN] = token
|
|
111
120
|
}
|
|
112
121
|
}
|
|
122
|
+
|
|
113
123
|
fetch(url, {
|
|
114
124
|
method: method,
|
|
115
125
|
headers: {
|
|
@@ -126,20 +136,14 @@ function startEventStream (url, params, headers = {}, onDataUpdate, onError, ser
|
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
requestCount++
|
|
129
|
-
console.log(`[EventStream] 请求已打开 (第${requestCount}次): ${url}
|
|
130
|
-
status: response.status,
|
|
131
|
-
statusText: response.statusText
|
|
132
|
-
})
|
|
139
|
+
console.log(`[EventStream] 请求已打开 (第${requestCount}次): ${url}`)
|
|
133
140
|
|
|
134
141
|
const reader = response.body.getReader()
|
|
135
142
|
const decoder = new TextDecoder()
|
|
136
143
|
let buffer = ''
|
|
137
144
|
|
|
138
145
|
function processStream () {
|
|
139
|
-
if (isStopped)
|
|
140
|
-
console.log(`[EventStream] 请求已停止,停止读取流: ${url}`)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
146
|
+
if (isStopped) return
|
|
143
147
|
|
|
144
148
|
reader.read().then(({ done, value }) => {
|
|
145
149
|
if (done) {
|
|
@@ -150,38 +154,23 @@ function startEventStream (url, params, headers = {}, onDataUpdate, onError, ser
|
|
|
150
154
|
const chunk = decoder.decode(value, { stream: true })
|
|
151
155
|
buffer += chunk
|
|
152
156
|
|
|
153
|
-
// 处理接收到的数据
|
|
154
157
|
const lines = buffer.split('\n')
|
|
155
|
-
buffer = lines.pop()
|
|
158
|
+
buffer = lines.pop()
|
|
156
159
|
|
|
157
160
|
let currentEvent = null
|
|
158
|
-
let currentData = null
|
|
159
161
|
|
|
160
162
|
for (const line of lines) {
|
|
161
163
|
if (line.startsWith('event: ')) {
|
|
162
164
|
currentEvent = line.slice(7)
|
|
163
165
|
} else if (line.startsWith('data: ')) {
|
|
164
|
-
currentData = line.slice(6)
|
|
166
|
+
const currentData = line.slice(6)
|
|
165
167
|
try {
|
|
166
168
|
const parsedData = JSON.parse(currentData)
|
|
167
|
-
|
|
168
|
-
// 触发 additionalInfo 回调
|
|
169
|
-
onDataUpdate?.(parsedData, 'additionalInfo')
|
|
170
|
-
} else {
|
|
171
|
-
// 普通消息数据
|
|
172
|
-
onDataUpdate?.(parsedData, 'sourceInfo')
|
|
173
|
-
}
|
|
169
|
+
onDataUpdate?.(parsedData, currentEvent === 'additionalInfo' ? 'additionalInfo' : 'sourceInfo')
|
|
174
170
|
} catch (e) {
|
|
175
|
-
|
|
176
|
-
// 触发 sourceInfo 回调
|
|
177
|
-
onDataUpdate?.(currentData, 'sourceInfo')
|
|
178
|
-
} else {
|
|
179
|
-
onDataUpdate?.(currentData, 'data')
|
|
180
|
-
}
|
|
171
|
+
onDataUpdate?.(currentData, currentEvent === 'sourceInfo' ? 'sourceInfo' : 'data')
|
|
181
172
|
}
|
|
182
|
-
// 重置当前事件和数据
|
|
183
173
|
currentEvent = null
|
|
184
|
-
currentData = null
|
|
185
174
|
}
|
|
186
175
|
}
|
|
187
176
|
|
|
@@ -204,7 +193,6 @@ function startEventStream (url, params, headers = {}, onDataUpdate, onError, ser
|
|
|
204
193
|
}
|
|
205
194
|
})
|
|
206
195
|
|
|
207
|
-
// 返回停止函数
|
|
208
196
|
return () => {
|
|
209
197
|
isStopped = true
|
|
210
198
|
controller.abort()
|
package/src/utils/request.js
CHANGED
|
@@ -12,9 +12,81 @@ import { logout, V4RefreshToken } from '@vue2-client/services/user'
|
|
|
12
12
|
import { LOGIN, SEARCH, V4_LOGIN } from '@vue2-client/services/apiService'
|
|
13
13
|
import { setV4AccessToken } from '@vue2-client/utils/login'
|
|
14
14
|
import EncryptUtil from '@vue2-client/utils/EncryptUtil'
|
|
15
|
+
|
|
15
16
|
// 是否显示重新登录
|
|
16
17
|
let isReloginShow
|
|
17
18
|
|
|
19
|
+
// ============ 请求去重管理 ============
|
|
20
|
+
// 存储进行中的请求 Map<requestKey, { promise, controller }>
|
|
21
|
+
const pendingRequests = new Map()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 生成请求唯一标识
|
|
25
|
+
* @param {object} config - axios 请求配置
|
|
26
|
+
* @returns {string} 请求标识
|
|
27
|
+
*/
|
|
28
|
+
function generateRequestKey (config) {
|
|
29
|
+
const { method, url, data, params } = config
|
|
30
|
+
let dataStr = ''
|
|
31
|
+
let paramsStr = ''
|
|
32
|
+
try {
|
|
33
|
+
dataStr = data ? JSON.stringify(data) : ''
|
|
34
|
+
paramsStr = params ? JSON.stringify(params) : ''
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// 循环引用或其他序列化问题,使用时间戳保证唯一性
|
|
37
|
+
console.warn('[请求去重] 参数序列化失败,跳过去重')
|
|
38
|
+
dataStr = `_${Date.now()}_${Math.random()}`
|
|
39
|
+
}
|
|
40
|
+
return `${method}:${url}:${dataStr}:${paramsStr}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 添加请求到 pending 列表
|
|
45
|
+
* @param {object} config - axios 请求配置
|
|
46
|
+
*/
|
|
47
|
+
function addPendingRequest (config) {
|
|
48
|
+
const requestKey = generateRequestKey(config)
|
|
49
|
+
config._requestKey = requestKey
|
|
50
|
+
|
|
51
|
+
if (!pendingRequests.has(requestKey)) {
|
|
52
|
+
// 创建 AbortController 用于取消请求
|
|
53
|
+
const controller = new AbortController()
|
|
54
|
+
config.signal = controller.signal
|
|
55
|
+
pendingRequests.set(requestKey, { controller, config })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 移除已完成的请求
|
|
61
|
+
* @param {object} config - axios 请求配置
|
|
62
|
+
*/
|
|
63
|
+
function removePendingRequest (config) {
|
|
64
|
+
const requestKey = config._requestKey || generateRequestKey(config)
|
|
65
|
+
if (pendingRequests.has(requestKey)) {
|
|
66
|
+
pendingRequests.delete(requestKey)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 检查是否有相同的请求正在进行
|
|
72
|
+
* @param {object} config - axios 请求配置
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function hasPendingRequest (config) {
|
|
76
|
+
const requestKey = generateRequestKey(config)
|
|
77
|
+
return pendingRequests.has(requestKey)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 清空所有 pending 请求
|
|
82
|
+
*/
|
|
83
|
+
export function clearPendingRequests () {
|
|
84
|
+
pendingRequests.forEach(({ controller }) => {
|
|
85
|
+
controller.abort()
|
|
86
|
+
})
|
|
87
|
+
pendingRequests.clear()
|
|
88
|
+
}
|
|
89
|
+
|
|
18
90
|
axios.defaults.timeout = 50000
|
|
19
91
|
axios.defaults.withCredentials = true
|
|
20
92
|
// 如果是microapp
|
|
@@ -145,6 +217,24 @@ function checkAuthorization (authType = AUTH_TYPE.BEARER) {
|
|
|
145
217
|
function loadInterceptors () {
|
|
146
218
|
// 加载请求拦截器
|
|
147
219
|
axios.interceptors.request.use(config => {
|
|
220
|
+
// ============ 请求去重逻辑 ============
|
|
221
|
+
// POST 请求默认开启去重,可通过 config.dedupe = false 关闭
|
|
222
|
+
const shouldDedupe = config.method?.toLowerCase() === 'post' && config.dedupe !== false
|
|
223
|
+
|
|
224
|
+
if (shouldDedupe && hasPendingRequest(config)) {
|
|
225
|
+
// 相同请求正在进行中,直接拒绝,不发起请求
|
|
226
|
+
console.warn(`重复请求被拦截: ${config.url}`)
|
|
227
|
+
const error = new Error('重复请求被取消')
|
|
228
|
+
error.code = 'ERR_DUPLICATE_REQUEST'
|
|
229
|
+
error.config = config
|
|
230
|
+
return Promise.reject(error)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (shouldDedupe) {
|
|
234
|
+
addPendingRequest(config)
|
|
235
|
+
}
|
|
236
|
+
// ============ 去重逻辑结束 ============
|
|
237
|
+
|
|
148
238
|
const token = localStorage.getItem(ACCESS_TOKEN)
|
|
149
239
|
// 如果 token 存在
|
|
150
240
|
// 让每个请求携带自定义 token 请根据实际情况自行修改
|
|
@@ -189,6 +279,9 @@ function loadInterceptors () {
|
|
|
189
279
|
}, errorHandler)
|
|
190
280
|
// 加载响应拦截器
|
|
191
281
|
axios.interceptors.response.use((res) => {
|
|
282
|
+
// 请求完成,移除 pending 记录
|
|
283
|
+
removePendingRequest(res.config)
|
|
284
|
+
|
|
192
285
|
// 判断是否需要解密
|
|
193
286
|
if (res.headers && res.headers['x-encrypted'] === '1') {
|
|
194
287
|
const v4SessionKey = localStorage.getItem('v4-session-key')
|
|
@@ -315,6 +408,16 @@ function loginExpire () {
|
|
|
315
408
|
}
|
|
316
409
|
// 异常拦截处理器
|
|
317
410
|
const errorHandler = (error) => {
|
|
411
|
+
// 请求失败,移除 pending 记录
|
|
412
|
+
if (error.config) {
|
|
413
|
+
removePendingRequest(error.config)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 如果是被取消的请求(去重导致或 AbortController),静默处理
|
|
417
|
+
if (error.name === 'CanceledError' || error.code === 'ERR_CANCELED' || error.code === 'ERR_DUPLICATE_REQUEST') {
|
|
418
|
+
return Promise.reject(error)
|
|
419
|
+
}
|
|
420
|
+
|
|
318
421
|
if (error.response) {
|
|
319
422
|
const data = error.response.data
|
|
320
423
|
// 从 localstorage 获取 token
|
package/vue.config.js
CHANGED
|
@@ -62,6 +62,11 @@ module.exports = {
|
|
|
62
62
|
target: 'http://127.0.0.1:9012',
|
|
63
63
|
changeOrigin: true
|
|
64
64
|
},
|
|
65
|
+
'/api/af-revenue/': {
|
|
66
|
+
pathRewrite: { '^/api/af-revenue/': '/' },
|
|
67
|
+
target: 'http://127.0.0.1:9026',
|
|
68
|
+
changeOrigin: true
|
|
69
|
+
},
|
|
65
70
|
// '/api/af-system/resource': {
|
|
66
71
|
// pathRewrite: { '^/api/af-system': '/' },
|
|
67
72
|
// target: testUpload,
|