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.
@@ -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
- return request(url, METHOD.POST, parameter, config)
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, method = 'GET') {
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
- // 增加 V4 登录请求头
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
- if (currentEvent === 'additionalInfo') {
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
- if (currentEvent === 'sourceInfo') {
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()
@@ -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,