vue2-client 1.18.46 → 1.18.49
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/package.json +1 -1
- package/src/base-client/components/common/XForm/XFormItem.vue +464 -407
- package/src/base-client/components/common/XFormTable/demo.vue +24 -35
- package/src/base-client/components/common/XTab/XTab.vue +31 -0
- package/src/base-client/components/his/XHisEditor/XHisEditor.vue +2 -1
- package/src/router/async/router.map.js +9 -14
- package/src/services/api/common.js +10 -2
- package/src/services/api/restTools.js +11 -0
- package/src/services/v3Api.js +25 -17
- package/src/utils/request.js +68 -80
- package/src/utils/requestDedupe.js +245 -0
|
@@ -14,12 +14,10 @@
|
|
|
14
14
|
:defaultQueryForm="{
|
|
15
15
|
s_f_user_name: '张三'
|
|
16
16
|
}"
|
|
17
|
-
serviceName="af-
|
|
17
|
+
serviceName="af-system"
|
|
18
18
|
ref="xFormTable"
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
<div style="height: 100px; width: 100%; background-color: #f0f2f5;">
|
|
22
|
-
</div>
|
|
19
|
+
></x-form-table>
|
|
20
|
+
<div style="height: 100px; width: 100%; background-color: #f0f2f5"></div>
|
|
23
21
|
</a-card>
|
|
24
22
|
</template>
|
|
25
23
|
|
|
@@ -29,12 +27,12 @@ import { microDispatch } from '@vue2-client/utils/microAppUtils'
|
|
|
29
27
|
export default {
|
|
30
28
|
name: 'Demo',
|
|
31
29
|
components: {
|
|
32
|
-
XFormTable
|
|
30
|
+
XFormTable
|
|
33
31
|
},
|
|
34
|
-
data
|
|
32
|
+
data() {
|
|
35
33
|
return {
|
|
36
34
|
// 查询配置文件名
|
|
37
|
-
queryParamsName: '
|
|
35
|
+
queryParamsName: 'ceshiCRUD',
|
|
38
36
|
// 查询配置左侧tree
|
|
39
37
|
xTreeConfigName: 'addressType',
|
|
40
38
|
// 新增表单固定值
|
|
@@ -47,30 +45,21 @@ export default {
|
|
|
47
45
|
selectedKeys: [],
|
|
48
46
|
selected: {
|
|
49
47
|
keys: [],
|
|
50
|
-
rows: []
|
|
51
|
-
}
|
|
48
|
+
rows: []
|
|
49
|
+
}
|
|
52
50
|
}
|
|
53
51
|
},
|
|
54
52
|
|
|
55
53
|
methods: {
|
|
56
54
|
// ========== 原有方法 ==========
|
|
57
|
-
rowDblClick
|
|
55
|
+
rowDblClick(record) {
|
|
58
56
|
console.log('rowDblClick', record)
|
|
59
57
|
},
|
|
60
58
|
// input框 行編輯參數傳遞
|
|
61
|
-
ceshi
|
|
59
|
+
ceshi(...args) {
|
|
62
60
|
// attr, value, currentRecord, currentIndex, nextRecord, nextIndex
|
|
63
|
-
const [attr, value, currentRecord, currentIndex, nextRecord, nextIndex] =
|
|
64
|
-
|
|
65
|
-
console.log(
|
|
66
|
-
'ceshi',
|
|
67
|
-
attr,
|
|
68
|
-
value,
|
|
69
|
-
currentRecord,
|
|
70
|
-
currentIndex,
|
|
71
|
-
nextRecord,
|
|
72
|
-
nextIndex
|
|
73
|
-
)
|
|
61
|
+
const [attr, value, currentRecord, currentIndex, nextRecord, nextIndex] = args
|
|
62
|
+
console.log('ceshi', attr, value, currentRecord, currentIndex, nextRecord, nextIndex)
|
|
74
63
|
// 示例:当按下 Enter 键且有下一行时,跳转到下一行的同一列
|
|
75
64
|
// 可以在这里执行业务逻辑(例如:保存数据)
|
|
76
65
|
console.log('当前行:', currentIndex, '下一行:', nextIndex)
|
|
@@ -81,42 +70,42 @@ export default {
|
|
|
81
70
|
// 跳转到下一行的同一列
|
|
82
71
|
this.$refs.xFormTable.$refs.xTable.focusInput(nextIndex, attr.model)
|
|
83
72
|
},
|
|
84
|
-
test
|
|
73
|
+
test() {
|
|
85
74
|
this.$refs.xFormTable.setTableData([])
|
|
86
75
|
},
|
|
87
|
-
defaultF
|
|
76
|
+
defaultF() {
|
|
88
77
|
this.$refs.xFormTable.setTableSize('default')
|
|
89
78
|
},
|
|
90
|
-
middleF
|
|
79
|
+
middleF() {
|
|
91
80
|
this.$refs.xFormTable.setTableSize('middle')
|
|
92
81
|
},
|
|
93
|
-
smallF
|
|
82
|
+
smallF() {
|
|
94
83
|
this.$refs.xFormTable.setTableSize('small')
|
|
95
84
|
},
|
|
96
|
-
columnClick
|
|
85
|
+
columnClick(key, value, record) {
|
|
97
86
|
microDispatch({
|
|
98
87
|
type: 'v3route',
|
|
99
88
|
path: '/bingliguanli/dianzibingliluru',
|
|
100
|
-
props: { selected: arguments[0].his_f_admission_id }
|
|
89
|
+
props: { selected: arguments[0].his_f_admission_id }
|
|
101
90
|
})
|
|
102
91
|
},
|
|
103
|
-
action
|
|
92
|
+
action(record, id, actionType) {
|
|
104
93
|
this.detailVisible = true
|
|
105
94
|
console.log('触发了详情操作', record, id, actionType)
|
|
106
95
|
},
|
|
107
|
-
onClose
|
|
96
|
+
onClose() {
|
|
108
97
|
this.detailVisible = false
|
|
109
98
|
// 关闭详情之后重新查询表单
|
|
110
99
|
this.$refs.xFormTable.refreshTable(true)
|
|
111
100
|
},
|
|
112
|
-
selectRow
|
|
101
|
+
selectRow(selectedRowKeys, selectedRows) {
|
|
113
102
|
this.selected = {
|
|
114
103
|
keys: selectedRowKeys,
|
|
115
|
-
rows: selectedRows
|
|
104
|
+
rows: selectedRows
|
|
116
105
|
}
|
|
117
106
|
console.log('selectedDemo', this.selected)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
120
109
|
}
|
|
121
110
|
</script>
|
|
122
111
|
|
|
@@ -200,6 +200,33 @@ export default {
|
|
|
200
200
|
if (initStatus) {
|
|
201
201
|
this.activeKey = this.defaultActiveKey
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
// 每次切换后,重新应用单个页签的显示/隐藏规则
|
|
205
|
+
this.applyTabHeaderVisibility()
|
|
206
|
+
},
|
|
207
|
+
applyTabHeaderVisibility () {
|
|
208
|
+
// 根级 showTabBar 为 false 时,整个头都不显示,就不再处理单个页签
|
|
209
|
+
if (!this.showTabBar || !this.config || !Array.isArray(this.config.data)) return
|
|
210
|
+
|
|
211
|
+
this.$nextTick(() => {
|
|
212
|
+
try {
|
|
213
|
+
const navTabs = this.$el.querySelectorAll('.ant-tabs-nav .ant-tabs-tab')
|
|
214
|
+
if (!navTabs || !navTabs.length) return
|
|
215
|
+
|
|
216
|
+
this.config.data.forEach((tab, index) => {
|
|
217
|
+
const tabNode = navTabs[index]
|
|
218
|
+
if (!tabNode) return
|
|
219
|
+
if (tab && tab.showTabBar === false) {
|
|
220
|
+
tabNode.style.display = 'none'
|
|
221
|
+
} else {
|
|
222
|
+
tabNode.style.display = ''
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
} catch (e) {
|
|
226
|
+
// DOM 结构异常时不影响主流程
|
|
227
|
+
console.error('applyTabHeaderVisibility error', e)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
203
230
|
},
|
|
204
231
|
|
|
205
232
|
// 新增:等待组件就绪的方法
|
|
@@ -331,6 +358,8 @@ export default {
|
|
|
331
358
|
if (this.config && this.config.data) {
|
|
332
359
|
this.markTabAsLoaded(this.defaultActiveKey)
|
|
333
360
|
}
|
|
361
|
+
// 初始化完成后,根据每个 tab 的 showTabBar 设置头部显示
|
|
362
|
+
this.applyTabHeaderVisibility()
|
|
334
363
|
},
|
|
335
364
|
getConfig () {
|
|
336
365
|
getConfigByName(this.configName, this.serverName, res => {
|
|
@@ -341,6 +370,8 @@ export default {
|
|
|
341
370
|
if (this.config && this.config.data) {
|
|
342
371
|
this.markTabAsLoaded(this.defaultActiveKey)
|
|
343
372
|
}
|
|
373
|
+
// 从服务端配置加载完成后,应用单个页签的显示/隐藏
|
|
374
|
+
this.applyTabHeaderVisibility()
|
|
344
375
|
}, this.env === 'dev')
|
|
345
376
|
},
|
|
346
377
|
getEventHandlers (tab, index) {
|
|
@@ -614,9 +614,10 @@ export default {
|
|
|
614
614
|
f_file_name: this.fileName
|
|
615
615
|
}
|
|
616
616
|
// 保存HTML文档和结构化数据到后端服务
|
|
617
|
+
const queryParams = {admissionId: this.admissionId, f_template_id: this.templateId}
|
|
617
618
|
runLogic(this.saveDataLogicName, data, this.serviceName).then(res => {
|
|
618
619
|
this.$message.success('保存成功')
|
|
619
|
-
this.
|
|
620
|
+
this.reload({...queryParams,resId: res.currResData.id})
|
|
620
621
|
this.$emit('saveafter', data.dataObject)
|
|
621
622
|
}).finally(() => {
|
|
622
623
|
this.resDataModalVisible = false
|
|
@@ -25,8 +25,7 @@ routerResource.chargeQuery = () => import('@vue2-client/base-client/components/c
|
|
|
25
25
|
// --------------------------------------仪表盘--------------------------------------
|
|
26
26
|
routerResource.dashboard = view.blank
|
|
27
27
|
// 工作台
|
|
28
|
-
routerResource.workplace = () =>
|
|
29
|
-
import('@vue2-client/pages/dashboard/workplace')
|
|
28
|
+
routerResource.workplace = () => import('@vue2-client/pages/dashboard/workplace')
|
|
30
29
|
// --------------------------------------系统配置--------------------------------------
|
|
31
30
|
routerResource.system = view.blank
|
|
32
31
|
// 字典管理
|
|
@@ -51,13 +50,13 @@ routerResource.dynamicStatistics = () => import('@vue2-client/pages/DynamicStati
|
|
|
51
50
|
routerResource.newDynamicStatistics = () => import('@vue2-client/pages/NewDynamicStatistics')
|
|
52
51
|
// 示例页面
|
|
53
52
|
routerResource.example = {
|
|
54
|
-
path: 'example',
|
|
53
|
+
path: 'example',
|
|
55
54
|
name: '示例主页面',
|
|
56
55
|
// component: () => import('@vue2-client/base-client/components/common/XDescriptions/demo.vue'),
|
|
57
56
|
// component: () => import('@vue2-client/base-client/components/his/HChart/demo.vue'),
|
|
58
57
|
// component: () => import('@vue2-client/pages/WorkflowDetail/WorkFlowDemo.vue'),
|
|
59
|
-
|
|
60
|
-
component: () => import('@vue2-client/base-client/components/common/ImagePreviewModal/demo.vue'),
|
|
58
|
+
component: () => import('@vue2-client/base-client/components/common/XFormTable/demo.vue')
|
|
59
|
+
// component: () => import('@vue2-client/base-client/components/common/ImagePreviewModal/demo.vue'),
|
|
61
60
|
// component: () => import('@vue2-client/components/xScrollBox/example.vue'),
|
|
62
61
|
// component: () => import('@vue2-client/pages/WorkflowDetail/WorkFlowDemo.vue'),
|
|
63
62
|
// component: () => import('@vue2-client/pages/addressSelect/addressDemo.vue'),
|
|
@@ -103,34 +102,30 @@ const routerMap = {
|
|
|
103
102
|
login: {
|
|
104
103
|
authority: '*',
|
|
105
104
|
path: '/login',
|
|
106
|
-
component: process.env.VUE_APP_LOGIN_VERSION === 'V3'
|
|
107
|
-
? view.loginv3 : view.login
|
|
105
|
+
component: process.env.VUE_APP_LOGIN_VERSION === 'V3' ? view.loginv3 : view.login
|
|
108
106
|
},
|
|
109
107
|
root: {
|
|
110
108
|
path: '/',
|
|
111
109
|
name: '首页',
|
|
112
110
|
// 只有在非微前端环境下才进行重定向,或者通过环境变量控制
|
|
113
111
|
redirect: window.__MICRO_APP_ENVIRONMENT__ ? undefined : homePage,
|
|
114
|
-
component: process.env.VUE_APP_SINGLE_PAPER === 'TRUE' ? view.blank : view.tabs
|
|
112
|
+
component: process.env.VUE_APP_SINGLE_PAPER === 'TRUE' ? view.blank : view.tabs
|
|
115
113
|
},
|
|
116
114
|
exp403: {
|
|
117
115
|
authority: '*',
|
|
118
116
|
name: 'exp403',
|
|
119
117
|
path: '403',
|
|
120
|
-
component: () =>
|
|
121
|
-
import('@vue2-client/pages/exception/403')
|
|
118
|
+
component: () => import('@vue2-client/pages/exception/403')
|
|
122
119
|
},
|
|
123
120
|
exp404: {
|
|
124
121
|
name: 'exp404',
|
|
125
122
|
path: '404',
|
|
126
|
-
component: () =>
|
|
127
|
-
import('@vue2-client/pages/exception/404')
|
|
123
|
+
component: () => import('@vue2-client/pages/exception/404')
|
|
128
124
|
},
|
|
129
125
|
exp500: {
|
|
130
126
|
name: 'exp500',
|
|
131
127
|
path: '500',
|
|
132
|
-
component: () =>
|
|
133
|
-
import('@vue2-client/pages/exception/500')
|
|
128
|
+
component: () => import('@vue2-client/pages/exception/500')
|
|
134
129
|
}
|
|
135
130
|
}
|
|
136
131
|
Object.assign(routerMap, routerResource)
|
|
@@ -141,13 +141,21 @@ export function parseConfig (configContent, configType) {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* 通用执行业务逻辑
|
|
144
|
+
* @param {string} logicName - 业务逻辑名称
|
|
145
|
+
* @param {object} parameter - 请求参数
|
|
146
|
+
* @param {string} serviceName - 服务名称
|
|
147
|
+
* @param {boolean} isDev - 是否开发环境
|
|
148
|
+
* @param {object} config - 请求配置
|
|
149
|
+
* @param {boolean} config.dedupe - 是否启用去重(默认 true)
|
|
150
|
+
* @param {string} config.dedupeStrategy - 去重策略:'reject'(拒绝)或 'reuse'(复用结果)
|
|
151
|
+
* @param {boolean|string} config.globalLoading - 是否显示全局 Loading
|
|
144
152
|
*/
|
|
145
|
-
export function runLogic (logicName, parameter, serviceName = process.env.VUE_APP_SYSTEM_NAME, isDev) {
|
|
153
|
+
export function runLogic (logicName, parameter, serviceName = process.env.VUE_APP_SYSTEM_NAME, isDev, config = {}) {
|
|
146
154
|
let apiPre = '/api/'
|
|
147
155
|
if (isDev) {
|
|
148
156
|
apiPre = '/devApi/'
|
|
149
157
|
}
|
|
150
|
-
return post(apiPre + serviceName + '/logic/' + logicName, parameter)
|
|
158
|
+
return post(apiPre + serviceName + '/logic/' + logicName, parameter, config)
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
/**
|
|
@@ -19,7 +19,18 @@ function get (url, parameter) {
|
|
|
19
19
|
* @param config 配置项
|
|
20
20
|
* @param {boolean|string} config.globalLoading - 是否显示全局 Loading,可传入字符串作为提示文字
|
|
21
21
|
* @param {boolean} config.dedupe - 是否启用请求去重(POST 默认开启,设为 false 可关闭)
|
|
22
|
+
* @param {string} config.dedupeStrategy - 去重策略:'reject'(拒绝,默认)或 'reuse'(复用结果)
|
|
22
23
|
* @returns {Promise<AxiosResponse<T>>}
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // 表单提交 - 默认拒绝重复(防止重复提交)
|
|
27
|
+
* post('/api/save', formData)
|
|
28
|
+
*
|
|
29
|
+
* // 下拉框数据 - 复用结果(多处同时请求共享结果)
|
|
30
|
+
* post('/api/options', params, { dedupe: true, dedupeStrategy: 'reuse' })
|
|
31
|
+
*
|
|
32
|
+
* // 列表查询 - 关闭去重(允许重复查询)
|
|
33
|
+
* post('/api/list', params, { dedupe: false })
|
|
23
34
|
*/
|
|
24
35
|
function post (url, parameter, config = {}) {
|
|
25
36
|
// 兼容 config 为 null 的情况
|
package/src/services/v3Api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { METHOD, request } from '@vue2-client/utils/request'
|
|
2
2
|
import { runLogic } from '@vue2-client/services/api/common'
|
|
3
3
|
|
|
4
|
-
function getLeafNodes
|
|
4
|
+
function getLeafNodes(nodes) {
|
|
5
5
|
// 确保 nodes 是数组
|
|
6
6
|
const nodeArray = Array.isArray(nodes) ? nodes : [nodes]
|
|
7
7
|
return nodeArray.reduce((leaves, node) => {
|
|
@@ -16,7 +16,7 @@ function getLeafNodes (nodes) {
|
|
|
16
16
|
}, [])
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function getLeafNodesByCondition
|
|
19
|
+
function getLeafNodesByCondition(type, nodes, parent = null) {
|
|
20
20
|
// 确保 nodes 是数组
|
|
21
21
|
const nodeArray = Array.isArray(nodes) ? nodes : [nodes]
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ function getLeafNodesByCondition (type, nodes, parent = null) {
|
|
|
29
29
|
if (isValidNode) {
|
|
30
30
|
leaves.push({
|
|
31
31
|
...node,
|
|
32
|
-
name: parent ? `${node.name}-${parent}` : node.name
|
|
32
|
+
name: parent ? `${node.name}-${parent}` : node.name
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
// 递归处理子节点
|
|
@@ -38,7 +38,7 @@ function getLeafNodesByCondition (type, nodes, parent = null) {
|
|
|
38
38
|
// 无子节点但符合条件时,直接加入叶子节点列表
|
|
39
39
|
leaves.push({
|
|
40
40
|
...node,
|
|
41
|
-
name: parent ? `${node.name}-${parent}` : node.name
|
|
41
|
+
name: parent ? `${node.name}-${parent}` : node.name
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,8 +46,8 @@ function getLeafNodesByCondition (type, nodes, parent = null) {
|
|
|
46
46
|
}, [])
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function transformData
|
|
50
|
-
function transform
|
|
49
|
+
function transformData(inputData) {
|
|
50
|
+
function transform(node) {
|
|
51
51
|
return {
|
|
52
52
|
label: node.name,
|
|
53
53
|
value: node.id,
|
|
@@ -61,23 +61,31 @@ function transformData (inputData) {
|
|
|
61
61
|
return inputData.map(transform)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// 复用策略配置:相同请求共享结果,避免重复网络请求
|
|
65
|
+
const REUSE_CONFIG = { dedupe: true, dedupeStrategy: 'reuse' }
|
|
66
|
+
|
|
67
|
+
function getResData(params, toCallback) {
|
|
68
|
+
const data = {
|
|
69
|
+
userId: params.userid,
|
|
70
|
+
roleName: params.roleName,
|
|
71
|
+
filter: params.filter,
|
|
72
|
+
filterType: params.filterType
|
|
73
|
+
}
|
|
66
74
|
if (params.source === '获取分公司') {
|
|
67
|
-
runLogic('getOrgBySearch', data, 'af-system').then(res => toCallback(res))
|
|
75
|
+
runLogic('getOrgBySearch', data, 'af-system', false, REUSE_CONFIG).then(res => toCallback(res))
|
|
68
76
|
} else if (params.source === '获取部门') {
|
|
69
|
-
runLogic('getDepBySearch', data, 'af-system').then(res => toCallback(res))
|
|
77
|
+
runLogic('getDepBySearch', data, 'af-system', false, REUSE_CONFIG).then(res => toCallback(res))
|
|
70
78
|
} else if (params.source === '获取人员') {
|
|
71
|
-
runLogic('getUserBySearch', data, 'af-system').then(res => toCallback(res))
|
|
79
|
+
runLogic('getUserBySearch', data, 'af-system', false, REUSE_CONFIG).then(res => toCallback(res))
|
|
72
80
|
} else if (params.source === '根据角色获取人员') {
|
|
73
|
-
runLogic('getUserBySearchRole', data, 'af-system').then(res => toCallback(res))
|
|
81
|
+
runLogic('getUserBySearchRole', data, 'af-system', false, REUSE_CONFIG).then(res => toCallback(res))
|
|
74
82
|
} else {
|
|
75
83
|
return search(params).then(res => toCallback(res))
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
export async function searchToOption
|
|
80
|
-
function toCallback
|
|
87
|
+
export async function searchToOption(params, callback) {
|
|
88
|
+
function toCallback(res) {
|
|
81
89
|
if (res.length) {
|
|
82
90
|
if (res[0].children && res[0].children.length) {
|
|
83
91
|
if (res[0].children[0].children) {
|
|
@@ -96,8 +104,8 @@ export async function searchToOption (params, callback) {
|
|
|
96
104
|
await getResData(params, toCallback)
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
export async function searchToListOption
|
|
100
|
-
function toCallback
|
|
107
|
+
export async function searchToListOption(params, callback) {
|
|
108
|
+
function toCallback(res) {
|
|
101
109
|
if (params.source.includes('人员')) {
|
|
102
110
|
callback(transformData(getLeafNodes(res)))
|
|
103
111
|
} else {
|
|
@@ -109,7 +117,7 @@ export async function searchToListOption (params, callback) {
|
|
|
109
117
|
await getResData(params, toCallback)
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
export async function search
|
|
120
|
+
export async function search(params) {
|
|
113
121
|
return request('/rs/search', METHOD.POST, params)
|
|
114
122
|
}
|
|
115
123
|
|
package/src/utils/request.js
CHANGED
|
@@ -12,81 +12,34 @@ 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
|
+
// 引入请求去重模块
|
|
16
|
+
import {
|
|
17
|
+
DEDUPE_STRATEGY,
|
|
18
|
+
generateRequestKey,
|
|
19
|
+
hasPendingRequest,
|
|
20
|
+
addPendingRequest,
|
|
21
|
+
handleDuplicateRequest,
|
|
22
|
+
resolvePendingRequest,
|
|
23
|
+
rejectPendingRequest,
|
|
24
|
+
clearPendingRequests as clearAllPendingRequests,
|
|
25
|
+
shouldEnableDedupe,
|
|
26
|
+
getDedupeStrategy
|
|
27
|
+
} from '@vue2-client/utils/requestDedupe'
|
|
15
28
|
|
|
16
29
|
// 是否显示重新登录
|
|
17
30
|
let isReloginShow
|
|
18
31
|
|
|
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
32
|
/**
|
|
81
33
|
* 清空所有 pending 请求
|
|
34
|
+
* @description 页面切换或需要取消所有请求时调用
|
|
82
35
|
*/
|
|
83
36
|
export function clearPendingRequests () {
|
|
84
|
-
|
|
85
|
-
controller.abort()
|
|
86
|
-
})
|
|
87
|
-
pendingRequests.clear()
|
|
37
|
+
clearAllPendingRequests()
|
|
88
38
|
}
|
|
89
39
|
|
|
40
|
+
// 导出去重策略常量,方便外部使用
|
|
41
|
+
export { DEDUPE_STRATEGY }
|
|
42
|
+
|
|
90
43
|
axios.defaults.timeout = 50000
|
|
91
44
|
axios.defaults.withCredentials = true
|
|
92
45
|
// 如果是microapp
|
|
@@ -219,19 +172,35 @@ function loadInterceptors () {
|
|
|
219
172
|
axios.interceptors.request.use(config => {
|
|
220
173
|
// ============ 请求去重逻辑 ============
|
|
221
174
|
// POST 请求默认开启去重,可通过 config.dedupe = false 关闭
|
|
222
|
-
|
|
175
|
+
// 支持两种策略:reject(拒绝,默认)和 reuse(复用结果)
|
|
176
|
+
const enableDedupe = shouldEnableDedupe(config)
|
|
223
177
|
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const error = new Error('重复请求被取消')
|
|
228
|
-
error.code = 'ERR_DUPLICATE_REQUEST'
|
|
229
|
-
error.config = config
|
|
230
|
-
return Promise.reject(error)
|
|
231
|
-
}
|
|
178
|
+
if (enableDedupe) {
|
|
179
|
+
const requestKey = generateRequestKey(config)
|
|
180
|
+
config._requestKey = requestKey
|
|
232
181
|
|
|
233
|
-
|
|
234
|
-
|
|
182
|
+
// 检查是否有相同请求正在进行
|
|
183
|
+
if (hasPendingRequest(requestKey)) {
|
|
184
|
+
const result = handleDuplicateRequest(requestKey, config)
|
|
185
|
+
|
|
186
|
+
if (!result.shouldProceed) {
|
|
187
|
+
if (result.isReuse && result.promise) {
|
|
188
|
+
// 复用策略:返回相同的 Promise,标记为复用请求
|
|
189
|
+
config._isReuseRequest = true
|
|
190
|
+
config._reusePromise = result.promise
|
|
191
|
+
// 返回一个特殊标记,让后续逻辑知道这是复用请求
|
|
192
|
+
return config
|
|
193
|
+
} else {
|
|
194
|
+
// 拒绝策略:直接返回错误
|
|
195
|
+
return Promise.reject(result.error)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 添加到 pending 列表
|
|
201
|
+
const strategy = getDedupeStrategy(config)
|
|
202
|
+
const { signal } = addPendingRequest(requestKey, config, strategy)
|
|
203
|
+
config.signal = signal
|
|
235
204
|
}
|
|
236
205
|
// ============ 去重逻辑结束 ============
|
|
237
206
|
|
|
@@ -279,8 +248,12 @@ function loadInterceptors () {
|
|
|
279
248
|
}, errorHandler)
|
|
280
249
|
// 加载响应拦截器
|
|
281
250
|
axios.interceptors.response.use((res) => {
|
|
282
|
-
|
|
283
|
-
|
|
251
|
+
const requestKey = res.config._requestKey
|
|
252
|
+
|
|
253
|
+
// 如果是复用请求,直接返回复用的 Promise 结果
|
|
254
|
+
if (res.config._isReuseRequest && res.config._reusePromise) {
|
|
255
|
+
return res.config._reusePromise
|
|
256
|
+
}
|
|
284
257
|
|
|
285
258
|
// 判断是否需要解密
|
|
286
259
|
if (res.headers && res.headers['x-encrypted'] === '1') {
|
|
@@ -358,10 +331,22 @@ function loadInterceptors () {
|
|
|
358
331
|
description: msg
|
|
359
332
|
})
|
|
360
333
|
} else {
|
|
334
|
+
// 请求成功,通知复用的请求并返回数据
|
|
335
|
+
if (requestKey) {
|
|
336
|
+
resolvePendingRequest(requestKey, res.data)
|
|
337
|
+
}
|
|
361
338
|
return res.data
|
|
362
339
|
}
|
|
340
|
+
// 请求失败,通知复用的请求
|
|
341
|
+
if (requestKey) {
|
|
342
|
+
rejectPendingRequest(requestKey, new Error(msg))
|
|
343
|
+
}
|
|
363
344
|
return Promise.reject(msg)
|
|
364
345
|
} else {
|
|
346
|
+
// 请求成功,通知复用的请求并返回数据
|
|
347
|
+
if (requestKey) {
|
|
348
|
+
resolvePendingRequest(requestKey, res.data)
|
|
349
|
+
}
|
|
365
350
|
return res.data
|
|
366
351
|
}
|
|
367
352
|
}, errorHandler)
|
|
@@ -408,9 +393,12 @@ function loginExpire () {
|
|
|
408
393
|
}
|
|
409
394
|
// 异常拦截处理器
|
|
410
395
|
const errorHandler = (error) => {
|
|
411
|
-
//
|
|
396
|
+
// 请求失败,通知复用的请求并移除 pending 记录
|
|
412
397
|
if (error.config) {
|
|
413
|
-
|
|
398
|
+
const requestKey = error.config._requestKey
|
|
399
|
+
if (requestKey) {
|
|
400
|
+
rejectPendingRequest(requestKey, error)
|
|
401
|
+
}
|
|
414
402
|
}
|
|
415
403
|
|
|
416
404
|
// 如果是被取消的请求(去重导致或 AbortController),静默处理
|