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,353 @@
1
+ # 请求工具使用说明
2
+
3
+ 本文档介绍项目中封装的请求工具及其配置方式。
4
+
5
+ ## 架构概览
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────┐
9
+ │ 业务组件 │
10
+ ├─────────────────────────────────────────────────────────────┤
11
+ │ usePost / useRunLogic (响应式 Hook) │
12
+ ├─────────────────────────────────────────────────────────────┤
13
+ │ restTools.js │
14
+ │ (post / get / del / put) │
15
+ ├─────────────────────────────────────────────────────────────┤
16
+ │ request.js │
17
+ │ (axios 拦截器 + 请求去重 + 错误处理) │
18
+ └─────────────────────────────────────────────────────────────┘
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 一、基础请求方法 (restTools.js)
24
+
25
+ ### 1. post - POST 请求
26
+
27
+ ```javascript
28
+ import { post } from '@vue2-client/services/api'
29
+
30
+ // 基础用法
31
+ const result = await post('/api/user/save', { name: '张三' })
32
+
33
+ // 带配置
34
+ const result = await post('/api/user/save', { name: '张三' }, {
35
+ globalLoading: '保存中...', // 显示全局 Loading
36
+ dedupe: false // 关闭请求去重(POST 默认开启)
37
+ })
38
+ ```
39
+
40
+ **配置项说明:**
41
+
42
+ | 参数 | 类型 | 默认值 | 说明 |
43
+ |------|------|--------|------|
44
+ | `globalLoading` | `boolean \| string` | `false` | 是否显示全局 Loading,传字符串可自定义提示文字 |
45
+ | `dedupe` | `boolean` | `true` | 请求去重开关,POST 请求默认开启 |
46
+
47
+ ### 2. get - GET 请求
48
+
49
+ ```javascript
50
+ import { get } from '@vue2-client/services/api'
51
+
52
+ const result = await get('/api/user/list', { page: 1, size: 10 })
53
+ ```
54
+
55
+ ### 3. del - DELETE 请求
56
+
57
+ ```javascript
58
+ import { del } from '@vue2-client/services/api'
59
+
60
+ const result = await del('/api/user/delete', { id: 1 })
61
+ ```
62
+
63
+ ### 4. put - PUT 请求
64
+
65
+ ```javascript
66
+ import { put } from '@vue2-client/services/api'
67
+
68
+ const result = await put('/api/user/update', { id: 1, name: '李四' })
69
+ ```
70
+
71
+ ### 5. postByServiceName - 按服务名请求
72
+
73
+ ```javascript
74
+ import { postByServiceName } from '@vue2-client/services/api'
75
+
76
+ // 自动拼接 URL: /api/{serviceName}/{url}
77
+ const result = await postByServiceName('/user/save', { name: '张三' }, 'af-system')
78
+
79
+ // 开发环境: /devApi/{serviceName}/{url}
80
+ const result = await postByServiceName('/user/save', { name: '张三' }, 'af-system', true)
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 二、Composition API Hooks (usePost.js)
86
+
87
+ ### 1. usePost - 响应式 POST 请求
88
+
89
+ 适用于需要在模板中绑定 loading 状态的场景。
90
+
91
+ ```javascript
92
+ import { usePost } from '@vue2-client/composables'
93
+
94
+ export default {
95
+ setup() {
96
+ const { loading, data, error, execute, reset } = usePost('/api/user/save', {
97
+ globalLoading: '保存中...',
98
+ dedupe: true
99
+ })
100
+
101
+ const handleSave = async (formData) => {
102
+ const { success, data, error } = await execute(formData)
103
+ if (success) {
104
+ message.success('保存成功')
105
+ } else {
106
+ // 错误处理(可选,拦截器已统一处理)
107
+ console.error(error)
108
+ }
109
+ }
110
+
111
+ return { loading, data, error, handleSave }
112
+ }
113
+ }
114
+ ```
115
+
116
+ **模板使用:**
117
+
118
+ ```html
119
+ <a-button :loading="loading" @click="handleSave(formData)">保存</a-button>
120
+ ```
121
+
122
+ **返回值说明:**
123
+
124
+ | 属性 | 类型 | 说明 |
125
+ |------|------|------|
126
+ | `loading` | `boolean` | 请求进行中状态(响应式) |
127
+ | `data` | `any` | 请求返回的数据(响应式) |
128
+ | `error` | `Error \| null` | 请求错误信息(响应式) |
129
+ | `execute(params, config)` | `Function` | 执行请求,返回 `{ success, data, error }` |
130
+ | `reset()` | `Function` | 重置状态 |
131
+
132
+ **execute 返回值:**
133
+
134
+ ```javascript
135
+ const { success, data, error } = await execute(params)
136
+ // success: boolean - 请求是否成功
137
+ // data: any - 成功时的响应数据
138
+ // error: Error | null - 失败时的错误信息
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 三、业务逻辑请求 (useRunLogic)
144
+
145
+ ### useRunLogic - 响应式业务逻辑调用
146
+
147
+ 用于调用后端 Logic 业务逻辑,自动拼接 URL。
148
+
149
+ ```javascript
150
+ import { useRunLogic } from '@vue2-client/composables'
151
+
152
+ export default {
153
+ setup() {
154
+ const { loading, data, error, execute, reset } = useRunLogic('getUserInfo', {
155
+ serviceName: 'af-system', // 可选,默认使用 VUE_APP_SYSTEM_NAME
156
+ isDev: false, // 可选,是否使用开发环境 API
157
+ globalLoading: '加载中...',
158
+ dedupe: true
159
+ })
160
+
161
+ const loadUser = async (userId) => {
162
+ const { success, data: result } = await execute({ userId })
163
+ if (success) {
164
+ console.log('用户信息:', result)
165
+ }
166
+ }
167
+
168
+ return { loading, data, loadUser }
169
+ }
170
+ }
171
+ ```
172
+
173
+ **URL 拼接规则:**
174
+ - 生产环境: `/api/{serviceName}/logic/{logicName}`
175
+ - 开发环境: `/devApi/{serviceName}/logic/{logicName}`
176
+
177
+ **配置项说明:**
178
+
179
+ | 参数 | 类型 | 默认值 | 说明 |
180
+ |------|------|--------|------|
181
+ | `serviceName` | `string` | `VUE_APP_SYSTEM_NAME` | 服务名称 |
182
+ | `isDev` | `boolean` | `false` | 是否使用开发环境 API |
183
+ | `globalLoading` | `boolean \| string` | `false` | 是否显示全局 Loading |
184
+ | `dedupe` | `boolean` | `false` | 请求去重开关 |
185
+
186
+ ---
187
+
188
+ ## 四、全局功能
189
+
190
+ ### 1. 请求去重
191
+
192
+ POST 请求默认开启去重,防止用户快速重复点击导致的重复提交。
193
+
194
+ **工作原理:**
195
+ - 相同的 URL + 参数 组合视为重复请求
196
+ - 重复请求会被直接拒绝(不发起网络请求),控制台输出警告
197
+ - 请求完成后自动清理记录
198
+
199
+ **关闭去重:**
200
+
201
+ ```javascript
202
+ post('/api/save', data, { dedupe: false })
203
+ ```
204
+
205
+ **错误码:**
206
+ - 重复请求被拦截时,error.code 为 `ERR_DUPLICATE_REQUEST`
207
+
208
+ ### 2. 全局 Loading
209
+
210
+ 显示全屏遮罩 Loading,阻止用户操作。
211
+
212
+ ```javascript
213
+ // 使用默认文字 "加载中..."
214
+ post('/api/save', data, { globalLoading: true })
215
+
216
+ // 自定义提示文字
217
+ post('/api/save', data, { globalLoading: '正在保存...' })
218
+ ```
219
+
220
+ **特性:**
221
+ - 支持引用计数,多个并发请求时正确处理显示/隐藏
222
+ - 默认 30 秒超时自动隐藏,防止异常情况下 Loading 卡死
223
+ - 可通过 `forceHideGlobalLoading()` 强制隐藏
224
+
225
+ **手动控制:**
226
+
227
+ ```javascript
228
+ import {
229
+ showGlobalLoading,
230
+ hideGlobalLoading,
231
+ forceHideGlobalLoading
232
+ } from '@vue2-client/composables/useGlobalLoading'
233
+
234
+ // 显示 Loading(默认 30 秒超时)
235
+ showGlobalLoading('处理中...')
236
+
237
+ // 显示 Loading,自定义超时时间(60 秒)
238
+ showGlobalLoading('处理中...', 60000)
239
+
240
+ // 显示 Loading,禁用超时
241
+ showGlobalLoading('处理中...', 0)
242
+
243
+ // 隐藏 Loading(引用计数减一)
244
+ hideGlobalLoading()
245
+
246
+ // 强制隐藏(重置计数器,立即隐藏)
247
+ forceHideGlobalLoading()
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 五、EventStream 流式请求
253
+
254
+ 用于 SSE (Server-Sent Events) 场景。
255
+
256
+ ```javascript
257
+ import { startEventStream, startEventStreamPOST } from '@vue2-client/services/api'
258
+
259
+ // GET 方式
260
+ const stop = startEventStream(
261
+ '/api/stream/data',
262
+ { query: 'test' },
263
+ {}, // headers
264
+ (data, eventType) => {
265
+ console.log('收到数据:', data, eventType)
266
+ },
267
+ (error) => {
268
+ console.error('错误:', error)
269
+ }
270
+ )
271
+
272
+ // 停止监听
273
+ stop()
274
+
275
+ // POST 方式
276
+ const stop = startEventStreamPOST('/api/stream/data', params, headers, onData, onError)
277
+ ```
278
+
279
+ ---
280
+
281
+ ## 六、最佳实践
282
+
283
+ ### 1. 表单提交场景
284
+
285
+ ```javascript
286
+ const { loading, execute } = usePost('/api/form/save', {
287
+ globalLoading: '提交中...',
288
+ dedupe: true // 防止重复提交
289
+ })
290
+
291
+ const handleSubmit = async () => {
292
+ const { success } = await execute(formData)
293
+ if (success) {
294
+ message.success('提交成功')
295
+ }
296
+ // 错误已由拦截器统一处理,无需额外处理
297
+ }
298
+ ```
299
+
300
+ ### 2. 列表查询场景
301
+
302
+ ```javascript
303
+ const { loading, data, execute } = usePost('/api/list/query')
304
+
305
+ const loadData = async () => {
306
+ const { success } = await execute({ page: 1, size: 10 })
307
+ if (success) {
308
+ tableData.value = data
309
+ }
310
+ }
311
+ ```
312
+
313
+ ### 3. 业务逻辑调用
314
+
315
+ ```javascript
316
+ const { loading, execute } = useRunLogic('calculatePrice', {
317
+ globalLoading: '计算中...'
318
+ })
319
+
320
+ const calculate = async () => {
321
+ const { success, data } = await execute({ productId: 1, quantity: 10 })
322
+ if (success) {
323
+ price.value = data.totalPrice
324
+ }
325
+ }
326
+ ```
327
+
328
+ ### 4. 错误处理模式
329
+
330
+ ```javascript
331
+ const { execute } = usePost('/api/save')
332
+
333
+ // 模式一:只关心成功(推荐,错误由拦截器统一处理)
334
+ const handleSave = async () => {
335
+ const { success, data } = await execute(formData)
336
+ if (success) {
337
+ message.success('保存成功')
338
+ }
339
+ }
340
+
341
+ // 模式二:需要自定义错误处理
342
+ const handleSave = async () => {
343
+ const { success, data, error } = await execute(formData)
344
+ if (success) {
345
+ message.success('保存成功')
346
+ } else if (error?.code === 'ERR_DUPLICATE_REQUEST') {
347
+ // 重复请求,静默处理
348
+ } else {
349
+ // 自定义错误处理
350
+ message.error('保存失败: ' + error?.message)
351
+ }
352
+ }
353
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-client",
3
- "version": "1.18.38",
3
+ "version": "1.18.40",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve --no-eslint",
@@ -23,6 +23,12 @@ export default defineComponent({
23
23
  components: { XFormDatePicker },
24
24
  setup () {
25
25
  const formValues = reactive({
26
+ '时间选择器-新增模式': null,
27
+ '时间选择器-查询EQUALS': null,
28
+ '时间选择器-查询BETWEEN': null,
29
+ '时间范围选择器-新增模式': null,
30
+ '时间范围选择器-查询BETWEEN': null,
31
+
26
32
  '单日选择器-新增模式': null,
27
33
  '单日选择器-查询EQUALS': null,
28
34
  '单日选择器-查询BETWEEN': null,
@@ -49,6 +55,31 @@ export default defineComponent({
49
55
  }])
50
56
 
51
57
  const all = reactive([
58
+ {
59
+ title: '时间选择器-新增模式',
60
+ attr: { type: 'timePicker' },
61
+ mode: '新增'
62
+ },
63
+ {
64
+ title: '时间选择器-查询EQUALS',
65
+ attr: { type: 'timePicker', queryType: 'EQUALS' },
66
+ mode: '查询'
67
+ },
68
+ {
69
+ title: '时间选择器-查询BETWEEN',
70
+ attr: { type: 'timePicker', queryType: 'BETWEEN' },
71
+ mode: '查询'
72
+ },
73
+ {
74
+ title: '时间范围选择器-新增模式',
75
+ attr: { type: 'timeRangePicker' },
76
+ mode: '新增'
77
+ },
78
+ {
79
+ title: '时间范围选择器-查询BETWEEN',
80
+ attr: { type: 'timeRangePicker', queryType: 'BETWEEN' },
81
+ mode: '查询'
82
+ },
52
83
  {
53
84
  title: '单日选择器-新增模式',
54
85
  attr: { type: 'datePicker' },
@@ -94,6 +94,26 @@ export default {
94
94
  this.yearShowOne = false
95
95
  },
96
96
  },
97
+ // 时间范围选择器的开始时间
98
+ timeRangeStartValue: {
99
+ get () {
100
+ return Array.isArray(this.value) && this.value.length > 0 ? this.value[0] : undefined
101
+ },
102
+ set (val) {
103
+ const end = (Array.isArray(this.value) && this.value.length > 1) ? this.value[1] : ''
104
+ this.$emit('change', [val || '', end])
105
+ },
106
+ },
107
+ // 时间范围选择器的结束时间
108
+ timeRangeEndValue: {
109
+ get () {
110
+ return Array.isArray(this.value) && this.value.length > 1 ? this.value[1] : undefined
111
+ },
112
+ set (val) {
113
+ const start = (Array.isArray(this.value) && this.value.length > 0) ? this.value[0] : ''
114
+ this.$emit('change', [start, val || ''])
115
+ },
116
+ },
97
117
  // 格式化类型
98
118
  formatType () {
99
119
  let defaultFormat = 'YYYY-MM-DD HH:mm:ss'
@@ -111,6 +131,8 @@ export default {
111
131
  defaultFormat = 'YYYY-MM'
112
132
  } else if (['yearRangePicker', 'yearPicker'].includes(this.attr.type)) {
113
133
  defaultFormat = 'YYYY'
134
+ } else if (['timePicker', 'timeRangePicker'].includes(this.attr.type)) {
135
+ defaultFormat = 'HH:mm:ss'
114
136
  }
115
137
  return (
116
138
  (this.mode === '查询' && this.attr.queryValueFormat
@@ -146,6 +168,7 @@ export default {
146
168
  rangePicker: ['开始日期', '结束日期'],
147
169
  monthRangePicker: ['开始月份', '结束月份'],
148
170
  yearRangePicker: ['开始年份', '结束年份'],
171
+ timeRangePicker: ['开始时间', '结束时间'],
149
172
  }
150
173
  return placeholders[this.attr.type] || ['开始', '结束']
151
174
  },
@@ -256,6 +279,44 @@ export default {
256
279
  "
257
280
  style="width: 100%"
258
281
  />
282
+ <!-- 时间选择器 -->
283
+ <a-time-picker
284
+ v-else-if="attr.type === 'timePicker'"
285
+ :getCalendarContainer="enablePopupToBody? undefined :getCalendarContainer"
286
+ :getPopupContainer="enablePopupToBody? getBodyContainer : undefined"
287
+ v-model="localValue"
288
+ :disabled="disabled || readOnly"
289
+ style="width: 100%"
290
+ placeholder="请选择时间"
291
+ :format="formatType"
292
+ :valueFormat="formatType"
293
+ />
294
+ <!-- 时间范围选择器 -->
295
+ <div
296
+ v-else-if="attr.type === 'timeRangePicker'"
297
+ style="display: flex; align-items: center; gap: 8px; width: 100%">
298
+ <a-time-picker
299
+ :getCalendarContainer="enablePopupToBody? undefined :getCalendarContainer"
300
+ :getPopupContainer="enablePopupToBody? getBodyContainer : undefined"
301
+ v-model="timeRangeStartValue"
302
+ :disabled="disabled || readOnly"
303
+ style="flex: 1"
304
+ :placeholder="placeholder[0]"
305
+ :format="formatType"
306
+ :valueFormat="formatType"
307
+ />
308
+ <span style="color: rgba(0, 0, 0, 0.45)"> - </span>
309
+ <a-time-picker
310
+ :getCalendarContainer="enablePopupToBody? undefined :getCalendarContainer"
311
+ :getPopupContainer="enablePopupToBody? getBodyContainer : undefined"
312
+ v-model="timeRangeEndValue"
313
+ :disabled="disabled || readOnly"
314
+ style="flex: 1"
315
+ :placeholder="placeholder[1]"
316
+ :format="formatType"
317
+ :valueFormat="formatType"
318
+ />
319
+ </div>
259
320
  <!-- 单日范围选择器 / 月份范围选择器/ 年份范围选择器 -->
260
321
  <a-range-picker
261
322
  v-else-if="
@@ -302,6 +302,8 @@ export default {
302
302
  monthRangePicker: 'YYYY-MM',
303
303
  datePicker: 'YYYY-MM-DD',
304
304
  rangePicker: 'YYYY-MM-DD HH:mm:ss',
305
+ timePicker: 'HH:mm:ss',
306
+ timeRangePicker: 'HH:mm:ss',
305
307
  }
306
308
  let format = formatMap[type]
307
309
 
@@ -331,7 +333,7 @@ export default {
331
333
  default:
332
334
  return undefined
333
335
  }
334
- if (['monthPicker', 'yearPicker', 'datePicker'].includes(type)) {
336
+ if (['monthPicker', 'yearPicker', 'datePicker', 'timePicker'].includes(type)) {
335
337
  if (queryType === 'BETWEEN') {
336
338
  return [start, end]
337
339
  }
@@ -340,7 +342,7 @@ export default {
340
342
  } else {
341
343
  return end
342
344
  }
343
- } else if (['rangePicker', 'yearRangePicker', 'monthRangePicker'].includes(type)) {
345
+ } else if (['rangePicker', 'yearRangePicker', 'monthRangePicker', 'timeRangePicker'].includes(type)) {
344
346
  return [start, end]
345
347
  }
346
348
  return [start, end]
@@ -348,7 +350,7 @@ export default {
348
350
  setFormProps (formData, item) {
349
351
  formData[item.model] = undefined
350
352
  if (item.queryFormDefault) {
351
- if (['datePicker', 'rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker'].includes(item.type)) {
353
+ if (['datePicker', 'rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker', 'timePicker', 'timeRangePicker'].includes(item.type)) {
352
354
  formData[item.model] = this.getDateRange(item)
353
355
  } else if (['treeSelect', 'select', 'checkbox'].includes(item.type) && ['curOrgId', 'curDepId', 'curUserId'].includes(item.queryFormDefault)) {
354
356
  if (item.queryFormDefault === 'curOrgId') {
@@ -383,7 +383,7 @@
383
383
  </x-form-col>
384
384
  <!-- 时间 日期 框整合 -->
385
385
  <x-form-col
386
- v-else-if="['datePicker', 'rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker'].includes(attr.type) && show"
386
+ v-else-if="['datePicker', 'rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker', 'timePicker', 'timeRangePicker'].includes(attr.type) && show"
387
387
  :labelCol="labelCol"
388
388
  :flex="attr.flex">
389
389
  <a-form-model-item