feima-shortcuts 0.2.0 → 1.0.0

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/bin/feima.js CHANGED
@@ -1,84 +1,14 @@
1
1
  #!/usr/bin/env node
2
- const inquirer = require("inquirer");
3
- const feima = require("../src/generate");
2
+ const inquirer = require("../src/inquirer");
3
+ const generateApi = require("../src/generateApi");
4
4
  const { version } = require("../package.json"); // 读取 package.json 中的版本号
5
5
 
6
- const run = () => {
7
- console.log(`🚀 当前版本:v${version}`);
8
6
 
9
- inquirer
10
- .prompt([
11
- {
12
- type: "list",
13
- name: "type",
14
- message: "请选择模版类型",
15
- choices: ["api", "template", "generate"],
16
- },
17
- // 页面模版创建方式
18
- {
19
- type: "input",
20
- message: "输入页面路径:",
21
- name: "template-path",
22
- default: "warehouseManagement/inventoryTransfer/internalAllocation/index", // 默认值
23
- when: function (answers) {
24
- return ["template"].includes(answers.type);
25
- },
26
- },
27
- {
28
- type: "list",
29
- name: "template-type",
30
- message: "请选择页面模版类型",
31
- choices: ["card", 'tabs', 'form-card','form-collapse(没有拆分组件)'],
32
- when: function (answers) {
33
- return answers['template-path'];
34
- },
35
- },
36
-
37
-
38
- // 接口创建方式
39
- {
40
- type: "list",
41
- name: "template",
42
- message: "请选择接口类型",
43
- choices: ["new", 'old'],
44
- when: function (answers) {
45
- return ["api"].includes(answers.type);
46
- },
47
- },
48
- {
49
- type: "input",
50
- message: "输入接口路径:",
51
- name: "path",
52
- default: "admin.product.xxx", // 默认值
53
- when: function (answers) {
54
- return ["old"].includes(answers.template);
55
- },
56
- },
57
-
58
- {
59
- type: "input",
60
- message: "输入接口路径:",
61
- name: "api",
62
- default: "/tenant/xxx", // 默认值
63
- when: function (answers) {
64
- return ["new"].includes(answers.template);
65
- },
66
- },
67
-
68
- // 组件生成方式
69
- {
70
- type: "input",
71
- message: "输入组件名称 (例如: button, input, card):",
72
- name: "component-name",
73
- default: "button", // 默认值
74
- when: function (answers) {
75
- return ["generate"].includes(answers.type);
76
- },
77
- },
78
- ])
79
- .then((answers) => {
80
- feima.run(answers);
81
- });
82
- };
83
-
84
- run();
7
+ // 如果传入了 gan-api/generate-api 子命令,则解析参数;否则走原有交互逻辑
8
+ const argv = process.argv.slice(2);
9
+ console.log(`🚀 当前版本:v${version}`);
10
+ if (["generate-api"].includes(argv[0])) {
11
+ generateApi.run();
12
+ } else {
13
+ inquirer.run();
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feima-shortcuts",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "快捷指令",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -23,4 +23,4 @@
23
23
  "uuid": "^9.0.0"
24
24
  },
25
25
  "packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6"
