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

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 +150 -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 +8 -6
  31. package/src/utils/cmpUtils/createCmpByZip.js +90 -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 +96 -0
  40. package/src/{cmpUtils → utils/cmpUtils}/pushCmp.js +82 -71
  41. package/src/utils/common.js +48 -0
  42. package/src/utils/generateEntries.js +73 -0
  43. package/src/{projectUtils → utils/projectUtils}/createCmpProjectByTemplate.js +10 -6
  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,7 @@ class NeoService {
133
157
  token: access_token,
134
158
  expiresAt: Date.now() + (expiresIn - 60) * 1000
135
159
  };
160
+ spinner.succeed('获取 token 成功。');
136
161
  return access_token;
137
162
  } catch (error) {
138
163
  console.error('\n获取 token 失败:', error.message);
@@ -167,8 +192,10 @@ class NeoService {
167
192
  if (!this.tokenCache.token) {
168
193
  return await this.getToken();
169
194
  } else if (this.isTokenExpired()) {
170
- console.info('token 已过期,正在刷新...');
171
- return await this.refreshToken();
195
+ const spinner = ora('token 已过期,正在刷新...').start();
196
+ const token = await this.refreshToken();
197
+ spinner.succeed('token 刷新成功。');
198
+ return token;
172
199
  }
173
200
  return this.tokenCache.token;
174
201
  }
@@ -216,13 +243,13 @@ class NeoService {
216
243
 
217
244
  const fileName = path.basename(filePath);
218
245
  const fileSizeKB = (fileStat.size / 1024).toFixed(2);
219
- console.info(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`);
246
+ const spinner = ora(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`).start();
220
247
 
221
248
  try {
222
249
  // 创建 FormData
223
250
  const formData = new FormData();
224
251
  const fieldName = options.fieldName || 'customComponentCode';
225
-
252
+
226
253
  // 使用文件流而不是读取整个文件到内存(对大文件更友好)
227
254
  const fileContent = fs.createReadStream(filePath);
228
255
 
@@ -265,7 +292,11 @@ class NeoService {
265
292
  resultData = responseData.trim();
266
293
  } else if (responseData && typeof responseData === 'object') {
267
294
  // 检查是否有错误码
268
- if (responseData.code !== undefined && responseData.code !== 200 && responseData.code !== 0) {
295
+ if (
296
+ responseData.code !== undefined &&
297
+ responseData.code !== 200 &&
298
+ responseData.code !== 0
299
+ ) {
269
300
  const errorMsg = responseData.message || responseData.msg || '未知错误';
270
301
  throw new Error(`上传失败: ${errorMsg} (code: ${responseData.code})`);
271
302
  }
@@ -296,36 +327,35 @@ class NeoService {
296
327
  } else if (resultData && typeof resultData === 'object' && resultData.url) {
297
328
  fileUrl = resultData.url;
298
329
  }
299
- console.info(`\n文件上传成功: ${fileName} -> ${fileUrl}`);
330
+ spinner.succeed(`文件上传成功: ${fileName} -> ${fileUrl}`);
300
331
  return fileUrl;
301
332
  } catch (error) {
302
- console.error(`\n上传文件失败: ${error.message},`);
303
- console.error(`文件路径: ${filePath}。\n`);
304
-
333
+ spinner.fail(`上传文件失败: ${error.message}, 文件路径: ${filePath}`);
334
+
305
335
  // 输出详细的错误信息
306
336
  if (error.response) {
307
337
  const status = error.response.status;
308
338
  const statusText = error.response.statusText;
309
339
  const responseData = error.response.data;
310
340
  const requestUrl = error.config?.url || this.uploadAPI();
311
-
341
+
312
342
  console.error(`\n========== 上传请求详情 ==========`);
313
343
  console.error(`请求 URL: ${requestUrl}`);
314
344
  console.error(`HTTP 状态码: ${status} ${statusText}`);
315
345
  console.error(`响应数据:`, responseData);
316
346
  console.error(`==================================\n`);
317
-
347
+
318
348
  if (status === 404) {
319
349
  throw new Error(
320
350
  `上传 API 不存在 (404): ${requestUrl}\n` +
321
- `请检查 neo.config.js 中的 neoBaseURL 配置是否正确,或者 API 路径是否存在。\n` +
322
- `当前配置的 API 路径: ${NeoCrmAPI.uploadAPI}`
351
+ `请检查 neo.config.js 中的 neoBaseURL 配置是否正确,或者 API 路径是否存在。\n` +
352
+ `当前配置的 API 路径: ${NeoCrmAPI.uploadAPI}`
323
353
  );
324
354
  }
325
355
  } else if (error.request) {
326
356
  console.error('请求已发送但未收到响应,请检查网络连接或代理配置。');
327
357
  }
328
-
358
+
329
359
  throw error;
330
360
  }
331
361
  }
@@ -345,6 +375,7 @@ class NeoService {
345
375
  console.error(`未找到自定义组件资源目录: ${this.assetsRoot}`);
346
376
  return;
347
377
  }
378
+
348
379
  // 当前组件信息
349
380
  const curCmpInfo = {
350
381
  cmpType
@@ -410,7 +441,7 @@ class NeoService {
410
441
  throw new Error('componentData 不能为空');
411
442
  }
412
443
 
413
- console.info('正在更新自定义组件...');
444
+ const spinner = ora('正在更新自定义组件...').start();
414
445
 
415
446
  try {
416
447
  const fullUpdateAPI = this.saveAPI();
@@ -421,15 +452,15 @@ class NeoService {
421
452
  'Content-Type': 'application/json'
422
453
  }
423
454
  });
424
- const {code, message} = response.data || {};
455
+ const { code, message } = response.data || {};
425
456
 
426
457
  if (code && code !== 200) {
427
458
  throw new Error(`更新组件失败: ${response.data.message || '未知错误'}`);
428
459
  }
429
-
430
460
 
431
- console.info(message ? `组件更新成功: ${message}。` : '组件更新成功。');
461
+ spinner.clear();
432
462
  } catch (error) {
463
+ spinner.fail('更新组件失败。');
433
464
  if (error.message) {
434
465
  console.error('更新组件失败:', error.message);
435
466
  } else {
@@ -439,6 +470,82 @@ class NeoService {
439
470
  }
440
471
  }
441
472
 
473
+ /**
474
+ * 获取线上自定义组件列表
475
+ * @returns {Promise<array>} 自定义组件列表
476
+ */
477
+ async getCustomCmpList() {
478
+ // 确保 token 有效
479
+ const token = await this.ensureValidToken();
480
+
481
+ /*
482
+ // 如果自定义组件列表已存在,则直接返回
483
+ if (this.cmpList && this.cmpList.length > 0) {
484
+ return this.cmpList;
485
+ }
486
+ */
487
+
488
+ try {
489
+ let queryAllAPI = this.buildFullUrl(NeoCrmAPI.queryAll);
490
+ queryAllAPI += `?fields=${cmpFields.join(',')}`;
491
+ const response = await axios.post(
492
+ queryAllAPI,
493
+ {},
494
+ {
495
+ headers: {
496
+ Authorization: `Bearer ${token}`,
497
+ 'xsy-inner-source': 'bff',
498
+ 'Content-Type': 'application/json'
499
+ }
500
+ }
501
+ );
502
+ const { code, message } = response.data || {};
503
+
504
+ if (code && code !== 200) {
505
+ throw new Error(`获取自定义组件列表失败: ${message || '未知错误'}`);
506
+ }
507
+
508
+ this.updateCustomCmpList(response.data.data || []);
509
+ } catch (error) {
510
+ if (error.message) {
511
+ console.error('获取自定义组件列表失败:', error.message);
512
+ } else {
513
+ console.error('响应数据:', error);
514
+ }
515
+ process.exit(1);
516
+ }
517
+
518
+ return this.cmpList || [];
519
+ }
520
+
521
+ // 获取指定框架的自定义组件列表
522
+ getCmpListByFramework(framework) {
523
+ if (!framework) {
524
+ return this.cmpList;
525
+ }
526
+ const curFramework = getFramework(framework);
527
+ return this.cmpList.filter((cmp) => cmp.framework === curFramework);
528
+ }
529
+
530
+ // 获取自定义组件信息
531
+ getCmpInfoByCmpType(cmpType) {
532
+ if (!cmpType) {
533
+ return null;
534
+ }
535
+ return this.cmpInfoMap[cmpType] || null;
536
+ }
537
+
538
+ // 更新自定义组件 Map
539
+ updateCustomCmpList(cmpList) {
540
+ if (!cmpList || !Array.isArray(cmpList)) {
541
+ return;
542
+ }
543
+ this.cmpList = cmpList;
544
+ cmpList.forEach((cmp) => {
545
+ this.cmpInfoMap[cmp.cmpType] = cmp;
546
+ });
547
+ }
548
+
442
549
  /**
443
550
  * 通用请求方法(确保 token 有效)
444
551
  * @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)