@ljjjjjjj/my-cli 1.0.0 → 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/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # my-cli
2
+
3
+ 一个基于 Node.js 的前端脚手架工具,帮助你快速生成 Vite 项目模板,并在已有项目中生成组件和页面骨架。
4
+
5
+ ## 功能特点
6
+
7
+ - 支持创建 Vue3 + Vite 或 React + Vite 项目模板
8
+ - 支持在当前项目中生成组件代码
9
+ - 支持在当前项目中生成页面代码
10
+ - 会自动识别项目是 Vue 还是 React
11
+ - 支持通过配置文件自定义生成目录
12
+ - 支持使用 --force 覆盖已存在文件
13
+
14
+ ## 支持的模板
15
+
16
+ - Vue3 + Vite
17
+ - React + Vite
18
+
19
+ ## 安装方式
20
+
21
+ ### 方式一:作为公开包使用(推荐给他人)
22
+
23
+ ```bash
24
+ npm install -g @ljjjjjjj/my-cli
25
+ ```
26
+
27
+ 安装后可以直接使用:
28
+
29
+ ```bash
30
+ my-cli --help
31
+ ```
32
+
33
+ ### 方式二:本地开发调试使用
34
+
35
+ ```bash
36
+ npm install
37
+ npm link
38
+ ```
39
+
40
+ 然后就可以在当前环境中直接执行:
41
+
42
+ ```bash
43
+ my-cli create your-project-name
44
+ ```
45
+
46
+ ## 命令说明
47
+
48
+ ### 0. 查看帮助
49
+
50
+ ```bash
51
+ my-cli help
52
+ ```
53
+
54
+ 或:
55
+
56
+ ```bash
57
+ my-cli --help
58
+ ```
59
+
60
+ 会看到当前支持的命令:
61
+
62
+ ```bash
63
+ my-cli create <项目名称>
64
+ my-cli add component <组件名称> [--force]
65
+ my-cli add page <页面名称> [--force]
66
+ my-cli help
67
+ ```
68
+
69
+ ### 1. 创建项目
70
+
71
+ ```bash
72
+ my-cli create your-project-name
73
+ ```
74
+
75
+ 执行后会提示你:
76
+
77
+ - 选择项目模板
78
+ - 输入项目描述
79
+
80
+ 创建成功后,会生成一个新的 Vite 项目目录。
81
+
82
+ ### 2. 生成组件
83
+
84
+ 在当前项目中执行:
85
+
86
+ ```bash
87
+ my-cli add component Button
88
+ ```
89
+
90
+ 脚手架会先从当前目录向上查找 package.json,定位项目根目录;随后根据项目类型自动选择对应模板。
91
+
92
+ 默认生成位置:
93
+
94
+ - Vue 项目:src/components/Button/
95
+ - React 项目:src/components/Button/
96
+
97
+ 默认生成内容:
98
+
99
+ Vue 项目:
100
+
101
+ ```text
102
+ src/components/Button/
103
+ ├── Button.vue
104
+ ├── Button.module.scss
105
+ ├── index.ts
106
+ ├── README.md
107
+ └── Button.test.ts
108
+ ```
109
+
110
+ React 项目:
111
+
112
+ ```text
113
+ src/components/Button/
114
+ ├── Button.jsx
115
+ ├── Button.module.css
116
+ ├── index.js
117
+ ├── README.md
118
+ └── Button.test.jsx
119
+ ```
120
+
121
+ ### 3. 生成页面
122
+
123
+ ```bash
124
+ my-cli add page User
125
+ ```
126
+
127
+ 默认生成位置:
128
+
129
+ - Vue 项目:src/pages/User/
130
+ - React 项目:src/pages/User/
131
+
132
+ 默认生成内容:
133
+
134
+ ```text
135
+ src/pages/User/
136
+ ├── index.vue
137
+ ├── style.scss
138
+ ├── api.ts
139
+ ├── hooks.ts
140
+ ├── store.ts
141
+ └── types.ts
142
+ ```
143
+
144
+ React 项目则会生成对应的 jsx 和 css 文件。
145
+
146
+ ### 4. 覆盖已存在文件
147
+
148
+ 如果目标文件已存在,默认会询问是否覆盖;如果你想直接覆盖,可以使用:
149
+
150
+ ```bash
151
+ my-cli add component Button --force
152
+ ```
153
+
154
+ 或简写:
155
+
156
+ ```bash
157
+ my-cli add page User -f
158
+ ```
159
+
160
+ ## 配置说明
161
+
162
+ 脚手架会先从当前目录向上查找 package.json,定位项目根目录。
163
+
164
+ 你可以在项目根目录创建 .my-cli.json 来指定生成目录和框架:
165
+
166
+ ```json
167
+ {
168
+ "framework": "vue",
169
+ "componentsDir": "src/components",
170
+ "pagesDir": "src/pages"
171
+ }
172
+ ```
173
+
174
+ 也可以在 package.json 中配置:
175
+
176
+ ```json
177
+ {
178
+ "myCli": {
179
+ "framework": "react",
180
+ "componentsDir": "src/components",
181
+ "pagesDir": "src/pages"
182
+ }
183
+ }
184
+ ```
185
+
186
+ 如果没有配置,脚手架会自动根据 package.json 或项目文件结构进行检测。
187
+
188
+ ## 适用场景
189
+
190
+ 这个脚手架适合用于:
191
+
192
+ - 快速初始化前端项目
193
+ - 在已有项目中快速生成组件和页面
194
+ - 作为学习和练习 CLI 工具的示例项目
195
+
196
+ ## 说明
197
+
198
+ 如果你希望把这个脚手架给他人使用,请直接使用发布到 npm 的包名进行安装。
package/bin/index.js CHANGED
@@ -5,64 +5,365 @@ import fs from 'fs-extra';
5
5
  import ora from 'ora';
