@qilitt-mickey/vue3-temp-skill 1.0.8
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 +229 -0
- package/SKILL.md +621 -0
- package/bin/cli.js +579 -0
- package/package.json +46 -0
- package/references/advanced-ui.md +302 -0
- package/references/api-check.md +272 -0
- package/references/base-code-dict.md +48 -0
- package/references/build-optim.md +282 -0
- package/references/code-quality.md +235 -0
- package/references/crud-pages.md +316 -0
- package/references/data-compare.md +501 -0
- package/references/data-mapping.md +213 -0
- package/references/data-screen.md +79 -0
- package/references/data-writeback.md +104 -0
- package/references/detail-page.md +99 -0
- package/references/directives-advanced.md +93 -0
- package/references/download-export.md +68 -0
- package/references/feedback-loading.md +60 -0
- package/references/feedback-ui.md +111 -0
- package/references/file-management.md +132 -0
- package/references/flowchart-g6.md +244 -0
- package/references/form-advanced.md +137 -0
- package/references/graph-relation.md +253 -0
- package/references/http-api.md +188 -0
- package/references/layout-theme.md +540 -0
- package/references/mobile-h5.md +271 -0
- package/references/permission-auth.md +235 -0
- package/references/project-inventory.md +326 -0
- package/references/qrcode-barcode.md +92 -0
- package/references/rich-text.md +73 -0
- package/references/seamless-scroll.md +38 -0
- package/references/tree-table.md +111 -0
- package/references/ui-components.md +161 -0
- package/references/verify-captcha.md +96 -0
- package/references/vue-core.md +209 -0
- package/references/websocket-realtime.md +176 -0
- package/references/workflow-bpmn.md +206 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# 知识图谱(relation-graph)
|
|
2
|
+
|
|
3
|
+
## 依赖
|
|
4
|
+
|
|
5
|
+
`relation-graph-vue3` — 关系图谱/知识图谱可视化组件。
|
|
6
|
+
|
|
7
|
+
## 页面结构
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import RelationGraph from 'relation-graph-vue3'
|
|
12
|
+
import { getRelation } from '@/api/graph'
|
|
13
|
+
import { useApp } from '@/hooks/useApp'
|
|
14
|
+
import bgImg from './img/relationBg.png'
|
|
15
|
+
|
|
16
|
+
defineOptions({ name: 'Graph' })
|
|
17
|
+
const { headerHeight, svgLoading } = useApp()
|
|
18
|
+
const graphRef = ref()
|
|
19
|
+
const loading = ref(false)
|
|
20
|
+
const rootId = ref('')
|
|
21
|
+
|
|
22
|
+
interface GraphNode {
|
|
23
|
+
id: string
|
|
24
|
+
text: string
|
|
25
|
+
width?: number
|
|
26
|
+
height?: number
|
|
27
|
+
nodeShape?: 0 | 1
|
|
28
|
+
opacity?: number
|
|
29
|
+
fixed?: boolean
|
|
30
|
+
data: {
|
|
31
|
+
selfFlag?: string
|
|
32
|
+
customerNo?: string
|
|
33
|
+
customerType?: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
interface GraphLine {
|
|
37
|
+
from: string
|
|
38
|
+
to: string
|
|
39
|
+
text?: string
|
|
40
|
+
opacity?: number
|
|
41
|
+
color?: string
|
|
42
|
+
textOffset_y?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nodes = ref<GraphNode[]>([])
|
|
46
|
+
const lines = ref<GraphLine[]>([])
|
|
47
|
+
const isLayoutDone = ref(true)
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 核心配置
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const options = ref({
|
|
55
|
+
backgroundImage: bgImg,
|
|
56
|
+
moveToCenterWhenRefresh: true,
|
|
57
|
+
allowShowMiniToolBar: false,
|
|
58
|
+
useAnimationWhenRefresh: false,
|
|
59
|
+
disableZoom: false,
|
|
60
|
+
disableDragNode: false,
|
|
61
|
+
allowSwitchLineShape: true,
|
|
62
|
+
allowSwitchJunctionPoint: true,
|
|
63
|
+
defaultFocusRootNode: true,
|
|
64
|
+
defaultNodeBorderWidth: 0,
|
|
65
|
+
defaultLineWidth: 1,
|
|
66
|
+
defaultLineMarker: {
|
|
67
|
+
markerWidth: 20, markerHeight: 30,
|
|
68
|
+
refX: 3, refY: 3,
|
|
69
|
+
data: 'M 0 0, V 6, L 4 3, Z',
|
|
70
|
+
},
|
|
71
|
+
defaultNodeShape: 0 as 0 | 1, // 0:圆形 1:矩形
|
|
72
|
+
defaultJunctionPoint: 'border' as 'border' | 'ltrb' | 'tb' | 'lr',
|
|
73
|
+
defaultNodeWidth: 60,
|
|
74
|
+
defaultNodeHeight: 60,
|
|
75
|
+
defaultLineShape: 1 as 1 | 2 | 3 | 4 | 5 | 6,
|
|
76
|
+
layout: {
|
|
77
|
+
layoutName: 'force', // tree/center/force
|
|
78
|
+
maxLayoutTimes: 20,
|
|
79
|
+
force_node_repulsion: 2, // 节点斥力系数 0.2~3
|
|
80
|
+
force_line_elastic: 0.2, // 连线牵引力系数 0.2~3
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 数据转换
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// 节点转换
|
|
89
|
+
const transformNodes = (nodesData): GraphNode[] => {
|
|
90
|
+
return nodesData.map((node) => ({
|
|
91
|
+
id: node.id,
|
|
92
|
+
text: node.name,
|
|
93
|
+
width: node.selfFlag === '1' ? 100 : 80,
|
|
94
|
+
height: node.selfFlag === '1' ? 100 : 80,
|
|
95
|
+
nodeShape: 0,
|
|
96
|
+
opacity: 1,
|
|
97
|
+
fixed: node.selfFlag === '1',
|
|
98
|
+
data: {
|
|
99
|
+
selfFlag: node.selfFlag || '',
|
|
100
|
+
customerNo: node.customerNo || '',
|
|
101
|
+
customerType: node.customerType || '',
|
|
102
|
+
},
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 连线转换 — 可根据目标节点类型设置颜色
|
|
107
|
+
const transformLines = (linesData): GraphLine[] => {
|
|
108
|
+
return linesData.map((line) => {
|
|
109
|
+
const targetNode = nodes.value.find(n => n.id === line.source)
|
|
110
|
+
let color = ''
|
|
111
|
+
if (targetNode?.data.customerType === '2') color = '#4F9DF8'
|
|
112
|
+
else if (targetNode?.data.customerType === '1') color = '#FF8234'
|
|
113
|
+
return { from: line.source, to: line.target, text: line.category, opacity: 1, color, textOffset_y: 15 }
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 交互模式
|
|
119
|
+
|
|
120
|
+
### 节点高亮(点击聚焦)
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const highlightNodes = (clickedNodeId) => {
|
|
124
|
+
const adjacentNodeIds = new Set<string>()
|
|
125
|
+
const graphInstance = graphRef.value!.getInstance()
|
|
126
|
+
const _all_nodes = graphInstance.getNodes()
|
|
127
|
+
const _all_lines = graphInstance.getLines()
|
|
128
|
+
|
|
129
|
+
lines.value.forEach((line) => {
|
|
130
|
+
if (line.from === clickedNodeId) adjacentNodeIds.add(line.to)
|
|
131
|
+
else if (line.to === clickedNodeId) adjacentNodeIds.add(line.from)
|
|
132
|
+
})
|
|
133
|
+
adjacentNodeIds.add(clickedNodeId)
|
|
134
|
+
|
|
135
|
+
_all_nodes.forEach((node) => {
|
|
136
|
+
node.opacity = adjacentNodeIds.has(node.id) ? 1 : 0.1
|
|
137
|
+
})
|
|
138
|
+
_all_lines.forEach((line) => {
|
|
139
|
+
line.opacity = adjacentNodeIds.has(line.from) && adjacentNodeIds.has(line.to) ? 1 : 0.1
|
|
140
|
+
})
|
|
141
|
+
graphInstance.dataUpdated()
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 搜索节点
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const queryNodes = () => {
|
|
149
|
+
if (!keyName.value) return
|
|
150
|
+
const graphInstance = graphRef.value!.getInstance()
|
|
151
|
+
const matchedNode = graphInstance.getNodes().find(n => n.text.includes(keyName.value))
|
|
152
|
+
if (matchedNode) {
|
|
153
|
+
highlightNodes(matchedNode.id)
|
|
154
|
+
graphInstance.focusNodeById(matchedNode.id)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 点击画布重置
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const onCanvasClick = ($event) => {
|
|
163
|
+
if ($event.target.tagName === 'CANVAS') {
|
|
164
|
+
const graphInstance = graphRef.value!.getInstance()
|
|
165
|
+
graphInstance.getNodes().forEach(n => n.opacity = 1)
|
|
166
|
+
graphInstance.getLines().forEach(l => l.opacity = 1)
|
|
167
|
+
graphInstance.dataUpdated()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 模板用法
|
|
173
|
+
|
|
174
|
+
```vue
|
|
175
|
+
<template>
|
|
176
|
+
<div v-loading.lock="loading" element-loading-text="图形加载中..."
|
|
177
|
+
:element-loading-spinner="svgLoading" element-loading-svg-view-box="0, 0, 20, 20">
|
|
178
|
+
<div v-if="rootId" ref="myPage">
|
|
179
|
+
<RelationGraph ref="graphRef" :options="options"
|
|
180
|
+
@canvas-click="onCanvasClick" @node-click="onNodeClick" @line-click="onLineClick">
|
|
181
|
+
<template #node="{ node }">
|
|
182
|
+
<div :data-node-id="node.id" class="my-node-style"
|
|
183
|
+
:class="{ 'bg-[#4F9DF8]': node.data.customerType === '2' }"
|
|
184
|
+
@mouseenter="showNodeTips(node)" @mouseleave="hideNodeTips">
|
|
185
|
+
<div class="c-node-name">{{ node.text }}</div>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
<template #graph-plug>
|
|
189
|
+
<!-- 搜索框、图例、tooltip 等覆盖层 -->
|
|
190
|
+
</template>
|
|
191
|
+
</RelationGraph>
|
|
192
|
+
</div>
|
|
193
|
+
<el-empty v-else description="暂无数据" />
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 初始化渲染
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
onMounted(async () => {
|
|
202
|
+
loading.value = true
|
|
203
|
+
const res = await getRelation({ customerNo: 'xxx' })
|
|
204
|
+
loading.value = false
|
|
205
|
+
if (res.status === 200) {
|
|
206
|
+
rootId.value = res.data?.customerRelationInfosNodes.find(i => i.selfFlag === '1')?.id
|
|
207
|
+
nodes.value = transformNodes(res.data.customerRelationInfosNodes)
|
|
208
|
+
lines.value = transformLines(res.data.customerRelationInfoDetails)
|
|
209
|
+
await graphRef.value.setJsonData(
|
|
210
|
+
{ rootId: rootId.value, nodes: nodes.value, lines: lines.value },
|
|
211
|
+
(graphInstance) => {
|
|
212
|
+
graphInstance.moveToCenter()
|
|
213
|
+
graphInstance.setZoom(70)
|
|
214
|
+
highlightNodes(rootId.value)
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Tooltip 悬浮
|
|
222
|
+
|
|
223
|
+
- 使用自定义 `div` 定位到节点右侧,通过 `data-node-id` 获取节点 DOM 位置。
|
|
224
|
+
- 用 `isMouseInTooltip` + `hideTimer` 实现鼠标从节点移到 tooltip 不消失。
|
|
225
|
+
- `onUnmounted` 中清理 `hideTimer`。
|
|
226
|
+
|
|
227
|
+
## 布局遮罩
|
|
228
|
+
|
|
229
|
+
- 使用 `isLayoutDone` 变量 + `v-loading` 遮罩层,避免力学布局过程中用户看到节点跳动。
|
|
230
|
+
- `setTimeout` 5 秒后关闭遮罩。
|
|
231
|
+
|
|
232
|
+
## 图例配置
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const legendConfig = ref([
|
|
236
|
+
{ type: 'self', label: '当前客户', color: '#675bf6', borderColor: '#9f93da', borderStyle: 'dashed' },
|
|
237
|
+
{ type: 'personal', label: '个人客户', color: '#FF8234', borderColor: '#FF8234' },
|
|
238
|
+
{ type: 'enterprise', label: '企业客户', color: '#4F9DF8', borderColor: '#4F9DF8' },
|
|
239
|
+
])
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 关键约定
|
|
243
|
+
|
|
244
|
+
1. 节点/连线类型只需声明项目中实际用到的字段,不必照搬库的完整类型。
|
|
245
|
+
2. 根节点通过 `selfFlag === '1'` 识别,设为 `fixed: true` 固定位置。
|
|
246
|
+
3. 图谱容器必须有明确高度(使用 `calc(100vh - ...)` 计算)。
|
|
247
|
+
4. `setJsonData` 的回调中执行居中、缩放、高亮等初始化操作。
|
|
248
|
+
5. 节点自定义样式通过 `#node` 插槽,class 根据 `data` 字段动态绑定。
|
|
249
|
+
|
|
250
|
+
## 关联文件
|
|
251
|
+
|
|
252
|
+
- [src/views/graph/index.vue](file:///e:/work/vue3-web-temp/src/views/graph/index.vue)
|
|
253
|
+
- [src/api/graph.ts](file:///e:/work/vue3-web-temp/src/api/graph.ts)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: http-api
|
|
3
|
+
description: 规范 HTTP 请求封装、API 接口文件组织、RSA+AES 加解密调用、文件下载等网络层开发模式。在编写或修改 API 接口调用时调用。
|
|
4
|
+
scope: project
|
|
5
|
+
tags: [axios, http, api, encryption, rsa, aes, download, request]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# HTTP 请求与 API 接口规范
|
|
9
|
+
|
|
10
|
+
## Http 封装架构
|
|
11
|
+
|
|
12
|
+
项目使用自定义 `Http` 类封装 Axios,位于 `src/utils/http/`。核心特性:
|
|
13
|
+
|
|
14
|
+
- 请求/响应拦截器统一处理 Token、错误码、Loading 状态
|
|
15
|
+
- RSA + AES 混合加密(通过 `@CryptoAPI` 注解配合后端)
|
|
16
|
+
- 请求去重(相同请求自动取消前一个)
|
|
17
|
+
- 统一响应类型 `Result<T>`
|
|
18
|
+
|
|
19
|
+
## 统一响应类型
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface Result<T = any> {
|
|
23
|
+
success: boolean;
|
|
24
|
+
data: T;
|
|
25
|
+
message: string;
|
|
26
|
+
code: number;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
所有 API 返回值必须包裹在 `Result<T>` 中,通过 `data` 字段获取业务数据。
|
|
31
|
+
|
|
32
|
+
## API 调用标准模式
|
|
33
|
+
|
|
34
|
+
### 接口文件组织
|
|
35
|
+
|
|
36
|
+
API 文件按业务模块拆分,放在 `src/api/` 目录下:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/api/
|
|
40
|
+
├── system.ts # 系统管理接口
|
|
41
|
+
├── order.ts # 订单模块接口
|
|
42
|
+
├── report.ts # 报表模块接口
|
|
43
|
+
└── modules/
|
|
44
|
+
└── auth.ts # 认证相关接口
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 接口定义格式
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import http from "@/utils/http";
|
|
51
|
+
import type { Result } from "@/utils/http/types";
|
|
52
|
+
import type { UserListParams, UserListResult, UserInfo } from "./types/system";
|
|
53
|
+
|
|
54
|
+
/** 获取用户列表 */
|
|
55
|
+
export function getUserListApi(data: UserListParams) {
|
|
56
|
+
return http.request<Result<UserListResult>>(
|
|
57
|
+
"post",
|
|
58
|
+
"/api/system/user/list",
|
|
59
|
+
{ data }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 获取用户详情 */
|
|
64
|
+
export function getUserDetailApi(id: string) {
|
|
65
|
+
return http.request<Result<UserInfo>>(
|
|
66
|
+
"post",
|
|
67
|
+
"/api/system/user/detail",
|
|
68
|
+
{ data: { id } }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 删除用户 */
|
|
73
|
+
export function deleteUserApi(id: string) {
|
|
74
|
+
return http.request<Result<null>>(
|
|
75
|
+
"post",
|
|
76
|
+
"/api/system/user/delete",
|
|
77
|
+
{ data: { id } }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 接口约定
|
|
83
|
+
|
|
84
|
+
1. **所有接口统一使用 POST 方法**,参数通过 `data` 传递。
|
|
85
|
+
2. 接口函数命名:`[动作] + [模块] + Api`,如 `getUserListApi`、`deleteOrderApi`。
|
|
86
|
+
3. 接口函数必须声明完整的参数类型和返回类型。
|
|
87
|
+
4. 接口类型定义放在对应的 `types/` 文件或同目录 `type.ts` 中。
|
|
88
|
+
5. 不要在组件中直接写 `http.request`,必须抽取到 `api/` 目录。
|
|
89
|
+
|
|
90
|
+
## 在组件中使用 API
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import { ref } from "vue";
|
|
95
|
+
import { getUserListApi } from "@/api/system";
|
|
96
|
+
import type { UserListResult } from "@/api/types/system";
|
|
97
|
+
|
|
98
|
+
const loading = ref(false);
|
|
99
|
+
const tableData = ref<UserListResult["list"]>([]);
|
|
100
|
+
const total = ref(0);
|
|
101
|
+
|
|
102
|
+
async function fetchData(params: UserListParams) {
|
|
103
|
+
loading.value = true;
|
|
104
|
+
try {
|
|
105
|
+
const { data } = await getUserListApi(params);
|
|
106
|
+
tableData.value = data.list;
|
|
107
|
+
total.value = data.total;
|
|
108
|
+
} finally {
|
|
109
|
+
loading.value = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 加密请求
|
|
116
|
+
|
|
117
|
+
项目支持 RSA + AES 混合加密,用于敏感数据传输(密码、身份证号等)。
|
|
118
|
+
|
|
119
|
+
### 使用方式
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// 普通请求(不加密)
|
|
123
|
+
http.request<Result<T>>("post", url, { data: params });
|
|
124
|
+
|
|
125
|
+
// 加密请求(通过配置项开启)
|
|
126
|
+
http.request<Result<T>>("post", url, {
|
|
127
|
+
data: params,
|
|
128
|
+
crypto: true, // 开启 RSA+AES 加密
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 加密流程
|
|
133
|
+
|
|
134
|
+
1. 前端生成随机 AES 密钥
|
|
135
|
+
2. 用 AES 密钥加密请求体
|
|
136
|
+
3. 用后端公钥(RSA)加密 AES 密钥
|
|
137
|
+
4. 将加密后的 AES 密钥 + 密文一起发送
|
|
138
|
+
5. 后端用私钥解密 AES 密钥,再解密请求体
|
|
139
|
+
6. 响应数据同样使用 AES 加密返回
|
|
140
|
+
|
|
141
|
+
## 文件下载
|
|
142
|
+
|
|
143
|
+
### 标准下载模式
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { blobDown } from "@/utils/download";
|
|
147
|
+
|
|
148
|
+
/** 导出 Excel */
|
|
149
|
+
export function exportUserExcelApi(data: ExportParams) {
|
|
150
|
+
return http.request<Blob>(
|
|
151
|
+
"post",
|
|
152
|
+
"/api/system/user/export",
|
|
153
|
+
{ data, responseType: "blob" }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 在组件中调用
|
|
158
|
+
async function handleExport() {
|
|
159
|
+
const res = await exportUserExcelApi(queryParams);
|
|
160
|
+
blobDown(res, "用户列表.xlsx");
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 下载工具函数
|
|
165
|
+
|
|
166
|
+
- `blobDown(data, filename)` — 将 Blob 数据下载为文件
|
|
167
|
+
- `download(url, filename)` — 通过 URL 下载文件
|
|
168
|
+
- `downloadAttachment(id)` — 根据附件 ID 下载附件
|
|
169
|
+
|
|
170
|
+
## 请求取消
|
|
171
|
+
|
|
172
|
+
使用 `AbortController` 实现请求取消,页面切换时自动取消未完成的请求:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// http 封装内部自动处理,组件中无需手动取消
|
|
176
|
+
// 但如果需要手动取消:
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
http.request("post", url, { data, signal: controller.signal });
|
|
179
|
+
controller.abort(); // 取消请求
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 常见反例
|
|
183
|
+
|
|
184
|
+
- 在组件中直接写 `axios.post()` 而不通过 http 封装。
|
|
185
|
+
- 接口函数不声明返回类型,使用 `any`。
|
|
186
|
+
- 使用 GET 方法传 query 参数(本项目统一用 POST + data)。
|
|
187
|
+
- 下载文件时忘记设置 `responseType: "blob"`。
|
|
188
|
+
- 把接口 URL 硬编码在组件中而不抽取到 `api/` 目录。
|