neo-cmp-cli 1.5.6 → 1.6.0-beta.10

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.
Files changed (48) hide show
  1. package/README.md +62 -47
  2. package/package.json +1 -1
  3. package/src/module/index.js +74 -19
  4. package/src/module/main.js +167 -267
  5. package/src/module/neoInit.js +1 -0
  6. package/src/neo/NeoUMDContent.js +6 -5
  7. package/src/neo/neoService.js +154 -43
  8. package/src/oss/publish2oss.js +174 -71
  9. package/src/template/antd-custom-cmp-template/README.md +26 -2
  10. package/src/template/antd-custom-cmp-template/neo.config.js +7 -4
  11. package/src/template/antd-custom-cmp-template/package.json +1 -0
  12. package/src/template/echarts-custom-cmp-template/README.md +26 -2
  13. package/src/template/echarts-custom-cmp-template/neo.config.js +9 -5
  14. package/src/template/echarts-custom-cmp-template/package.json +1 -0
  15. package/src/template/empty-custom-cmp-template/README.md +26 -0
  16. package/src/template/empty-custom-cmp-template/neo.config.js +7 -4
  17. package/src/template/empty-custom-cmp-template/package.json +1 -0
  18. package/src/template/neo-custom-cmp-template/README.md +26 -2
  19. package/src/template/neo-custom-cmp-template/neo.config.js +12 -9
  20. package/src/template/neo-custom-cmp-template/package.json +2 -1
  21. package/src/template/react-custom-cmp-template/README.md +26 -2
  22. package/src/template/react-custom-cmp-template/neo.config.js +7 -4
  23. package/src/template/react-custom-cmp-template/package.json +1 -0
  24. package/src/template/react-ts-custom-cmp-template/README.md +26 -2
  25. package/src/template/react-ts-custom-cmp-template/neo.config.js +7 -4
  26. package/src/template/react-ts-custom-cmp-template/package.json +1 -0
  27. package/src/template/vue2-custom-cmp-template/README.md +26 -2
  28. package/src/template/vue2-custom-cmp-template/neo.config.js +7 -4
  29. package/src/template/vue2-custom-cmp-template/package.json +1 -0
  30. package/src/{cmpUtils → utils/cmpUtils}/createCmpByTemplate.js +9 -7
  31. package/src/utils/cmpUtils/createCmpByZip.js +87 -0
  32. package/src/{cmpUtils → utils/cmpUtils}/createCommonModulesCode.js +2 -2
  33. package/src/{cmpUtils → utils/cmpUtils}/getCmpModelRegisterCode.js +1 -1
  34. package/src/{cmpUtils → utils/cmpUtils}/getCmpPreviewCode.js +1 -1
  35. package/src/{cmpUtils → utils/cmpUtils}/getCmpRegisterCode.js +1 -1
  36. package/src/{cmpUtils → utils/cmpUtils}/getCmpTypeByDir.js +2 -2
  37. package/src/{cmpUtils → utils/cmpUtils}/hasCmpTypeByDir.js +1 -1
  38. package/src/{cmpUtils → utils/cmpUtils}/previewCmp.js +3 -3
  39. package/src/utils/cmpUtils/pullCmp.js +95 -0
  40. package/src/{cmpUtils → utils/cmpUtils}/pushCmp.js +83 -73
  41. package/src/utils/common.js +48 -0
  42. package/src/utils/generateEntries.js +73 -0
  43. package/src/{projectUtils → utils/projectUtils}/createCmpProjectByTemplate.js +11 -7
  44. package/src/{projectUtils → utils/projectUtils}/createCmpProjectZip.js +18 -20
  45. package/src/{projectUtils → utils/projectUtils}/getEntries.js +2 -2
  46. package/src/{projectUtils → utils/projectUtils}/getEntriesWithAutoRegister.js +2 -2
  47. package/src/{projectUtils → utils/projectUtils}/hasNeoProject.js +2 -2
  48. package/src/{projectUtils → utils/projectUtils}/updatePublishLog.js +1 -1
@@ -2,17 +2,44 @@ const axios = require('axios');
2
2
  const FormData = require('form-data');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const ora = require('ora');
5
6
  const _ = require('lodash');
