gd-web-core 0.0.9 → 0.0.11
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/README.md +111 -443
- package/docs/utils/index.md +6 -4
- package/docs/utils/request.md +174 -0
- package/package.json +52 -27
- package/src/components/STable/STable.vue +1 -1
- package/src/components/SliderCaptcha/SliderCaptcha.vue +3 -3
- package/src/components/SliderCaptcha/SliderCaptchaContent.vue +0 -6
- package/src/components/SliderCaptcha/apis.ts +3 -3
- package/src/components/SliderCaptcha/useSliderCaptcha.ts +2 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/request.ts +320 -0
- package/LICENSE +0 -21
- package/src/env.d.ts +0 -7
package/README.md
CHANGED
|
@@ -1,443 +1,111 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Vue 3
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# 输出示例
|
|
114
|
-
# index.js 150KB (ES Module, 支持 tree-shaking)
|
|
115
|
-
# index.umd.cjs 180KB (UMD, 不支持 tree-shaking)
|
|
116
|
-
# index.d.ts 50KB (TypeScript 类型定义)
|
|
117
|
-
# style.css 20KB (所有组件样式)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## 组件
|
|
121
|
-
|
|
122
|
-
### Echarts 图表
|
|
123
|
-
|
|
124
|
-
需要安装 `echarts` 依赖。
|
|
125
|
-
|
|
126
|
-
```vue
|
|
127
|
-
<template>
|
|
128
|
-
<Echarts :options="chartOptions" height="400px" @click="handleClick" />
|
|
129
|
-
</template>
|
|
130
|
-
|
|
131
|
-
<script setup lang="ts">
|
|
132
|
-
import { Echarts } from 'gd-web-core'
|
|
133
|
-
|
|
134
|
-
const chartOptions = {
|
|
135
|
-
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
|
|
136
|
-
yAxis: { type: 'value' },
|
|
137
|
-
series: [{ data: [120, 200, 150], type: 'bar' }]
|
|
138
|
-
}
|
|
139
|
-
</script>
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
143
|
-
|------|------|--------|------|
|
|
144
|
-
| options | `EChartsOption` | 必填 | ECharts 配置项 |
|
|
145
|
-
| width | `string` | `'100%'` | 宽度 |
|
|
146
|
-
| height | `string` | `'300px'` | 高度 |
|
|
147
|
-
| theme | `string` | `''` | 主题 |
|
|
148
|
-
|
|
149
|
-
### ScrollContainer 滚动容器
|
|
150
|
-
|
|
151
|
-
自定义滚动条组件。
|
|
152
|
-
|
|
153
|
-
```vue
|
|
154
|
-
<template>
|
|
155
|
-
<ScrollContainer height="300px" direction="y">
|
|
156
|
-
<div>滚动内容...</div>
|
|
157
|
-
</ScrollContainer>
|
|
158
|
-
</template>
|
|
159
|
-
|
|
160
|
-
<script setup lang="ts">
|
|
161
|
-
import { ScrollContainer } from 'gd-web-core'
|
|
162
|
-
</script>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
166
|
-
|------|------|--------|------|
|
|
167
|
-
| height | `string \| number` | `'100%'` | 高度 |
|
|
168
|
-
| maxHeight | `string \| number` | - | 最大高度 |
|
|
169
|
-
| direction | `'x' \| 'y' \| 'both'` | `'y'` | 滚动方向 |
|
|
170
|
-
|
|
171
|
-
### STable 表格
|
|
172
|
-
|
|
173
|
-
需要安装 `@surely-vue/table` 和 `ant-design-vue` 依赖。
|
|
174
|
-
|
|
175
|
-
```vue
|
|
176
|
-
<template>
|
|
177
|
-
<STable :columns="columns" :data="loadData" />
|
|
178
|
-
</template>
|
|
179
|
-
|
|
180
|
-
<script setup lang="ts">
|
|
181
|
-
import { STable } from 'gd-web-core'
|
|
182
|
-
|
|
183
|
-
const columns = [
|
|
184
|
-
{ title: '姓名', dataIndex: 'name' },
|
|
185
|
-
{ title: '年龄', dataIndex: 'age' }
|
|
186
|
-
]
|
|
187
|
-
|
|
188
|
-
const loadData = async (params) => {
|
|
189
|
-
const res = await fetchData(params)
|
|
190
|
-
return { data: res.list, total: res.total }
|
|
191
|
-
}
|
|
192
|
-
</script>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 工具函数
|
|
196
|
-
|
|
197
|
-
### 日期格式化
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
import { formatDate, dateFormat } from 'gd-web-core'
|
|
201
|
-
|
|
202
|
-
formatDate(new Date()) // '2026-02-02 10:30:00'
|
|
203
|
-
formatDate('2026-01-01', 'YYYY/MM/DD') // '2026/01/01'
|
|
204
|
-
dateFormat('YY-mm-dd HH:MM:SS', new Date()) // '2026-02-02 10:30:00'
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### 数字处理
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
import { formatNumber, formatFileSize, toFixed, round } from 'gd-web-core'
|
|
211
|
-
|
|
212
|
-
formatNumber(1234567.89) // '1,234,567.89'
|
|
213
|
-
formatFileSize(1048576) // '1 MB'
|
|
214
|
-
toFixed(3.1415926, 2) // 3.14 (不四舍五入)
|
|
215
|
-
round(0.7 * 3, 2) // 2.1 (四舍五入,精度安全)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Storage 封装
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
import { storage, createStorage } from 'gd-web-core'
|
|
222
|
-
|
|
223
|
-
storage.set('token', 'xxx', 3600000) // 1小时后过期
|
|
224
|
-
storage.get('token')
|
|
225
|
-
|
|
226
|
-
const myStorage = createStorage('session', { prefix: 'myapp' })
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### 防抖节流
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
import { debounce, throttle, createDebounce } from 'gd-web-core'
|
|
233
|
-
|
|
234
|
-
// Promise 防抖
|
|
235
|
-
await debounce('search', 300)
|
|
236
|
-
|
|
237
|
-
// 节流函数
|
|
238
|
-
const throttledFn = throttle(fn, 100)
|
|
239
|
-
|
|
240
|
-
// 创建防抖函数
|
|
241
|
-
const debouncedFn = createDebounce(fn, 300)
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### URL 参数
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
import { useUrlParams, updateUrlParams } from 'gd-web-core'
|
|
248
|
-
|
|
249
|
-
const params = useUrlParams()
|
|
250
|
-
updateUrlParams({ page: '2' })
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### DOM 操作
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
import { scrollView, setHtmlTitle, print } from 'gd-web-core'
|
|
257
|
-
|
|
258
|
-
scrollView('element-id')
|
|
259
|
-
setHtmlTitle('新标题')
|
|
260
|
-
await print('#print-area')
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### 加密
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
import { useRsa, useAes, disuseAes } from 'gd-web-core'
|
|
267
|
-
|
|
268
|
-
const encrypted = await useRsa('data', publicKey)
|
|
269
|
-
const aesEncrypted = await useAes('data', key)
|
|
270
|
-
const decrypted = await disuseAes(aesEncrypted, key)
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### 页面可见性
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
import { onVisibilityChange } from 'gd-web-core'
|
|
277
|
-
|
|
278
|
-
const unsubscribe = onVisibilityChange((hidden) => {
|
|
279
|
-
console.log('页面隐藏:', hidden)
|
|
280
|
-
})
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### Excel 导入导出
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
import { importExcel, importExcelAll, exportJsonToExcel } from 'gd-web-core'
|
|
287
|
-
|
|
288
|
-
// 导入
|
|
289
|
-
const data = await importExcel(file)
|
|
290
|
-
const allSheets = await importExcelAll(file)
|
|
291
|
-
|
|
292
|
-
// 导出
|
|
293
|
-
await exportJsonToExcel(['姓名', '年龄'], [['张三', 25]], '用户列表')
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### 后端数据解析
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
import { rowSet, pageRowSet } from 'gd-web-core'
|
|
300
|
-
|
|
301
|
-
const { colKey, data } = rowSet(result)
|
|
302
|
-
const { colKey, data, total } = pageRowSet(pageResult)
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
## 常见问题
|
|
306
|
-
|
|
307
|
-
### 为什么按需引入后打包体积还是很大?
|
|
308
|
-
|
|
309
|
-
1. **检查样式文件**:CSS 是全量加载的,确保只在入口文件导入一次
|
|
310
|
-
2. **检查导入方式**:避免使用 `import * as Core from 'gd-web-core'`
|
|
311
|
-
3. **检查生产模式**:确保打包工具运行在生产模式 (`NODE_ENV=production`)
|
|
312
|
-
4. **检查打包配置**:确认 Tree-shaking 已启用
|
|
313
|
-
|
|
314
|
-
### 组件依赖的第三方库会被打包吗?
|
|
315
|
-
|
|
316
|
-
不会。以下依赖被标记为 `external`,不会打包进库中:
|
|
317
|
-
- `vue` (必需)
|
|
318
|
-
- `vue-router` (可选)
|
|
319
|
-
- `echarts` (按需)
|
|
320
|
-
- `ant-design-vue` (按需)
|
|
321
|
-
- `@surely-vue/table` (按需)
|
|
322
|
-
|
|
323
|
-
用户需要在自己的项目中安装这些依赖。
|
|
324
|
-
|
|
325
|
-
### 可以只安装部分依赖吗?
|
|
326
|
-
|
|
327
|
-
可以。例如,如果不使用 `Echarts` 组件,无需安装 `echarts`:
|
|
328
|
-
|
|
329
|
-
```bash
|
|
330
|
-
# 最小安装
|
|
331
|
-
pnpm add gd-web-core vue
|
|
332
|
-
|
|
333
|
-
# 使用 Echarts 组件
|
|
334
|
-
pnpm add echarts
|
|
335
|
-
|
|
336
|
-
# 使用 STable 组件
|
|
337
|
-
pnpm add ant-design-vue @surely-vue/table
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### TypeScript 类型定义支持按需引用吗?
|
|
341
|
-
|
|
342
|
-
完全支持。所有类型定义都会正确推导:
|
|
343
|
-
|
|
344
|
-
```ts
|
|
345
|
-
import { formatDate } from 'gd-web-core'
|
|
346
|
-
// formatDate 函数会有完整的类型提示
|
|
347
|
-
|
|
348
|
-
import type { EchartsProps } from 'gd-web-core'
|
|
349
|
-
// 可以单独导入类型
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
## 目录结构
|
|
353
|
-
|
|
354
|
-
```
|
|
355
|
-
src/
|
|
356
|
-
├── components/
|
|
357
|
-
│ ├── Echarts/
|
|
358
|
-
│ ├── ScrollContainer/
|
|
359
|
-
│ └── STable/
|
|
360
|
-
├── utils/
|
|
361
|
-
│ ├── format.ts
|
|
362
|
-
│ ├── storage.ts
|
|
363
|
-
│ ├── loadRemote.ts
|
|
364
|
-
│ ├── dom.ts
|
|
365
|
-
│ ├── url.ts
|
|
366
|
-
│ ├── number.ts
|
|
367
|
-
│ ├── async.ts
|
|
368
|
-
│ ├── crypto.ts
|
|
369
|
-
│ ├── visibility.ts
|
|
370
|
-
│ ├── rowSet.ts
|
|
371
|
-
│ └── xlsx.ts
|
|
372
|
-
├── assets/
|
|
373
|
-
└── index.ts
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
## 📚 文档
|
|
377
|
-
|
|
378
|
-
完整文档请查看 [docs/](./docs/) 目录:
|
|
379
|
-
|
|
380
|
-
- **快速开始**
|
|
381
|
-
- [安装指南](./docs/getting-started/installation.md)
|
|
382
|
-
- [快速上手](./docs/getting-started/quick-start.md)
|
|
383
|
-
|
|
384
|
-
- **进阶指南**
|
|
385
|
-
- [按需引用](./docs/guides/tree-shaking.md) - Tree-shaking 优化
|
|
386
|
-
- [TypeScript 支持](./docs/guides/typescript.md)
|
|
387
|
-
- [最佳实践](./docs/guides/best-practices.md)
|
|
388
|
-
|
|
389
|
-
- **发布和部署** ⭐
|
|
390
|
-
- [发布流程完整指南](./docs/deployment/publishing.md)
|
|
391
|
-
- [版本管理规范](./docs/deployment/versioning.md)
|
|
392
|
-
- [发布检查清单](./docs/deployment/checklist.md)
|
|
393
|
-
- [CI/CD 集成](./docs/deployment/ci-cd.md)
|
|
394
|
-
|
|
395
|
-
- **开发指南**
|
|
396
|
-
- [开发环境搭建](./docs/development/setup.md)
|
|
397
|
-
- [贡献指南](./docs/development/contributing.md)
|
|
398
|
-
|
|
399
|
-
## 🚀 开发
|
|
400
|
-
|
|
401
|
-
### 快速开始
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
# 安装依赖
|
|
405
|
-
pnpm install
|
|
406
|
-
|
|
407
|
-
# 启动开发服务器(带 playground 预览)
|
|
408
|
-
pnpm dev
|
|
409
|
-
|
|
410
|
-
# 构建库
|
|
411
|
-
pnpm build
|
|
412
|
-
|
|
413
|
-
# 发布到 npm
|
|
414
|
-
pnpm release:patch # 或 minor、major
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### 发布流程
|
|
418
|
-
|
|
419
|
-
详细的发布指南请查看 [发布流程文档](./docs/deployment/publishing.md)
|
|
420
|
-
|
|
421
|
-
**快速发布**:
|
|
422
|
-
```bash
|
|
423
|
-
# 1. 构建
|
|
424
|
-
pnpm build
|
|
425
|
-
|
|
426
|
-
# 2. 发布(自动更新版本号)
|
|
427
|
-
pnpm release:patch # Bug 修复 (0.0.1 -> 0.0.2)
|
|
428
|
-
pnpm release:minor # 新功能 (0.0.2 -> 0.1.0)
|
|
429
|
-
pnpm release:major # 破坏性变更 (0.1.0 -> 1.0.0)
|
|
430
|
-
|
|
431
|
-
# 3. 推送 tag
|
|
432
|
-
git push origin main --tags
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### 目录说明
|
|
436
|
-
|
|
437
|
-
- `src/` - 库源代码
|
|
438
|
-
- `playground/` - 开发预览应用
|
|
439
|
-
- `dist/` - 构建输出
|
|
440
|
-
|
|
441
|
-
## License
|
|
442
|
-
|
|
443
|
-
MIT
|
|
1
|
+
# Vue 3 + TypeScript + Ant Design Vue + Vite 管理系统
|
|
2
|
+
|
|
3
|
+
这是一个基于 Vue 3、TypeScript、Vite 和 Ant Design Vue 构建的后台管理系统模板。项目集成了 Pinia 状态管理、Vue Router 路由控制以及 ESLint/Prettier 代码规范工具,旨在提供一个快速、高效的开发起点。
|
|
4
|
+
|
|
5
|
+
## 📚 文档
|
|
6
|
+
|
|
7
|
+
* [**脚手架配置 (Scaffold)**](./docs/脚手架配置.md): 统一封装了Vite、代理、插件等配置。
|
|
8
|
+
* [**路由说明 (Router)**](./docs/路由说明.md): 详细介绍了路由配置、权限控制及新增页面的方法。
|
|
9
|
+
* [**开发服务器 (DevServer)**](./docs/开发服务器.md): 介绍代理配置、BaseURL 注入及运行时配置原理。
|
|
10
|
+
* [**基座组件 (Core)**](./docs/基座组件.md): 介绍基座组件的设计理念、使用方法及注意事项。
|
|
11
|
+
* [**全局水印 (Watermark)**](./docs/全局水印.md): 介绍全局水印功能的开启、配置与实现原理。
|
|
12
|
+
|
|
13
|
+
## 🛠️ 技术栈
|
|
14
|
+
|
|
15
|
+
* **框架**: [Vue 3](https://vuejs.org/) (Composition API)
|
|
16
|
+
* **语言**: TypeScript
|
|
17
|
+
* **构建工具**: [Vite](https://vitejs.dev/)
|
|
18
|
+
* **UI 组件库**: [Ant Design Vue](https://antdv.com/)
|
|
19
|
+
* **状态管理**: [Pinia](https://pinia.vuejs.org/)
|
|
20
|
+
* **路由管理**: [Vue Router](https://router.vuejs.org/)
|
|
21
|
+
* **CSS 预处理**: Less
|
|
22
|
+
* **HTTP 请求**: Axios
|
|
23
|
+
* **代码规范**: ESLint + Prettier
|
|
24
|
+
|
|
25
|
+
## 🚀 快速开始
|
|
26
|
+
|
|
27
|
+
### 环境要求
|
|
28
|
+
|
|
29
|
+
* **Node.js**: version **20+**
|
|
30
|
+
* **Package Manager**: **pnpm**
|
|
31
|
+
|
|
32
|
+
推荐使用 `nvm` 管理 Node 版本:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
nvm use 20
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 安装依赖
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 开发环境运行
|
|
45
|
+
|
|
46
|
+
启动本地开发服务器,支持热重载:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 生产环境构建
|
|
53
|
+
|
|
54
|
+
编译并压缩代码用于生产环境:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## ⚙️ 配置说明
|
|
61
|
+
|
|
62
|
+
### 开发服务器代理
|
|
63
|
+
|
|
64
|
+
`devServer.config.ts` 用于配置开发环境的代理设置。
|
|
65
|
+
|
|
66
|
+
示例:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const proxy = {
|
|
70
|
+
// Key: 代理路径前缀 (也是开发模式下的 baseUrl)
|
|
71
|
+
// Value: 目标 API 服务器地址
|
|
72
|
+
respj: 'http://dddev.yixx.cn/respj',
|
|
73
|
+
};
|
|
74
|
+
export default proxy;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
在 `pnpm dev` 模式下,第一个 key (如 `respj`) 将自动作为 `baseUrl`。
|
|
78
|
+
|
|
79
|
+
## 📂 目录结构
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
project-root/
|
|
83
|
+
├── core/ # 基座组件:提供通用组件、函数、服务
|
|
84
|
+
│ ├── components/ # 通用组件
|
|
85
|
+
├── src/ # 业务逻辑目录
|
|
86
|
+
│ ├── api/ # 业务 API 接口 (TypeScript)
|
|
87
|
+
│ ├── assets/ # 静态资源
|
|
88
|
+
│ ├── components/ # 业务组件
|
|
89
|
+
│ ├── layout/ # 布局组件 (BasicLayout, TopLayout, SideLayout)
|
|
90
|
+
│ ├── router/ # 路由配置 (index.ts, permission.ts)
|
|
91
|
+
│ ├── store/ # 状态管理 (User, Menu, Setting, Permission,均为 .ts)
|
|
92
|
+
│ ├── styles/ # 全局样式
|
|
93
|
+
│ │ ├── variables.less # 变量
|
|
94
|
+
│ │ ├── global.less # 全局样式
|
|
95
|
+
│ │ └── index.less # 样式入口
|
|
96
|
+
│ ├── utils/ # 工具函数 (request.ts 等)
|
|
97
|
+
│ ├── views/ # 页面视图
|
|
98
|
+
│ │ ├── home/ # 首页
|
|
99
|
+
│ │ ├── login/ # 登录页
|
|
100
|
+
│ │ └── system/ # 系统管理页
|
|
101
|
+
│ ├── App.vue # 根组件
|
|
102
|
+
│ └── main.ts # 入口文件
|
|
103
|
+
├── devServer.config.ts # 开发环境代理配置
|
|
104
|
+
├── vite.config.ts # Vite 配置 (TypeScript)
|
|
105
|
+
├── tsconfig.json # TypeScript 主配置
|
|
106
|
+
└── tsconfig.node.json # Node 相关 TypeScript 配置
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 💻 IDE 支持
|
|
110
|
+
|
|
111
|
+
推荐使用 [VS Code](https://code.visualstudio.com/) 配合 [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 插件以获得最佳的 Vue 3 开发体验。
|
package/docs/utils/index.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# 工具函数索引
|
|
2
2
|
|
|
3
|
-
> 📅 索引生成时间:2026-
|
|
3
|
+
> 📅 索引生成时间:2026-03-27
|
|
4
4
|
|
|
5
5
|
## 文档列表
|
|
6
6
|
|
|
7
7
|
| 文档 | 描述 | 状态 |
|
|
8
8
|
|------|------|------|
|
|
9
9
|
| [async](./async.md) | 防抖、节流等异步控制函数 | ✅ |
|
|
10
|
+
| [request](./request.md) | HTTP 请求封装,支持自定义消息组件 | ✅ |
|
|
10
11
|
| [crypto](./crypto.md) | RSA/AES 加密解密工具 | ✅ |
|
|
11
12
|
| [dom](./dom.md) | DOM 操作(滚动、打印、标题) | ✅ |
|
|
12
13
|
| [format](./format.md) | 日期、数字、文件大小格式化 | ✅ |
|
|
@@ -20,14 +21,14 @@
|
|
|
20
21
|
|
|
21
22
|
## 分类统计
|
|
22
23
|
|
|
23
|
-
- 总数量:
|
|
24
|
+
- 总数量:12
|
|
24
25
|
- 需要更新:0
|
|
25
26
|
|
|
26
27
|
## 快速导入
|
|
27
28
|
|
|
28
29
|
```typescript
|
|
29
30
|
// 导入所有工具函数
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
31
32
|
debounce, throttle, createDebounce,
|
|
32
33
|
useRsa, useAes, disuseAes,
|
|
33
34
|
scrollView, setHtmlTitle, print,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
createStorage, storage,
|
|
39
40
|
useUrlParams, updateUrlParams,
|
|
40
41
|
onVisibilityChange,
|
|
41
|
-
importExcel, importExcelAll, exportJsonToExcel, exportTableToExcel, tableToExcel, exportMultipleTableToExcel
|
|
42
|
+
importExcel, importExcelAll, exportJsonToExcel, exportTableToExcel, tableToExcel, exportMultipleTableToExcel,
|
|
43
|
+
request, configureRequest, getRequestOptions
|
|
42
44
|
} from 'gd-web-core/utils'
|
|
43
45
|
```
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# request
|
|
2
|
+
|
|
3
|
+
基于 Axios 的 HTTP 请求封装,支持自定义消息组件和响应处理。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
提供统一的 HTTP 请求拦截器、响应处理、错误提示和登录超时处理。默认使用 ant-design-vue 的消息组件,支持消费者自定义。
|
|
8
|
+
|
|
9
|
+
## 导入方式
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import request, { configureRequest, getRequestOptions } from 'gd-web-core/utils'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 配置函数
|
|
16
|
+
|
|
17
|
+
### configureRequest
|
|
18
|
+
|
|
19
|
+
全局配置请求实例。
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
configureRequest(options: RequestOptions): void
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**RequestOptions 参数**
|
|
26
|
+
|
|
27
|
+
| 参数名 | 类型 | 默认值 | 描述 |
|
|
28
|
+
|--------|------|--------|------|
|
|
29
|
+
| message | MessageHandler | ant-design-vue | 自定义消息组件 |
|
|
30
|
+
| autoSuccessMsg | boolean | true | 是否自动显示成功提示 |
|
|
31
|
+
| loginUrl | string | '#/login' | 登录跳转路径 |
|
|
32
|
+
| transformResponse | (res, config) => any | - | 自定义响应处理 |
|
|
33
|
+
|
|
34
|
+
### getRequestOptions
|
|
35
|
+
|
|
36
|
+
获取当前全局配置。
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
getRequestOptions(): RequestOptions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 接口定义
|
|
43
|
+
|
|
44
|
+
### RequestConfig
|
|
45
|
+
|
|
46
|
+
扩展的 Axios 请求配置。
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export interface RequestConfig extends AxiosRequestConfig {
|
|
50
|
+
hideErrMsg?: boolean; // 是否隐藏错误提示
|
|
51
|
+
hideLoading?: boolean; // 是否隐藏 loading
|
|
52
|
+
useToken?: boolean; // 是否使用 token
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### MessageHandler
|
|
57
|
+
|
|
58
|
+
消息处理组件接口。
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export interface MessageHandler {
|
|
62
|
+
loading: (content: string, duration?: number) => (() => void) | undefined;
|
|
63
|
+
success: (options: { content: string; duration?: number; onClose?: () => void }) => void;
|
|
64
|
+
error: (options: { content: string; duration?: number; onClose?: () => void }) => void;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### ResponseData
|
|
69
|
+
|
|
70
|
+
响应数据结构。
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
export interface ResponseData {
|
|
74
|
+
error?: { code: number; message?: string };
|
|
75
|
+
result?: { code: number; msg?: string };
|
|
76
|
+
[key: string]: any;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 使用示例
|
|
81
|
+
|
|
82
|
+
### 基础使用
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import request from 'gd-web-core/utils'
|
|
86
|
+
|
|
87
|
+
// GET 请求
|
|
88
|
+
const data = await request.get('/api/user', { hideErrMsg: true })
|
|
89
|
+
|
|
90
|
+
// POST 请求
|
|
91
|
+
const result = await request.post('/api/submit', { name: 'test' })
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 自定义消息组件
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import request, { configureRequest } from 'gd-web-core/utils'
|
|
98
|
+
|
|
99
|
+
configureRequest({
|
|
100
|
+
message: {
|
|
101
|
+
loading: (content) => {
|
|
102
|
+
console.log('loading:', content)
|
|
103
|
+
return () => console.log('close loading')
|
|
104
|
+
},
|
|
105
|
+
success: ({ content }) => console.log('success:', content),
|
|
106
|
+
error: ({ content }) => console.log('error:', content),
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 自定义响应处理
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import request, { configureRequest } from 'gd-web-core/utils'
|
|
115
|
+
|
|
116
|
+
configureRequest({
|
|
117
|
+
transformResponse: (res, config) => {
|
|
118
|
+
// 自定义处理逻辑
|
|
119
|
+
if (config.url?.includes('/custom')) {
|
|
120
|
+
return res.data
|
|
121
|
+
}
|
|
122
|
+
return res
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 禁用成功提示
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// 方式1:全局禁用
|
|
131
|
+
configureRequest({
|
|
132
|
+
autoSuccessMsg: false
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// 方式2:单个请求禁用
|
|
136
|
+
request.post('/api/submit', { data: 1 }, { hideErrMsg: true })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 响应数据结构
|
|
140
|
+
|
|
141
|
+
请求成功时返回的响应结构:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// 业务错误(error 字段)
|
|
145
|
+
{
|
|
146
|
+
error: {
|
|
147
|
+
code: -403,
|
|
148
|
+
message: '会话已超时'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 操作结果(result 字段)
|
|
153
|
+
{
|
|
154
|
+
result: {
|
|
155
|
+
code: 0, // >= 0 表示成功
|
|
156
|
+
msg: '操作成功'
|
|
157
|
+
},
|
|
158
|
+
data: {...} // 业务数据
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 正常返回
|
|
162
|
+
{
|
|
163
|
+
data: {...} // 业务数据
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 错误处理
|
|
168
|
+
|
|
169
|
+
请求失败时会自动 reject Promise,并通过消息组件提示错误:
|
|
170
|
+
|
|
171
|
+
- **登录超时**:code === -403 或 message 包含"会话已超时",自动跳转登录页
|
|
172
|
+
- **业务错误**:显示 error.message
|
|
173
|
+
- **操作失败**:显示 result.msg
|
|
174
|
+
- **网络错误**:显示"请求超时"、"网络错误"等
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gd-web-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Vue 3 基座能力封装库 - 组件与工具函数",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
-
"src",
|
|
8
|
-
"
|
|
7
|
+
"src/assets",
|
|
8
|
+
"src/components",
|
|
9
|
+
"src/utils",
|
|
10
|
+
"src/index.ts",
|
|
11
|
+
"docs/components",
|
|
12
|
+
"docs/utils",
|
|
13
|
+
"docs/INDEX.md"
|
|
9
14
|
],
|
|
10
15
|
"main": "./src/index.ts",
|
|
11
16
|
"module": "./src/index.ts",
|
|
@@ -34,6 +39,10 @@
|
|
|
34
39
|
"build:check": "vue-tsc --noEmit && vite build",
|
|
35
40
|
"build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist",
|
|
36
41
|
"preview": "vite preview",
|
|
42
|
+
"lint": "eslint . --fix",
|
|
43
|
+
"format": "prettier --write .",
|
|
44
|
+
"test": "vitest",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
37
46
|
"release:patch": "npm version patch && npm publish",
|
|
38
47
|
"release:minor": "npm version minor && npm publish",
|
|
39
48
|
"release:major": "npm version major && npm publish",
|
|
@@ -53,27 +62,6 @@
|
|
|
53
62
|
"ant-design-vue": "^4.0.0",
|
|
54
63
|
"echarts": "^5.0.0"
|
|
55
64
|
},
|
|
56
|
-
"devDependencies": {
|
|
57
|
-
"@types/node": "^25.2.0",
|
|
58
|
-
"@types/prismjs": "^1.26.5",
|
|
59
|
-
"@vitejs/plugin-vue": "^6.0.4",
|
|
60
|
-
"@vitest/ui": "^4.0.18",
|
|
61
|
-
"ant-design-vue": "^4.2.0",
|
|
62
|
-
"echarts": "^5.5.0",
|
|
63
|
-
"happy-dom": "^20.5.0",
|
|
64
|
-
"highlight.js": "^11.11.1",
|
|
65
|
-
"marked": "^17.0.1",
|
|
66
|
-
"typescript": "~5.6.0",
|
|
67
|
-
"unplugin-auto-import": "^21.0.0",
|
|
68
|
-
"unplugin-vue-components": "^31.0.0",
|
|
69
|
-
"vite": "^7.2.0",
|
|
70
|
-
"vite-plugin-css-injected-by-js": "^3.5.2",
|
|
71
|
-
"vite-plugin-dts": "^4.2.0",
|
|
72
|
-
"vitest": "^4.0.18",
|
|
73
|
-
"vue": "^3.5.0",
|
|
74
|
-
"vue-router": "^4.4.0",
|
|
75
|
-
"vue-tsc": "^2.1.0"
|
|
76
|
-
},
|
|
77
65
|
"keywords": [
|
|
78
66
|
"vue",
|
|
79
67
|
"vue3",
|
|
@@ -97,8 +85,45 @@
|
|
|
97
85
|
"dependencies": {
|
|
98
86
|
"@surely-vue/table": "4.1.9",
|
|
99
87
|
"axios": "^1.13.6",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
88
|
+
"dayjs": "^1.11.19",
|
|
89
|
+
"echarts": "^6.0.0",
|
|
90
|
+
"nprogress": "^0.2.0",
|
|
91
|
+
"pinia": "^3.0.4",
|
|
92
|
+
"prismjs": "^1.30.0"
|
|
93
|
+
},
|
|
94
|
+
"devDependencies": {
|
|
95
|
+
"@ant-design/icons-vue": "^7.0.1",
|
|
96
|
+
"@eslint/js": "^9.39.2",
|
|
97
|
+
"@types/node": "^22.10.6",
|
|
98
|
+
"@types/nprogress": "^0.2.3",
|
|
99
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
100
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
101
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
102
|
+
"@vitest/ui": "^4.0.18",
|
|
103
|
+
"ant-design-vue": "^4.2.6",
|
|
104
|
+
"echarts": "^5.5.0",
|
|
105
|
+
"eslint": "^9.39.2",
|
|
106
|
+
"eslint-config-prettier": "^10.1.8",
|
|
107
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
108
|
+
"eslint-plugin-vue": "^10.6.2",
|
|
109
|
+
"gd-vite-core": "^1.0.8",
|
|
110
|
+
"globals": "^17.0.0",
|
|
111
|
+
"happy-dom": "^20.5.0",
|
|
112
|
+
"highlight.js": "^11.11.1",
|
|
113
|
+
"less": "^4.5.1",
|
|
114
|
+
"marked": "^17.0.1",
|
|
115
|
+
"prettier": "^3.7.4",
|
|
116
|
+
"typescript": "~5.6.0",
|
|
117
|
+
"unplugin-auto-import": "^21.0.0",
|
|
118
|
+
"unplugin-vue-components": "^30.0.0",
|
|
119
|
+
"vite": "^7.2.4",
|
|
120
|
+
"vite-plugin-css-injected-by-js": "^3.5.2",
|
|
121
|
+
"vite-plugin-dts": "^4.2.0",
|
|
122
|
+
"vite-plugin-require-transform": "^1.0.21",
|
|
123
|
+
"vitest": "^4.0.18",
|
|
124
|
+
"vue": "^3.5.24",
|
|
125
|
+
"vue-eslint-parser": "^10.2.0",
|
|
126
|
+
"vue-router": "^4.6.4",
|
|
127
|
+
"vue-tsc": "^2.2.4"
|
|
103
128
|
}
|
|
104
129
|
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<script setup lang="ts">
|
|
32
32
|
import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue'
|
|
33
33
|
import type { Component } from 'vue'
|
|
34
|
-
import * as xlsx from '
|
|
34
|
+
import * as xlsx from '../../utils/xlsx'
|
|
35
35
|
import '@surely-vue/table/dist/index.css';
|
|
36
36
|
// import 'ant-design-vue/es/menu/style'
|
|
37
37
|
// import 'ant-design-vue/es/dropdown/style'
|
|
@@ -204,7 +204,7 @@ export interface SliderCaptchaProps {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
export interface SliderCaptchaEmits {
|
|
207
|
-
(e: 'success'): void
|
|
207
|
+
(e: 'success', x: number): void
|
|
208
208
|
(e: 'fail'): void
|
|
209
209
|
(e: 'refresh'): void
|
|
210
210
|
(e: 'close'): void
|
|
@@ -247,8 +247,8 @@ const {
|
|
|
247
247
|
} = useSliderCaptcha({
|
|
248
248
|
fetchImage: fetchImageFn,
|
|
249
249
|
verify: verifyFn,
|
|
250
|
-
onSuccess: () => {
|
|
251
|
-
emit('success')
|
|
250
|
+
onSuccess: (x: number) => {
|
|
251
|
+
emit('success', x)
|
|
252
252
|
if (isDialogMode.value) {
|
|
253
253
|
setTimeout(() => {
|
|
254
254
|
emit('close')
|
|
@@ -42,12 +42,6 @@ defineEmits<{
|
|
|
42
42
|
|
|
43
43
|
function onBgLoad(event: Event) {
|
|
44
44
|
img.value = event.target as HTMLImageElement
|
|
45
|
-
console.log('onBgLoad', {
|
|
46
|
-
width: img.value.naturalWidth,
|
|
47
|
-
height: img.value.naturalHeight,
|
|
48
|
-
currentWidth: img.value.width,
|
|
49
|
-
currentHeight: img.value.height
|
|
50
|
-
})
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
function onSliderLoad() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 内置接口 - 滑块验证码后端交互
|
|
3
3
|
* 注意:仅适用于 monorepo 或源码引用场景
|
|
4
4
|
*/
|
|
5
|
-
import request from '
|
|
5
|
+
import request from '../../utils/request'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 获取验证码图片
|
|
@@ -12,7 +12,7 @@ export async function defaultFetchImage(): Promise<{
|
|
|
12
12
|
sliderImage: string
|
|
13
13
|
y: number
|
|
14
14
|
}> {
|
|
15
|
-
const res = await request('/yzm/getSlideYzmInfo.jsmeb', {}, { hideErrMsg: true, hideLoading: true })
|
|
15
|
+
const res = await request.post('/yzm/getSlideYzmInfo.jsmeb', {}, { hideErrMsg: true, hideLoading: true })
|
|
16
16
|
const data = res.result
|
|
17
17
|
return {
|
|
18
18
|
bgImage: 'data:image/jpeg;base64,' + data.backgroundImg,
|
|
@@ -25,6 +25,6 @@ export async function defaultFetchImage(): Promise<{
|
|
|
25
25
|
* 验证滑块位置
|
|
26
26
|
*/
|
|
27
27
|
export async function defaultVerify(x: number): Promise<{ success: boolean }> {
|
|
28
|
-
const res = await request(
|
|
28
|
+
const res = await request.post('/yzm/check.jsmeb', [Math.round(x)], { hideErrMsg: true, hideLoading: true })
|
|
29
29
|
return res.result
|
|
30
30
|
}
|
|
@@ -4,7 +4,7 @@ import type { CaptchaData, VerifyResult } from './SliderCaptcha.vue'
|
|
|
4
4
|
export interface UseSliderCaptchaOptions {
|
|
5
5
|
fetchImage?: () => Promise<CaptchaData>
|
|
6
6
|
verify?: (x: number) => Promise<VerifyResult>
|
|
7
|
-
onSuccess?: () => void
|
|
7
|
+
onSuccess?: (x: number) => void
|
|
8
8
|
onFail?: () => void
|
|
9
9
|
getContainerWidth?: () => number
|
|
10
10
|
}
|
|
@@ -213,7 +213,7 @@ export function useSliderCaptcha(options: UseSliderCaptchaOptions) {
|
|
|
213
213
|
|
|
214
214
|
if (result) {
|
|
215
215
|
state.success = true
|
|
216
|
-
options.onSuccess?.()
|
|
216
|
+
options.onSuccess?.(state.offsetX)
|
|
217
217
|
} else {
|
|
218
218
|
// 验证失败:显示错误提示,等待1.5秒后再刷新,不显示刷新按钮
|
|
219
219
|
state.error = true
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import axios, {
|
|
2
|
+
type AxiosRequestConfig,
|
|
3
|
+
type AxiosInstance,
|
|
4
|
+
type AxiosError,
|
|
5
|
+
type InternalAxiosRequestConfig,
|
|
6
|
+
type AxiosResponse,
|
|
7
|
+
} from 'axios';
|
|
8
|
+
|
|
9
|
+
// 定义扩展的请求配置接口
|
|
10
|
+
export interface RequestConfig extends AxiosRequestConfig {
|
|
11
|
+
/** 是否隐藏错误提示 */
|
|
12
|
+
hideErrMsg?: boolean;
|
|
13
|
+
/** 是否隐藏 loading */
|
|
14
|
+
hideLoading?: boolean;
|
|
15
|
+
useToken?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 定义通用的响应结构
|
|
19
|
+
export type ApiResponse<T = any> = T;
|
|
20
|
+
|
|
21
|
+
// 消息组件接口
|
|
22
|
+
export interface MessageHandler {
|
|
23
|
+
loading: (content: string, duration?: number) => (() => void) | undefined;
|
|
24
|
+
success: (options: { content: string; duration?: number; onClose?: () => void }) => void;
|
|
25
|
+
error: (options: { content: string; duration?: number; onClose?: () => void }) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 响应错误结构
|
|
29
|
+
export interface ResponseError {
|
|
30
|
+
code: number;
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 响应结果结构
|
|
35
|
+
export interface ResponseResult {
|
|
36
|
+
code: number;
|
|
37
|
+
msg?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 响应数据结构
|
|
41
|
+
export interface ResponseData {
|
|
42
|
+
error?: ResponseError;
|
|
43
|
+
result?: ResponseResult;
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 登录错误码
|
|
48
|
+
const LOGIN_ERROR_CODES = [-403];
|
|
49
|
+
const LOGIN_ERROR_MESSAGES = ['会话已超时', 'Session is null'];
|
|
50
|
+
|
|
51
|
+
// 接口配置接口
|
|
52
|
+
export interface RequestOptions {
|
|
53
|
+
/** 消息处理组件,默认使用 ant-design-vue */
|
|
54
|
+
message?: MessageHandler;
|
|
55
|
+
/** 是否自动处理成功提示(result.code !== 0) */
|
|
56
|
+
autoSuccessMsg?: boolean;
|
|
57
|
+
/** 登录跳转路径 */
|
|
58
|
+
loginUrl?: string;
|
|
59
|
+
/** 自定义响应处理,返回值将作为最终响应数据 */
|
|
60
|
+
transformResponse?: (res: ResponseData, config: RequestConfig) => any;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface CustomAxiosInstance extends AxiosInstance {
|
|
64
|
+
<T = any>(config: RequestConfig): Promise<ApiResponse<T>>;
|
|
65
|
+
<T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
66
|
+
get<T = any, R = ApiResponse<T>>(url: string, config?: RequestConfig): Promise<R>;
|
|
67
|
+
post<T = any, R = ApiResponse<T>>(url: string, data?: any, config?: RequestConfig): Promise<R>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 请求队列,用于处理重复请求
|
|
71
|
+
const pendingMap = new Map<string, AbortController>();
|
|
72
|
+
|
|
73
|
+
let hideLoadingMessage: (() => void) | null = null;
|
|
74
|
+
const USEAUTHTOKEN = false; // 是否启动headers Authorization token
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 生成请求的唯一 key
|
|
78
|
+
*/
|
|
79
|
+
const getRequestKey = (config: InternalAxiosRequestConfig): string => {
|
|
80
|
+
if (config.params?._t) {
|
|
81
|
+
delete config.params._t;
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
config.method,
|
|
85
|
+
config.url,
|
|
86
|
+
typeof config.params === 'string' ? config.params : JSON.stringify(config.params || {}),
|
|
87
|
+
typeof config.data === 'string' ? config.data : JSON.stringify(config.data || {}),
|
|
88
|
+
].join('&');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 添加loading到队列
|
|
93
|
+
*/
|
|
94
|
+
const addPending = (config: InternalAxiosRequestConfig) => {
|
|
95
|
+
const key = getRequestKey(config);
|
|
96
|
+
if (pendingMap.has(key)) {
|
|
97
|
+
const controller = pendingMap.get(key);
|
|
98
|
+
controller?.abort();
|
|
99
|
+
pendingMap.delete(key);
|
|
100
|
+
}
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
config.signal = controller.signal;
|
|
103
|
+
pendingMap.set(key, controller);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 移除loading队列
|
|
108
|
+
*/
|
|
109
|
+
const removePending = (config: InternalAxiosRequestConfig) => {
|
|
110
|
+
const key = getRequestKey(config);
|
|
111
|
+
if (pendingMap.has(key)) {
|
|
112
|
+
pendingMap.delete(key);
|
|
113
|
+
}
|
|
114
|
+
// 关闭 Loading
|
|
115
|
+
if (pendingMap.size === 0 && hideLoadingMessage) {
|
|
116
|
+
hideLoadingMessage();
|
|
117
|
+
hideLoadingMessage = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const baseURL =
|
|
122
|
+
import.meta.env.MODE === 'development'
|
|
123
|
+
? (window as any).config?.baseUrl
|
|
124
|
+
: `/${location.pathname.split('/')[1]}`;
|
|
125
|
+
(window as any).basePath = (window as any).basePath ? (window as any).basePath : baseURL;
|
|
126
|
+
|
|
127
|
+
// 默认的 ant-design-vue 消息处理
|
|
128
|
+
let defaultMessage: MessageHandler | undefined;
|
|
129
|
+
|
|
130
|
+
const getDefaultMessage = (): MessageHandler => {
|
|
131
|
+
if (!defaultMessage) {
|
|
132
|
+
// 动态导入避免循环依赖
|
|
133
|
+
const antMessage = (window as any).antMessage;
|
|
134
|
+
if (antMessage) {
|
|
135
|
+
defaultMessage = {
|
|
136
|
+
loading: (content: string, duration = 200) => antMessage.loading(content, duration),
|
|
137
|
+
success: (options: { content: string; duration?: number; onClose?: () => void }) =>
|
|
138
|
+
antMessage.success(options),
|
|
139
|
+
error: (options: { content: string; duration?: number; onClose?: () => void }) =>
|
|
140
|
+
antMessage.error(options),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return defaultMessage!;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// 默认配置
|
|
148
|
+
let globalOptions: RequestOptions = {};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 配置请求实例
|
|
152
|
+
*/
|
|
153
|
+
export const configureRequest = (options: RequestOptions) => {
|
|
154
|
+
globalOptions = {
|
|
155
|
+
autoSuccessMsg: true,
|
|
156
|
+
loginUrl: '#/login',
|
|
157
|
+
...options,
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 获取当前配置
|
|
163
|
+
*/
|
|
164
|
+
export const getRequestOptions = (): RequestOptions => globalOptions;
|
|
165
|
+
|
|
166
|
+
// 创建 axios 实例
|
|
167
|
+
const request: CustomAxiosInstance = axios.create({
|
|
168
|
+
// 优先使用环境变量,否则回退到空字符串(相对路径)
|
|
169
|
+
baseURL: baseURL || '',
|
|
170
|
+
timeout: 30000,
|
|
171
|
+
method: 'post',
|
|
172
|
+
headers: {},
|
|
173
|
+
}) as CustomAxiosInstance;
|
|
174
|
+
|
|
175
|
+
// 获取当前配置的消息处理函数
|
|
176
|
+
const getMessage = (): MessageHandler => {
|
|
177
|
+
return globalOptions.message || getDefaultMessage();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 请求拦截器
|
|
181
|
+
request.interceptors.request.use(
|
|
182
|
+
(config: InternalAxiosRequestConfig & RequestConfig) => {
|
|
183
|
+
const message = getMessage();
|
|
184
|
+
if (!isFormData(config.data)) {
|
|
185
|
+
if (pendingMap.size === 0 && !config.hideLoading) {
|
|
186
|
+
hideLoadingMessage = message.loading('加载中...', 200);
|
|
187
|
+
}
|
|
188
|
+
addPending(config);
|
|
189
|
+
}
|
|
190
|
+
if (USEAUTHTOKEN) {
|
|
191
|
+
const token = config.useToken || localStorage.getItem('token');
|
|
192
|
+
if (token) {
|
|
193
|
+
if (!config.headers) {
|
|
194
|
+
config.headers = {} as any;
|
|
195
|
+
}
|
|
196
|
+
(config.headers as any).Authorization = `Bearer ${token}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 添加时间戳防止缓存
|
|
201
|
+
if (config.method?.toUpperCase() === 'POST' || config.method?.toUpperCase() === 'GET') {
|
|
202
|
+
config.params = {
|
|
203
|
+
...config.params,
|
|
204
|
+
_t: Date.now(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return config;
|
|
209
|
+
},
|
|
210
|
+
(error: AxiosError) => {
|
|
211
|
+
return Promise.reject(error);
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// 响应拦截器
|
|
216
|
+
request.interceptors.response.use(
|
|
217
|
+
(response: AxiosResponse<ResponseData>) => {
|
|
218
|
+
const { config } = response;
|
|
219
|
+
if (!isFormData(config.data)) {
|
|
220
|
+
removePending(config as InternalAxiosRequestConfig);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const res = response.data;
|
|
224
|
+
const message = getMessage();
|
|
225
|
+
|
|
226
|
+
// 自定义响应处理
|
|
227
|
+
if (globalOptions.transformResponse) {
|
|
228
|
+
return globalOptions.transformResponse(res, config as RequestConfig);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 处理业务错误(error 字段)
|
|
232
|
+
if (res.error) {
|
|
233
|
+
const { code, message: errMessage } = res.error;
|
|
234
|
+
|
|
235
|
+
// 检查是否是登录超时
|
|
236
|
+
const isLoginTimeout =
|
|
237
|
+
LOGIN_ERROR_CODES.includes(code) ||
|
|
238
|
+
LOGIN_ERROR_MESSAGES.some((msg) => errMessage?.includes(msg));
|
|
239
|
+
|
|
240
|
+
if (isLoginTimeout) {
|
|
241
|
+
message.error({
|
|
242
|
+
content: '登录超时,请重新登录',
|
|
243
|
+
duration: 2,
|
|
244
|
+
onClose: () => {
|
|
245
|
+
window.location.href = globalOptions.loginUrl || '#/login';
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
return Promise.reject(new Error('登录超时'));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 默认业务错误处理
|
|
252
|
+
if (!(config as RequestConfig).hideErrMsg) {
|
|
253
|
+
const msg = errMessage?.replace(/0:java.lang.Exception:/g, '') || '请求失败';
|
|
254
|
+
message.error({ content: msg });
|
|
255
|
+
}
|
|
256
|
+
return Promise.reject(new Error(errMessage || '请求失败'));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 处理 result 结构
|
|
260
|
+
if (res.result) {
|
|
261
|
+
const { code, msg } = res.result;
|
|
262
|
+
|
|
263
|
+
// 成功
|
|
264
|
+
if (code >= 0) {
|
|
265
|
+
if (globalOptions.autoSuccessMsg && msg && code !== 1002 && !(config as RequestConfig).hideErrMsg) {
|
|
266
|
+
// code === 0 时保持静默,其它正数弹出成功提示
|
|
267
|
+
if (code !== 0) {
|
|
268
|
+
message.success({ content: msg });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return res;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 失败
|
|
275
|
+
if (code < 0) {
|
|
276
|
+
if (!(config as RequestConfig).hideErrMsg) {
|
|
277
|
+
message.error({ content: msg || '操作失败' });
|
|
278
|
+
}
|
|
279
|
+
return Promise.reject(new Error(msg || '操作失败'));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return res;
|
|
284
|
+
},
|
|
285
|
+
(error: AxiosError) => {
|
|
286
|
+
const message = getMessage();
|
|
287
|
+
// 移除 pending
|
|
288
|
+
if (error.config) {
|
|
289
|
+
if (!isFormData(error.config.data)) {
|
|
290
|
+
removePending(error.config as InternalAxiosRequestConfig);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 处理取消请求
|
|
295
|
+
if (axios.isCancel(error)) {
|
|
296
|
+
console.log('Request canceled', error.message);
|
|
297
|
+
return Promise.reject(error);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 处理网络错误
|
|
301
|
+
const config = error.config as RequestConfig | undefined;
|
|
302
|
+
if (!config?.hideErrMsg) {
|
|
303
|
+
let msg = '请求异常';
|
|
304
|
+
if (error.message.includes('timeout')) msg = '请求超时';
|
|
305
|
+
if (error.message.includes('Network Error')) msg = '网络错误';
|
|
306
|
+
if (error.response?.status === 404) msg = '404';
|
|
307
|
+
if (error.response?.status === 500) msg = '服务器内部错误';
|
|
308
|
+
|
|
309
|
+
message.error({ content: msg });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return Promise.reject(error);
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
function isFormData(value: any) {
|
|
317
|
+
return Object.prototype.toString.call(value) === '[object FormData]';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export default request;
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 [Your Organization Name]
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|