26
- }
26
+ }
@@ -0,0 +1,242 @@
1
+ const { Command } = require("commander");
2
+ const { spawn } = require("child_process");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+
7
+ const requestStr = `/**
8
+ * 通过 feima generate-api 命令生成
9
+ * 请勿手动修改此文件
10
+ */
11
+ import axios from "axios";
12
+ import type { AxiosResponse, InternalAxiosRequestConfig } from "axios";
13
+ import { ElMessage } from "element-plus";
14
+ import qs from "qs";
15
+ import { useGlobalStore } from "@/store"; // 路径根据实际情况调整
16
+ import { wrapEncryption, encryptRequestParams, decrypt } from "@/utils/crypto";
17
+ import other from "@/utils/other";
18
+
19
+ import type { ApiRequestOptions } from "./ApiRequestOptions";
20
+ import { CancelablePromise } from "./CancelablePromise";
21
+ import type { OpenAPIConfig } from "./OpenAPI";
22
+
23
+ // 常用header
24
+ export enum CommonHeaderEnum {
25
+ "TENANT_ID" = "TENANT-ID",
26
+ "ENC_FLAG" = "Enc-Flag",
27
+ "AUTHORIZATION" = "Authorization",
28
+ "VERSION" = "VERSION",
29
+ }
30
+
31
+ const service: any = axios.create({
32
+ baseURL: import.meta.env.VITE_API_URL,
33
+ timeout: 50000, // 全局超时时间
34
+ paramsSerializer: {
35
+ serialize: (params: any) => {
36
+ return qs.stringify(params, { arrayFormat: "repeat" });
37
+ },
38
+ },
39
+ });
40
+
41
+ /**
42
+ * Axios请求拦截器,对请求进行处理
43
+ * 1. 序列化get请求参数
44
+ * 2. 统一增加Authorization和TENANT-ID请求头
45
+ * 3. 自动适配单体、微服务架构不同的URL
46
+ * @param config AxiosRequestConfig对象,包含请求配置信息
47
+ */
48
+ service.interceptors.request.use(
49
+ (config: InternalAxiosRequestConfig) => {
50
+ // 统一增加Authorization请求头, skipToken 跳过增加token
51
+ const token = localStorage.getItem("token");
52
+ if (token && !config.headers?.skipToken) {
53
+ config.headers![CommonHeaderEnum.AUTHORIZATION] = \`Bearer \${token}\`;
54
+ }
55
+
56
+ // 统一增加TENANT-ID请求头, skipTenant 跳过增加租户ID
57
+ const tenantId = localStorage.getItem("tenantId");
58
+ if (tenantId && !config.headers?.skipTenant) {
59
+ config.headers![CommonHeaderEnum.TENANT_ID] = tenantId;
60
+ }
61
+
62
+ // 增加 gray_version 请求头
63
+ const version = import.meta.env.VITE_GRAY_VERSION;
64
+ if (version) {
65
+ config.headers![CommonHeaderEnum.VERSION] = version;
66
+ }
67
+
68
+ // 请求报文加密 ,如果请求头中不包含 ENC_FLAG : false 则加密
69
+ if (config.data && !config.headers![CommonHeaderEnum.ENC_FLAG]) {
70
+ config.data = wrapEncryption(config.data);
71
+ }
72
+
73
+ // 如果是 GET ,加密 config.param 的每一个参数,并URLencode
74
+ if (config.method === "get" && config.params) {
75
+ config.params = encryptRequestParams(config.params);
76
+ }
77
+
78
+ // 自动适配单体和微服务架构不同的URL
79
+ config.url = other.adaptationUrl(config.url);
80
+
81
+ // 处理完毕,返回config对象
82
+ return config;
83
+ },
84
+ (error: any) => {
85
+ // 对请求错误进行处理
86
+ return Promise.reject(error);
87
+ }
88
+ );
89
+
90
+ /**
91
+ * 响应拦截器处理函数
92
+ * @param response 响应结果
93
+ * @returns 如果响应成功,则返回响应的data属性;否则,抛出错误或者执行其他操作
94
+ */
95
+ const handleResponse = (response: AxiosResponse<any>) => {
96
+ if (response.data.code === 1) {
97
+ throw response.data;
98
+ }
99
+
100
+ // 针对密文返回解密
101
+ if (response.data.encryption) {
102
+ response.data = decrypt(response.data.encryption);
103
+ return response.data;
104
+ }
105
+
106
+ return response.data;
107
+ };
108
+
109
+ /**
110
+ * 添加 Axios 的响应拦截器,用于全局响应结果处理
111
+ */
112
+ service.interceptors.response.use(handleResponse, (error: any) => {
113
+ const status = Number(error.response.status) || 200;
114
+
115
+ if (status === 424) {
116
+ const { logout } = useGlobalStore();
117
+ logout();
118
+ ElMessage.error("令牌状态已过期,请点击重新登录");
119
+ window.location.replace(\`\${import.meta.env.BASE_URL}login\`);
120
+ }
121
+
122
+ if (status === 426) {
123
+ const { logout } = useGlobalStore();
124
+ logout();
125
+ ElMessage.error("租户状态已过期,请联系管理员");
126
+ window.location.replace(\`\${import.meta.env.BASE_URL}login\`);
127
+ }
128
+
129
+ // 针对密文返回解密
130
+ if (error.response?.data?.encryption) {
131
+ error.response.data = decrypt(error.response?.data.encryption);
132
+ }
133
+
134
+ return Promise.reject(error.response.data);
135
+ });
136
+
137
+ export const request = <T>(
138
+ config: OpenAPIConfig,
139
+ options: ApiRequestOptions
140
+ ): CancelablePromise<T> => {
141
+ return new CancelablePromise((resolve, reject, onCancel) => {
142
+ // Get the request URL. Depending on your needs, this might need additional processing,
143
+ // @see ./src/templates/core/functions/getUrl.hbs
144
+ const url = \`\${config.BASE}\${options.path}\`;
145
+
146
+ // Optional: Get and link the cancelation token, so the request can be aborted.
147
+ const source = service.CancelToken.source();
148
+ onCancel(() => source.cancel("The user aborted a request."));
149
+
150
+ // Execute the request. This is a minimal example, in real world scenarios
151
+ // you will need to add headers, process form data, etc.
152
+ // @see ./src/templates/core/axios/request.hbs
153
+ service
154
+ .request({
155
+ url,
156
+ data: options.body,
157
+ method: options.method,
158
+ cancelToken: source.token,
159
+ })
160
+ .then((data: any) => {
161
+ resolve(data);
162
+ })
163
+ .catch((error: any) => {
164
+ reject(error);
165
+ });
166
+ });
167
+ };`
168
+
169
+
170
+ exports.run = () => {
171
+ const program = new Command();
172
+
173
+ program
174
+ .name("feima generate-api")
175
+ .description("从 Swagger/OpenAPI 文档地址生成 API 元数据")
176
+ .option(
177
+ "-i, --input <url>",
178
+ "Swagger/OpenAPI 文档地址,例如 http://192.168.0.74:9999/app/v3/api-docs"
179
+ )
180
+ .option(
181
+ "-o, --output <dir>",
182
+ "输出目录,默认 src/api",
183
+ "src/api"
184
+ )
185
+ .allowUnknownOption(true) // 保持向后兼容,忽略未知参数
186
+ .action((opts) => {
187
+ const input = opts?.input || "http://192.168.0.74:9999/app/v3/api-docs";
188
+ const output = opts?.output || "src/api";
189
+
190
+ console.log(`📥 generate-api 输入地址: ${input}`);
191
+ console.log(`📦 输出目录: ${output}`);
192
+
193
+ // 确保 ./tmpRequest.ts 使用内置的 requestStr(放在项目根目录)
194
+ const requestFile = path.resolve(process.cwd(), "tmpRequest.ts");
195
+ try {
196
+ fs.writeFileSync(requestFile, requestStr, "utf8");
197
+ console.log("📝 已写入 ./tmpRequest.ts");
198
+ } catch (e) {
199
+ console.error("❌ 写入 ./tmpRequest.ts 失败:", e.message);
200
+ process.exit(1);
201
+ }
202
+ // 在当前执行目录生成到指定 output,并指定自定义 request 实现
203
+ const args = [
204
+ "openapi-typescript-codegen",
205
+ "--input",
206
+ input,
207
+ "--output",
208
+ output,
209
+ "--request",
210
+ "./tmpRequest.ts",
211
+ ];
212
+
213
+ const child = spawn("npx", ["--yes", ...args], {
214
+ stdio: "inherit",
215
+ cwd: process.cwd(),
216
+ shell: false,
217
+ });
218
+
219
+ const cleanupTempRequest = () => {
220
+ try {
221
+ if (fs.existsSync(requestFile)) {
222
+ fs.unlinkSync(requestFile);
223
+ console.log("🧹 已清理临时文件 ./tmpRequest.ts");
224
+ }
225
+ } catch (e) {
226
+ console.warn("⚠️ 清理临时文件失败:", e.message);
227
+ }
228
+ };
229
+
230
+ child.on("close", (code) => {
231
+ cleanupTempRequest();
232
+ if (code === 0) {
233
+ console.log(`✅ OpenAPI 代码已生成到 ${output}`);
234
+ process.exit(0);
235
+ } else {
236
+ console.error(`❌ OpenAPI 代码生成失败,退出码: ${code}`);
237
+ process.exit(code || 1);
238
+ }
239
+ });
240
+ });
241
+ program.parse(process.argv);
242
+ };
@@ -0,0 +1,28 @@
1
+ const inquirer = require("inquirer");
2
+ const template = require("./scripts/template");
3
+
4
+
5
+ exports.run = () => {
6
+ inquirer
7
+ .prompt([
8
+ // 页面模版创建方式
9
+ {
10
+ type: "input",
11
+ message: "输入页面路径:",
12
+ name: "template-path",
13
+ default: "warehouseManagement/inventoryTransfer/internalAllocation/index", // 默认值
14
+ },
15
+ {
16
+ type: "list",
17
+ name: "template-type",
18
+ message: "请选择页面模版类型",
19
+ choices: ["card", 'tabs', 'form-card','form-collapse(没有拆分组件)'],
20
+ when: function (answers) {
21
+ return answers['template-path'];
22
+ },
23
+ },
24
+ ])
25
+ .then((answers) => {
26
+ template.run(answers)
27
+ });
28
+ };
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { makeDir } = require("../../utils/makeDir");
2
+ const { makeDir } = require("../../../utils/makeDir");
3
3
 