6
- const updatePublishLog = require('../projectUtils/updatePublishLog');
7
+ const { resolve } = require('akfun');
8
+ const updatePublishLog = require('../utils/projectUtils/updatePublishLog');
9
+ const { getFramework } = require('../utils/common');
7
10
 
8
11
  // NeoCRM 平台默认 API 配置
9
12
  const NeoCrmAPI = {
10
13
  neoBaseURL: 'https://crm.xiaoshouyi.com', // 平台根地址
11
14
  tokenAPI: 'https://login.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址
12
15
  uploadAPI: '/rest/metadata/v3.0/ui/customComponents/actions/upload', // 文件上传接口地址
16
+ delete: '/rest/metadata/v3.0/ui/customComponents',
17
+ query: '/rest/metadata/v3.0/ui/customComponents/actions/queryCustomComponents', // 带分页
18
+ queryAll: '/rest/metadata/v3.0/ui/customComponents/actions/queryAllCustomComponents', // 不带分页
13
19
  saveAPI: '/rest/metadata/v3.0/ui/customComponents/actions/saveOrUpdateComponent' // 创建或者保存接口地址
14
20
  };
15
21
 
22
+ const cmpFields = [
23
+ 'cmpType',
24
+ 'label',
25
+ 'componentCategory',
26
+ 'description',
27
+ 'framework',
28
+ 'icon',
29
+ 'orderNo',
30
+ 'version',
31
+ 'propsSchema',
32
+ 'defaultProps',
33
+ 'previewProps',
34
+ 'events',
35
+ 'actions',
36
+ 'asset',
37
+ 'plugin',
38
+ 'modelAsset',
39
+ 'cssAsset',
40
+ 'codeLib'
41
+ ];
42
+
16
43
  /**
17
44
  * Neo 平台服务类
18
45
  * 提供 token 管理、文件上传、组件更新等功能
@@ -23,32 +50,29 @@ class NeoService {
23
50
  * @param {object} config 配置信息
24
51
  * @param {string} config.neoBaseURL Neo 平台根地址
25
52
  * @param {string} config.tokenAPI Token 获取接口地址
26
- * @param {object} config.authConfig 授权信息
27
- * @param {string} config.authConfig.client_id 客户端 ID
28
- * @param {string} config.authConfig.client_secret 客户端密钥
29
- * @param {string} config.authConfig.username 用户名
30
- * @param {string} config.authConfig.password 密码
53
+ * @param {object} config.auth 授权信息
54
+ * @param {string} config.auth.client_id 客户端 ID
55
+ * @param {string} config.auth.client_secret 客户端密钥
56
+ * @param {string} config.auth.username 用户名
57
+ * @param {string} config.auth.password 密码
31
58
  */