6
6
  import inquirer from 'inquirer';
7
7
  import { fileURLToPath } from 'url';
8
+ import { program } from 'commander'; // ← 关键新增
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
 
13
+ // ---------- 读取 package.json ----------
14
+ const pkg = await fs.readJson(path.join(__dirname, '../package.json'));
15
+
16
+ // ---------- 模板映射 ----------
12
17
  const TEMPLATE_MAP = {
13
18
  'Vue3 + Vite': path.resolve(__dirname, '../templates/vue3-vite'),
14
19
  'React + Vite': path.resolve(__dirname, '../templates/react-vite')
15
20
  };
16
21
 
17
- const args = process.argv.slice(2);
18
- const command = args[0];
19
- const projectName = args[1];
22
+ // ---------- 工具函数 ----------
23
+ function toPascalCase(value) {
24
+ return value
25
+ .replace(/[_\-\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
26
+ .replace(/^(.)/, (char) => char.toUpperCase());
27
+ }
28
+
29
+ function renderTemplate(templatePath, replacements) {
30
+ const template = fs.readFileSync(templatePath, 'utf8');
31
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => replacements[key] ?? '');
32
+ }
33
+
34
+ // ---------- 查找项目根目录 ----------
35
+ function findProjectRoot(startDir = process.cwd()) {
36
+ let current = startDir;
37
+ while (current !== path.parse(current).root) {
38
+ const pkgPath = path.join(current, 'package.json');
39
+ if (fs.existsSync(pkgPath)) {
40
+ return current;
41
+ }
42
+ current = path.dirname(current);
43
+ }
44
+ return startDir;
45
+ }
46
+
47
+ // ---------- 加载项目配置 ----------
48
+ function loadProjectConfig(projectRoot) {
49
+ const config = {
50
+ framework: null,
51
+ componentsDir: 'src/components',
52
+ pagesDir: 'src/pages'
53
+ };
54
+
55
+ const cliConfigPath = path.join(projectRoot, '.my-cli.json');
56
+ if (fs.existsSync(cliConfigPath)) {
57
+ try {
58
+ const userConfig = fs.readJsonSync(cliConfigPath);
59
+ Object.assign(config, userConfig);
60
+ } catch {
61
+ console.warn('⚠️ 解析 .my-cli.json 失败,使用默认配置');
62
+ }
63
+ }
64
+
65
+ const pkgPath = path.join(projectRoot, 'package.json');
66
+ if (fs.existsSync(pkgPath)) {
67
+ try {
68
+ const pkg = fs.readJsonSync(pkgPath);
69
+ if (pkg.myCli) {
70
+ Object.assign(config, pkg.myCli);
71
+ }
72
+ } catch { }
73
+ }
74
+
75
+ config.componentsDir = config.componentsDir.replace(/^\.?\//, '');
76
+ config.pagesDir = config.pagesDir.replace(/^\.?\//, '');
77
+ return config;
78
+ }
79
+
80
+ // ---------- 检测项目类型 ----------
81
+ function detectProjectType(projectRoot, config) {
82
+ if (config.framework) {
83
+ if (config.framework === 'react' || config.framework === 'vue') {
84
+ return config.framework;
85
+ }
86
+ console.warn(`⚠️ 未知框架 "${config.framework}",将自动检测`);
87
+ }
88
+
89
+ const pkgPath = path.join(projectRoot, 'package.json');
90
+ if (fs.existsSync(pkgPath)) {
91
+ try {
92
+ const pkg = fs.readJsonSync(pkgPath);
93
+ const deps = {
94
+ ...(pkg.dependencies || {}),
95
+ ...(pkg.devDependencies || {})
96
+ };
97
+ if (deps.react || deps['@vitejs/plugin-react'] || deps.preact) return 'react';
98
+ if (deps.vue || deps['@vitejs/plugin-vue']) return 'vue';
99
+ } catch { }
100
+ }
101
+
102
+ const srcPath = path.join(projectRoot, 'src');
103
+ if (fs.existsSync(srcPath)) {
104
+ const files = fs.readdirSync(srcPath);
105
+ if (files.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'))) return 'react';
106
+ if (files.some(f => f.endsWith('.vue'))) return 'vue';
107
+ }
108
+ return 'vue';
109
+ }
110
+
111
+ // ---------- 获取脚手架配置 ----------
112
+ function getScaffoldConfig(projectRoot, projectType) {
113
+ const templateRoot = path.resolve(__dirname, '../templates/generator');
114
+ if (projectType === 'react') {
115
+ return {
116
+ projectType,
117
+ componentExt: 'jsx',
118
+ styleExt: 'css',
119
+ entryExt: 'js',
120
+ componentTemplatePath: path.join(templateRoot, 'component/react/component.jsx.tpl'),
121
+ styleTemplatePath: path.join(templateRoot, 'component/react/component.css.tpl'),
122
+ pageTemplatePath: path.join(templateRoot, 'page/react/page.jsx.tpl'),
123
+ pageStyleTemplatePath: path.join(templateRoot, 'page/react/page.css.tpl'),
124
+ pageFiles: [
125
+ { name: 'api.ts', templatePath: path.join(templateRoot, 'page/react/api.ts.tpl') },
126
+ { name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/react/hooks.ts.tpl') },
127
+ { name: 'store.ts', templatePath: path.join(templateRoot, 'page/react/store.ts.tpl') },
128
+ { name: 'types.ts', templatePath: path.join(templateRoot, 'page/react/types.ts.tpl') }
129
+ ],
130
+ testTemplate: (componentName) =>
131
+ `import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
132
+ };
133
+ }
134
+ // vue
135
+ return {
136
+ projectType: 'vue',
137
+ componentExt: 'vue',
138
+ styleExt: 'scss',
139
+ entryExt: 'ts',
140
+ componentTemplatePath: path.join(templateRoot, 'component/vue/component.vue.tpl'),
141
+ styleTemplatePath: path.join(templateRoot, 'component/vue/component.scss.tpl'),
142
+ pageTemplatePath: path.join(templateRoot, 'page/vue/page.vue.tpl'),
143
+ pageStyleTemplatePath: path.join(templateRoot, 'page/vue/page.scss.tpl'),
144
+ pageFiles: [
145
+ { name: 'api.ts', templatePath: path.join(templateRoot, 'page/vue/api.ts.tpl') },
146
+ { name: 'hooks.ts', templatePath: path.join(templateRoot, 'page/vue/hooks.ts.tpl') },
147
+ { name: 'store.ts', templatePath: path.join(templateRoot, 'page/vue/store.ts.tpl') },
148
+ { name: 'types.ts', templatePath: path.join(templateRoot, 'page/vue/types.ts.tpl') }
149
+ ],
150
+ testTemplate: (componentName) =>
151
+ `import { describe, it, expect } from 'vitest';\n\ndescribe('${componentName}', () => {\n it('renders correctly', () => {\n expect(true).toBe(true);\n });\n});\n`
152
+ };
153
+ }
20
154
 
21
- if (command !== 'create' || !projectName) {
22
- console.log('使用方法:my-cli create 项目名称');
23
- process.exit(0);
155
+ // ---------- 安全写入文件 ----------
156
+ async function safeWriteFile(filePath, content, force = false) {
157
+ if (fs.existsSync(filePath) && !force) {
158
+ const { overwrite } = await inquirer.prompt([
159
+ {
160
+ type: 'confirm',
161
+ name: 'overwrite',
162
+ message: `文件 ${path.basename(filePath)} 已存在,是否覆盖?`,
163
+ default: false
164
+ }
165
+ ]);
166
+ if (!overwrite) return false;
167
+ }
168
+ await fs.outputFile(filePath, content);
169
+ return true;
24
170
  }
25
171
 
26
- (async function run() {
27
- console.log(`✨ 正在创建项目: ${projectName}`);
172
+ // ---------- 生成组件 ----------
173
+ async function generateComponentFiles(name, projectRoot, config, force) {
174
+ const componentName = toPascalCase(name);
175
+ const baseDir = path.join(projectRoot, config.componentsDir, componentName);
176
+ await fs.ensureDir(baseDir);
28
177
 
29
- const answers = await inquirer.prompt([
178
+ const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
179
+ const replacements = {
180
+ componentName,
181
+ componentNameLower: componentName.toLowerCase()
182
+ };
183
+
184
+ const filesToGenerate = [
185
+ {
186
+ path: path.join(baseDir, `${componentName}.${scaffoldConfig.componentExt}`),
187
+ content: renderTemplate(scaffoldConfig.componentTemplatePath, replacements)
188
+ },
30
189
  {
31
- type: 'select',
32
- name: 'templateKey',
33
- message: '请选择项目模板:',
34
- choices: Object.keys(TEMPLATE_MAP)
190
+ path: path.join(baseDir, `${componentName}.module.${scaffoldConfig.styleExt}`),
191
+ content: renderTemplate(scaffoldConfig.styleTemplatePath, replacements)
35
192
  },
36
193
  {
37
- type: 'input',
38
- name: 'description',
39
- message: '项目描述:',
40
- default: 'A new project'
194
+ path: path.join(baseDir, `index.${scaffoldConfig.entryExt}`),
195
+ content: `export { default } from './${componentName}.${scaffoldConfig.componentExt}';\n`
196
+ },
197
+ {
198
+ path: path.join(baseDir, 'README.md'),
199
+ content: `# ${componentName}\n\n${componentName} 组件说明。\n`
200
+ },
201
+ {
202
+ path: path.join(baseDir, `${componentName}.${scaffoldConfig.projectType === 'react' ? 'test.jsx' : 'test.ts'}`),
203
+ content: scaffoldConfig.testTemplate(componentName)
204
+ }
205
+ ];
206
+
207
+ let overwrittenCount = 0, skippedCount = 0;
208
+ for (const file of filesToGenerate) {
209
+ const result = await safeWriteFile(file.path, file.content, force);
210
+ result ? overwrittenCount++ : skippedCount++;
211
+ }
212
+ return { overwrittenCount, skippedCount, baseDir };
213
+ }
214
+
215
+ // ---------- 生成页面 ----------
216
+ async function generatePageFiles(name, projectRoot, config, force) {
217
+ const pageName = toPascalCase(name);
218
+ const baseDir = path.join(projectRoot, config.pagesDir, pageName);
219
+ await fs.ensureDir(baseDir);
220
+
221
+ const scaffoldConfig = getScaffoldConfig(projectRoot, config.framework || detectProjectType(projectRoot, config));
222
+ const replacements = {
223
+ pageName,
224
+ pageNameLower: pageName.toLowerCase()
225
+ };
226
+
227
+ const filesToGenerate = [
228
+ {
229
+ path: path.join(baseDir, `index.${scaffoldConfig.componentExt}`),
230
+ content: renderTemplate(scaffoldConfig.pageTemplatePath, replacements)
231
+ },
232
+ {
233
+ path: path.join(baseDir, `style.${scaffoldConfig.styleExt}`),
234
+ content: renderTemplate(scaffoldConfig.pageStyleTemplatePath, replacements)
41
235
  }
42
- ]);
236
+ ];
237
+ for (const pageFile of scaffoldConfig.pageFiles) {
238
+ filesToGenerate.push({
239
+ path: path.join(baseDir, pageFile.name),
240
+ content: renderTemplate(pageFile.templatePath, replacements)
241
+ });
242
+ }
43
243
 
44
- const targetPath = path.resolve(process.cwd(), projectName);
45
- const sourceTemplatePath = TEMPLATE_MAP[answers.templateKey];
244
+ let overwrittenCount = 0, skippedCount = 0;
245
+ for (const file of filesToGenerate) {
246
+ const result = await safeWriteFile(file.path, file.content, force);
247
+ result ? overwrittenCount++ : skippedCount++;
248
+ }
249
+ return { overwrittenCount, skippedCount, baseDir };
250
+ }
46
251
 
47
- const spinner = ora('正在复制模板文件...').start();
48
- try {
49
- await fs.copy(sourceTemplatePath, targetPath);
252
+ // ---------- Commander 配置 ----------
253
+ program
254
+ .version(pkg.version, '-V, --version', '显示版本号')
255
+ .description('一个快速生成 Vue3/React + Vite 项目的脚手架工具');
50
256
 
51
- const pkgJsonPath = path.join(targetPath, 'package.json');
52
- const pkg = await fs.readJson(pkgJsonPath);
53
- pkg.description = answers.description;
54
- await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
257
+ // ---------- create 命令 ----------
258
+ program
259
+ .command('create <project-name>')
260
+ .description('创建一个新项目')
261
+ .option('-t, --template <name>', '指定模板名称(Vue3 + Vite / React + Vite)')
262
+ .option('-d, --description <desc>', '项目描述')
263
+ .action(async (projectName, options) => {
264
+ console.log(`✨ 正在创建项目: ${projectName}`);
55
265
 
56
- spinner.succeed('✅ 项目创建成功!');
57
- console.log(`
266
+ let templateKey = options.template;
267
+ let description = options.description;
268
+
269
+ if (!templateKey) {
270
+ const answer = await inquirer.prompt([
271
+ {
272
+ type: 'select',
273
+ name: 'templateKey',
274
+ message: '请选择项目模板:',
275
+ choices: Object.keys(TEMPLATE_MAP)
276
+ }
277
+ ]);
278
+ templateKey = answer.templateKey;
279
+ } else if (!Object.keys(TEMPLATE_MAP).includes(templateKey)) {
280
+ console.error(`❌ 未知模板: ${templateKey}`);
281
+ console.log(`可用模板: ${Object.keys(TEMPLATE_MAP).join(', ')}`);
282
+ process.exit(1);
283
+ }
284
+
285
+ if (!description) {
286
+ const answer = await inquirer.prompt([
287
+ {
288
+ type: 'input',
289
+ name: 'description',
290
+ message: '项目描述:',
291
+ default: 'A new project'
292
+ }
293
+ ]);
294
+ description = answer.description;
295
+ }
296
+
297
+ const targetPath = path.resolve(process.cwd(), projectName);
298
+ const sourceTemplatePath = TEMPLATE_MAP[templateKey];
299
+
300
+ const spinner = ora('正在复制模板文件...').start();
301
+ try {
302
+ await fs.copy(sourceTemplatePath, targetPath);
303
+ const pkgJsonPath = path.join(targetPath, 'package.json');
304
+ const pkg = await fs.readJson(pkgJsonPath);
305
+ pkg.description = description;
306
+ await fs.writeJson(pkgJsonPath, pkg, { spaces: 2 });
307
+ spinner.succeed('✅ 项目创建成功!');
308
+ console.log(`
58
309
  接下来执行命令:
59
- cd ${projectName}
60
- npm install
61
- npm run dev
310
+ cd ${projectName}
311
+ npm install
312
+ npm run dev
62
313
  `);
314
+ } catch (err) {
315
+ spinner.fail('❌ 创建项目失败');
316
+ console.error(err);
317
+ }
318
+ });
63
319
 
64
- } catch (err) {
65
- spinner.fail('❌ 创建项目失败');
66
- console.error(err);
67
- }
68
- })();
320
+ // ---------- add 命令 ----------
321
+ program
322
+ .command('add <type> <name>')
323
+ .description('添加组件或页面(type: component | page)')
324
+ .option('-f, --force', '强制覆盖已存在的文件')
325
+ .action(async (type, name, options) => {
326
+ if (!['component', 'page'].includes(type)) {
327
+ console.error('❌ 类型必须是 component 或 page');
328
+ process.exit(1);
329
+ }
330
+
331
+ const projectRoot = findProjectRoot(process.cwd());
332
+ const projectConfig = loadProjectConfig(projectRoot);
333
+ const framework = projectConfig.framework || detectProjectType(projectRoot, projectConfig);
334
+ projectConfig.framework = framework;
335
+
336
+ const spinner = ora('正在生成业务代码...').start();
337
+ try {
338
+ let result;
339
+ if (type === 'component') {
340
+ result = await generateComponentFiles(name, projectRoot, projectConfig, options.force);
341
+ spinner.succeed(
342
+ `✅ 已生成组件文件: ${toPascalCase(name)}\n` +
343
+ ` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
344
+ ` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
345
+ );
346
+ } else {
347
+ result = await generatePageFiles(name, projectRoot, projectConfig, options.force);
348
+ spinner.succeed(
349
+ `✅ 已生成页面文件: ${toPascalCase(name)}\n` +
350
+ ` 位置: ${path.relative(projectRoot, result.baseDir)}\n` +
351
+ ` 覆盖: ${result.overwrittenCount} 个文件,跳过: ${result.skippedCount} 个`
352
+ );
353
+ }
354
+ } catch (err) {
355
+ spinner.fail('❌ 生成业务代码失败');
356
+ console.error(err);
357
+ }
358
+ });
359
+
360
+ program.on('command:*', () => {
361
+ console.error('❌ 未知命令: %s', program.args.join(' '));
362
+ program.help();
363
+ });
364
+
365
+ program.parse(process.argv);
366
+
367
+ if (!process.argv.slice(2).length) {
368
+ program.help();
369
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ljjjjjjj/my-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "vite脚手架,快速生成Vue3/React+vite项目模板",
5
5
  "main": "./bin/index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
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>
@@ -0,0 +1,3 @@
1
+ export const fetch{{pageName}}Data = async () => {
2
+ return {};
3
+ };
@@ -0,0 +1,3 @@
1
+ export function use{{pageName}}() {
2
+ return {};
3
+ }
@@ -0,0 +1,3 @@
1
+ .{{pageNameLower}} {
2
+ padding: 16px;
3
+ }
@@ -0,0 +1,5 @@
1
+ import './style.css';
2
+
3
+ export default function {{pageName}}() {
4
+ return <section className="{{pageNameLower}}">{{pageName}}</section>;
5
+ }
@@ -0,0 +1,3 @@
1
+ export const {{pageName}}Store = {
2
+ state: {}
3
+ };
@@ -0,0 +1,3 @@
1
+ export interface {{pageName}}State {
2
+ id?: string;
3
+ }
@@ -0,0 +1,3 @@
1
+ export const fetch{{pageName}}Data = async () => {
2
+ return {};
3
+ };
@@ -0,0 +1,3 @@
1
+ export function use{{pageName}}() {
2
+ return {};
3
+ }
@@ -0,0 +1,3 @@
1
+ .{{pageNameLower}} {
2
+ padding: 16px;
3
+ }
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <section class="{{pageNameLower}}">
3
+ <h1>{{pageName}}</h1>
4
+ </section>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ </script>
9
+
10
+ <style scoped lang="scss">
11
+ .{{pageNameLower}} {
12
+ padding: 16px;
13
+ }
14
+ </style>
@@ -0,0 +1,3 @@
1
+ export const {{pageName}}Store = {
2
+ state: {}
3
+ };
@@ -0,0 +1,3 @@
1
+ export interface {{pageName}}State {
2
+ id?: string;
3
+ }