4
4
 
5
5
  function toKebabCase(str) {
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { makeDir } = require("../../utils/makeDir");
2
+ const { makeDir } = require("../../../utils/makeDir");
3
3
 
4
4
 
5
5
  function toKebabCase(str) {
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { makeDir } = require("../../utils/makeDir");
2
+ const { makeDir } = require("../../../utils/makeDir");
3
3
 
4
4
 
5
5
  function toKebabCase(str) {
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { makeDir } = require("../../utils/makeDir");
2
+ const { makeDir } = require("../../../utils/makeDir");
3
3
 
4
4
 
5
5
  function toKebabCase(str) {
package/index.js DELETED
@@ -1,5 +0,0 @@
1
- const hello = function (key) {
2
- console.log('Hello World!');
3
- };
4
-
5
- exports.hello = hello;
package/src/generate.js DELETED
@@ -1,15 +0,0 @@
1
- const restfulApi = require("./scripts/restful-api");
2
- const template = require("./scripts/template");
3
- const generate = require("./scripts/generate");
4
-
5
- exports.run = function (answers) {
6
- if (answers.type == 'api') {
7
- restfulApi.run(answers);
8
- }
9
- if (answers.type == 'template') {
10
- template.run(answers)
11
- }
12
- if (answers.type == 'generate') {
13
- generate.run(answers)
14
- }
15
- };
@@ -1,152 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- function toKebabFromComponent(componentName) {
5
- const nameWithoutF = componentName.replace(/^F/, '');
6
- return nameWithoutF
7
- .replace(/([A-Z])/g, '-$1')
8
- .toLowerCase()
9
- .replace(/^-/, '');
10
- }
11
-
12
- function ensureDir(dirPath) {
13
- if (!fs.existsSync(dirPath)) {
14
- fs.mkdirSync(dirPath, { recursive: true });
15
- }
16
- }
17
-
18
- function buildMarkdown(componentName) {
19
- const kebab = toKebabFromComponent(componentName);
20
- const demoName = `Demo${componentName}`;
21
- return `# ${componentName}\n\nA thin wrapper around Element Plus \`El${componentName.replace(/^F/, '')}\`.\n\n## Preview\n\n:::: demo ${demoName}\n::::\n`;
22
- }
23
-
24
- function buildDemoVue(componentName) {
25
- const tag = componentName;
26
- return `<template>
27
- <div>
28
- <${tag} />
29
- </div>
30
- </template>
31
- <script setup lang="ts">
32
- import { ${componentName} } from "feima-vue-ui";
33
- </script>
34
- `;
35
- }
36
-
37
- function updateVitepressConfig(componentName, docsRoot) {
38
- const configPath = path.resolve(docsRoot, '.vitepress/config.ts');
39
- if (!fs.existsSync(configPath)) {
40
- console.log('⚠️ docs/.vitepress/config.ts 不存在,跳过侧边栏更新');
41
- return;
42
- }
43
-
44
- let content = fs.readFileSync(configPath, 'utf8');
45
-
46
- // Find the components items array inside sidebar
47
- const itemsSectionMatch = content.match(/(items:\s*\[)([\s\S]*?)(\]\s*,\s*\}\s*,?\s*\]\s*,?\s*\},?)/);
48
- // Fallback: specifically locate the Components section with text: "组件"
49
- let sectionStartIndex = -1;
50
- let sectionEndIndex = -1;
51
- if (!itemsSectionMatch) {
52
- const compSectionStart = content.indexOf('{\n text: "组件"');
53
- if (compSectionStart !== -1) {
54
- const itemsStart = content.indexOf('items: [', compSectionStart);
55
- if (itemsStart !== -1) {
56
- sectionStartIndex = itemsStart + 'items: ['.length;
57
- // naive bracket matching for the items array
58
- let depth = 1;
59
- let i = itemsStart + 'items: ['.length;
60
- while (i < content.length && depth > 0) {
61
- const ch = content[i];
62
- if (ch === '[') depth++;
63
- if (ch === ']') depth--;
64
- i++;
65
- }
66
- sectionEndIndex = i - 1; // position of closing ']'
67
- }
68
- }
69
- }
70
-
71
- const kebab = toKebabFromComponent(componentName);
72
- const newItem = ` { text: "${componentName}", link: "/components/f-${kebab}" },`;
73
-
74
- if (itemsSectionMatch) {
75
- const fullMatch = itemsSectionMatch[0];
76
- const inner = itemsSectionMatch[2];
77
- if (!new RegExp(`text:\\s*"${componentName}"`).test(inner)) {
78
- const hasTrailingNewline = /\n\s*$/.test(inner);
79
- const injectedInner = inner + (hasTrailingNewline ? '' : '\n') + newItem + '\n ';
80
- const replaced = fullMatch.replace(inner, injectedInner);
81
- content = content.replace(fullMatch, replaced);
82
- fs.writeFileSync(configPath, content);
83
- console.log('📝 已更新 docs/.vitepress/config.ts 侧边栏');
84
- } else {
85
- console.log(`ℹ️ 侧边栏已包含 ${componentName},跳过`);
86
- }
87
- return;
88
- }
89
-
90
- if (sectionStartIndex !== -1 && sectionEndIndex !== -1) {
91
- const inner = content.slice(sectionStartIndex, sectionEndIndex);
92
- if (!new RegExp(`text:\\s*"${componentName}"`).test(inner)) {
93
- const before = content.slice(0, sectionStartIndex);
94
- const after = content.slice(sectionEndIndex);
95
- const injected = inner + (inner.endsWith('\n') ? '' : '\n') + newItem;
96
- content = before + injected + after;
97
- fs.writeFileSync(configPath, content);
98
- console.log('📝 已更新 docs/.vitepress/config.ts 侧边栏');
99
- } else {
100
- console.log(`ℹ️ 侧边栏已包含 ${componentName},跳过`);
101
- }
102
- } else {
103
- console.log('⚠️ 未能定位到侧边栏组件分组,跳过更新');
104
- }
105
- }
106
-
107
- function run(answers) {
108
- if (!answers || !answers['component-name']) {
109
- console.log('❌ 请提供组件名称');
110
- return;
111
- }
112
- let componentName = answers['component-name'];
113
- if (!componentName.startsWith('F')) {
114
- componentName = 'F' + componentName.charAt(0).toUpperCase() + componentName.slice(1);
115
- }
116
-
117
- const cwd = process.cwd();
118
- const docsRoot = path.resolve(cwd, 'docs');
119
- const compsDir = path.resolve(docsRoot, 'components');
120
- const examplesDir = path.resolve(docsRoot, 'examples');
121
- ensureDir(docsRoot);
122
- ensureDir(compsDir);
123
- ensureDir(examplesDir);
124
-
125
- const kebab = toKebabFromComponent(componentName);
126
-
127
- // Write markdown page
128
- const mdPath = path.resolve(compsDir, `f-${kebab}.md`);
129
- if (!fs.existsSync(mdPath)) {
130
- fs.writeFileSync(mdPath, buildMarkdown(componentName));
131
- console.log(`✅ 文档已生成: docs/components/f-${kebab}.md`);
132
- } else {
133
- console.log(`ℹ️ 文档已存在: docs/components/f-${kebab}.md`);
134
- }
135
-
136
- // Write demo component
137
- const demoName = `Demo${componentName}`;
138
- const demoPath = path.resolve(examplesDir, `${demoName}.vue`);
139
- if (!fs.existsSync(demoPath)) {
140
- fs.writeFileSync(demoPath, buildDemoVue(componentName));
141
- console.log(`✅ 示例已生成: docs/examples/${demoName}.vue`);
142
- } else {
143
- console.log(`ℹ️ 示例已存在: docs/examples/${demoName}.vue`);
144
- }
145
-
146
- // Update vitepress sidebar
147
- updateVitepressConfig(componentName, docsRoot);
148
- }
149
-
150
- module.exports = { run };
151
-
152
-
@@ -1,257 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const docsGenerator = require('./docs');
4
-
5
- /**
6
- * 生成新的Vue组件
7
- * @param {Object} answers - 用户输入的答案
8
- */
9
- function generateComponent(answers) {
10
- let componentName = answers['component-name'];
11
-
12
- // 确保组件名以F开头且首字母大写
13
- if (!componentName.startsWith('F')) {
14
- componentName = 'F' + componentName.charAt(0).toUpperCase() + componentName.slice(1);
15
- }
16
-
17
- // 移除开头的F,然后转换为kebab-case
18
- const nameWithoutF = componentName.replace(/^F/, '');
19
- const kebabName = nameWithoutF.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
20
- const folderName = `f-${kebabName}`;
21
-
22
- // 创建组件目录结构
23
- const componentDir = path.resolve(process.cwd(), `src/${folderName}`);
24
- const srcDir = path.join(componentDir, 'src');
25
- const testsDir = path.join(componentDir, '__tests__');
26
-
27
- // 创建目录
28
- if (!fs.existsSync(componentDir)) {
29
- fs.mkdirSync(componentDir, { recursive: true });
30
- }
31
- if (!fs.existsSync(srcDir)) {
32
- fs.mkdirSync(srcDir, { recursive: true });
33
- }
34
- if (!fs.existsSync(testsDir)) {
35
- fs.mkdirSync(testsDir, { recursive: true });
36
- }
37
-
38
- // 生成Vue组件文件
39
- const vueTemplate = generateVueTemplate(componentName);
40
- fs.writeFileSync(path.join(srcDir, `${componentName}.vue`), vueTemplate);
41
-
42
- // 生成index.ts文件
43
- const indexTemplate = generateIndexTemplate(componentName);
44
- fs.writeFileSync(path.join(componentDir, 'index.ts'), indexTemplate);
45
-
46
- // 生成测试文件
47
- const testTemplate = generateTestTemplate(componentName);
48
- fs.writeFileSync(path.join(testsDir, `${componentName}.spec.ts`), testTemplate);
49
-
50
- // 更新主入口文件
51
- updateMainIndex(componentName);
52
-
53
- // 更新解析器
54
- updateResolver(componentName);
55
-
56
- // 生成文档与示例(独立脚本)
57
- try {
58
- docsGenerator.run({ 'component-name': componentName });
59
- } catch (e) {
60
- console.log('⚠️ 生成文档时发生错误:', e && e.message ? e.message : e);
61
- }
62
-
63
- console.log(`✅ 组件 ${componentName} 生成成功!`);
64
- console.log(`📁 组件目录: ${folderName}`);
65
- console.log(`🔧 已自动更新 index.ts 和 resolver.ts`);
66
- }
67
-
68
- /**
69
- * 生成Vue组件模板
70
- */
71
- function generateVueTemplate(componentName) {
72
- return `<template>
73
- <div class="feima-${componentName.toLowerCase()}">
74
- ${componentName} 组件内容
75
- </div>
76
- </template>
77
-
78
- <script setup lang="ts">
79
- defineOptions({ name: "${componentName}" });
80
-
81
- // 组件属性定义
82
- interface Props {
83
- // 在这里定义组件属性
84
- }
85
-
86
- // 组件事件定义
87
- interface Emits {
88
- // 在这里定义组件事件
89
- }
90
-
91
- const props = withDefaults(defineProps<Props>(), {
92
- // 默认属性值
93
- });
94
-
95
- const emit = defineEmits<Emits>();
96
- </script>
97
-
98
- <style scoped>
99
- .feima-${componentName.toLowerCase()} {
100
- /* 组件样式 */
101
- }
102
- </style>
103
- `;
104
- }
105
-
106
- /**
107
- * 生成index.ts模板
108
- */
109
- function generateIndexTemplate(componentName) {
110
- return `import type { App } from "vue";
111
- import ${componentName}Vue from "./src/${componentName}.vue";
112
-
113
- export const ${componentName} = ${componentName}Vue;
114
-
115
- export default {
116
- install(app: App) {
117
- app.component("${componentName}", ${componentName});
118
- },
119
- };
120
- `;
121
- }
122
-
123
- /**
124
- * 生成测试文件模板
125
- */
126
- function generateTestTemplate(componentName) {
127
- return `import { describe, it, expect } from 'vitest';
128
- import { mount } from '@vue/test-utils';
129
- import ${componentName} from '../src/${componentName}.vue';
130
-
131
- describe('${componentName}', () => {
132
- it('renders properly', () => {
133
- const wrapper = mount(${componentName});
134
- expect(wrapper.exists()).toBe(true);
135
- });
136
- });
137
- `;
138
- }
139
-
140
- /**
141
- * 更新主入口文件
142
- */
143
- function updateMainIndex(componentName) {
144
- const indexPath = path.resolve(process.cwd(), 'src/index.ts');
145
-
146
- if (!fs.existsSync(indexPath)) {
147
- console.log('⚠️ index.ts 文件不存在,跳过更新');
148
- return;
149
- }
150
-
151
- let content = fs.readFileSync(indexPath, 'utf8');
152
-
153
- // 添加导入语句
154
- const nameWithoutF = componentName.replace(/^F/, '');
155
- const kebabName = nameWithoutF.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
156
- const folderName = `f-${kebabName}`;
157
- const importLine = `import ${componentName}Installer, { ${componentName} } from "./${folderName}";`;
158
-
159
- // 查找现有的导入语句位置,保持原有格式
160
- const importMatch = content.match(/(import.*from "\.\/.*";\s*)+/);
161
- if (importMatch) {
162
- // 在最后一个导入语句后添加新的导入,保持换行格式
163
- const lastImport = importMatch[0].trim();
164
- const hasNewlineAfter = content.match(importMatch[0] + '\n');
165
- const newlineAfter = hasNewlineAfter ? '\n' : '\n';
166
- content = content.replace(importMatch[0], importMatch[0] + importLine + newlineAfter);
167
- } else {
168
- // 如果没有找到导入语句,在文件开头添加
169
- content = content.replace(/^/, importLine + '\n');
170
- }
171
-
172
- // 添加到安装器数组,保持原有缩进格式
173
- const installerMatch = content.match(/const installers = \[([\s\S]*?)\];/);
174
- if (installerMatch) {
175
- // 解析现有的安装器列表,保持每行的格式
176
- const installerContent = installerMatch[1];
177
- const lines = installerContent.split('\n').map(line => line.trim()).filter(line => line);
178
-
179
- // 清理每行末尾的逗号
180
- const cleanLines = lines.map(line => line.replace(/,$/, ''));
181
-
182
- // 添加新的安装器,保持格式
183
- cleanLines.push(`${componentName}Installer`);
184
-
185
- // 重新构建数组,每行都有正确的缩进
186
- const formattedInstallers = cleanLines.map(line => ` ${line}`).join(',\n');
187
- content = content.replace(installerMatch[0], `const installers = [\n${formattedInstallers}\n];`);
188
- }
189
-
190
- // 添加到导出列表,保持原有格式
191
- const exportMatch = content.match(/export \{ ([^}]+) \};/);
192
- if (exportMatch) {
193
- let currentExports = exportMatch[1].trim();
194
-
195
- // 清理可能存在的多余逗号
196
- currentExports = currentExports.replace(/,\s*,+/g, ','); // 移除连续逗号
197
- currentExports = currentExports.replace(/,\s*$/, ''); // 移除末尾逗号
198
- currentExports = currentExports.replace(/^\s*,/, ''); // 移除开头逗号
199
-
200
- // 检查是否已经有内容,如果有则添加逗号,否则直接添加
201
- const separator = currentExports ? ', ' : '';
202
- const newExports = currentExports + separator + componentName;
203
- content = content.replace(exportMatch[0], `export { ${newExports} };`);
204
- }
205
-
206
- fs.writeFileSync(indexPath, content);
207
- console.log(`📝 已更新 index.ts`);
208
- }
209
-
210
- /**
211
- * 更新解析器文件
212
- */
213
- function updateResolver(componentName) {
214
- const resolverPath = path.resolve(process.cwd(), 'src/resolver.ts');
215
-
216
- if (!fs.existsSync(resolverPath)) {
217
- console.log('⚠️ resolver.ts 文件不存在,跳过更新');
218
- return;
219
- }
220
-
221
- let content = fs.readFileSync(resolverPath, 'utf8');
222
-
223
- // 添加到支持的组件列表,保持原有缩进格式
224
- const supportedMatch = content.match(/const supportedComponents = \[([\s\S]*?)\];/);
225
- if (supportedMatch) {
226
- // 解析现有的组件列表,保持每行的格式
227
- const componentContent = supportedMatch[1];
228
- const lines = componentContent.split('\n').map(line => line.trim()).filter(line => line);
229
-
230
- // 清理每行末尾的逗号
231
- const cleanLines = lines.map(line => line.replace(/,$/, ''));
232
-
233
- // 检查是否已经存在该组件,避免重复添加
234
- const existingComponents = cleanLines.map(line => line.replace(/['"]/g, '').trim());
235
- if (!existingComponents.includes(componentName)) {
236
- // 添加新的组件,保持格式
237
- cleanLines.push(`"${componentName}"`);
238
-
239
- // 重新构建数组,每行都有正确的缩进
240
- const formattedComponents = cleanLines.map(line => ` ${line}`).join(',\n');
241
- content = content.replace(supportedMatch[0], `const supportedComponents = [\n${formattedComponents}\n];`);
242
- } else {
243
- console.log(`⚠️ 组件 ${componentName} 已存在于 supportedComponents 中,跳过添加`);
244
- }
245
- }
246
-
247
- fs.writeFileSync(resolverPath, content);
248
- console.log(`📝 已更新 resolver.ts`);
249
- }
250
-
251
- exports.run = function (answers) {
252
- if (answers['component-name']) {
253
- generateComponent(answers);
254
- } else {
255
- console.log('❌ 请提供组件名称');
256
- }
257
- };
@@ -1,10 +0,0 @@
1
- const oldApi = require("./old");
2
- const newApi = require("./new");
3
-
4
- exports.run = function (answers) {
5
- if (answers.template === "old") {
6
- oldApi.run(answers);
7
- } else {
8
- newApi.run(answers);
9
- }
10
- };
@@ -1,72 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { makeDir } = require("../../utils/makeDir");
4
-
5
- const fileName = "index.js";
6
-
7
- // 生成 API 函数模板
8
- const postTemplate = (url, functionName) => {
9
- return `export const ${functionName} = (options) => {
10
- return request('${url}', {
11
- method: "post",
12
- ...(options || {}),
13
- });
14
- };`;
15
- };
16
-
17
- // 读取文件内容,如果函数已存在则返回 null,否则返回文件内容
18
- const fileData = (functionName, filePath) => {
19
- if (fs.existsSync(filePath)) {
20
- const data = fs.readFileSync(filePath, "utf8");
21
- if (data.includes(`export const ${functionName} `)) {
22
- console.log(`⚠️ 函数名 ${functionName} 已存在,跳过生成`);
23
- return null;
24
- }
25
- return data;
26
- }
27
- return `import request from '@/utils/service'`;
28
- };
29
-
30
- // 根据 URL 获取函数名(取最后一段)
31
- const getFunctionName = (url) => {
32
- const parts = url.split('/').filter(Boolean);
33
- return parts[parts.length - 1];
34
- };
35
-
36
- // 从 API 路径获取目录结构(去掉最后一段)
37
- const getDirFromApi = (api) => {
38
- const parts = api.split('/').filter(Boolean);
39
- parts.pop(); // 移除最后一部分(函数名)
40
- return parts.join('/');
41
- };
42
-
43
- // 生成 API 方法并写入文件
44
- const apiAllTemplate = (apiPath, functionName, pagePath) => {
45
- const fullFilePath = path.join(pagePath, fileName);
46
- const fileTemplate = fileData(functionName, fullFilePath);
47
-
48
- if (!fileTemplate) return;
49
-
50
- const newContent = `${fileTemplate}\n\n${postTemplate(apiPath, functionName)}\n`;
51
-
52
- fs.writeFileSync(fullFilePath, newContent, "utf8");
53
- console.log(`✅ 成功生成 API 方法: ${functionName} -> ${apiPath}`);
54
- };
55
-
56
- // 脚本入口函数
57
- exports.run = function ({ api }) {
58
- if (!api || typeof api !== "string") {
59
- console.error("❌ 参数错误,需传入 { api: string }");
60
- return;
61
- }
62
-
63
- const functionName = getFunctionName(api);
64
- const dir = getDirFromApi(api);
65
-
66
- // 基于当前工作目录计算 API 路径
67
- const pagePath = path.resolve(process.cwd(), `src/service/${dir}`);
68
-
69
- // 创建目录并生成 API 方法
70
- makeDir(pagePath);
71
- apiAllTemplate(api, functionName, pagePath);
72
- };
@@ -1,73 +0,0 @@
1
- const fs = require("fs");
2
- const { makeDir } = require("../../utils/makeDir");
3
-
4
- const fileName = "index.js";
5
-
6
- const postTemplat = (apiPath, functionName) => {
7
- return `export const ${functionName} = (options) => {
8
- const isFormData = options?.data instanceof FormData;
9
-
10
- const data = isFormData
11
- ? (() => {
12
- const formData = options.data;
13
- formData.append("api", "${apiPath}");
14
- return formData;
15
- })()
16
- : {
17
- ...(options?.data || {}),
18
- api: "${apiPath}",
19
- };
20
-
21
- return request({
22
- method: "post",
23
- ...(options || {}),
24
- data,
25
- });
26
- };
27
- `
28
- };
29
-
30
- function splitString(input) {
31
- const parts = input.split('.');
32
- const path = parts.slice(0, -1).join('/'); // 取前面的部分并用 '/' 连接
33
- const functionName = parts[parts.length - 1]; // 取最后一部分
34
- return { path, functionName };
35
- }
36
-
37
- const isFunctionExists = (data, functionName) => {
38
- const regex = new RegExp(`export\\s+const\\s+${functionName}\\s*=`, 'g');
39
- return regex.test(data);
40
- };
41
-
42
- const fileData = (answers, functionName) => {
43
- if (fs.existsSync(`./${fileName}`)) {
44
- const data = fs.readFileSync(`./${fileName}`, "utf8");
45
- if (isFunctionExists(data, functionName)) {
46
- return "";
47
- }
48
- return data;
49
- }
50
- return `import request from '@/utils/request'`
51
- }
52
-
53
- const apiAllTemplate = (answers,functionName) => {
54
- const fileTemplate = fileData(answers, functionName);
55
-
56
- if (!fileTemplate) return console.error(`无需重复添加 ${answers.path}`);
57
-
58
- const template = `${fileTemplate}
59
-
60
- ${postTemplat(answers.path, functionName)}`;
61
-
62
- fs.writeFileSync(`./${fileName}`, template);
63
- };
64
-
65
- exports.run = function (answers) {
66
- const { path, functionName } = splitString(answers.path);
67
- const pagePath = `./src/api/${path}`;
68
-
69
- makeDir(pagePath);
70
- process.chdir(pagePath);
71
-
72
- apiAllTemplate(answers, functionName);
73
- };