@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,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: ui-components
|
|
3
|
+
description: 规范 Re* 前缀自定义组件的目录结构、导出方式、类型定义和开发流程。在创建或修改公共组件时调用。
|
|
4
|
+
scope: project
|
|
5
|
+
tags: [vue3, component, re-component, library, typescript]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Re* 自定义组件开发规范
|
|
9
|
+
|
|
10
|
+
## 组件目录结构
|
|
11
|
+
|
|
12
|
+
每个 Re 组件必须遵循以下目录结构:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/components/RePagination/
|
|
16
|
+
├── src/
|
|
17
|
+
│ └── pagination.vue # 组件实现文件
|
|
18
|
+
├── index.ts # 统一导出入口
|
|
19
|
+
└── type.ts # 类型定义(Props / Emits / Slots)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 规则说明
|
|
23
|
+
|
|
24
|
+
1. 组件目录使用大驼峰命名:`Re[ComponentName]`。
|
|
25
|
+
2. 组件实现文件放在 `src/` 子目录下,使用短横线命名:`pagination.vue`。
|
|
26
|
+
3. `index.ts` 是组件的唯一导出入口,外部通过 `import { RePagination } from "@/components/RePagination"` 引入。
|
|
27
|
+
4. `type.ts` 集中定义 Props、Emits、Slots 类型。
|
|
28
|
+
|
|
29
|
+
## 导出模式
|
|
30
|
+
|
|
31
|
+
### index.ts 标准写法
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// src/components/RePagination/index.ts
|
|
35
|
+
import pagination from "./src/pagination.vue";
|
|
36
|
+
|
|
37
|
+
export const RePagination = pagination;
|
|
38
|
+
export default RePagination;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 多组件导出
|
|
42
|
+
|
|
43
|
+
如果一个目录下有多个组件:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// src/components/ReDialog/index.ts
|
|
47
|
+
import dialog from "./src/dialog.vue";
|
|
48
|
+
import dialogHeader from "./src/dialog-header.vue";
|
|
49
|
+
|
|
50
|
+
export const ReDialog = dialog;
|
|
51
|
+
export const ReDialogHeader = dialogHeader;
|
|
52
|
+
export default ReDialog;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 类型定义
|
|
56
|
+
|
|
57
|
+
### type.ts 标准写法
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// src/components/RePagination/type.ts
|
|
61
|
+
import type { ComponentPublicInstance } from "vue";
|
|
62
|
+
|
|
63
|
+
export interface RePaginationProps {
|
|
64
|
+
/** 当前页码 */
|
|
65
|
+
currentPage: number;
|
|
66
|
+
/** 每页条数 */
|
|
67
|
+
pageSize: number;
|
|
68
|
+
/** 总条数 */
|
|
69
|
+
total: number;
|
|
70
|
+
/** 每页条数选项 */
|
|
71
|
+
pageSizes?: number[];
|
|
72
|
+
/** 是否显示总数 */
|
|
73
|
+
showTotal?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RePaginationEmits {
|
|
77
|
+
(e: "update:currentPage", page: number): void;
|
|
78
|
+
(e: "update:pageSize", size: number): void;
|
|
79
|
+
(e: "change", page: number, size: number): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type RePaginationInstance = ComponentPublicInstance<RePaginationProps>;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 类型约定
|
|
86
|
+
|
|
87
|
+
1. Props 类型命名:`Re[ComponentName]Props`。
|
|
88
|
+
2. Emits 类型命名:`Re[ComponentName]Emits`,使用函数签名格式。
|
|
89
|
+
3. 如需在组件外部引用实例类型,导出 `Re[ComponentName]Instance`。
|
|
90
|
+
4. 每个 Props 字段必须写 JSDoc 注释说明用途。
|
|
91
|
+
|
|
92
|
+
## 组件实现模板
|
|
93
|
+
|
|
94
|
+
```vue
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import type { ReComponentNameProps, ReComponentNameEmits } from "../../type";
|
|
97
|
+
|
|
98
|
+
defineOptions({
|
|
99
|
+
name: "ReComponentName",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const props = withDefaults(defineProps<ReComponentNameProps>(), {
|
|
103
|
+
pageSizes: () => [10, 20, 50, 100],
|
|
104
|
+
showTotal: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const emit = defineEmits<ReComponentNameEmits>();
|
|
108
|
+
|
|
109
|
+
function handleChange(page: number, size: number) {
|
|
110
|
+
emit("update:currentPage", page);
|
|
111
|
+
emit("update:pageSize", size);
|
|
112
|
+
emit("change", page, size);
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="re-component-name">
|
|
118
|
+
<!-- 组件内容 -->
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<style lang="scss" scoped>
|
|
123
|
+
.re-component-name {
|
|
124
|
+
/* 样式 */
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 现有 Re 组件清单
|
|
130
|
+
|
|
131
|
+
| 组件名 | 用途 | 关键特性 |
|
|
132
|
+
|--------|------|----------|
|
|
133
|
+
| ReDialog | 弹窗 | 拖拽、全屏、表单嵌套 |
|
|
134
|
+
| RePagination | 分页 | 配合 useTableSearch |
|
|
135
|
+
| ReTableBar | 表格工具栏 | 列配置、密度、全屏 |
|
|
136
|
+
| ReGrid | 数据表格 | 基于 el-table 封装 |
|
|
137
|
+
| ReTableOperate | 操作列 | 按钮组、权限控制 |
|
|
138
|
+
| ReSelectQuery | 查询选择器 | 下拉+搜索+分页 |
|
|
139
|
+
| ReCascader | 级联选择 | 支持异步加载 |
|
|
140
|
+
| ReRichText | 富文本编辑器 | 基于 wangEditor |
|
|
141
|
+
| ReQrcode | 二维码 | 支持 Logo 嵌入 |
|
|
142
|
+
| ReBarcode | 条形码 | 多种编码格式 |
|
|
143
|
+
| ReVerify | 验证码 | 图形/滑块/拖拽验证 |
|
|
144
|
+
| ReSeamlessScroll | 无缝滚动 | 支持多方向 |
|
|
145
|
+
| ReNoticeBar | 通知栏 | 滚动/关闭 |
|
|
146
|
+
| ReIconPicker | 图标选择器 | 支持多图标库 |
|
|
147
|
+
| ReSegmented | 分段控制器 | 自定义选项 |
|
|
148
|
+
| ReCheckCard | 多选卡片 | 卡片式选择 |
|
|
149
|
+
| ReTreeLine | 树形连线 | 树结构可视化 |
|
|
150
|
+
| ReFlicker | 闪烁效果 | 数据大屏常用 |
|
|
151
|
+
| ReTypeit | 打字效果 | 逐字显示动画 |
|
|
152
|
+
| ReAuth | 权限组件 | 按钮级权限控制 |
|
|
153
|
+
|
|
154
|
+
## 常见反例
|
|
155
|
+
|
|
156
|
+
- 组件没有 `index.ts` 导出文件,外部直接引用 `.vue` 文件。
|
|
157
|
+
- Props 类型内联在组件中,没有抽取到 `type.ts`。
|
|
158
|
+
- 组件 `name` 没有使用 `defineOptions` 显式声明。
|
|
159
|
+
- 样式没有使用 `scoped`,导致全局污染。
|
|
160
|
+
- 组件 class 名没有使用 `re-` 前缀。
|
|
161
|
+
- Props 字段没有 JSDoc 注释。
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 验证码与校验组件
|
|
2
|
+
|
|
3
|
+
规范滑块拼图、拖拽验证、图片验证码、短信倒计时等校验组件的使用。在登录、敏感操作、防刷场景时参照。
|
|
4
|
+
|
|
5
|
+
## 组件选型
|
|
6
|
+
|
|
7
|
+
| 场景 | 组件 | 说明 |
|
|
8
|
+
| ------------ | --------------- | ------------------------ |
|
|
9
|
+
| 滑块拼图弹窗 | `ReVerify` | 基于 `vue3-puzzle-vcode` |
|
|
10
|
+
| 拖拽滑块 | `ReDragVerify` | 自定义拖拽验证条 |
|
|
11
|
+
| 图片验证码 | `ReImageVerify` | canvas 随机字符 |
|
|
12
|
+
| 短信验证码 | `ReSmsCode` | 发送按钮 + 倒计时 |
|
|
13
|
+
|
|
14
|
+
## ReVerify 滑块拼图
|
|
15
|
+
|
|
16
|
+
```vue
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import ReVerify from "@/components/ReVerify";
|
|
19
|
+
|
|
20
|
+
const verifyShow = ref(true);
|
|
21
|
+
|
|
22
|
+
function onVerifyPass(success: boolean) {
|
|
23
|
+
console.log("验证通过", success);
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<ReVerify v-model:verify-show="verifyShow" @verify-pass="onVerifyPass" />
|
|
29
|
+
</template>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## ReDragVerify 拖拽验证
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<ReDragVerify
|
|
36
|
+
v-model:value="isPass"
|
|
37
|
+
width="100%"
|
|
38
|
+
:height="40"
|
|
39
|
+
:text="$t('请拖动滑块完成验证')"
|
|
40
|
+
:success-text="$t('验证成功')"
|
|
41
|
+
@success="onSuccess"
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Events
|
|
46
|
+
|
|
47
|
+
- `success({ isPassing, time })`:验证通过,返回耗时(秒)。
|
|
48
|
+
- `update:value`:同步验证状态。
|
|
49
|
+
- `start` / `move` / `end`:拖拽生命周期。
|
|
50
|
+
|
|
51
|
+
### Expose
|
|
52
|
+
|
|
53
|
+
- `resume()`:重置验证条状态。
|
|
54
|
+
|
|
55
|
+
## ReImageVerify 图片验证码
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import ReImageVerify from "@/components/ReImageVerify";
|
|
60
|
+
|
|
61
|
+
const code = ref("");
|
|
62
|
+
const imgVerifyRef = ref();
|
|
63
|
+
|
|
64
|
+
function refresh() {
|
|
65
|
+
imgVerifyRef.value?.getImgCode();
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<ReImageVerify ref="imgVerifyRef" v-model:code="code" @click="refresh" />
|
|
71
|
+
</template>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## ReSmsCode 短信倒计时
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<script setup lang="ts">
|
|
78
|
+
import ReSmsCode from "@/components/ReSmsCode";
|
|
79
|
+
|
|
80
|
+
const phone = ref("13800138000");
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<ReSmsCode :phone="phone" :seconds="120" />
|
|
85
|
+
</template>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- 内置手机号正则校验。
|
|
89
|
+
- 发送成功后自动倒计时,按钮禁用期间不可重复点击。
|
|
90
|
+
|
|
91
|
+
## 关键约定
|
|
92
|
+
|
|
93
|
+
1. 滑块验证通过后通过 `update:verifyShow` 关闭弹窗,不要直接修改 prop。
|
|
94
|
+
2. 图片验证码支持 `getImgCode()` 主动刷新。
|
|
95
|
+
3. 短信验证码 `seconds` 默认 120 秒,按需调整。
|
|
96
|
+
4. 校验组件仅做前端防刷,关键业务必须配合后端二次校验。
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: vue-core
|
|
3
|
+
description: Vue 3 项目核心规范,涵盖技术栈、目录结构、组件开发标准、路由系统、状态管理和国际化。开发任何新功能或组件前必须参考此技能。
|
|
4
|
+
scope: project
|
|
5
|
+
tags: [vue3, typescript, vite, component, router, pinia, i18n, conventions]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue 3 项目核心规范
|
|
9
|
+
|
|
10
|
+
## 技术栈
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
const techStack = {
|
|
14
|
+
vue: "3.5.x",
|
|
15
|
+
typescript: "6.x",
|
|
16
|
+
vite: "8.x",
|
|
17
|
+
elementPlus: "2.14.x", // PC 端 UI
|
|
18
|
+
vant: "4.10.x", // 移动端 UI
|
|
19
|
+
pinia: "3.x",
|
|
20
|
+
vueRouter: "5.x",
|
|
21
|
+
unocss: "66.x",
|
|
22
|
+
axios: "1.18.x",
|
|
23
|
+
vueI18n: "11.x",
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 目录结构
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/
|
|
31
|
+
├── api/ # API 接口(按业务模块分文件)
|
|
32
|
+
├── components/ # 可复用组件(Re* 前缀)
|
|
33
|
+
├── hooks/ # 组合式函数
|
|
34
|
+
├── store/ # Pinia 状态管理(模块化)
|
|
35
|
+
├── router/ # 路由配置(PC + 移动端)
|
|
36
|
+
├── utils/ # 工具函数
|
|
37
|
+
├── styles/ # 全局样式(SCSS + UnoCSS)
|
|
38
|
+
├── views/ # 页面组件
|
|
39
|
+
├── directives/ # 自定义指令
|
|
40
|
+
├── layout/ # 布局系统
|
|
41
|
+
└── locales/ # 国际化文件
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 组件开发规范
|
|
45
|
+
|
|
46
|
+
### 命名规则
|
|
47
|
+
|
|
48
|
+
- 全局自定义组件:`Re[ComponentName]`,如 `RePagination`、`ReDialog`。
|
|
49
|
+
- 页面组件:放在 `views/[module]/index.vue`,`name` 使用大驼峰,如 `BaseTable`、`LoginPage`。
|
|
50
|
+
- 组件 class 名:`re-[component-name]`,如 `.re-pagination`。
|
|
51
|
+
|
|
52
|
+
### 标准模板
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import type { ComponentNameProps, ComponentNameEmits } from "./type";
|
|
57
|
+
|
|
58
|
+
defineOptions({
|
|
59
|
+
name: "ReComponentName",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const props = withDefaults(defineProps<ComponentNameProps>(), {
|
|
63
|
+
// 默认值
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<ComponentNameEmits>();
|
|
67
|
+
|
|
68
|
+
// 组件逻辑
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<div class="re-component-name">
|
|
73
|
+
<!-- 内容 -->
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style lang="scss" scoped>
|
|
78
|
+
.re-component-name {
|
|
79
|
+
/* 组件样式 */
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 必须遵守
|
|
85
|
+
|
|
86
|
+
1. 始终使用 `<script setup lang="ts">` + Composition API,禁止 Options API。
|
|
87
|
+
2. `script` 标签第一行引入类型:`import type { ... } from "./type"`。
|
|
88
|
+
3. 使用 `defineOptions` 显式声明组件 `name`。
|
|
89
|
+
4. `props` 与 `emits` 的类型定义必须抽取到同目录 `type.ts` 文件。
|
|
90
|
+
5. 样式使用 `lang="scss" scoped`;UnoCSS 原子类可直接写在 template 中。
|
|
91
|
+
6. 系统常规组件(Element Plus / Vant)已自动引入,无需手动 `import`。
|
|
92
|
+
7. 自定义 Re 组件需显式引入:`import { RePagination } from "@/components/RePagination"`。
|
|
93
|
+
|
|
94
|
+
### 常见反例
|
|
95
|
+
|
|
96
|
+
- 使用 Options API(`export default { data(), methods }`)。
|
|
97
|
+
- `defineProps` 内直接写内联类型而不抽取到 `type.ts`。
|
|
98
|
+
- 在组件中硬编码中文文案(应使用 `$t()` 国际化)。
|
|
99
|
+
- 忘记写 `defineOptions({ name })` 导致 keep-alive 失效。
|
|
100
|
+
|
|
101
|
+
## 路由系统
|
|
102
|
+
|
|
103
|
+
### 静态路由模块格式
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
export default {
|
|
107
|
+
path: "/system",
|
|
108
|
+
redirect: "/system/user",
|
|
109
|
+
meta: {
|
|
110
|
+
title: "系统管理",
|
|
111
|
+
icon: "ep:tools",
|
|
112
|
+
rank: 1,
|
|
113
|
+
},
|
|
114
|
+
children: [
|
|
115
|
+
{
|
|
116
|
+
path: "/system/user",
|
|
117
|
+
name: "SystemUser",
|
|
118
|
+
component: () => import("@/views/system/user/index.vue"),
|
|
119
|
+
meta: { title: "用户管理", roles: ["admin"] },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
} satisfies RouteConfigsTable;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 路由约定
|
|
126
|
+
|
|
127
|
+
- 路由文件放在 `src/router/modules/` 下,按业务模块拆分。
|
|
128
|
+
- `meta.rank` 控制菜单排序,数值越小越靠前。
|
|
129
|
+
- `meta.roles` 控制页面级权限,不设置则表示无权限限制。
|
|
130
|
+
- 动态路由通过后端接口返回,前端使用 `addRoute()` 动态注册。
|
|
131
|
+
- 移动端路由放在 `src/router/mobile.ts`,与 PC 端隔离。
|
|
132
|
+
|
|
133
|
+
## Pinia 状态管理
|
|
134
|
+
|
|
135
|
+
### Store 模块格式
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { defineStore } from "pinia";
|
|
139
|
+
|
|
140
|
+
export const useUserStore = defineStore("user", {
|
|
141
|
+
state: () => ({
|
|
142
|
+
token: "",
|
|
143
|
+
userInfo: null as UserInfo | null,
|
|
144
|
+
}),
|
|
145
|
+
|
|
146
|
+
actions: {
|
|
147
|
+
setToken(token: string) {
|
|
148
|
+
this.token = token;
|
|
149
|
+
},
|
|
150
|
+
async login(params: LoginParams) {
|
|
151
|
+
const { data } = await http.request<Result<LoginResult>>(
|
|
152
|
+
"post", "/api/login", { data: params }
|
|
153
|
+
);
|
|
154
|
+
this.token = data.token;
|
|
155
|
+
this.userInfo = data.userInfo;
|
|
156
|
+
},
|
|
157
|
+
logout() {
|
|
158
|
+
this.$reset();
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
persist: {
|
|
163
|
+
key: "user-store",
|
|
164
|
+
storage: localStorage,
|
|
165
|
+
paths: ["token"],
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 组件外部使用的 Hook(保证在 setup 外也能获取 store 实例)
|
|
170
|
+
export const useUserStoreHook = () => {
|
|
171
|
+
return useUserStore();
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Store 约定
|
|
176
|
+
|
|
177
|
+
- 文件名使用 `useXxxStore` 命名,如 `useUserStore.ts`。
|
|
178
|
+
- 必须导出 `useXxxStoreHook()` 函数,供组件外部(如工具函数、路由守卫)调用。
|
|
179
|
+
- 需要持久化的字段在 `persist.paths` 中声明。
|
|
180
|
+
- 不要在 store 中直接操作 DOM 或调用路由方法。
|
|
181
|
+
|
|
182
|
+
## 国际化 (i18n)
|
|
183
|
+
|
|
184
|
+
### 使用方式
|
|
185
|
+
|
|
186
|
+
```vue
|
|
187
|
+
<template>
|
|
188
|
+
<el-button>{{ $t("buttons.submit") }}</el-button>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<script setup lang="ts">
|
|
192
|
+
import { useI18n } from "vue-i18n";
|
|
193
|
+
const { t } = useI18n();
|
|
194
|
+
const label = t("system.title");
|
|
195
|
+
</script>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 约定
|
|
199
|
+
|
|
200
|
+
- 所有用户可见文案必须通过 `$t()` 或 `t()` 引用。
|
|
201
|
+
- 语言包文件放在 `src/locales/` 下,按模块拆分。
|
|
202
|
+
- 开发阶段可先用中文 key 占位,后续替换为正式文案。
|
|
203
|
+
- 避免使用 `more`、`view details` 等无业务语义的占位文案。
|
|
204
|
+
|
|
205
|
+
## 通用原则
|
|
206
|
+
|
|
207
|
+
- 仅在系统边界(用户输入、外部 API)做校验,不添加无意义的防御代码。
|
|
208
|
+
- 新功能需同时考虑 PC 端与移动端兼容性。
|
|
209
|
+
- 用户可见文案优先通过 i18n 或后端配置化提供。
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# WebSocket 实时通信
|
|
2
|
+
|
|
3
|
+
## 架构
|
|
4
|
+
|
|
5
|
+
项目封装了多实例 WebSocket 管理系统:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/utils/websocket/
|
|
9
|
+
├── index.ts # 全局实例管理(init/close/get 函数)
|
|
10
|
+
├── core.ts # WebSocketManager 类(连接、重连、事件派发)
|
|
11
|
+
└── types.ts # 类型定义
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
业务层通过 `useWebSocket` Hook 接入。
|
|
15
|
+
|
|
16
|
+
## useWebSocket Hook
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// src/hooks/useWebSocket.ts
|
|
20
|
+
import { getWebSocketInstance } from '@/utils/websocket'
|
|
21
|
+
|
|
22
|
+
export function useWebSocket(instanceName: string = 'default') {
|
|
23
|
+
const wsInstance = getWebSocketInstance(instanceName)
|
|
24
|
+
const isConnected = ref(wsInstance?.isConnected || false)
|
|
25
|
+
|
|
26
|
+
// 监听连接状态
|
|
27
|
+
const onStatusChange = (status: unknown) => {
|
|
28
|
+
isConnected.value = status as boolean
|
|
29
|
+
}
|
|
30
|
+
wsInstance?.emitter.on('connection-status', onStatusChange)
|
|
31
|
+
|
|
32
|
+
// 记录监听器,组件卸载时自动清理
|
|
33
|
+
const listeners: { type: string, handler: any }[] = []
|
|
34
|
+
|
|
35
|
+
// 订阅消息
|
|
36
|
+
const subscribe = <T = any>(type: string, handler: (data: T) => void) => {
|
|
37
|
+
wsInstance?.emitter.on(type, handler as any)
|
|
38
|
+
listeners.push({ type, handler })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 手动取消订阅
|
|
42
|
+
const unsubscribe = (type: string, handler: any) => {
|
|
43
|
+
wsInstance?.emitter.off(type, handler)
|
|
44
|
+
const index = listeners.findIndex(l => l.type === type && l.handler === handler)
|
|
45
|
+
if (index !== -1) listeners.splice(index, 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 标准格式发送 { type, data }
|
|
49
|
+
const send = (type: string, data?: any) => {
|
|
50
|
+
return wsInstance?.send({ type, data }) ?? false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 原生格式发送(不做包装)
|
|
54
|
+
const sendRaw = (data: any) => {
|
|
55
|
+
return wsInstance?.send(data) ?? false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 核心:组件卸载时自动清理所有监听器
|
|
59
|
+
onUnmounted(() => {
|
|
60
|
+
if (!wsInstance) return
|
|
61
|
+
wsInstance.emitter.off('connection-status', onStatusChange)
|
|
62
|
+
listeners.forEach(({ type, handler }) => {
|
|
63
|
+
wsInstance.emitter.off(type, handler)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return { isConnected, subscribe, unsubscribe, send, sendRaw, instance: wsInstance }
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 初始化与关闭
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { initWebSocket, closeWebSocket } from '@/utils/websocket'
|
|
75
|
+
|
|
76
|
+
// 全局初始化(main.ts 或 app 启动时)
|
|
77
|
+
initWebSocket('wss://example.com/ws') // 默认实例 'default'
|
|
78
|
+
initWebSocket('wss://example.com/ws', 'chat') // 命名实例
|
|
79
|
+
|
|
80
|
+
// 关闭
|
|
81
|
+
closeWebSocket() // 关闭默认实例
|
|
82
|
+
closeWebSocket('chat') // 关闭指定实例
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 页面使用示例
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<script setup lang="ts">
|
|
89
|
+
import { useWebSocket } from '@/hooks/useWebSocket'
|
|
90
|
+
import { closeWebSocket, initWebSocket } from '@/utils/websocket'
|
|
91
|
+
|
|
92
|
+
defineOptions({ name: 'WebSocketDemo' })
|
|
93
|
+
|
|
94
|
+
const wsUrl = ref(import.meta.env.VITE_WS_URL || '')
|
|
95
|
+
const INSTANCE_NAME = 'demo-ws'
|
|
96
|
+
const messageList = ref<{ time: string, text: string, type: 'send' | 'receive' }[]>([])
|
|
97
|
+
const inputVal = ref('')
|
|
98
|
+
const isConnected = ref(false)
|
|
99
|
+
const isInit = ref(false)
|
|
100
|
+
let subscribe: any = null
|
|
101
|
+
let sendRaw: any = null
|
|
102
|
+
|
|
103
|
+
const handleConnect = () => {
|
|
104
|
+
if (!wsUrl.value) return
|
|
105
|
+
// 1. 按需初始化
|
|
106
|
+
initWebSocket(wsUrl.value, INSTANCE_NAME)
|
|
107
|
+
isInit.value = true
|
|
108
|
+
|
|
109
|
+
// 2. 获取 Hook
|
|
110
|
+
const wsHook = useWebSocket(INSTANCE_NAME)
|
|
111
|
+
isConnected.value = wsHook.isConnected.value
|
|
112
|
+
wsHook.subscribe('connection-status', (status: any) => {
|
|
113
|
+
isConnected.value = !!status
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
subscribe = wsHook.subscribe
|
|
117
|
+
sendRaw = wsHook.sendRaw
|
|
118
|
+
|
|
119
|
+
// 3. 订阅消息(非标准格式派发到 'message' 事件)
|
|
120
|
+
subscribe('message', (data: any) => {
|
|
121
|
+
messageList.value.push({
|
|
122
|
+
time: new Date().toLocaleTimeString(),
|
|
123
|
+
text: typeof data === 'string' ? data : JSON.stringify(data),
|
|
124
|
+
type: 'receive',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleDisconnect = () => {
|
|
130
|
+
closeWebSocket(INSTANCE_NAME)
|
|
131
|
+
isInit.value = false
|
|
132
|
+
isConnected.value = false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleSend = () => {
|
|
136
|
+
if (!inputVal.value || !isConnected.value) return
|
|
137
|
+
sendRaw(inputVal.value)
|
|
138
|
+
messageList.value.push({
|
|
139
|
+
time: new Date().toLocaleTimeString(),
|
|
140
|
+
text: inputVal.value,
|
|
141
|
+
type: 'send',
|
|
142
|
+
})
|
|
143
|
+
inputVal.value = ''
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onUnmounted(() => {
|
|
147
|
+
closeWebSocket(INSTANCE_NAME)
|
|
148
|
+
})
|
|
149
|
+
</script>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 环境变量
|
|
153
|
+
|
|
154
|
+
```env
|
|
155
|
+
# .env.development
|
|
156
|
+
VITE_WS_URL=wss://dev.example.com/ws
|
|
157
|
+
|
|
158
|
+
# .env.production
|
|
159
|
+
VITE_WS_URL=wss://api.example.com/ws
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 关键约定
|
|
163
|
+
|
|
164
|
+
1. **多实例管理**:每个 WebSocket 连接是独立实例,互不干扰。全局 `default` 实例用于通用推送,页面级实例用于特定业务。
|
|
165
|
+
2. **自动清理**:`useWebSocket` 在 `onUnmounted` 中自动移除当前组件注册的所有监听器,防止内存泄漏和重复消费。
|
|
166
|
+
3. **事件派发**:标准格式 `{ type, data }` 按 `type` 派发;非标准格式统一派发到 `'message'` 事件。
|
|
167
|
+
4. **连接状态**:`connection-status` 事件通知连接/断开状态,UI 可直接绑定显示在线/离线标识。
|
|
168
|
+
5. **页面销毁**:页面级 WebSocket 在 `onUnmounted` 中调用 `closeWebSocket(INSTANCE_NAME)` 关闭连接。
|
|
169
|
+
6. **重连机制**:底层 `WebSocketManager` 内置自动重连,业务层无需处理。
|
|
170
|
+
|
|
171
|
+
## 关联文件
|
|
172
|
+
|
|
173
|
+
- [src/hooks/useWebSocket.ts](file:///e:/work/vue3-web-temp/src/hooks/useWebSocket.ts)
|
|
174
|
+
- [src/utils/websocket/index.ts](file:///e:/work/vue3-web-temp/src/utils/websocket/index.ts)
|
|
175
|
+
- [src/utils/websocket/core.ts](file:///e:/work/vue3-web-temp/src/utils/websocket/core.ts)
|
|
176
|
+
- [src/views/websocket/index.vue](file:///e:/work/vue3-web-temp/src/views/websocket/index.vue)
|