@ljjjjjjj/my-cli 1.0.0 → 1.1.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/README.md +3 -0
- package/bin/index.js +380 -32
- package/package.json +2 -2
- package/templates/generator/component/react/component.css.tpl +70 -0
- package/templates/generator/component/react/component.jsx.tpl +19 -0
- package/templates/generator/component/vue/component.scss.tpl +73 -0
- package/templates/generator/component/vue/component.vue.tpl +40 -0
- package/templates/generator/page/react/api.ts.tpl +3 -0
- package/templates/generator/page/react/hooks.ts.tpl +3 -0
- package/templates/generator/page/react/page.css.tpl +3 -0
- package/templates/generator/page/react/page.jsx.tpl +5 -0
- package/templates/generator/page/react/store.ts.tpl +3 -0
- package/templates/generator/page/react/types.ts.tpl +3 -0
- package/templates/generator/page/vue/api.ts.tpl +3 -0
- package/templates/generator/page/vue/hooks.ts.tpl +3 -0
- package/templates/generator/page/vue/page.scss.tpl +3 -0
- package/templates/generator/page/vue/page.vue.tpl +14 -0
- package/templates/generator/page/vue/store.ts.tpl +3 -0
- package/templates/generator/page/vue/types.ts.tpl +3 -0
package/README.md
ADDED
package/bin/index.js
CHANGED
|
@@ -9,60 +9,408 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
|
|
12
|
+
// ---------- 模板映射(不变) ----------
|
|
12
13
|
const TEMPLATE_MAP = {
|
|
13
14
|
'Vue3 + Vite': path.resolve(__dirname, '../templates/vue3-vite'),
|
|
14
15
|
'React + Vite': path.resolve(__dirname, '../templates/react-vite')
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// ---------- 工具函数 ----------
|
|
19
|
+
function toPascalCase(value) {
|
|
20
|
+
return value
|
|
21
|
+
.replace(/[_\-\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
|
|
22
|
+
.replace(/^(.)/, (char) => char.toUpperCase());
|
|
23
|
+
}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
function renderTemplate(templatePath, replacements) {
|
|
26
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
27
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => replacements[key] ?? '');
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
console.log(
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.log(`使用方法:
|
|
32
|
+
my-cli create <项目名称>
|
|
33
|
+
my-cli add component <组件名称> [--force]
|
|
34
|
+
my-cli add page <页面名称> [--force]
|
|
35
|
+
my-cli help`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------- 查找项目根目录 ----------
|
|
39
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
40
|
+
let current = startDir;
|
|
41
|
+
while (current !== path.parse(current).root) {
|
|
42
|
+
const pkgPath = path.join(current, 'package.json');
|
|
43
|
+
if (fs.existsSync(pkgPath)) {
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
current = path.dirname(current);
|
|
47
|
+
}
|
|
48
|
+
return startDir; // 未找到则返回当前目录
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------- 加载项目配置 ----------
|
|
52
|
+
function loadProjectConfig(projectRoot) {
|
|
53
|
+
const config = {
|
|
54
|
+
framework: null, // 'vue' | 'react' | null
|
|
55
|
+
componentsDir: 'src/components',
|
|
56
|
+
pagesDir: 'src/pages'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// 1. 读取 .my-cli.json
|
|
60
|
+
const cliConfigPath = path.join(projectRoot, '.my-cli.json');
|
|
61
|
+
if (fs.existsSync(cliConfigPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const userConfig = fs.readJsonSync(cliConfigPath);
|
|
64
|
+
Object.assign(config, userConfig);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn('⚠️ 解析 .my-cli.json 失败,使用默认配置');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. 读取 package.json 中的 myCli 字段
|
|
71
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
72
|
+
if (fs.existsSync(pkgPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
75
|
+
if (pkg.myCli) {
|
|
76
|
+
Object.assign(config, pkg.myCli);
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 确保路径使用相对路径(相对于项目根)
|
|
84
|
+
config.componentsDir = config.componentsDir.replace(/^\.?\//, '');
|
|
85
|
+
config.pagesDir = config.pagesDir.replace(/^\.?\//, '');
|
|
86
|
+
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------- 检测项目类型(增强版) ----------
|
|
91
|
+
function detectProjectType(projectRoot, config) {
|
|
92
|
+
// 优先使用配置文件指定的框架
|
|
93
|
+
if (config.framework) {
|
|
94
|
+
if (config.framework === 'react' || config.framework === 'vue') {
|
|
95
|
+
return config.framework;
|
|
96
|
+
}
|
|
97
|
+
console.warn(`⚠️ 未知框架 "${config.framework}",将自动检测`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 自动检测
|
|
101
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
102
|
+
if (fs.existsSync(pkgPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
105
|
+
const deps = {
|
|
106
|
+
...(pkg.dependencies || {}),
|
|
107
|
+
...(pkg.devDependencies || {})
|
|
108
|
+
};
|
|
109
|
+
if (deps.react || deps['@vitejs/plugin-react'] || deps.preact) {
|
|
110
|
+
return 'react';
|
|
111
|
+
}
|
|
112
|
+
if (deps.vue || deps['@vitejs/plugin-vue']) {
|
|
113
|
+
return 'vue';
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 通过文件存在性检测
|
|
121
|
+
const srcPath = path.join(projectRoot, 'src');
|
|
122
|
+
if (fs.existsSync(srcPath)) {
|
|
123
|
+
const files = fs.readdirSync(srcPath);
|
|
124
|
+
if (files.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'))) {
|
|
125
|
+
return 'react';
|
|
126
|
+
}
|
|
127
|
+
if (files.some(f => f.endsWith('.vue'))) {
|
|
128
|
+
return 'vue';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 默认返回 vue(兼容旧逻辑)
|
|
133
|
+
return 'vue';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- 获取脚手架配置 ----------
|
|
137
|
+
function getScaffoldConfig(projectRoot, projectType) {
|
|
138
|
+
const templateRoot = path.resolve(__dirname, '../templates/generator');
|
|
139
|
+
|
|
140
|
+
if (projectType === 'react') {
|
|
141
|
+
return {
|
|
142
|
+
projectType,
|
|
143
|
+
componentExt: 'jsx',
|
|
144
|
+
styleExt: 'css',
|
|
145
|
+
entryExt: 'js',
|
|
146
|
+
componentTemplatePath: path.join(templateRoot, 'component/react/component.jsx.tpl'),
|
|
147
|
+
styleTemplatePath: path.join(templateRoot, 'component/react/component.css.tpl'),
|
|
148
|
+
pageTemplatePath: path.join(templateRoot, 'page/react/page.jsx.tpl'),
|
|
149
|
+
pageStyleTemplatePath: path.join(templateRoot, 'page/react/page.css.tpl'),
|
|
150
|
+
pageFiles: [
|
|
151
|
+
{ name: 'api.ts', templatePath: path.join(templateRoot, 'page/react/api.ts.tpl') },
|
|
152
|
+
{ name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/react/hooks.ts.tpl') },
|
|
153
|
+
{ name: 'store.ts', templatePath: path.join(templateRoot, 'page/react/store.ts.tpl') },
|
|
154
|
+
{ name: 'types.ts', templatePath: path.join(templateRoot, 'page/react/types.ts.tpl') }
|
|
155
|
+
],
|
|
156
|
+
testTemplate: (componentName) =>
|
|
157
|
+
`import { describe, it, expect } from 'vitest';\n\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// vue
|
|
162
|
+
return {
|
|
163
|
+
projectType: 'vue',
|
|
164
|
+
componentExt: 'vue',
|
|
165
|
+
styleExt: 'scss',
|
|
166
|
+
entryExt: 'ts',
|
|
167
|
+
componentTemplatePath: path.join(templateRoot, 'component/vue/component.vue.tpl'),
|
|
168
|
+
styleTemplatePath: path.join(templateRoot, 'component/vue/component.scss.tpl'),
|
|
169
|
+
pageTemplatePath: path.join(templateRoot, 'page/vue/page.vue.tpl'),
|
|
170
|
+
pageStyleTemplatePath: path.join(templateRoot, 'page/vue/page.scss.tpl'),
|
|
171
|
+
pageFiles: [
|
|
172
|
+
{ name: 'api.ts', templatePath: path.join(templateRoot, 'page/vue/api.ts.tpl') },
|
|
173
|
+
{ name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/vue/hooks.ts.tpl') },
|
|
174
|
+
{ name: 'store.ts', templatePath: path.join(templateRoot, 'page/vue/store.ts.tpl') },
|
|
175
|
+
{ name: 'types.ts', templatePath: path.join(templateRoot, 'page/vue/types.ts.tpl') }
|
|
176
|
+
],
|
|
177
|
+
testTemplate: (componentName) =>
|
|
178
|
+
`import { describe, it, expect } from 'vitest';\n\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
28
181
|
|
|
29
|
-
|
|
182
|
+
// ---------- 辅助:安全写入文件(带覆盖确认) ----------
|
|
183
|
+
async function safeWriteFile(filePath, content, force = false) {
|
|
184
|
+
if (fs.existsSync(filePath) && !force) {
|
|
185
|
+
const { overwrite } = await inquirer.prompt([
|
|
186
|
+
{
|
|
187
|
+
type: 'confirm',
|
|
188
|
+
name: 'overwrite',
|
|
189
|
+
message: `文件 ${path.basename(filePath)} 已存在,是否覆盖?`,
|
|
190
|
+
default: false
|
|
191
|
+
}
|
|
192
|
+
]);
|
|
193
|
+
if (!overwrite) {
|
|
194
|
+
return false; // 跳过
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await fs.outputFile(filePath, content);
|
|
198
|
+
return true; // 写入成功
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------- 生成组件 ----------
|
|
202
|
+
async function generateComponentFiles(name, projectRoot, config, force) {
|
|
203
|
+
const componentName = toPascalCase(name);
|
|
204
|
+
const baseDir = path.join(projectRoot, config.componentsDir, componentName);
|
|
205
|
+
await fs.ensureDir(baseDir);
|
|
206
|
+
|
|
207
|
+
const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
|
|
208
|
+
|
|
209
|
+
const replacements = {
|
|
210
|
+
componentName,
|
|
211
|
+
componentNameLower: componentName.toLowerCase()
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const filesToGenerate = [
|
|
215
|
+
{
|
|
216
|
+
path: path.join(baseDir, `${componentName}.${scaffoldConfig.componentExt}`),
|
|
217
|
+
content: renderTemplate(scaffoldConfig.componentTemplatePath, replacements)
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
path: path.join(baseDir, `${componentName}.module.${scaffoldConfig.styleExt}`),
|
|
221
|
+
content: renderTemplate(scaffoldConfig.styleTemplatePath, replacements)
|
|
222
|
+
},
|
|
30
223
|
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
message: '请选择项目模板:',
|
|
34
|
-
choices: Object.keys(TEMPLATE_MAP)
|
|
224
|
+
path: path.join(baseDir, `index.${scaffoldConfig.entryExt}`),
|
|
225
|
+
content: `export { default } from './${componentName}.${scaffoldConfig.componentExt}';\n`
|
|
35
226
|
},
|
|
36
227
|
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
228
|
+
path: path.join(baseDir, 'README.md'),
|
|
229
|
+
content: `# ${componentName}\n\n${componentName} 组件说明。\n`
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
path: path.join(baseDir, `${componentName}.${scaffoldConfig.projectType === 'react' ? 'test.jsx' : 'test.ts'}`),
|
|
233
|
+
content: scaffoldConfig.testTemplate(componentName)
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
let overwrittenCount = 0;
|
|
238
|
+
let skippedCount = 0;
|
|
239
|
+
|
|
240
|
+
for (const file of filesToGenerate) {
|
|
241
|
+
const result = await safeWriteFile(file.path, file.content, force);
|
|
242
|
+
if (result) {
|
|
243
|
+
overwrittenCount++;
|
|
244
|
+
} else {
|
|
245
|
+
skippedCount++;
|
|
41
246
|
}
|
|
42
|
-
|
|
247
|
+
}
|
|
43
248
|
|
|
44
|
-
|
|
45
|
-
|
|
249
|
+
return { overwrittenCount, skippedCount, baseDir };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------- 生成页面 ----------
|
|
253
|
+
async function generatePageFiles(name, projectRoot, config, force) {
|
|
254
|
+
const pageName = toPascalCase(name);
|
|
255
|
+
const baseDir = path.join(projectRoot, config.pagesDir, pageName);
|
|
256
|
+
await fs.ensureDir(baseDir);
|
|
257
|
+
|
|
258
|
+
const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
|
|
46
259
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
260
|
+
const replacements = {
|
|
261
|
+
pageName,
|
|
262
|
+
pageNameLower: pageName.toLowerCase()
|
|
263
|
+
};
|
|
50
264
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
265
|
+
const filesToGenerate = [
|
|
266
|
+
{
|
|
267
|
+
path: path.join(baseDir, `index.${scaffoldConfig.componentExt}`),
|
|
268
|
+
content: renderTemplate(scaffoldConfig.pageTemplatePath, replacements)
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
path: path.join(baseDir, `style.${scaffoldConfig.styleExt}`),
|
|
272
|
+
content: renderTemplate(scaffoldConfig.pageStyleTemplatePath, replacements)
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
// 添加额外的页面文件(api.ts, hooks.ts 等)
|
|
277
|
+
for (const pageFile of scaffoldConfig.pageFiles) {
|
|
278
|
+
filesToGenerate.push({
|
|
279
|
+
path: path.join(baseDir, pageFile.name),
|
|
280
|
+
content: renderTemplate(pageFile.templatePath, replacements)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let overwrittenCount = 0;
|
|
285
|
+
let skippedCount = 0;
|
|
286
|
+
|
|
287
|
+
for (const file of filesToGenerate) {
|
|
288
|
+
const result = await safeWriteFile(file.path, file.content, force);
|
|
289
|
+
if (result) {
|
|
290
|
+
overwrittenCount++;
|
|
291
|
+
} else {
|
|
292
|
+
skippedCount++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { overwrittenCount, skippedCount, baseDir };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------- 主函数 ----------
|
|
300
|
+
(async function run() {
|
|
301
|
+
const args = process.argv.slice(2);
|
|
302
|
+
let command = args[0];
|
|
303
|
+
let rest = args.slice(1);
|
|
55
304
|
|
|
56
|
-
|
|
57
|
-
|
|
305
|
+
// 解析 --force / -f
|
|
306
|
+
let force = false;
|
|
307
|
+
const filteredArgs = [];
|
|
308
|
+
for (const arg of rest) {
|
|
309
|
+
if (arg === '--force' || arg === '-f') {
|
|
310
|
+
force = true;
|
|
311
|
+
} else {
|
|
312
|
+
filteredArgs.push(arg);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
rest = filteredArgs;
|
|
316
|
+
|
|
317
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
318
|
+
printHelp();
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (command === 'create') {
|
|
323
|
+
const projectName = rest[0];
|
|
324
|
+
if (!projectName) {
|
|
325
|
+
printHelp();
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(`✨ 正在创建项目: ${projectName}`);
|
|
330
|
+
|
|
331
|
+
const answers = await inquirer.prompt([
|
|
332
|
+
{
|
|
333
|
+
type: 'select',
|
|
334
|
+
name: 'templateKey',
|
|
335
|
+
message: '请选择项目模板:',
|
|
336
|
+
choices: Object.keys(TEMPLATE_MAP)
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: 'input',
|
|
340
|
+
name: 'description',
|
|
341
|
+
message: '项目描述:',
|
|
342
|
+
default: 'A new project'
|
|
343
|
+
}
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
const targetPath = path.resolve(process.cwd(), projectName);
|
|
347
|
+
const sourceTemplatePath = TEMPLATE_MAP[answers.templateKey];
|
|
348
|
+
|
|
349
|
+
const spinner = ora('正在复制模板文件...').start();
|
|
350
|
+
try {
|
|
351
|
+
await fs.copy(sourceTemplatePath, targetPath);
|
|
352
|
+
|
|
353
|
+
const pkgJsonPath = path.join(targetPath, 'package.json');
|
|
354
|
+
const pkg = await fs.readJson(pkgJsonPath);
|
|
355
|
+
pkg.description = answers.description;
|
|
356
|
+
await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
|
|
357
|
+
|
|
358
|
+
spinner.succeed('✅ 项目创建成功!');
|
|
359
|
+
console.log(`
|
|
58
360
|
接下来执行命令:
|
|
59
361
|
cd ${projectName}
|
|
60
362
|
npm install
|
|
61
363
|
npm run dev
|
|
62
364
|
`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
spinner.fail('❌ 创建项目失败');
|
|
367
|
+
console.error(err);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (command === 'add') {
|
|
373
|
+
const type = rest[0];
|
|
374
|
+
const name = rest[1];
|
|
375
|
+
|
|
376
|
+
if (!type || !name || !['component', 'page'].includes(type)) {
|
|
377
|
+
printHelp();
|
|
378
|
+
process.exit(0);
|
|
379
|
+
}
|
|
63
380
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
381
|
+
// 定位项目根目录
|
|
382
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
383
|
+
const projectConfig = loadProjectConfig(projectRoot);
|
|
384
|
+
|
|
385
|
+
// 确定框架类型(用于模板选择)
|
|
386
|
+
const framework = projectConfig.framework || detectProjectType(projectRoot, projectConfig);
|
|
387
|
+
projectConfig.framework = framework; // 确保后续使用
|
|
388
|
+
|
|
389
|
+
const spinner = ora('正在生成业务代码...').start();
|
|
390
|
+
try {
|
|
391
|
+
let result;
|
|
392
|
+
if (type === 'component') {
|
|
393
|
+
result = await generateComponentFiles(name, projectRoot, projectConfig, force);
|
|
394
|
+
spinner.succeed(
|
|
395
|
+
`✅ 已生成组件文件: ${toPascalCase(name)}\n` +
|
|
396
|
+
` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
|
|
397
|
+
` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
result = await generatePageFiles(name, projectRoot, projectConfig, force);
|
|
401
|
+
spinner.succeed(
|
|
402
|
+
`✅ 已生成页面文件: ${toPascalCase(name)}\n` +
|
|
403
|
+
` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
|
|
404
|
+
` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
spinner.fail('❌ 生成业务代码失败');
|
|
409
|
+
console.error(err);
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
67
412
|
}
|
|
413
|
+
|
|
414
|
+
printHelp();
|
|
415
|
+
process.exit(0);
|
|
68
416
|
})();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ljjjjjjj/my-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "vite脚手架,快速生成Vue3/React+vite项目模板",
|
|
5
5
|
"main": "./bin/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "node --test tests/"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"cli",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* 基础样式 */
|
|
2
|
+
.button {
|
|
3
|
+
font-family: inherit;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
border: none;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
transition: all 0.2s ease-in-out;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: 6px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* 尺寸变体 */
|
|
16
|
+
.small {
|
|
17
|
+
padding: 6px 16px;
|
|
18
|
+
font-size: 14px;
|
|
19
|
+
}
|
|
20
|
+
.medium {
|
|
21
|
+
padding: 10px 24px;
|
|
22
|
+
font-size: 16px;
|
|
23
|
+
}
|
|
24
|
+
.large {
|
|
25
|
+
padding: 14px 32px;
|
|
26
|
+
font-size: 18px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* 颜色变体 */
|
|
30
|
+
.primary {
|
|
31
|
+
background-color: #646cff;
|
|
32
|
+
color: #fff;
|
|
33
|
+
}
|
|
34
|
+
.primary:hover:not(:disabled) {
|
|
35
|
+
background-color: #535bf2;
|
|
36
|
+
transform: translateY(-2px);
|
|
37
|
+
box-shadow: 0 4px 12px rgba(100, 108, 255, 0.4);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.secondary {
|
|
41
|
+
background-color: #e9ecef;
|
|
42
|
+
color: #495057;
|
|
43
|
+
}
|
|
44
|
+
.secondary:hover:not(:disabled) {
|
|
45
|
+
background-color: #dee2e6;
|
|
46
|
+
transform: translateY(-2px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.danger {
|
|
50
|
+
background-color: #ff6b6b;
|
|
51
|
+
color: #fff;
|
|
52
|
+
}
|
|
53
|
+
.danger:hover:not(:disabled) {
|
|
54
|
+
background-color: #fa5252;
|
|
55
|
+
transform: translateY(-2px);
|
|
56
|
+
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 禁用状态 */
|
|
60
|
+
.button:disabled {
|
|
61
|
+
opacity: 0.5;
|
|
62
|
+
cursor: not-allowed;
|
|
63
|
+
transform: none !important;
|
|
64
|
+
box-shadow: none !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 点击反馈 */
|
|
68
|
+
.button:active:not(:disabled) {
|
|
69
|
+
transform: scale(0.96) !important;
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import styles from './{{componentName}}.module.css';
|
|
2
|
+
|
|
3
|
+
export default function {{componentName}}({
|
|
4
|
+
children,
|
|
5
|
+
onClick,
|
|
6
|
+
variant = 'primary',
|
|
7
|
+
disabled = false,
|
|
8
|
+
size = 'medium'
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<button
|
|
12
|
+
className={`${styles.button} ${styles[variant]} ${styles[size]}`}
|
|
13
|
+
onClick={onClick}
|
|
14
|
+
disabled={disabled}
|
|
15
|
+
>
|
|
16
|
+
{children || '{{componentName}}'}
|
|
17
|
+
</button>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 基础样式
|
|
2
|
+
.button {
|
|
3
|
+
font-family: inherit;
|
|
4
|
+
font-weight: 600;
|
|
5
|
+
border: none;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
transition: all 0.2s ease-in-out;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: 6px;
|
|
13
|
+
|
|
14
|
+
// 尺寸变体
|
|
15
|
+
&.small {
|
|
16
|
+
padding: 6px 16px;
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
}
|
|
19
|
+
&.medium {
|
|
20
|
+
padding: 10px 24px;
|
|
21
|
+
font-size: 16px;
|
|
22
|
+
}
|
|
23
|
+
&.large {
|
|
24
|
+
padding: 14px 32px;
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 颜色变体
|
|
29
|
+
&.primary {
|
|
30
|
+
background-color: #646cff;
|
|
31
|
+
color: #fff;
|
|
32
|
+
|
|
33
|
+
&:hover:not(:disabled) {
|
|
34
|
+
background-color: #535bf2;
|
|
35
|
+
transform: translateY(-2px);
|
|
36
|
+
box-shadow: 0 4px 12px rgba(100, 108, 255, 0.4);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&.secondary {
|
|
41
|
+
background-color: #e9ecef;
|
|
42
|
+
color: #495057;
|
|
43
|
+
|
|
44
|
+
&:hover:not(:disabled) {
|
|
45
|
+
background-color: #dee2e6;
|
|
46
|
+
transform: translateY(-2px);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&.danger {
|
|
51
|
+
background-color: #ff6b6b;
|
|
52
|
+
color: #fff;
|
|
53
|
+
|
|
54
|
+
&:hover:not(:disabled) {
|
|
55
|
+
background-color: #fa5252;
|
|
56
|
+
transform: translateY(-2px);
|
|
57
|
+
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 禁用状态
|
|
62
|
+
&:disabled {
|
|
63
|
+
opacity: 0.5;
|
|
64
|
+
cursor: not-allowed;
|
|
65
|
+
transform: none !important;
|
|
66
|
+
box-shadow: none !important;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 点击反馈
|
|
70
|
+
&:active:not(:disabled) {
|
|
71
|
+
transform: scale(0.96) !important;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
:class="[styles.button, styles[variant], styles[size]]"
|
|
4
|
+
:disabled="disabled"
|
|
5
|
+
@click="handleClick"
|
|
6
|
+
>
|
|
7
|
+
<!-- 支持插槽,方便传入复杂内容 -->
|
|
8
|
+
<slot>{{ componentName }}</slot>
|
|
9
|
+
</button>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
// 导入同名的 CSS Modules 样式文件
|
|
14
|
+
import styles from './{{componentName}}.module.scss';
|
|
15
|
+
|
|
16
|
+
// 定义组件属性
|
|
17
|
+
defineProps({
|
|
18
|
+
variant: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'primary',
|
|
21
|
+
validator: (val) => ['primary', 'secondary', 'danger'].includes(val)
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'medium',
|
|
26
|
+
validator: (val) => ['small', 'medium', 'large'].includes(val)
|
|
27
|
+
},
|
|
28
|
+
disabled: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 定义事件
|
|
35
|
+
const emit = defineEmits(['click']);
|
|
36
|
+
|
|
37
|
+
const handleClick = (event) => {
|
|
38
|
+
emit('click', event);
|
|
39
|
+
};
|
|
40
|
+
</script>
|