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.
- package/README.md +62 -47
- package/package.json +1 -1
- package/src/module/index.js +74 -19
- package/src/module/main.js +167 -267
- package/src/module/neoInit.js +1 -0
- package/src/neo/NeoUMDContent.js +6 -5
- package/src/neo/neoService.js +154 -43
- package/src/oss/publish2oss.js +174 -71
- package/src/template/antd-custom-cmp-template/README.md +26 -2
- package/src/template/antd-custom-cmp-template/neo.config.js +7 -4
- package/src/template/antd-custom-cmp-template/package.json +1 -0
- package/src/template/echarts-custom-cmp-template/README.md +26 -2
- package/src/template/echarts-custom-cmp-template/neo.config.js +9 -5
- package/src/template/echarts-custom-cmp-template/package.json +1 -0
- package/src/template/empty-custom-cmp-template/README.md +26 -0
- package/src/template/empty-custom-cmp-template/neo.config.js +7 -4
- package/src/template/empty-custom-cmp-template/package.json +1 -0
- package/src/template/neo-custom-cmp-template/README.md +26 -2
- package/src/template/neo-custom-cmp-template/neo.config.js +12 -9
- package/src/template/neo-custom-cmp-template/package.json +2 -1
- package/src/template/react-custom-cmp-template/README.md +26 -2
- package/src/template/react-custom-cmp-template/neo.config.js +7 -4
- package/src/template/react-custom-cmp-template/package.json +1 -0
- package/src/template/react-ts-custom-cmp-template/README.md +26 -2
- package/src/template/react-ts-custom-cmp-template/neo.config.js +7 -4
- package/src/template/react-ts-custom-cmp-template/package.json +1 -0
- package/src/template/vue2-custom-cmp-template/README.md +26 -2
- package/src/template/vue2-custom-cmp-template/neo.config.js +7 -4
- package/src/template/vue2-custom-cmp-template/package.json +1 -0
- package/src/{cmpUtils → utils/cmpUtils}/createCmpByTemplate.js +9 -7
- package/src/utils/cmpUtils/createCmpByZip.js +87 -0
- package/src/{cmpUtils → utils/cmpUtils}/createCommonModulesCode.js +2 -2
- package/src/{cmpUtils → utils/cmpUtils}/getCmpModelRegisterCode.js +1 -1
- package/src/{cmpUtils → utils/cmpUtils}/getCmpPreviewCode.js +1 -1
- package/src/{cmpUtils → utils/cmpUtils}/getCmpRegisterCode.js +1 -1
- package/src/{cmpUtils → utils/cmpUtils}/getCmpTypeByDir.js +2 -2
- package/src/{cmpUtils → utils/cmpUtils}/hasCmpTypeByDir.js +1 -1
- package/src/{cmpUtils → utils/cmpUtils}/previewCmp.js +3 -3
- package/src/utils/cmpUtils/pullCmp.js +95 -0
- package/src/{cmpUtils → utils/cmpUtils}/pushCmp.js +83 -73
- package/src/utils/common.js +48 -0
- package/src/utils/generateEntries.js +73 -0
- package/src/{projectUtils → utils/projectUtils}/createCmpProjectByTemplate.js +11 -7
- package/src/{projectUtils → utils/projectUtils}/createCmpProjectZip.js +18 -20
- package/src/{projectUtils → utils/projectUtils}/getEntries.js +2 -2
- package/src/{projectUtils → utils/projectUtils}/getEntriesWithAutoRegister.js +2 -2
- package/src/{projectUtils → utils/projectUtils}/hasNeoProject.js +2 -2
- package/src/{projectUtils → utils/projectUtils}/updatePublishLog.js +1 -1
package/src/neo/neoService.js
CHANGED
|
@@ -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
|
|
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.
|
|
27
|
-
* @param {string} config.
|
|
28
|
-
* @param {string} config.
|
|
29
|
-
* @param {string} config.
|
|
30
|
-
* @param {string} config.
|
|
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,
|
|
34
|
-
if (!
|
|
35
|
-
throw new Error('
|
|
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
|
-
'
|
|
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.
|
|
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
|
-
|
|
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.
|
|
110
|
-
formData.append('client_secret', this.
|
|
111
|
-
formData.append('username', this.
|
|
112
|
-
formData.append('password', this.
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
331
|
+
spinner.succeed(`文件上传成功: ${fileName} -> ${fileUrl}`);
|
|
300
332
|
return fileUrl;
|
|
301
333
|
} catch (error) {
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 方法
|
package/src/oss/publish2oss.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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/
|
|
6
|
-
- src/components/
|
|
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)
|