32
59
  constructor(config = {}) {
33
- const { assetsRoot, neoBaseURL, tokenAPI, authConfig } = config;
34
- if (!authConfig) {
35
- throw new Error('authConfig 不能为空');
60
+ const { assetsRoot, neoBaseURL, tokenAPI, auth } = config || {};
61
+ if (!auth) {
62
+ throw new Error('auth 不能为空');
36
63
  }
37
- if (
38
- !authConfig.client_id ||
39
- !authConfig.client_secret ||
40
- !authConfig.username ||
41
- !authConfig.password
42
- ) {
64
+ if (!auth.client_id || !auth.client_secret || !auth.username || !auth.password) {
43
65
  throw new Error(
44
- 'authConfig 配置不完整,需要包含 client_id、client_secret、username、password'
66
+ 'neoConfig / auth 配置不完整,需要包含 client_id、client_secret、username、password'
45
67
  );
46
68
  }
47
69
 
48
- this.assetsRoot = assetsRoot;
70
+ this.assetsRoot = assetsRoot || resolve('dist');
49
71
  this.neoBaseURL = neoBaseURL || NeoCrmAPI.neoBaseURL;
50
72
  this.tokenAPI = tokenAPI || NeoCrmAPI.tokenAPI;
51
- this.authConfig = authConfig;
73
+ this.auth = auth;
74
+ this.cmpList = [];
75
+ this.cmpInfoMap = {};
52
76
 
53
77
  // Token 缓存
54
78
  this.tokenCache = {
@@ -96,20 +120,20 @@ class NeoService {
96
120
  * @returns {Promise<string>} token
97
121
  */
98
122
  async getToken() {
123
+ const spinner = ora('获取 token...').start();
99
124
  // 检查缓存是否有效
100
125
  if (!this.isTokenExpired()) {
101
- console.info('使用缓存的 token');
126
+ spinner.succeed('使用缓存的 token');
102
127
  return this.tokenCache.token;
103
128
  }
104
- console.info('获取 token...');
105
129
 
106
130
  // 构建表单数据格式的请求参数
107
131
  const formData = new URLSearchParams();
108
132
  formData.append('grant_type', 'password');
109
- formData.append('client_id', this.authConfig.client_id);
110
- formData.append('client_secret', this.authConfig.client_secret);
111
- formData.append('username', this.authConfig.username);
112
- formData.append('password', this.authConfig.password);
133
+ formData.append('client_id', this.auth.client_id);
134
+ formData.append('client_secret', this.auth.client_secret);
135
+ formData.append('username', this.auth.username);
136
+ formData.append('password', this.auth.password);
113
137
 
114
138
  const tokenUrl = this.buildFullUrl(this.tokenAPI);
115
139
 
@@ -123,7 +147,7 @@ class NeoService {
123
147
  const { access_token, expires_in } = response.data || {};
124
148
 
125
149
  if (!access_token) {
126
- console.error('\n获取 token 失败(授权配置错误):响应中未包含 access_token', response.data);
150
+ spinner.fail('获取 token 失败(授权配置错误):响应中未包含 access_token', response.data);
127
151
  process.exit(1);
128
152
  }
129
153
 
@@ -133,6 +157,8 @@ class NeoService {
133
157
  token: access_token,
134
158
  expiresAt: Date.now() + (expiresIn - 60) * 1000
135
159
  };
160
+ spinner.clear();
161
+ spinner.stop();
136
162
  return access_token;
137
163
  } catch (error) {
138
164
  console.error('\n获取 token 失败:', error.message);
@@ -167,8 +193,10 @@ class NeoService {
167
193
  if (!this.tokenCache.token) {
168
194
  return await this.getToken();
169
195
  } else if (this.isTokenExpired()) {
170
- console.info('token 已过期,正在刷新...');
171
- return await this.refreshToken();
196
+ const spinner = ora('token 已过期,正在刷新...').start();
197
+ const token = await this.refreshToken();
198
+ spinner.succeed('token 刷新成功。');
199
+ return token;
172
200
  }
173
201
  return this.tokenCache.token;
174
202
  }
@@ -216,13 +244,13 @@ class NeoService {
216
244
 
217
245
  const fileName = path.basename(filePath);
218
246
  const fileSizeKB = (fileStat.size / 1024).toFixed(2);
219
- console.info(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`);
247
+ const spinner = ora(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`).start();
220
248
 
221
249
  try {
222
250
  // 创建 FormData
223
251
  const formData = new FormData();
224
252
  const fieldName = options.fieldName || 'customComponentCode';
225
-
253
+
226
254
  // 使用文件流而不是读取整个文件到内存(对大文件更友好)
227
255
  const fileContent = fs.createReadStream(filePath);
228
256
 
@@ -265,7 +293,11 @@ class NeoService {
265
293
  resultData = responseData.trim();
266
294
  } else if (responseData && typeof responseData === 'object') {
267
295
  // 检查是否有错误码
268
- if (responseData.code !== undefined && responseData.code !== 200 && responseData.code !== 0) {
296
+ if (
297
+ responseData.code !== undefined &&
298
+ responseData.code !== 200 &&
299
+ responseData.code !== 0
300
+ ) {
269
301
  const errorMsg = responseData.message || responseData.msg || '未知错误';
270
302
  throw new Error(`上传失败: ${errorMsg} (code: ${responseData.code})`);
271
303
  }
@@ -296,36 +328,35 @@ class NeoService {
296
328
  } else if (resultData && typeof resultData === 'object' && resultData.url) {
297
329
  fileUrl = resultData.url;
298
330
  }
299
- console.info(`\n文件上传成功: ${fileName} -> ${fileUrl}`);
331
+ spinner.succeed(`文件上传成功: ${fileName} -> ${fileUrl}`);
300
332
  return fileUrl;
301
333
  } catch (error) {
302
- console.error(`\n上传文件失败: ${error.message},`);
303
- console.error(`文件路径: ${filePath}。\n`);
304
-
334
+ spinner.fail(`上传文件失败: ${error.message}, 文件路径: ${filePath}`);
335
+
305
336
  // 输出详细的错误信息
306
337
  if (error.response) {
307
338
  const status = error.response.status;
308
339
  const statusText = error.response.statusText;
309
340
  const responseData = error.response.data;
310
341
  const requestUrl = error.config?.url || this.uploadAPI();
311
-
342
+
312
343
  console.error(`\n========== 上传请求详情 ==========`);
313
344
  console.error(`请求 URL: ${requestUrl}`);
314
345
  console.error(`HTTP 状态码: ${status} ${statusText}`);
315
346
  console.error(`响应数据:`, responseData);
316
347
  console.error(`==================================\n`);
317
-
348
+
318
349
  if (status === 404) {
319
350
  throw new Error(
320
351
  `上传 API 不存在 (404): ${requestUrl}\n` +
321
- `请检查 neo.config.js 中的 neoBaseURL 配置是否正确,或者 API 路径是否存在。\n` +
322
- `当前配置的 API 路径: ${NeoCrmAPI.uploadAPI}`
352
+ `请检查 neo.config.js 中的 neoBaseURL 配置是否正确,或者 API 路径是否存在。\n` +
353
+ `当前配置的 API 路径: ${NeoCrmAPI.uploadAPI}`
323
354
  );
324
355
  }
325
356
  } else if (error.request) {
326
357
  console.error('请求已发送但未收到响应,请检查网络连接或代理配置。');
327
358
  }
328
-
359
+
329
360
  throw error;
330
361
  }
331
362
  }
@@ -345,6 +376,7 @@ class NeoService {
345
376
  console.error(`未找到自定义组件资源目录: ${this.assetsRoot}`);
346
377
  return;
347
378
  }
379
+
348
380
  // 当前组件信息
349
381
  const curCmpInfo = {
350
382
  cmpType
@@ -410,7 +442,7 @@ class NeoService {
410
442
  throw new Error('componentData 不能为空');
411
443
  }
412
444
 
413
- console.info('正在更新自定义组件...');
445
+ const spinner = ora('正在更新自定义组件...').start();
414
446
 
415
447
  try {
416
448
  const fullUpdateAPI = this.saveAPI();
@@ -421,15 +453,16 @@ class NeoService {
421
453
  'Content-Type': 'application/json'
422
454
  }
423
455
  });
424
- const {code, message} = response.data || {};
456
+ const { code, message } = response.data || {};
425
457
 
426
458
  if (code && code !== 200) {
427
459
  throw new Error(`更新组件失败: ${response.data.message || '未知错误'}`);
428
460
  }
429
-
430
461
 
431
- console.info(message ? `组件更新成功: ${message}。` : '组件更新成功。');
462
+ spinner.clear();
463
+ spinner.stop();
432
464
  } catch (error) {
465
+ spinner.fail('更新组件失败。');
433
466
  if (error.message) {
434
467
  console.error('更新组件失败:', error.message);
435
468
  } else {
@@ -439,6 +472,84 @@ class NeoService {
439
472
  }
440
473
  }
441
474
 
475
+ /**
476
+ * 获取线上自定义组件列表
477
+ * @returns {Promise<array>} 自定义组件列表
478
+ */
479
+ async getCustomCmpList() {
480
+ // 确保 token 有效
481
+ const token = await this.ensureValidToken();
482
+
483
+ /*
484
+ // 如果自定义组件列表已存在,则直接返回
485
+ if (this.cmpList && this.cmpList.length > 0) {
486
+ return this.cmpList;
487
+ }
488
+ */
489
+
490
+ try {
491
+ let queryAllAPI = this.buildFullUrl(NeoCrmAPI.queryAll);
492
+ queryAllAPI += `?fields=${cmpFields.join(',')}`;
493
+ const response = await axios.post(
494
+ queryAllAPI,
495
+ {
496
+ data: {}
497
+ },
498
+ {
499
+ headers: {
500
+ Authorization: `Bearer ${token}`,
501
+ 'xsy-inner-source': 'bff',
502
+ 'Content-Type': 'application/json'
503
+ }
504
+ }
505
+ );
506
+ const { code, message } = response.data || {};
507
+
508
+ if (code && code !== 200) {
509
+ throw new Error(`获取自定义组件列表失败: ${message || '未知错误'}`);
510
+ }
511
+
512
+ this.updateCustomCmpList(response.data.data || []);
513
+ } catch (error) {
514
+ if (error.message) {
515
+ console.error('获取自定义组件列表失败:', error.message);
516
+ } else {
517
+ console.error('响应数据:', error);
518
+ }
519
+ process.exit(1);
520
+ }
521
+
522
+ return this.cmpList || [];
523
+ }
524
+
525
+ // 获取指定框架的自定义组件列表
526
+ getCmpListByFramework(framework) {
527
+ if (!framework) {
528
+ return this.cmpList;
529
+ }
530
+ const curFramework = getFramework(framework);
531
+ return this.cmpList.filter((cmp) => cmp.framework === curFramework);
532
+ }
533
+
534
+ // 获取自定义组件信息
535
+ getCmpInfoByCmpType(cmpType) {
536
+ if (!cmpType) {
537
+ return null;
538
+ }
539
+ return this.cmpInfoMap[cmpType] || null;
540
+ }
541
+
542
+ // 更新自定义组件 Map
543
+ updateCustomCmpList(cmpList) {
544
+ if (!cmpList || !Array.isArray(cmpList)) {
545
+ return;
546
+ }
547
+ this.cmpList = cmpList;
548
+ cmpList.forEach((cmp) => {
549
+ this.cmpInfoMap[cmp.cmpType] = cmp;
550
+ });
551
+ }
552
+
442
553
  /**
443
554
  * 通用请求方法(确保 token 有效)
444
555
  * @param {string} method HTTP 方法
@@ -2,9 +2,10 @@ const { aliBOS, baiduBOS } = require('akfun');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const _ = require('lodash');
5
+ const ora = require('ora');
5
6
  const { catchCurPackageJson } = require('../utils/pathUtils');
6
7
  const getConfigObj = require('../utils/getConfigObj');
7
- const updatePublishLog = require('../projectUtils/updatePublishLog');
8
+ const updatePublishLog = require('../utils/projectUtils/updatePublishLog');
8
9
 
9
10
  // 获取当前项目的package文件
10
11
  const currentPackageJsonDir = catchCurPackageJson();
@@ -40,7 +41,11 @@ const getFilePath = (ossType, bucket, objectKey) => {
40
41
  baidu: 'bj.bcebos.com',
41
42
  ali: 'oss-cn-beijing.aliyuncs.com'
42
43
  };
43
- return `https://${bucket}.${regions[ossType]}/${objectKey}`;
44
+ const region = regions[ossType];
45
+ if (!region) {
46
+ throw new Error(`不支持的oss类型: ${ossType}`);
47
+ }
48
+ return `https://${bucket}.${region}/${objectKey}`;
44
49
  };
45
50
 
46
51
  // 获取上传成功和失败的文件信息
@@ -92,12 +97,12 @@ const getResultFilesByWidgetName = (files) => {
92
97
  }
93
98
  let widgetName = file.widgetName;
94
99
  const curCmpInfo = {
95
- 'cmpType': _.kebabCase(widgetName),
96
- }
100
+ cmpType: _.kebabCase(widgetName)
101
+ };
97
102
 
98
103
  if (widgetName.includes('Model')) {
99
104
  widgetName = widgetName.replace('Model', '');
100
- curCmpInfo.cmpType = _.kebabCase(widgetName),
105
+ curCmpInfo.cmpType = _.kebabCase(widgetName);
101
106
  curCmpInfo.modelAsset = !file.success ? `${file.error}[${file.ossPath}]` : file.ossPath;
102
107
  } else if (file.fileName.endsWith('.css')) {
103
108
  curCmpInfo.cssAsset = !file.success ? `${file.error}[${file.ossPath}]` : file.ossPath;
@@ -146,6 +151,75 @@ function addVersionToFilename(
146
151
  return `${projectName}/${baseName}-${version}${extension}`;
147
152
  }
148
153
 
154
+ /**
155
+ * 上传单个文件到 OSS
156
+ * @param {object} bosClient OSS 客户端实例
157
+ * @param {string} filePath 文件路径
158
+ * @param {string} objectKey OSS 对象键
159
+ * @param {string} ossType OSS 类型
160
+ * @param {object} ossConfig OSS 配置
161
+ * @param {object} fileInfo 文件信息
162
+ * @returns {Promise<object>} 上传结果
163
+ */
164
+ async function uploadSingleFile(bosClient, filePath, objectKey, ossType, ossConfig, fileInfo) {
165
+ const fileName = path.basename(filePath);
166
+ const fileSizeKB = (fs.statSync(filePath).size / 1024).toFixed(2);
167
+ const spinner = ora(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`).start();
168
+
169
+ try {
170
+ // 判断线上是否存在重名文件,避免被覆盖
171
+ const historyResult = await bosClient.get(objectKey);
172
+ if (historyResult && historyResult.url) {
173
+ spinner.warn(`文件已存在,跳过上传: ${fileName}`);
174
+ return {
175
+ success: false,
176
+ status: '文件上传失败',
177
+ widgetName: fileInfo.name,
178
+ fileName: fileName,
179
+ filepath: filePath,
180
+ ossPath: historyResult.url,
181
+ error: '线上存在重名文件。'
182
+ };
183
+ }
184
+
185
+ // 上传文件
186
+ const result = await bosClient.upload(objectKey, filePath);
187
+ const ossPath = getFilePath(ossType, ossConfig.bucket, objectKey);
188
+ spinner.succeed(`文件上传成功: ${fileName} -> ${ossPath}`);
189
+
190
+ return {
191
+ success: true,
192
+ status: '文件上传成功',
193
+ fileName: fileName,
194
+ widgetName: fileInfo.name,
195
+ filepath: filePath,
196
+ ossPath: ossPath,
197
+ resultMsg: result
198
+ };
199
+ } catch (error) {
200
+ spinner.fail(`文件上传失败: ${fileName}`);
201
+
202
+ // 输出详细的错误信息
203
+ console.error(`\n========== 上传文件详情 ==========`);
204
+ console.error(`文件路径: ${filePath}`);
205
+ console.error(`OSS 对象键: ${objectKey}`);
206
+ console.error(`错误信息: ${error.message}`);
207
+ if (error.stack) {
208
+ console.error(`错误堆栈:`, error.stack);
209
+ }
210
+ console.error(`==================================\n`);
211
+
212
+ return {
213
+ success: false,
214
+ status: '文件上传失败',
215
+ widgetName: fileInfo.name,
216
+ fileName: fileName,
217
+ filepath: filePath,
218
+ error: error.message
219
+ };
220
+ }
221
+ }
222
+
149
223
  /**
150
224
  * 将构建产物上传到指定 oss 存储桶
151
225
  *
@@ -154,91 +228,120 @@ function addVersionToFilename(
154
228
  * @param {string} assetsRoot 构建产物的目录
155
229
  * @param {array} fileExtensions 需要上传的文件类型,默认 ['.js', '.css']
156
230
  */
157
- const publish2oss = (ossType, ossConfig, assetsRoot, fileExtensions = ['.js', '.css']) => {
231
+ const publish2oss = async (ossType, ossConfig, assetsRoot, fileExtensions = ['.js', '.css']) => {
232
+ // 参数验证
158
233
  if (ossType !== 'baidu' && ossType !== 'ali') {
159
234
  console.error(`不支持的oss类型: ${ossType}`);
160
235
  return;
161
236
  }
162
- const bosClient = getBosClient(ossType, ossConfig);
237
+
238
+ if (!ossConfig) {
239
+ console.error('ossConfig 不能为空');
240
+ return;
241
+ }
242
+
163
243
  if (!assetsRoot) {
164
244
  console.error('assetsRoot 不能为空');
165
245
  return;
166
246
  }
247
+
248
+ // 检查目录是否存在
167
249
  if (!fs.existsSync(assetsRoot)) {
168
250
  console.error(`assetsRoot 不存在: ${assetsRoot}`);
169
251
  return;
170
252
  }
171
- const files = fs.readdirSync(assetsRoot); // 读取构建目录下的所有文件
172
-
173
- // 并行上传所有指定类型的文件
174
- const uploadPromises = files.map(async (file) => {
175
- const filePath = path.join(assetsRoot, file);
176
- // 获取文件状态
177
- const fileStat = fs.statSync(filePath);
178
- // 检查文件扩展名
179
- // const fileExt = path.extname(file);
180
- const fileInfo = path.parse(filePath);
181
- if (fileStat.isFile() && fileExtensions.includes(fileInfo.ext)) {
182
- const objectKey = addVersionToFilename(file);
253
+
254
+ // 检查目录是否为目录
255
+ const assetsRootStat = fs.statSync(assetsRoot);
256
+ if (!assetsRootStat.isDirectory()) {
257
+ console.error(`assetsRoot 不是目录: ${assetsRoot}`);
258
+ return;
259
+ }
260
+
261
+ // 创建 OSS 客户端
262
+ const bosClient = getBosClient(ossType, ossConfig);
263
+ if (!bosClient) {
264
+ console.error(`无法创建 OSS 客户端,请检查 ossType 和 ossConfig 配置`);
265
+ return;
266
+ }
267
+
268
+ // 读取构建目录下的所有文件
269
+ const files = fs.readdirSync(assetsRoot);
270
+ if (files.length === 0) {
271
+ console.warn(`构建目录为空: ${assetsRoot}`);
272
+ return;
273
+ }
274
+
275
+ const spinner = ora('正在准备上传文件...').start();
276
+
277
+ try {
278
+ // 过滤出需要上传的文件
279
+ const filesToUpload = files.filter((file) => {
280
+ const filePath = path.join(assetsRoot, file);
183
281
  try {
184
- // 判断线上是否存在重名文件,避免被覆盖
185
- const historyResult = await bosClient.get(objectKey);
186
- if (historyResult && historyResult.url) {
187
- return {
188
- success: false,
189
- status: '文件上传失败',
190
- widgetName: fileInfo.name,
191
- fileName: file,
192
- filepath: filePath,
193
- ossPath: historyResult.url,
194
- error: '线上存在重名文件。'
195
- };
282
+ const fileStat = fs.statSync(filePath);
283
+ if (!fileStat.isFile()) {
284
+ return false;
196
285
  }
197
- // 上传文件
198
- const result = await bosClient.upload(objectKey, filePath);
199
- return {
200
- success: true,
201
- status: '文件上传成功',
202
- fileName: file,
203
- widgetName: fileInfo.name,
204
- filepath: filePath,
205
- ossPath: getFilePath(ossType, ossConfig.bucket, objectKey),
206
- resultMsg: result
207
- };
286
+ const fileInfo = path.parse(filePath);
287
+ return fileExtensions.includes(fileInfo.ext);
208
288
  } catch (error) {
209
- return {
210
- success: false,
211
- status: '文件上传失败',
212
- widgetName: fileInfo.name,
213
- fileName: file,
214
- filepath: filePath,
215
- error: error.message
216
- };
289
+ console.warn(`无法读取文件状态: ${filePath}`, error.message);
290
+ return false;
217
291
  }
292
+ });
293
+
294
+ if (filesToUpload.length === 0) {
295
+ spinner.warn('未找到需要上传的文件');
296
+ return;
218
297
  }
219
- });
220
298
 
221
- Promise.all(uploadPromises)
222
- .then((results) => {
223
- /*
224
- const { succeedFiles, errorFiles } = getResultFiles(results);
225
- if (succeedFiles.length > 0) {
226
- console.info('已成功上传如下文件:\n', succeedFiles);
227
- }
228
- if (errorFiles.length > 0) {
229
- console.info('上传失败的文件有:\n', errorFiles);
230
- }
231
- */
232
- const widgetFilesMap = getResultFilesByWidgetName(results);
233
- if (widgetFilesMap) {
234
- console.info('上传至 OSS 的文件信息:\n', widgetFilesMap);
235
- // 更新发布日志
236
- updatePublishLog(widgetFilesMap);
237
- }
238
- })
239
- .catch((error) => {
240
- console.error('批量上传文件异常:\n', error);
299
+ spinner.succeed(`找到 ${filesToUpload.length} 个文件需要上传`);
300
+
301
+ // 并行上传所有指定类型的文件
302
+ const uploadPromises = filesToUpload.map(async (file) => {
303
+ const filePath = path.join(assetsRoot, file);
304
+ const fileInfo = path.parse(filePath);
305
+ const objectKey = addVersionToFilename(file);
306
+
307
+ return await uploadSingleFile(bosClient, filePath, objectKey, ossType, ossConfig, fileInfo);
241
308
  });
309
+
310
+ // 等待所有文件上传完成
311
+ const results = await Promise.all(uploadPromises);
312
+
313
+ // 过滤掉 undefined 结果(不符合条件的文件)
314
+ const validResults = results.filter((result) => result !== undefined);
315
+
316
+ // 处理上传结果
317
+ const widgetFilesMap = getResultFilesByWidgetName(validResults);
318
+ if (widgetFilesMap && Object.keys(widgetFilesMap).length > 0) {
319
+ console.info('\n上传至 OSS 的文件信息:\n', widgetFilesMap);
320
+ // 更新发布日志
321
+ updatePublishLog(widgetFilesMap);
322
+ } else {
323
+ console.warn('未生成有效的文件上传结果');
324
+ }
325
+
326
+ // 统计上传结果
327
+ const succeedCount = validResults.filter((r) => r && r.success).length;
328
+ const failCount = validResults.filter((r) => r && !r.success).length;
329
+
330
+ if (failCount > 0) {
331
+ console.warn(`\n上传完成:成功 ${succeedCount} 个,失败 ${failCount} 个`);
332
+ } else {
333
+ console.info(`\n✅ 所有文件上传成功!共 ${succeedCount} 个文件`);
334
+ }
335
+ } catch (error) {
336
+ spinner.fail('批量上传文件异常');
337
+ console.error('\n========== 批量上传异常 ==========');
338
+ console.error('错误信息:', error.message);
339
+ if (error.stack) {
340
+ console.error('错误堆栈:', error.stack);
341
+ }
342
+ console.error('==================================\n');
343
+ throw error;
344
+ }
242
345
  };
243
346
 
244
347
  module.exports = publish2oss;
@@ -2,8 +2,8 @@
2
2
  - src: 自定义组件源码;
3
3
  - src/assets: 存放组件静态资源,比如 css、img等;
4
4
  - src/components: 存放自定义组件代码,每个自定义组件以自身名称(cmpType 数值)作为目录进行存放;
5
- - src/components/info-card/index.tsx: 自定义组件的内容文件;
6
- - src/components/info-card/model.ts: 自定义组件的模型文件,用于对接页面设计器;
5
+ - src/components/xxCmp/index.tsx: 自定义组件的内容文件;
6
+ - src/components/xxCmp/model.ts: 自定义组件的模型文件,用于对接页面设计器;
7
7
  - neo.config.js: neo-cmp-cli 配置文件。
8
8
 
9
9
  ### 组件开发规范
@@ -43,5 +43,29 @@ $ npm run linkDebug
43
43
  $ npm run pushCmp
44
44
  ```
45
45
 
46
+ ##### 需自行添加 NeoCRM 授权配置
47
+ ```javascript
48
+ module.exports = {
49
+ neoConfig: {
50
+ neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
51
+ tokenAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.xiaoshouyi.com/auc/oauth2/token)
52
+ // NeoCRM 授权配置
53
+ auth: {
54
+ client_id: 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
55
+ client_secret: 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
56
+ username: 'xx', // 用户在销售易系统中的用户名
57
+ /**
58
+ * password 为 用户在销售易系统中的账号密码加上 8 位安全令牌。
59
+ * 例如,用户密码为 123456,安全令牌为 ABCDEFGH,则 password 的值应为 123456ABCDEFGH。
60
+ */
61
+ password: 'xx xx' // 用户账户密码 + 8 位安全令牌
62
+ },
63
+ },
64
+ }
65
+ ```
66
+ 1、客户端 ID 和 客户端秘钥 需通过 创建连接器 获取,获取方式见:[https://doc.xiaoshouyi.com](https://doc.xiaoshouyi.com) / 创建连接器;
67
+ 2、如何获取 安全令牌 见:[https://doc.xiaoshouyi.com](https://doc.xiaoshouyi.com) / OAuth安全认证 / 密码模式 / 获取令牌;
68
+ 3、发布成功后即可在 NeoCRM 对应租户环境的页面设计器和表单设计器中使用此自定义组件。
69
+
46
70
  ### 配置项说明(neo-cmp-cli)
47
71
  [请查看neo-cmp-cli](https://github.com/wibetter/neo-cmp-cli)