@reus-able/frontend-helper-mcp 1.0.13 → 1.1.1
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/dist/index.js
CHANGED
|
@@ -1,59 +1,115 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer as R } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport as
|
|
4
|
-
import
|
|
3
|
+
import { StdioServerTransport as F } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import c from "fs/promises";
|
|
5
5
|
import a from "path";
|
|
6
|
-
import { fileURLToPath as
|
|
7
|
-
import { ListPromptsRequestSchema as
|
|
8
|
-
import
|
|
9
|
-
function
|
|
6
|
+
import { fileURLToPath as p } from "url";
|
|
7
|
+
import { ListPromptsRequestSchema as P, GetPromptRequestSchema as x, ListResourcesRequestSchema as $, ReadResourceRequestSchema as M, ListToolsRequestSchema as C, CallToolRequestSchema as L } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import N from "fs";
|
|
9
|
+
function T(e) {
|
|
10
10
|
try {
|
|
11
|
-
return
|
|
11
|
+
return N.statSync(e).isDirectory();
|
|
12
12
|
} catch {
|
|
13
13
|
return !1;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
let
|
|
16
|
+
function u(e, t, o = 8) {
|
|
17
|
+
let r = e;
|
|
18
18
|
for (let n = 0; n <= o; n++) {
|
|
19
|
-
const s = a.resolve(
|
|
20
|
-
if (
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
19
|
+
const s = a.resolve(r, t);
|
|
20
|
+
if (T(s)) return s;
|
|
21
|
+
const i = a.dirname(r);
|
|
22
|
+
if (i === r) break;
|
|
23
|
+
r = i;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
const
|
|
30
|
-
|
|
26
|
+
function y() {
|
|
27
|
+
const e = process.env.MCP_COMMAND_PROMPTS_DIR ?? process.env.MCP_PROMPTS_DIR;
|
|
28
|
+
if (e) return e;
|
|
29
|
+
const t = a.dirname(p(import.meta.url)), o = u(
|
|
30
|
+
t,
|
|
31
31
|
"resources/command-prompts"
|
|
32
32
|
);
|
|
33
33
|
if (o) return o;
|
|
34
|
-
const
|
|
34
|
+
const r = u(
|
|
35
35
|
process.cwd(),
|
|
36
36
|
"resources/command-prompts"
|
|
37
37
|
);
|
|
38
|
-
return
|
|
38
|
+
return r || a.resolve(process.cwd(), "resources/command-prompts");
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
function _() {
|
|
41
|
+
const e = process.env.MCP_SKILLS_DIR;
|
|
42
|
+
if (e) return e;
|
|
43
|
+
const t = a.dirname(p(import.meta.url)), o = u(t, "resources/skills");
|
|
44
|
+
if (o) return o;
|
|
45
|
+
const r = u(process.cwd(), "resources/skills");
|
|
46
|
+
return r || a.resolve(process.cwd(), "resources/skills");
|
|
47
|
+
}
|
|
48
|
+
async function d(e) {
|
|
41
49
|
try {
|
|
42
|
-
return await
|
|
50
|
+
return await c.access(e), (await c.readdir(e)).filter(
|
|
43
51
|
(o) => [".txt", ".md", ".json"].includes(a.extname(o).toLowerCase())
|
|
44
52
|
);
|
|
45
|
-
} catch (
|
|
46
|
-
return console.error(`Error accessing prompts directory ${
|
|
53
|
+
} catch (t) {
|
|
54
|
+
return console.error(`Error accessing prompts directory ${e}:`, t), [];
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
return (
|
|
57
|
+
function D(e) {
|
|
58
|
+
return !(!e || e.includes("..") || e.includes("/") || e.includes("\\"));
|
|
51
59
|
}
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
60
|
+
function q(e) {
|
|
61
|
+
const t = e.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
62
|
+
if (!t) return {};
|
|
63
|
+
const o = {};
|
|
64
|
+
for (const r of t[1].split(`
|
|
65
|
+
`)) {
|
|
66
|
+
const n = r.trim();
|
|
67
|
+
if (!n) continue;
|
|
68
|
+
const s = n.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/);
|
|
69
|
+
if (!s) continue;
|
|
70
|
+
const i = s[1], m = s[2];
|
|
71
|
+
i === "name" && (o.name = m), i === "description" && (o.description = m);
|
|
72
|
+
}
|
|
73
|
+
return o;
|
|
74
|
+
}
|
|
75
|
+
async function w(e) {
|
|
76
|
+
try {
|
|
77
|
+
await c.access(e);
|
|
78
|
+
const o = (await c.readdir(e, { withFileTypes: !0 })).filter((n) => n.isDirectory()).map((n) => n.name), r = [];
|
|
79
|
+
for (const n of o) {
|
|
80
|
+
const s = a.join(e, n, "SKILL.md");
|
|
81
|
+
try {
|
|
82
|
+
const i = await c.readFile(s, "utf-8"), m = q(i);
|
|
83
|
+
r.push({
|
|
84
|
+
dirName: n,
|
|
85
|
+
name: m.name,
|
|
86
|
+
description: m.description,
|
|
87
|
+
filePath: s
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return r.sort((n, s) => n.dirName.localeCompare(s.dirName)), r;
|
|
94
|
+
} catch (t) {
|
|
95
|
+
return console.error(`Error accessing skills directory ${e}:`, t), [];
|
|
96
|
+
}
|
|
55
97
|
}
|
|
56
|
-
|
|
98
|
+
async function E(e, t) {
|
|
99
|
+
if (!D(t)) return;
|
|
100
|
+
const r = (await w(e)).find((s) => s.dirName === t || s.name === t);
|
|
101
|
+
if (!r) return;
|
|
102
|
+
const n = await c.readFile(r.filePath, "utf-8");
|
|
103
|
+
return { entry: r, markdown: n };
|
|
104
|
+
}
|
|
105
|
+
async function v(e, t) {
|
|
106
|
+
return (await d(e)).find((r) => a.parse(r).name === t);
|
|
107
|
+
}
|
|
108
|
+
function f(e) {
|
|
109
|
+
const t = a.extname(e).toLowerCase();
|
|
110
|
+
return t === ".md" ? "text/markdown" : t === ".json" ? "application/json" : "text/plain";
|
|
111
|
+
}
|
|
112
|
+
const S = {
|
|
57
113
|
"code-refactor": {
|
|
58
114
|
name: "代码重构",
|
|
59
115
|
description: "对代码进行结构调整,不改变功能逻辑"
|
|
@@ -65,97 +121,171 @@ const w = {
|
|
|
65
121
|
"component-refactor": {
|
|
66
122
|
name: "组件重构",
|
|
67
123
|
description: "对组件进行结构调整,不改变功能逻辑"
|
|
124
|
+
},
|
|
125
|
+
"install-skill": {
|
|
126
|
+
name: "安装技能",
|
|
127
|
+
description: "安装mcp中定义的技能"
|
|
68
128
|
}
|
|
69
129
|
};
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
130
|
+
function j(e) {
|
|
131
|
+
const t = y();
|
|
132
|
+
e.server.setRequestHandler(P, async () => {
|
|
73
133
|
try {
|
|
74
|
-
return { prompts: (await
|
|
134
|
+
return { prompts: (await d(t)).map((n) => {
|
|
75
135
|
const s = a.parse(n).name;
|
|
76
136
|
return {
|
|
77
137
|
name: s,
|
|
78
|
-
description:
|
|
138
|
+
description: S[s]?.description || `Content of ${n}`
|
|
79
139
|
};
|
|
80
140
|
}) };
|
|
81
141
|
} catch (o) {
|
|
82
142
|
return console.error("Error listing prompts:", o), { prompts: [] };
|
|
83
143
|
}
|
|
84
|
-
}),
|
|
85
|
-
const
|
|
144
|
+
}), e.server.setRequestHandler(x, async (o) => {
|
|
145
|
+
const r = o.params.name;
|
|
86
146
|
try {
|
|
87
|
-
const n = await
|
|
147
|
+
const n = await v(t, r);
|
|
88
148
|
if (!n)
|
|
89
|
-
throw new Error(`Prompt not found: ${
|
|
149
|
+
throw new Error(`Prompt not found: ${r}`);
|
|
90
150
|
return {
|
|
91
151
|
messages: [
|
|
92
152
|
{
|
|
93
153
|
role: "user",
|
|
94
154
|
content: {
|
|
95
155
|
type: "text",
|
|
96
|
-
text: await
|
|
156
|
+
text: await c.readFile(a.join(t, n), "utf-8")
|
|
97
157
|
}
|
|
98
158
|
}
|
|
99
159
|
]
|
|
100
160
|
};
|
|
101
161
|
} catch (n) {
|
|
102
|
-
throw new Error(`Failed to load prompt ${
|
|
162
|
+
throw new Error(`Failed to load prompt ${r}: ${n}`);
|
|
103
163
|
}
|
|
104
164
|
});
|
|
105
165
|
}
|
|
106
|
-
function
|
|
107
|
-
const
|
|
108
|
-
|
|
166
|
+
function H(e) {
|
|
167
|
+
const t = y();
|
|
168
|
+
e.server.setRequestHandler($, async () => {
|
|
109
169
|
try {
|
|
110
|
-
return { resources: (await
|
|
111
|
-
const s = a.parse(n).name,
|
|
170
|
+
return { resources: (await d(t)).map((n) => {
|
|
171
|
+
const s = a.parse(n).name, i = f(n);
|
|
112
172
|
return {
|
|
113
173
|
uri: `prompt://${s}`,
|
|
114
174
|
name: s,
|
|
115
|
-
mimeType:
|
|
116
|
-
description:
|
|
175
|
+
mimeType: i,
|
|
176
|
+
description: S[s]?.description || `Content of ${n}`
|
|
117
177
|
};
|
|
118
178
|
}) };
|
|
119
179
|
} catch (o) {
|
|
120
180
|
return console.error("Error listing resources:", o), { resources: [] };
|
|
121
181
|
}
|
|
122
|
-
}),
|
|
123
|
-
|
|
182
|
+
}), e.server.setRequestHandler(
|
|
183
|
+
M,
|
|
124
184
|
async (o) => {
|
|
125
|
-
const
|
|
185
|
+
const r = o.params.uri, n = r.replace(/^prompt:\/\//, "");
|
|
126
186
|
try {
|
|
127
|
-
const s = await
|
|
187
|
+
const s = await v(t, n);
|
|
128
188
|
if (!s)
|
|
129
|
-
throw new Error(`Resource not found: ${
|
|
130
|
-
const
|
|
189
|
+
throw new Error(`Resource not found: ${r}`);
|
|
190
|
+
const i = await c.readFile(a.join(t, s), "utf-8"), m = f(s);
|
|
131
191
|
return {
|
|
132
192
|
contents: [
|
|
133
193
|
{
|
|
134
|
-
uri:
|
|
135
|
-
mimeType:
|
|
136
|
-
text:
|
|
194
|
+
uri: r,
|
|
195
|
+
mimeType: m,
|
|
196
|
+
text: i
|
|
137
197
|
}
|
|
138
198
|
]
|
|
139
199
|
};
|
|
140
200
|
} catch (s) {
|
|
141
|
-
throw new Error(`Failed to read resource ${
|
|
201
|
+
throw new Error(`Failed to read resource ${r}: ${s}`);
|
|
142
202
|
}
|
|
143
203
|
}
|
|
144
204
|
);
|
|
145
205
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
206
|
+
function I(e) {
|
|
207
|
+
const t = _(), o = {
|
|
208
|
+
name: "get_skill",
|
|
209
|
+
description: "按名称获取 resources/skills 下的 SKILL.md 内容",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {
|
|
213
|
+
name: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "skill 名称(目录名或 SKILL.md frontmatter 的 name)"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
required: ["name"]
|
|
219
|
+
},
|
|
220
|
+
annotations: { readOnlyHint: !0, idempotentHint: !0 }
|
|
221
|
+
};
|
|
222
|
+
e.server.setRequestHandler(C, async () => ({ tools: [o] })), e.server.setRequestHandler(L, async (r) => {
|
|
223
|
+
if (r.params.name === "get_skill") {
|
|
224
|
+
const n = r.params.arguments, s = n && typeof n == "object" && typeof n.name == "string" ? n.name : void 0;
|
|
225
|
+
if (!s)
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: "Missing required argument: name"
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
isError: !0
|
|
234
|
+
};
|
|
235
|
+
try {
|
|
236
|
+
const i = await E(t, s);
|
|
237
|
+
if (!i) {
|
|
238
|
+
const h = (await w(t)).map((l) => {
|
|
239
|
+
const k = [l.dirName, l.name].filter(Boolean).join(", "), g = l.description ? ` - ${l.description}` : "";
|
|
240
|
+
return `- ${k}${g}`;
|
|
241
|
+
}).join(`
|
|
242
|
+
`);
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: `Skill not found: ${s}
|
|
248
|
+
|
|
249
|
+
Available skills:
|
|
250
|
+
` + (h || "- (none)")
|
|
251
|
+
}
|
|
252
|
+
],
|
|
253
|
+
isError: !0
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: i.markdown
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
};
|
|
264
|
+
} catch (i) {
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: `Failed to read skill: ${i}`
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
isError: !0
|
|
273
|
+
};
|
|
152
274
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: `Tool not found: ${r.params.name}`
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
isError: !0
|
|
284
|
+
};
|
|
285
|
+
});
|
|
156
286
|
}
|
|
157
|
-
async function
|
|
158
|
-
const
|
|
287
|
+
async function O() {
|
|
288
|
+
const e = a.dirname(p(import.meta.url)), t = a.resolve(e, "../package.json"), o = JSON.parse(await c.readFile(t, "utf-8")), r = new R(
|
|
159
289
|
{
|
|
160
290
|
name: o.name,
|
|
161
291
|
version: o.version
|
|
@@ -168,13 +298,13 @@ async function D() {
|
|
|
168
298
|
}
|
|
169
299
|
}
|
|
170
300
|
);
|
|
171
|
-
return
|
|
301
|
+
return j(r), H(r), I(r), r;
|
|
172
302
|
}
|
|
173
|
-
async function
|
|
303
|
+
async function b() {
|
|
174
304
|
console.error("Starting MCP server serving...");
|
|
175
|
-
const
|
|
176
|
-
await
|
|
305
|
+
const e = await O(), t = new F();
|
|
306
|
+
await e.connect(t);
|
|
177
307
|
}
|
|
178
|
-
|
|
179
|
-
console.error("Fatal error:",
|
|
308
|
+
b().catch((e) => {
|
|
309
|
+
console.error("Fatal error:", e), process.exit(1);
|
|
180
310
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## 帮助用户进行技能安装
|
|
2
|
+
|
|
3
|
+
请你协助用户对mcp中定义的技能进行安装。请按照如下步骤进行操作
|
|
4
|
+
|
|
5
|
+
## 1. 获取用户输入的技能名称
|
|
6
|
+
用户在使用当前 command 时,应该附带技能名称作为参数。如果没有,需要提示用户进行输入。
|
|
7
|
+
当用户输入的是「全部」,那么需要安装目前所有的技能。技能列表如下:
|
|
8
|
+
|
|
9
|
+
1. vue-components
|
|
10
|
+
2. vue-hooks
|
|
11
|
+
|
|
12
|
+
## 2. 获取技能信息
|
|
13
|
+
|
|
14
|
+
你可以使用 `get_skill` mcp 工具来获取用户输入的技能名称对应的技能信息。如果存在skill,那么其会返回skill的 `SKILL.md` 的内容
|
|
15
|
+
|
|
16
|
+
## 3. 检测项目中是否已安装该技能
|
|
17
|
+
|
|
18
|
+
你需要在 `.claude/skills/` 目录下检测是否已存在该技能的目录。如果存在,那么需要提示用户进行选择
|
|
19
|
+
1. 覆盖安装
|
|
20
|
+
2. 取消安装
|
|
21
|
+
|
|
22
|
+
询问的格式如下:
|
|
23
|
+
```
|
|
24
|
+
⚠️ 当前项目中已经包含该技能,是否要覆盖安装?(y/n)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 4. 写入技能
|
|
28
|
+
|
|
29
|
+
根据用户的选择,你需要执行不同的操作:
|
|
30
|
+
- 如果用户选择了覆盖安装,那么需要删除已存在的技能目录,然后创建新的技能目录,并将技能信息写入其中。
|
|
31
|
+
- 如果用户选择了取消安装,那么需要提示用户取消安装操作。
|
|
32
|
+
|
|
33
|
+
你需要在 `.claude/skills/` 目录下创建一个新的目录,目录名就是技能名称。然后把技能信息写入到 `SKILL.md` 文件中。
|
|
34
|
+
|
|
35
|
+
## 5. 安装完成
|
|
36
|
+
|
|
37
|
+
安装完成后,需要提示用户安装成功。格式如下:
|
|
38
|
+
```
|
|
39
|
+
✅ 技能安装成功:<技能名称>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
如果用户选择了取消安装,那么需要提示用户取消安装操作。格式如下:
|
|
43
|
+
```
|
|
44
|
+
❌ 技能安装取消:<技能名称>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
如果用户输入的技能名称不存在,那么需要提示用户技能不存在。格式如下:
|
|
48
|
+
```
|
|
49
|
+
❌ 技能不存在:<技能名称>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
如果安装了多个技能,那么需要提示用户安装完成。格式如下:
|
|
53
|
+
```
|
|
54
|
+
✅ 技能安装成功:<技能名称1>, <技能名称2>, ...
|
|
55
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-component
|
|
3
|
+
description: 构建 Vue3 SFC 组件,采用分层架构,包含组件、样式、模板、脚本等文件。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 构建Vue3 SFC组件
|
|
7
|
+
|
|
8
|
+
你需要按照如下要求构建一个 Vue3 SFC 组件
|
|
9
|
+
|
|
10
|
+
### 1) 组件文件结构
|
|
11
|
+
组件文件结构应该如下:
|
|
12
|
+
```
|
|
13
|
+
./MyComponent/
|
|
14
|
+
├── components/
|
|
15
|
+
│ └── // 子组件目录,可选
|
|
16
|
+
├── hooks/
|
|
17
|
+
│ └── useMyComponent.ts
|
|
18
|
+
├── constants.ts
|
|
19
|
+
├── utils.ts
|
|
20
|
+
├── types.ts
|
|
21
|
+
└── index.vue
|
|
22
|
+
```
|
|
23
|
+
components中应该递归地去编写子组件,每个子组件的文件结构与 MyComponent 相同。
|
|
24
|
+
|
|
25
|
+
### 2) 各个文件的内容与规范
|
|
26
|
+
|
|
27
|
+
#### 2.1) index.vue
|
|
28
|
+
index.vue 是组件的入口文件,应该包含
|
|
29
|
+
|
|
30
|
+
1. defineOptions 定义组件选项,必须包含name属性,值为组件的名称。
|
|
31
|
+
2. defineProps 定义组件的 props,需要以类型定义的方式定义,例如:defineProps<IProps>()
|
|
32
|
+
3. defineEmits 定义组件的 emits,需要以类型定义的方式定义,例如:defineEmits<IEmits>()
|
|
33
|
+
4. 类型定义应该都放在 types.ts 文件中,例如:IProps、IEmits 等。
|
|
34
|
+
5. 组件内编写顺序应该为:script setup -> template -> style scoped
|
|
35
|
+
|
|
36
|
+
对于组件的逻辑层,应该编写在 useMyComponent.ts 文件中。该文件的编写请使用 `vue-hooks` SKILL。
|
|
37
|
+
|
|
38
|
+
以下是完整示例
|
|
39
|
+
```vue
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { ref } from 'vue'
|
|
42
|
+
import { IProps, IEmits } from './types'
|
|
43
|
+
import { useMyComponent } from './hooks/useMyComponent'
|
|
44
|
+
|
|
45
|
+
defineOptions({
|
|
46
|
+
name: 'MyComponent',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const props = defineProps<IProps>()
|
|
50
|
+
const emits = defineEmits<IEmits>()
|
|
51
|
+
|
|
52
|
+
const { title } = useMyComponent(props, emits)
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div class="my-component">
|
|
57
|
+
<h1>{{ title }}</h1>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped lang="scss">
|
|
62
|
+
.my-component {
|
|
63
|
+
h1 {
|
|
64
|
+
color: red;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
#### 2.2) types.ts
|
|
72
|
+
types.ts 文件应该包含组件的 props、emits 类型定义。例如:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export interface IProps {
|
|
76
|
+
title: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface IEmits {
|
|
80
|
+
(e: 'click'): void
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### 2.3) hooks/useMyComponent.ts
|
|
85
|
+
hooks/useMyComponent.ts 文件应该包含组件的逻辑层代码。该文件的编写请使用 `vue-hooks` SKILL。
|
|
86
|
+
|
|
87
|
+
#### 2.4) constants.ts
|
|
88
|
+
constants.ts 文件应该包含组件的常量定义。例如:
|
|
89
|
+
```ts
|
|
90
|
+
export const DEFAULT_TITLE = 'My Component'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 2.5) utils.ts
|
|
94
|
+
utils.ts 文件应该包含组件的工具函数。例如:
|
|
95
|
+
|
|
96
|
+
请注意
|
|
97
|
+
1. **工具函数应该是纯函数,不应该包含任何副作用。**
|
|
98
|
+
2. **遵守DRY原则,优先查看项目中的 utils.ts 文件,查看是否有重复的工具函数。**
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export function formatTitle(title: string) {
|
|
102
|
+
return title.toUpperCase()
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3) 其他事项
|
|
107
|
+
|
|
108
|
+
1. 对于样式,你需要判断项目是否使用scss、是否使用tailwind。在style标签中,根据项目情况优先使用 @apply tailwind 类名,其次使用 scss 类名。
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-hooks
|
|
3
|
+
description: 构建 Vue3 组件的 hooks 函数。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 构建Vue3 组件的 hooks 函数
|
|
7
|
+
|
|
8
|
+
你需要按照如下要求构建一个 Vue3 组件的 hooks 函数
|
|
9
|
+
|
|
10
|
+
### 1) 入参
|
|
11
|
+
|
|
12
|
+
hooks 函数的入参应该包含组件的 props 和 emits。例如:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
export function useMyComponent(props: IProps, emits: IEmits) {
|
|
16
|
+
// 组件逻辑
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
IProps 和 IEmits 是组件的 props 和 emits 类型定义。应该从 types.ts 文件中引入。
|
|
21
|
+
|
|
22
|
+
### 2) 文件内容与规范
|
|
23
|
+
|
|
24
|
+
文件应该按照如下规则与顺序编写:
|
|
25
|
+
|
|
26
|
+
#### 2.1) 状态定义区
|
|
27
|
+
|
|
28
|
+
hooks 函数中应该包含组件的状态定义,以及相关的操作行为。避免hooks外部直接对状态进行操作。例如:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export function useMyComponent(props: IProps, emits: IEmits) {
|
|
32
|
+
const visible = ref(false);
|
|
33
|
+
|
|
34
|
+
const handleVisibleChange = (value: boolean) => {
|
|
35
|
+
visible.value = value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 业务逻辑
|
|
39
|
+
// ...
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
visible: readonly(visible),
|
|
43
|
+
handleVisibleChange,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
要求:
|
|
49
|
+
1. 状态定义区应该在 hooks 函数的顶部。
|
|
50
|
+
2. 对于复杂状态的类型,应该使用 TypeScript 类型定义,并提取到 types.ts 文件中。
|
|
51
|
+
3. 状态返回值应该是 readonly,避免外部直接修改状态。
|
|
52
|
+
4. 状态应该有相对应的操作行为,如果没有的话设置一个简单的set方法即可。
|
|
53
|
+
|
|
54
|
+
#### 2.2) 业务逻辑区
|
|
55
|
+
|
|
56
|
+
hooks 函数中应该包含组件的业务逻辑。例如:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export function useMyComponent(props: IProps, emits: IEmits) {
|
|
60
|
+
const visible = ref(false);
|
|
61
|
+
|
|
62
|
+
const handleVisibleChange = (value: boolean) => {
|
|
63
|
+
visible.value = value;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 业务逻辑
|
|
67
|
+
const handleOpenDialog = () => {
|
|
68
|
+
handleVisibleChange(true);
|
|
69
|
+
// 具体逻辑...
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const onXXX = () => {
|
|
73
|
+
// 具体逻辑...
|
|
74
|
+
emit('xxx');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
// ...状态定义
|
|
79
|
+
handleOpenDialog,
|
|
80
|
+
onXXX,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
要求:
|
|
86
|
+
1. 业务逻辑区应该在状态定义区下方。
|
|
87
|
+
2. 业务逻辑中需要操作状态的地方,应该调用相关的操作行为。
|
|
88
|
+
3. 业务逻辑中,避免出现与当前状态无相关的函数,如果只是单纯的工具函数,应该提取到 utils.ts 文件中。
|
|
89
|
+
4. 相关的业务逻辑应该被集中编写
|
|
90
|
+
|
|
91
|
+
#### 2.3) 声明周期函数区
|
|
92
|
+
|
|
93
|
+
如果组件需要在生命周期的某个阶段执行一些逻辑,应该在声明周期函数区中实现,并导出在组件中使用。例如:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export function useMyComponent(props: IProps, emits: IEmits) {
|
|
97
|
+
// 状态声明
|
|
98
|
+
// ...
|
|
99
|
+
|
|
100
|
+
// 业务逻辑
|
|
101
|
+
// ...
|
|
102
|
+
|
|
103
|
+
const handleMounted = () => {
|
|
104
|
+
handleInit();
|
|
105
|
+
// 具体逻辑...
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
// ...状态定义
|
|
110
|
+
// ...业务逻辑
|
|
111
|
+
handleMounted,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
要求:
|
|
117
|
+
1. 声明周期函数区应该在业务逻辑区下方。
|
|
118
|
+
2. 声明周期函数中,应该是对业务逻辑的组合调用,避免在其中进行复杂的逻辑处理。
|
|
119
|
+
|
|
120
|
+
#### 2.4) hooks的拆分
|
|
121
|
+
|
|
122
|
+
如果一个 hooks 函数的业务逻辑比较复杂,应该考虑将其拆分成多个 hooks 函数。每个 hooks 函数负责处理一个独立的业务逻辑。例如
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
export function useMyComponent(props: IProps, emits: IEmits) {
|
|
126
|
+
const logicA = useMyComponentLogicA(props, emits);
|
|
127
|
+
const logicB = useMyComponentLogicB(props, emits);
|
|
128
|
+
|
|
129
|
+
const handleMounted = () => {
|
|
130
|
+
logicA.handleInit();
|
|
131
|
+
logicB.handleInit();
|
|
132
|
+
// 具体逻辑...
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
// 导出逻辑A
|
|
137
|
+
...logicA,
|
|
138
|
+
// 导出逻辑B
|
|
139
|
+
...logicB,
|
|
140
|
+
|
|
141
|
+
// ... 其余导出
|
|
142
|
+
handleMounted,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
1. 其中每一个 hooks 函数都应该符合以上的要求。
|
|
148
|
+
2. hooks统一存放在 `hooks` 目录下。
|