@aibai/create-myvue 0.0.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,132 @@
1
+ # create-my-vue
2
+
3
+ 一个对 `pnpm create vue@latest` 命令的二次封装,支持通过配置文件自定义默认选项。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 全局安装
9
+ npm install -g create-my-vue
10
+
11
+ # 或者使用 npx
12
+ npx create-my-vue
13
+ ```
14
+
15
+ ## 使用方法
16
+
17
+ ### 基本使用
18
+
19
+ ```bash
20
+ # 使用默认配置创建项目
21
+ create-my-vue
22
+
23
+ # 指定项目名称
24
+ create-my-vue my-vue-project
25
+
26
+ # 使用默认选项
27
+ create-my-vue --default
28
+ ```
29
+
30
+ ### 命令行选项
31
+
32
+ ```
33
+ Usage: create-my-vue [options] [projectName]
34
+
35
+ A wrapper around pnpm create vue@latest with custom defaults
36
+
37
+ Arguments:
38
+ projectName Name of the project to create
39
+
40
+ Options:
41
+ -V, --version output the version number
42
+ -d, --default Use default options
43
+ --typescript Add TypeScript
44
+ --no-typescript Do not add TypeScript
45
+ --jsx Add JSX Support
46
+ --no-jsx Do not add JSX Support
47
+ --router Add Vue Router
48
+ --no-router Do not add Vue Router
49
+ --pinia Add Pinia
50
+ --no-pinia Do not add Pinia
51
+ --vitest Add Vitest for unit testing
52
+ --no-vitest Do not add Vitest for unit testing
53
+ --cypress Add Cypress for end-to-end testing
54
+ --no-cypress Do not add Cypress for end-to-end testing
55
+ --eslint Add ESLint for code quality
56
+ --no-eslint Do not add ESLint for code quality
57
+ --prettier Add Prettier for code formatting
58
+ --no-prettier Do not add Prettier for code formatting
59
+ -h, --help display help for command
60
+ ```
61
+
62
+ ## 配置文件
63
+
64
+ 你可以在当前目录下创建 `create-my-vue.config.js` 文件来自定义默认选项。
65
+
66
+ ### 配置示例
67
+
68
+ ```javascript
69
+ // create-my-vue.config.js
70
+ module.exports = {
71
+ // 项目名称(可选,也可以通过命令行参数指定)
72
+ // projectName: 'my-vue-project',
73
+
74
+ // 使用默认配置(等同于 --default)
75
+ // default: true,
76
+
77
+ // 具体选项配置
78
+ typescript: true, // 添加 TypeScript
79
+ jsx: true, // 添加 JSX 支持
80
+ router: true, // 添加 Vue Router
81
+ pinia: true, // 添加 Pinia
82
+ vitest: true, // 添加 Vitest 用于单元测试
83
+ cypress: false, // 不添加 Cypress 用于端到端测试
84
+ eslint: true, // 添加 ESLint 用于代码质量
85
+ prettier: true // 添加 Prettier 用于代码格式化
86
+ };
87
+ ```
88
+
89
+ ### 配置优先级
90
+
91
+ 命令行参数的优先级高于配置文件,例如:
92
+
93
+ ```bash
94
+ # 即使配置文件中 typescript 为 false,这里也会创建 TypeScript 项目
95
+ create-my-vue --typescript
96
+ ```
97
+
98
+ ## 示例
99
+
100
+ ### 1. 使用配置文件创建项目
101
+
102
+ 1. 创建 `create-my-vue.config.js` 文件:
103
+ ```javascript
104
+ module.exports = {
105
+ typescript: true,
106
+ router: true,
107
+ pinia: true,
108
+ eslint: true,
109
+ prettier: true
110
+ };
111
+ ```
112
+
113
+ 2. 运行命令:
114
+ ```bash
115
+ create-my-vue my-vue-app
116
+ ```
117
+
118
+ ### 2. 直接使用命令行选项
119
+
120
+ ```bash
121
+ create-my-vue my-vue-app --typescript --router --pinia --eslint --prettier
122
+ ```
123
+
124
+ ### 3. 使用默认选项
125
+
126
+ ```bash
127
+ create-my-vue my-vue-app --default
128
+ ```
129
+
130
+ ## 许可证
131
+
132
+ ISC
package/bin/cli.js ADDED
@@ -0,0 +1,1114 @@
1
+ const { program } = require('commander');
2
+ const { spawn } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const inquirer = require('inquirer');
6
+
7
+ // 插件配置
8
+ const plugins = [
9
+ {
10
+ name: 'unplugin-auto-import',
11
+ description: '按需引入 Vue 相关 API',
12
+ dependencies: ['unplugin-auto-import'],
13
+ config: {
14
+ vite: {
15
+ imports: [`import AutoImport from 'unplugin-auto-import/vite'`],
16
+ plugins: [
17
+ `AutoImport({
18
+ include: [
19
+ /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
20
+ /\.vue$/,
21
+ /\.vue\?vue/ // .vue
22
+ ],
23
+ imports: [
24
+ 'vue',
25
+ 'vue-router',
26
+ 'pinia'
27
+ ],
28
+ dts: 'src/types/auto-import.d.ts',
29
+ eslintrc: {
30
+ enabled: true,
31
+ filepath: './.eslintrc-auto-import.json',
32
+ globalsPropValue: true,
33
+ },
34
+ }),`
35
+ ]
36
+ }
37
+ }
38
+ },
39
+ {
40
+ name: 'unplugin-vue-components',
41
+ description: '按需引入 Vue 组件',
42
+ dependencies: ['unplugin-vue-components'],
43
+ config: {
44
+ vite: {
45
+ imports: [`import Components from 'unplugin-vue-components/vite'`],
46
+ plugins: [
47
+ `Components({
48
+ dirs: ['src/components'],
49
+ extensions: ['vue'],
50
+ dts: 'src/types/auto-components.d.ts',
51
+ resolvers: []
52
+ }),`
53
+ ]
54
+ }
55
+ }
56
+ },
57
+ {
58
+ name: 'unocss',
59
+ description: '按需引入 UnoCSS 样式',
60
+ dependencies: ['unocss'],
61
+ config: {
62
+ vite: {
63
+ imports: [`import UnoCSS from 'unocss/vite'`],
64
+ plugins: [
65
+ `UnoCSS()`
66
+ ]
67
+ },
68
+ files: [
69
+ {
70
+ path: 'uno.config.ts',
71
+ content: `
72
+ import { defineConfig, transformerDirectives, transformerVariantGroup } from 'unocss'
73
+
74
+ export default defineConfig({theme: {
75
+ colors: {
76
+ primary: 'var(--primary-color)',
77
+ },
78
+ },
79
+ shortcuts: {
80
+ 'content': 'relative max-w-[1024px] mx-auto',
81
+ 'btn': '',
82
+ },
83
+ transformers: [
84
+ transformerDirectives(),
85
+ transformerVariantGroup()
86
+ ]
87
+ })
88
+ `
89
+ },
90
+ {
91
+ path: 'src/main.js',
92
+ content: "import 'virtual:uno.css';\n",
93
+ append: true
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ {
99
+ name: 'vite-plugin-svg-icons',
100
+ description: '用于创建 SVG 精灵的 Vite 插件',
101
+ dependencies: ['vite-plugin-svg-icons'],
102
+ config: {
103
+ vite: {
104
+ imports: [`import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'`],
105
+ plugins: [
106
+ `createSvgIconsPlugin({
107
+ // 指定需要缓存的图标文件夹
108
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
109
+ // 指定symbolId格式
110
+ symbolId: 'icon-[dir]-[name]',
111
+ }),`
112
+ ]
113
+ },
114
+ files: [
115
+ {
116
+ path: 'src/components/Icon.vue',
117
+ content: `
118
+ <script setup lang="ts">
119
+ import { computed } from 'vue';
120
+
121
+ const props = defineProps({
122
+ prefix: { type: String, default: 'icon', },
123
+ iconName: { type: String, required: true, },
124
+ color: { type: String, default: '' }
125
+ })
126
+
127
+ const symbolId = computed(() => '#' + props.prefix + '-' + props.iconName);
128
+ </script>
129
+
130
+ <template>
131
+ <svg aria-hidden="true" class="svg-icon">
132
+ <use :xlink:href="symbolId" :fill="color" />
133
+ </svg>
134
+ </template>
135
+
136
+ <style>
137
+ .svg-icon {
138
+ width: 1em;
139
+ height: 1em;
140
+ vertical-align: -0.15em;
141
+ overflow: hidden;
142
+ fill: currentColor;
143
+ }
144
+ </style>
145
+ `
146
+ },
147
+ {
148
+ path: 'src/main.js',
149
+ content: "import 'virtual:svg-icons-register';\n",
150
+ append: true
151
+ }
152
+ ]
153
+ }
154
+ },
155
+ {
156
+ name: 'axios',
157
+ description: '基于 Promise 的 HTTP 客户端,用于浏览器和 node.js',
158
+ dependencies: ['axios'],
159
+ config: {
160
+ files: [
161
+ {
162
+ path: 'src/utils/axiosConfig.ts',
163
+ content: `
164
+ // axios 配置文件
165
+ // 定义全局axios配置,包括基础URL、超时时间、响应格式验证等
166
+
167
+ // API基础URL,使用相对路径以便通过Vite代理转发
168
+ export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://110.41.59.11/api';
169
+ // 注意:请勿使用绝对URL,否则会绕过Vite代理
170
+
171
+ // 请求超时时间(毫秒)
172
+ export const REQUEST_TIMEOUT = 40000;
173
+
174
+ // 响应数据格式验证
175
+ export const validateResponseFormat = (data: unknown): boolean => {
176
+ return typeof data === 'object' && data !== null;
177
+ };
178
+
179
+ // 错误消息映射
180
+ export const errorMessageMap: Record<number, string> = {
181
+ 400: '请求参数错误',
182
+ 401: '未授权,请重新登录',
183
+ 403: '拒绝访问',
184
+ 404: '请求的资源不存在',
185
+ 405: '请求方法不允许',
186
+ 500: '服务器内部错误',
187
+ 502: '网关错误',
188
+ 503: '服务不可用',
189
+ 504: '网关超时',
190
+ };
191
+
192
+ `
193
+ },
194
+ {
195
+ path: 'src/utils/axiosInstance.ts',
196
+ content: `
197
+ // axios 实例封装
198
+ import axios, { type AxiosRequestConfig, type AxiosError } from 'axios';
199
+ import { API_BASE_URL, REQUEST_TIMEOUT } from './axiosConfig';
200
+ import { handleAxiosError, isBusinessError, formatBusinessError } from './errorHandler';
201
+
202
+ interface HttpResponse<T = unknown> {
203
+ code: number;
204
+ msg: string;
205
+ data: T;
206
+ }
207
+
208
+ // 创建axios实例
209
+ const http = axios.create({
210
+ baseURL: API_BASE_URL,
211
+ timeout: REQUEST_TIMEOUT,
212
+ headers: {
213
+ 'Content-Type': 'application/json;charset=UTF-8',
214
+ },
215
+ });
216
+
217
+ // 请求拦截器
218
+ http.interceptors.request.use(
219
+ (config) => {
220
+ config.headers = config.headers || {};
221
+ // 从本地存储获取token
222
+ const token = localStorage.getItem('APP_TOKEN');
223
+ // 如果存在token,则添加到请求头
224
+ if (token) {
225
+ config.headers = config.headers || {};
226
+ config.headers['x-token'] = token;
227
+ }
228
+ return config;
229
+ },
230
+ (error) => {
231
+ // 请求错误处理
232
+ console.error('请求配置错误:', error);
233
+ return Promise.reject(error);
234
+ }
235
+ );
236
+
237
+ // 响应拦截器
238
+ http.interceptors.response.use(
239
+ (response) => {
240
+ const data = response.data;
241
+
242
+ // 检查是否为业务错误
243
+ if (isBusinessError(data)) {
244
+ const businessError = formatBusinessError(data);
245
+ // globalErrorHandler(businessError);
246
+ return Promise.reject(businessError);
247
+ }
248
+
249
+ // 直接返回响应数据
250
+ return data.data;
251
+ },
252
+ (error: AxiosError) => {
253
+ // 处理HTTP错误
254
+ const apiError = handleAxiosError(error);
255
+ // globalErrorHandler(apiError);
256
+ return Promise.reject(apiError);
257
+ }
258
+ );
259
+
260
+ // 导出http实例
261
+ export default http;
262
+
263
+ // 导出常用的请求方法
264
+ export const request = {
265
+ // 使用类型断言来匹配响应拦截器实际返回的data.data
266
+ get: <T = unknown>(url: string, config?: AxiosRequestConfig) => http.get<HttpResponse<T>>(url, config) as unknown as Promise<T>,
267
+ post: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) => http.post<HttpResponse<T>>(url, data, config) as unknown as Promise<T>,
268
+ // 导出axios引用,用于取消请求等高级功能
269
+ axios: axios
270
+ };
271
+
272
+ `
273
+ },
274
+ {
275
+ path: 'src/utils/errorHandler.ts',
276
+ content: `
277
+ // 错误处理工具
278
+ import type { AxiosError } from 'axios';
279
+ import { errorMessageMap } from './axiosConfig';
280
+
281
+ // 错误类型接口
282
+ export interface ApiError extends Error {
283
+ code?: number;
284
+ msg?: string;
285
+ data?: unknown;
286
+ }
287
+
288
+ /**
289
+ * 创建API错误对象
290
+ * @param message 错误消息
291
+ * @param status 状态码
292
+ * @param code 错误代码
293
+ * @param data 错误数据
294
+ * @returns ApiError对象
295
+ */
296
+ export function createApiError<T>(message: string, code?: number, data?: T): ApiError {
297
+ const error = new Error(message) as ApiError;
298
+ error.msg = message;
299
+ error.code = code;
300
+ error.data = data;
301
+ return error;
302
+ }
303
+
304
+ /**
305
+ * 处理axios错误
306
+ * @param error axios错误对象
307
+ * @returns 标准化的ApiError
308
+ */
309
+ export function handleAxiosError<T>(error: AxiosError): ApiError {
310
+ // 网络错误
311
+ if (!error.response) {
312
+ return createApiError('网络错误,请检查您的网络连接', -1);
313
+ }
314
+
315
+ const { response } = error;
316
+ const status = response.status;
317
+
318
+ // 从响应数据中提取错误信息
319
+ const responseData = response.data;
320
+ // 安全地访问响应数据
321
+ const data = responseData as Record<string, T>;
322
+ const errorMessage = data?.msg as string ||
323
+ errorMessageMap[status] ||
324
+ \`请求失败,状态码: ${status}\`;
325
+
326
+ const errorCode = data?.code as number || status;
327
+
328
+ return createApiError(errorMessage, errorCode, responseData);
329
+ }
330
+
331
+ /**
332
+ * 处理业务错误
333
+ * @param data 响应数据
334
+ * @returns 是否为业务错误
335
+ */
336
+ export function isBusinessError(data: unknown): boolean {
337
+ // 这里根据后端返回的业务错误格式进行判断
338
+ return data != null && typeof data === 'object' && 'code' in data && data.code !== 0;
339
+ }
340
+
341
+ /**
342
+ * 格式化业务错误
343
+ * @param data 响应数据
344
+ * @returns ApiError对象
345
+ */
346
+ export function formatBusinessError<T>(data: T): ApiError {
347
+ const businessData = data as Record<string, T>;
348
+ return createApiError(
349
+ (businessData?.msg as string) || '业务处理失败',
350
+ (businessData?.code as number) || 400,
351
+ data
352
+ );
353
+ }
354
+
355
+ /**
356
+ * 全局错误处理器
357
+ * 可以在这里添加错误日志记录、上报等功能
358
+ * @param error 错误对象
359
+ */
360
+ export function globalErrorHandler(error: Error | ApiError): void {
361
+ console.error('Global Error:', error);
362
+
363
+ // 可以在这里添加错误上报逻辑
364
+ // 例如:上报到Sentry等错误监控服务
365
+ // reportErrorToSentry(error);
366
+
367
+ // 根据错误类型进行不同处理
368
+ // if ('code' in error && error.code === 401) {
369
+ // // 处理未授权错误,如跳转到登录页
370
+ // handleUnauthorizedError();
371
+ // }
372
+ }
373
+
374
+ /**
375
+ * 处理未授权错误
376
+ */
377
+ // function handleUnauthorizedError(): void {
378
+ // // 清除本地存储的token
379
+ // localStorage.removeItem('token');
380
+
381
+ // // 可以在这里添加跳转到登录页的逻辑
382
+ // // 注意:由于这是工具函数,避免直接操作Vue Router
383
+ // // 实际使用时,应该在组件或业务逻辑中处理跳转
384
+ // console.warn('用户未授权,请重新登录');
385
+ // }
386
+
387
+ `
388
+ },
389
+ {
390
+ path: 'src/utils/imageUtils.ts',
391
+ content: `
392
+ // 图片地址处理工具函数
393
+
394
+ // 导出默认的图片基础URL配置(可以从环境变量读取)
395
+ export const DEFAULT_IMAGE_BASE_URL = import.meta.env.VITE_IMAGE_BASE_URL || '';
396
+
397
+ /**
398
+ * 图片地址拼接函数
399
+ * @param baseUrl 基础图片域名或路径
400
+ * @param imagePath 图片相对路径
401
+ * @returns 完整的图片URL
402
+ */
403
+ export const joinImageUrl = (imagePath: string, baseUrl?: string): string => {
404
+ // 处理空值情况
405
+ if (!imagePath) return '';
406
+
407
+ // 如果imagePath已经是完整的URL(包含http或https),直接返回
408
+ if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
409
+ return imagePath;
410
+ }
411
+
412
+ // 如果baseUrl未提供,使用默认值
413
+ const newBaseUrl = baseUrl || DEFAULT_IMAGE_BASE_URL;
414
+
415
+ // 处理baseUrl结尾的斜杠和imagePath开头的斜杠,避免重复
416
+ const normalizedBaseUrl = newBaseUrl.endsWith('/') ? newBaseUrl.slice(0, -1) : newBaseUrl;
417
+ let normalizedImagePath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
418
+ // 判断链接开头是否包含 '/api'前缀,包含则删除
419
+ if (normalizedImagePath.startsWith('api/')) {
420
+ normalizedImagePath = normalizedImagePath.slice(4);
421
+ }
422
+ // 拼接完整URL
423
+ return \`${normalizedBaseUrl}/${normalizedImagePath}\`;
424
+ };
425
+
426
+ /**
427
+ * 批量拼接图片URL
428
+ * @param baseUrl 基础图片域名或路径
429
+ * @param imagePaths 图片相对路径数组
430
+ * @returns 完整图片URL数组
431
+ */
432
+ export const joinImageUrls = (imagePaths: string[], baseUrl?: string): string[] => {
433
+ return imagePaths.map(path => joinImageUrl(path, baseUrl));
434
+ };
435
+
436
+ /**
437
+ * Vue组件:图片URL拼接工具
438
+ * 可以在模板中直接使用
439
+ */
440
+ export const useImageUrl = () => {
441
+
442
+ return {
443
+ // 单个图片URL拼接
444
+ getImageUrl: (imagePath: string, customBaseUrl?: string) => {
445
+ return joinImageUrl(imagePath, customBaseUrl);
446
+ },
447
+
448
+ // 批量图片URL拼接
449
+ getImageUrls: (imagePaths: string[], customBaseUrl?: string) => {
450
+ return joinImageUrls(imagePaths, customBaseUrl);
451
+ }
452
+ };
453
+ };
454
+ `
455
+ },
456
+ {
457
+ path: 'src/utils/README.md',
458
+ content: `
459
+ # Axios 二次封装说明
460
+
461
+ ## 项目结构
462
+
463
+ \`\`\`
464
+ /src/utils/
465
+ ├── axiosConfig.ts # axios 配置文件
466
+ ├── axiosInstance.ts # axios 实例封装
467
+ ├── errorHandler.ts # 错误处理工具
468
+ ├── axiosExample.ts # 使用示例
469
+ └── README.md # 说明文档
470
+
471
+ /src/api/
472
+ ├── index.ts # API管理入口
473
+ ├── userApi.ts # 用户相关API
474
+ └── commonApi.ts # 通用API
475
+ \`\`\`
476
+
477
+ ## 功能特性
478
+
479
+ 1. **统一配置管理**:通过\`axiosConfig.ts\`集中管理API基础URL、超时时间等配置
480
+ 2. **请求拦截器**:自动添加token认证、时间戳防缓存
481
+ 3. **响应拦截器**:统一处理响应数据格式和错误
482
+ 4. **错误处理机制**:HTTP错误、业务错误统一处理,支持错误日志上报
483
+ 5. **API模块化管理**:按业务模块划分API接口,方便维护
484
+ 6. **TypeScript支持**:完整的类型定义,提供良好的开发体验
485
+ 7. **文件上传支持**:简化文件上传操作
486
+ 8. **取消请求支持**:支持取消正在进行的请求
487
+
488
+ ## 安装和配置
489
+
490
+ 项目已安装axios依赖,如需手动安装:
491
+
492
+ \`\`\`bash
493
+ pnpm install axios
494
+ \`\`\`
495
+
496
+ ### 环境变量配置
497
+
498
+ 在项目根目录创建`.env`文件,配置API基础URL:
499
+
500
+ \`\`\`
501
+ VITE_API_BASE_URL=https://your-api-domain.com/api
502
+ \`\`\`
503
+
504
+ ## 使用方法
505
+
506
+ ### 1. 导入方式
507
+
508
+ \`\`\`typescript
509
+ // 方式1:导入API模块(推荐)
510
+ import { api } from '@/api';
511
+
512
+ // 方式2:直接使用request工具函数
513
+ import { request } from '@/api';
514
+
515
+ // 方式3:导入原始axios实例
516
+ import { http } from '@/api';
517
+ \`\`\`
518
+
519
+ ### 2. 基本使用示例
520
+
521
+ #### 用户登录
522
+
523
+ \`\`\`typescript
524
+ async function login() {
525
+ try {
526
+ const result = await api.user.login({
527
+ username: 'admin',
528
+ password: '123456'
529
+ });
530
+
531
+ // 保存token
532
+ localStorage.setItem('token', result.token);
533
+ console.log('登录成功');
534
+ } catch (error) {
535
+ console.error('登录失败:', error.message);
536
+ }
537
+ }
538
+ \`\`\`
539
+
540
+ #### 获取用户信息
541
+
542
+ \`\`\`typescript
543
+ async function getUserInfo() {
544
+ try {
545
+ const userInfo = await api.user.getUserInfo();
546
+ console.log('用户信息:', userInfo);
547
+ } catch (error) {
548
+ console.error('获取用户信息失败:', error);
549
+ }
550
+ }
551
+ \`\`\`
552
+
553
+ #### 通用请求方法
554
+
555
+ \`\`\`typescript
556
+ // GET请求
557
+ const data = await request.get('/api/data', {
558
+ params: { id: 123 },
559
+ timeout: 5000
560
+ });
561
+
562
+ // POST请求
563
+ const result = await request.post('/api/data', {
564
+ name: 'test',
565
+ value: 'example'
566
+ });
567
+
568
+ // PUT请求
569
+ await request.put('/api/data/123', { name: 'updated' });
570
+
571
+ // DELETE请求
572
+ await request.delete('/api/data/123');
573
+ \`\`\`
574
+
575
+ ### 3. 文件上传示例
576
+
577
+ \`\`\`typescript
578
+ async function uploadAvatar(file: File) {
579
+ try {
580
+ const result = await api.common.uploadFile(file, 'image');
581
+ console.log('上传成功,文件URL:', result.url);
582
+ return result.url;
583
+ } catch (error) {
584
+ console.error('上传失败:', error);
585
+ return null;
586
+ }
587
+ }
588
+ \`\`\`
589
+
590
+ ### 4. 错误处理
591
+
592
+ \`\`\`typescript
593
+ import { api, ApiError } from '@/api';
594
+
595
+ async function handleRequest() {
596
+ try {
597
+ await api.user.getUserInfo();
598
+ } catch (error) {
599
+ if (error instanceof ApiError) {
600
+ // 根据错误状态码处理
601
+ if (error.status === 401) {
602
+ // 未授权,跳转到登录页
603
+ console.log('请重新登录');
604
+ } else if (error.status === 403) {
605
+ console.log('没有权限访问');
606
+ } else {
607
+ console.log('其他错误:', error.message);
608
+ }
609
+
610
+ // 根据错误代码处理
611
+ if (error.code === 'USER_NOT_FOUND') {
612
+ console.log('用户不存在');
613
+ }
614
+ }
615
+ }
616
+ }
617
+ \`\`\`
618
+
619
+ ### 5. 并发请求
620
+
621
+ \`\`\`typescript
622
+ async function fetchMultipleData() {
623
+ try {
624
+ const [userInfo, systemStatus] = await Promise.all([
625
+ api.user.getUserInfo(),
626
+ api.common.getSystemStatus()
627
+ ]);
628
+
629
+ console.log('用户信息:', userInfo);
630
+ console.log('系统状态:', systemStatus);
631
+ } catch (error) {
632
+ console.error('请求失败:', error);
633
+ }
634
+ }
635
+ \`\`\`
636
+
637
+ ### 6. 取消请求
638
+
639
+ \`\`\`typescript
640
+ import { request } from '@/api';
641
+
642
+ // 创建取消令牌
643
+ const CancelToken = request.axios.CancelToken;
644
+ const source = CancelToken.source();
645
+
646
+ // 发送请求
647
+ request.get('/api/long-running', {
648
+ cancelToken: source.token
649
+ });
650
+
651
+ // 取消请求
652
+ function cancelRequest() {
653
+ source.cancel('用户取消了请求');
654
+ }
655
+ \`\`\`
656
+
657
+ ## Vue 3组件中使用示例
658
+
659
+ \`\`\`vue
660
+ <script setup lang="ts">
661
+ import { ref, onMounted } from 'vue';
662
+ import { api } from '@/api';
663
+
664
+ const loading = ref(false);
665
+ const userList = ref([]);
666
+ const errorMsg = ref('');
667
+
668
+ const fetchUsers = async (page = 1) => {
669
+ loading.value = true;
670
+ errorMsg.value = '';
671
+
672
+ try {
673
+ const result = await api.user.getUserList({ page, pageSize: 10 });
674
+ userList.value = result.list;
675
+ } catch (error) {
676
+ errorMsg.value = error.message || '获取用户列表失败';
677
+ } finally {
678
+ loading.value = false;
679
+ }
680
+ };
681
+
682
+ onMounted(() => {
683
+ fetchUsers();
684
+ });
685
+ </script>
686
+
687
+ <template>
688
+ <div class="user-list">
689
+ <div v-if="loading">加载中...</div>
690
+ <div v-else-if="errorMsg" class="error">{{ errorMsg }}</div>
691
+ <div v-else>
692
+ <div v-for="user in userList" :key="user.id" class="user-item">
693
+ {{ user.name }} - {{ user.email }}
694
+ </div>
695
+ </div>
696
+ </div>
697
+ </template>
698
+ \`\`\`
699
+
700
+ ## 扩展说明
701
+
702
+ ### 1. 添加新的API模块
703
+
704
+ 1. 在\`/src/api/\`目录下创建新的API文件,例如\`productApi.ts\`
705
+ 2. 在文件中定义API接口和相关类型
706
+ 3. 在`/ src / api / index.ts`中导入并导出新模块
707
+
708
+ ### 2. 自定义配置
709
+
710
+ 可以在\`axiosConfig.ts\`中修改配置:
711
+ - \`API_BASE_URL\`:修改API基础URL
712
+ - \`REQUEST_TIMEOUT\`:调整请求超时时间
713
+ - \`errorMessageMap\`:自定义错误消息 (可选)
714
+
715
+ ### 3. 全局错误处理
716
+
717
+ 可以在\`errorHandler.ts\`的\`globalErrorHandler\`函数中添加:
718
+ - 错误日志上报
719
+ - 统一错误提示
720
+ - 根据错误类型执行不同操作
721
+
722
+ ## 注意事项
723
+
724
+ 1. 所有API调用都应该使用\`try/catch\`进行错误处理
725
+ 2. 登录成功后,token会自动保存到localStorage,并在后续请求中自动添加
726
+ 3. 未授权错误(401)会自动清除token,可以在组件中监听此错误进行页面跳转
727
+ 4. 文件上传时,需要使用FormData格式
728
+ 5. 对于大型项目,可以根据业务模块创建更多API文件
729
+
730
+ **************************************************
731
+ # 工具函数库
732
+
733
+ ## 图片地址处理工具
734
+
735
+ ### 主要功能
736
+ - 拼接图片基础URL和相对路径
737
+ - 智能处理URL中的斜杠,避免重复
738
+ - 识别并保留完整URL(以http或https开头的路径)
739
+ - 支持批量图片URL处理
740
+ - 提供Vue组件和组合式API使用方式
741
+
742
+ ### 使用方法
743
+
744
+ #### 1. 直接使用工具函数
745
+
746
+ \`\`\`typescript
747
+ import { joinImageUrl, joinImageUrls } from '@/utils/imageUtils';
748
+
749
+ // 单个图片URL拼接
750
+ const imageUrl = joinImageUrl('https://cdn.example.com', 'images/product.jpg');
751
+ // 结果: https://cdn.example.com/images/product.jpg
752
+
753
+ // 批量图片URL拼接
754
+ const imageUrls = joinImageUrls([
755
+ 'images/1.jpg',
756
+ 'images/2.jpg'
757
+ ], 'https://cdn.example.com');
758
+ \`\`\`
759
+
760
+ #### 2. 使用组合式API(在Vue组件中)
761
+
762
+ \`\`\`typescript
763
+ import { useImageUrl } from '@/utils/imageUtils';
764
+
765
+ // 创建自定义基础URL的图片工具
766
+ const imageTools = useImageUrl('https://cdn.example.com');
767
+
768
+ // 单个图片URL
769
+ const imageUrl = imageTools.getImageUrl('images/product.jpg');
770
+
771
+ // 或使用自定义基础URL覆盖默认值
772
+ const imageUrlWithCustomBase = imageTools.getImageUrl('images/product.jpg', 'https://custom.example.com');
773
+ \`\`\`
774
+
775
+ #### 3. 使用Vue组件
776
+
777
+ \`\`\`vue
778
+ <template>
779
+ <!-- 简单使用 -->
780
+ <ImageUrl path="images/product.jpg" />
781
+
782
+ <!-- 使用自定义基础URL -->
783
+ <ImageUrl path="images/product.jpg" base-url="https://cdn.example.com" />
784
+
785
+ <!-- 使用插槽自定义渲染 -->
786
+ <ImageUrl path="images/product.jpg" #default="{ url }">
787
+ <img :src="url" alt="产品图片" />
788
+ </ImageUrl>
789
+ </template>
790
+
791
+ \`\`\`
792
+
793
+ ### 配置说明
794
+
795
+ 可以在环境变量文件(.env)中配置默认图片基础URL:
796
+
797
+ \`\`\`
798
+ VITE_IMAGE_BASE_URL=https://cdn.example.com
799
+ \`\`\`
800
+
801
+ 如果未配置,默认值为'/'(网站根目录)。
802
+
803
+ ### 注意事项
804
+ - 工具会自动处理URL中的斜杠,避免重复
805
+ - 如果提供的图片路径已经是完整URL,将直接返回该URL
806
+ - 对于空路径,函数会返回空字符串,避免生成无效URL
807
+ `
808
+ },
809
+ {
810
+ path: 'src/api/index.ts',
811
+ content: `
812
+ // API接口管理入口文件
813
+
814
+ // 导出各个模块的API
815
+ import * as yApi from './app/yApi';
816
+
817
+ // 统一导出所有API
818
+ export {
819
+ yApi,
820
+ };
821
+
822
+ // 导出axios实例和请求方法,便于直接使用
823
+ import http, { request } from '../utils/axiosInstance';
824
+ export { http, request };
825
+
826
+ // 导出错误处理相关工具
827
+ export * from '../utils/errorHandler';
828
+
829
+ `
830
+ },
831
+ ]
832
+ }
833
+ }
834
+ ];
835
+
836
+ // 读取配置文件
837
+ function getConfig() {
838
+ const configPath = path.join(process.cwd(), 'create-my-vue.config.js');
839
+ if (fs.existsSync(configPath)) {
840
+ return require(configPath);
841
+ }
842
+ return {};
843
+ }
844
+
845
+ // 解析配置为命令行参数
846
+ function parseConfigToArgs(config) {
847
+ const args = [];
848
+
849
+ // 转换配置为 pnpm create vue@latest 的命令行参数格式
850
+ if (config.projectName) {
851
+ args.push(config.projectName);
852
+ }
853
+
854
+ // 添加 --default 或具体选项
855
+ if (config.default) {
856
+ args.push('--default');
857
+ } else {
858
+ // 处理具体选项
859
+ if (config.typescript !== undefined) {
860
+ args.push(config.typescript ? '--typescript' : '--no-typescript');
861
+ }
862
+ if (config.jsx !== undefined) {
863
+ args.push(config.jsx ? '--jsx' : '--no-jsx');
864
+ }
865
+ if (config.router !== undefined) {
866
+ args.push(config.router ? '--router' : '--no-router');
867
+ }
868
+ if (config.pinia !== undefined) {
869
+ args.push(config.pinia ? '--pinia' : '--no-pinia');
870
+ }
871
+ if (config.vitest !== undefined) {
872
+ args.push(config.vitest ? '--vitest' : '--no-vitest');
873
+ }
874
+ if (config.cypress !== undefined) {
875
+ args.push(config.cypress ? '--cypress' : '--no-cypress');
876
+ }
877
+ if (config.eslint !== undefined) {
878
+ args.push(config.eslint ? '--eslint' : '--no-eslint');
879
+ }
880
+ if (config.prettier !== undefined) {
881
+ args.push(config.prettier ? '--prettier' : '--no-prettier');
882
+ }
883
+ }
884
+
885
+ return args;
886
+ }
887
+
888
+ // 执行命令并返回Promise
889
+ function executeCommand(command, args, options = {}) {
890
+ return new Promise((resolve, reject) => {
891
+ const child = spawn(command, args, {
892
+ stdio: 'inherit',
893
+ shell: true,
894
+ ...options
895
+ });
896
+
897
+ child.on('error', (error) => {
898
+ reject(error);
899
+ });
900
+
901
+ child.on('close', (code) => {
902
+ if (code === 0) {
903
+ resolve();
904
+ } else {
905
+ reject(new Error(`Command failed with exit code ${code}`));
906
+ }
907
+ });
908
+ });
909
+ }
910
+
911
+ // 修改项目配置文件
912
+ function updateProjectConfig(projectPath, selectedPlugins) {
913
+ // 1. 处理vite.config.js
914
+ const viteConfigPath = path.join(projectPath, 'vite.config.js');
915
+ if (fs.existsSync(viteConfigPath)) {
916
+ let content = fs.readFileSync(viteConfigPath, 'utf8');
917
+
918
+ // 收集所有需要添加的导入和插件
919
+ const allImports = [];
920
+ const allPlugins = [];
921
+
922
+ selectedPlugins.forEach(pluginName => {
923
+ const plugin = plugins.find(p => p.name === pluginName);
924
+ if (plugin && plugin.config.vite) {
925
+ allImports.push(...plugin.config.vite.imports);
926
+ allPlugins.push(...plugin.config.vite.plugins);
927
+ }
928
+ });
929
+
930
+ // 添加导入语句
931
+ if (allImports.length > 0) {
932
+ const importStatements = allImports.join('\n');
933
+ content = content.replace(/import\s+\w+\s+from/g, match => `${importStatements}\n${match}`);
934
+ }
935
+
936
+ // 添加插件配置
937
+ if (allPlugins.length > 0) {
938
+ const pluginStatements = allPlugins.join(',\n ');
939
+ content = content.replace(/plugins:\s*\[([\s\S]*?)\]/g, (match, existingPlugins) => {
940
+ if (existingPlugins.trim()) {
941
+ return `plugins: [
942
+ ${existingPlugins.trim()},
943
+ ${pluginStatements}
944
+ ]`;
945
+ } else {
946
+ return `plugins: [
947
+ ${pluginStatements}
948
+ ]`;
949
+ }
950
+ });
951
+ }
952
+
953
+ fs.writeFileSync(viteConfigPath, content, 'utf8');
954
+ console.log('成功更新 vite.config.js');
955
+ } else {
956
+ console.log('未找到 vite.config.js,跳过...');
957
+ }
958
+
959
+ // 2. 处理插件配置的文件
960
+ selectedPlugins.forEach(pluginName => {
961
+ const plugin = plugins.find(p => p.name === pluginName);
962
+ if (plugin && plugin.config.files) {
963
+ plugin.config.files.forEach(fileConfig => {
964
+ const filePath = path.join(projectPath, fileConfig.path);
965
+
966
+ // 确保目录存在
967
+ const dirPath = path.dirname(filePath);
968
+ if (!fs.existsSync(dirPath)) {
969
+ fs.mkdirSync(dirPath, { recursive: true });
970
+ }
971
+
972
+ if (fileConfig.append && fs.existsSync(filePath)) {
973
+ // 追加内容
974
+ const existingContent = fs.readFileSync(filePath, 'utf8');
975
+ fs.writeFileSync(filePath, existingContent + fileConfig.content, 'utf8');
976
+ console.log(`成功追加到 ${fileConfig.path}`);
977
+ } else {
978
+ // 创建或覆盖文件
979
+ fs.writeFileSync(filePath, fileConfig.content, 'utf8');
980
+ console.log(`成功创建 ${fileConfig.path}`);
981
+ }
982
+ });
983
+ }
984
+ });
985
+ }
986
+
987
+ // 主流程
988
+ async function main(projectName, options) {
989
+ // 获取配置文件
990
+ const config = getConfig();
991
+
992
+ // 合并命令行参数和配置文件
993
+ const finalConfig = {
994
+ projectName: projectName || config.projectName,
995
+ ...config,
996
+ ...options
997
+ };
998
+
999
+ // 构建 pnpm create vue@latest 命令
1000
+ const args = ['create', 'vue@latest', ...parseConfigToArgs(finalConfig)];
1001
+
1002
+ console.log(`Running: pnpm ${args.join(' ')}`);
1003
+
1004
+ // 如果是 dry-run,只显示命令不执行
1005
+ if (finalConfig.dryRun) {
1006
+ console.log('Dry run 完成。未创建实际项目。');
1007
+ process.exit(0);
1008
+ }
1009
+
1010
+ // 执行创建命令
1011
+ try {
1012
+ await executeCommand('pnpm', args);
1013
+
1014
+ // 问询用户是否添加额外插件
1015
+ const projectPath = path.join(process.cwd(), finalConfig.projectName);
1016
+
1017
+ if (fs.existsSync(projectPath)) {
1018
+ console.log('\n项目创建成功!');
1019
+
1020
+ // 创建 composables 和 utils 文件夹
1021
+ const composablesPath = path.join(projectPath, 'src/composables');
1022
+ const utilsPath = path.join(projectPath, 'src/utils');
1023
+ const iconPath = path.join(projectPath, 'src/assets/icons');
1024
+ const apiPath = path.join(projectPath, 'src/api');
1025
+
1026
+ if (!fs.existsSync(composablesPath)) {
1027
+ fs.mkdirSync(composablesPath, { recursive: true });
1028
+ console.log(`成功创建 ${composablesPath}`);
1029
+ }
1030
+
1031
+ if (!fs.existsSync(utilsPath)) {
1032
+ fs.mkdirSync(utilsPath, { recursive: true });
1033
+ console.log(`成功创建 ${utilsPath}`);
1034
+ }
1035
+
1036
+ if (!fs.existsSync(iconPath)) {
1037
+ fs.mkdirSync(iconPath, { recursive: true });
1038
+ console.log(`成功创建 ${iconPath}`);
1039
+ }
1040
+
1041
+ if (!fs.existsSync(apiPath)) {
1042
+ fs.mkdirSync(apiPath, { recursive: true });
1043
+ console.log(`成功创建 ${apiPath}`);
1044
+ }
1045
+
1046
+ const { selectedPlugins } = await inquirer.prompt([
1047
+ {
1048
+ type: 'checkbox',
1049
+ name: 'selectedPlugins',
1050
+ message: '您想要添加哪些额外的插件?',
1051
+ choices: plugins.map(plugin => ({
1052
+ name: `${plugin.name} - ${plugin.description}`,
1053
+ value: plugin.name
1054
+ }))
1055
+ }
1056
+ ]);
1057
+
1058
+ if (selectedPlugins.length > 0) {
1059
+ console.log('\n正在安装选定的插件...');
1060
+
1061
+ // 收集所有依赖
1062
+ const allDependencies = [];
1063
+ selectedPlugins.forEach(pluginName => {
1064
+ const plugin = plugins.find(p => p.name === pluginName);
1065
+ if (plugin) {
1066
+ allDependencies.push(...plugin.dependencies);
1067
+ }
1068
+ });
1069
+
1070
+ // 安装依赖
1071
+ await executeCommand('pnpm', ['install', '--save-dev', ...allDependencies], {
1072
+ cwd: projectPath
1073
+ });
1074
+
1075
+ // 更新项目配置
1076
+ updateProjectConfig(projectPath, selectedPlugins);
1077
+
1078
+ console.log('\n所有插件安装和配置成功!');
1079
+ }
1080
+ }
1081
+ } catch (error) {
1082
+ console.error(`Error: ${error.message}`);
1083
+ process.exit(1);
1084
+ }
1085
+ }
1086
+
1087
+ program
1088
+ .name('create-my-vue')
1089
+ .description('pnpm create vue@latest 的二次封装,支持自定义默认选项')
1090
+ .version('1.0.0')
1091
+ .argument('[project-name]', '要创建的项目名称')
1092
+ .option('-d, --default', '使用默认选项')
1093
+ .option('--dry-run', '显示将要执行的命令,但不实际运行')
1094
+ .option('--typescript', '添加 TypeScript')
1095
+ .option('--no-typescript', '不添加 TypeScript')
1096
+ .option('--jsx', '添加 JSX 支持')
1097
+ .option('--no-jsx', '不添加 JSX 支持')
1098
+ .option('--router', '添加 Vue Router')
1099
+ .option('--no-router', '不添加 Vue Router')
1100
+ .option('--pinia', '添加 Pinia')
1101
+ .option('--no-pinia', '不添加 Pinia')
1102
+ .option('--vitest', '添加 Vitest 用于单元测试')
1103
+ .option('--no-vitest', '不添加 Vitest 用于单元测试')
1104
+ .option('--cypress', '添加 Cypress 用于端到端测试')
1105
+ .option('--no-cypress', '不添加 Cypress 用于端到端测试')
1106
+ .option('--eslint', '添加 ESLint 用于代码质量')
1107
+ .option('--no-eslint', '不添加 ESLint 用于代码质量')
1108
+ .option('--prettier', '添加 Prettier 用于代码格式化')
1109
+ .option('--no-prettier', '不添加 Prettier 用于代码格式化')
1110
+ .action((projectName, options) => {
1111
+ main(projectName, options);
1112
+ });
1113
+
1114
+ program.parse(process.argv);
@@ -0,0 +1,20 @@
1
+ // create-my-vue.config.js 示例配置文件
2
+ // 复制此文件并重命名为 create-my-vue.config.js 以使用自定义配置
3
+
4
+ module.exports = {
5
+ // 项目名称
6
+ // projectName: 'my-vue-project',
7
+
8
+ // 使用默认配置(等同于 --default)
9
+ // default: true,
10
+
11
+ // 具体选项配置
12
+ typescript: true, // 添加 TypeScript
13
+ jsx: true, // 添加 JSX 支持
14
+ router: true, // 添加 Vue Router
15
+ pinia: true, // 添加 Pinia
16
+ vitest: true, // 添加 Vitest 用于单元测试
17
+ cypress: false, // 不添加 Cypress 用于端到端测试
18
+ eslint: true, // 添加 ESLint 用于代码质量
19
+ prettier: true // 添加 Prettier 用于代码格式化
20
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@aibai/create-myvue",
3
+ "version": "0.0.1",
4
+ "description": "pnpm create vue@latest 的二次封装,支持自定义默认选项",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "create-my-vue": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "vue",
14
+ "cli",
15
+ "create-vue"
16
+ ],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "type": "commonjs",
20
+ "dependencies": {
21
+ "commander": "^12.1.0",
22
+ "inquirer": "^13.2.1"
23
+ }
24
+ }