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.
- 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 +150 -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 +8 -6
- package/src/utils/cmpUtils/createCmpByZip.js +90 -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 +96 -0
- package/src/{cmpUtils → utils/cmpUtils}/pushCmp.js +82 -71
- package/src/utils/common.js +48 -0
- package/src/utils/generateEntries.js +73 -0
- package/src/{projectUtils → utils/projectUtils}/createCmpProjectByTemplate.js +10 -6
- 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,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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
330
|
+
spinner.succeed(`文件上传成功: ${fileName} -> ${fileUrl}`);
|
|
300
331
|
return fileUrl;
|
|
301
332
|
} catch (error) {
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 方法
|
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)
|