neo-cmp-cli 1.3.10 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/cmpUtils/publishCmp.js +221 -0
- package/src/module/index.js +42 -2
- package/src/module/main.js +89 -0
- package/src/neo/neoService.js +423 -0
- package/src/oss/publish2oss.js +95 -95
- package/src/template/neo-custom-cmp-template/README.md +3 -3
- package/src/template/neo-custom-cmp-template/auth.config.js +12 -0
- package/src/template/neo-custom-cmp-template/neo.config.js +14 -5
- package/src/template/react-custom-cmp-template/.prettierrc.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neo-cmp-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "前端脚手架:自定义组件开发工具,支持react 和 vue2.0技术栈。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neo-cli",
|
|
@@ -41,10 +41,13 @@
|
|
|
41
41
|
"url": "https://github.com/wibetter/neo-cmp-cli/issues"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"adm-zip": "^0.5.10",
|
|
44
45
|
"akfun": "^5.1.12",
|
|
46
|
+
"axios": "^0.27.2",
|
|
45
47
|
"chalk": "^4.0.0",
|
|
46
48
|
"deepmerge": "^4.2.2",
|
|
47
49
|
"figlet": "^1.2.0",
|
|
50
|
+
"form-data": "^4.0.0",
|
|
48
51
|
"fs-extra": "^10.1.0",
|
|
49
52
|
"inquirer": "^7.3.3",
|
|
50
53
|
"ora": "^4.0.4",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const AdmZip = require('adm-zip');
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const { catchCurPackageJson, resolveToCurrentRoot } = require('../utils/pathUtils');
|
|
6
|
+
const getConfigObj = require('../utils/getConfigObj');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const NeoService = require('../neo/neoService');
|
|
9
|
+
|
|
10
|
+
// 获取当前项目的package文件
|
|
11
|
+
const currentPackageJsonDir = catchCurPackageJson();
|
|
12
|
+
const currentPackageJson = getConfigObj(currentPackageJsonDir);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 将构建产物打包成 zip 文件
|
|
16
|
+
* @param {string} assetsRoot 构建产物的目录
|
|
17
|
+
* @returns {Promise<string>} zip 文件路径
|
|
18
|
+
*/
|
|
19
|
+
const createZipPackage = async (assetsRoot) => {
|
|
20
|
+
if (!fs.existsSync(assetsRoot)) {
|
|
21
|
+
throw new Error(`assetsRoot 不存在: ${assetsRoot}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = fs.readdirSync(assetsRoot);
|
|
25
|
+
const zip = new AdmZip();
|
|
26
|
+
|
|
27
|
+
files.forEach((file) => {
|
|
28
|
+
const filePath = path.join(assetsRoot, file);
|
|
29
|
+
const fileStat = fs.statSync(filePath);
|
|
30
|
+
if (fileStat.isFile()) {
|
|
31
|
+
// 只添加 .js 文件
|
|
32
|
+
if (file.endsWith('.js')) {
|
|
33
|
+
zip.addLocalFile(filePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const zipPath = path.join(assetsRoot, `${currentPackageJson.name}.zip`);
|
|
39
|
+
zip.writeZip(zipPath);
|
|
40
|
+
console.info(`已创建 zip 文件: ${zipPath}`);
|
|
41
|
+
|
|
42
|
+
return zipPath;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 获取技术栈标识
|
|
47
|
+
* 目的:兼容用户非标准写法
|
|
48
|
+
* 0: React, 1: vue2, 2: jQuery, 3: vue3
|
|
49
|
+
*/
|
|
50
|
+
export function getFramework(_framework) {
|
|
51
|
+
let defaultFramework = '0'; // 默认 React 技术栈
|
|
52
|
+
if (!_framework) {
|
|
53
|
+
return defaultFramework;
|
|
54
|
+
}
|
|
55
|
+
let curFramework = _framework.toLowerCase().trim();
|
|
56
|
+
switch (curFramework) {
|
|
57
|
+
case 'jquery':
|
|
58
|
+
case 'jq':
|
|
59
|
+
curFramework = '2';
|
|
60
|
+
break;
|
|
61
|
+
case 'vue2':
|
|
62
|
+
case 'vue 2':
|
|
63
|
+
case 'vue2.0':
|
|
64
|
+
case 'vue 2.0':
|
|
65
|
+
curFramework = '1';
|
|
66
|
+
break;
|
|
67
|
+
case 'vue':
|
|
68
|
+
case 'vue3':
|
|
69
|
+
case 'vue 3':
|
|
70
|
+
case 'vue3.0':
|
|
71
|
+
case 'vue 3.0':
|
|
72
|
+
curFramework = '3';
|
|
73
|
+
console.error(`${consoleTag} 暂不支持 vue3.0 技术栈。`);
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
curFramework = '0';
|
|
77
|
+
}
|
|
78
|
+
return curFramework;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 构建组件数据映射
|
|
83
|
+
* @param {string} assetsRoot 构建产物的目录
|
|
84
|
+
* @param {object} cmpInfo 自定义组件信息
|
|
85
|
+
* @returns {object} 自定义组件数据(含自定义组件模型信息)
|
|
86
|
+
*/
|
|
87
|
+
const buildComponentData = (assetsRoot, cmpInfo) => {
|
|
88
|
+
if (!cmpInfo || !cmpInfo.cmpType) {
|
|
89
|
+
console.error('自定义组件信息或组件名称不能为空');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { cmpType } = cmpInfo;
|
|
94
|
+
|
|
95
|
+
if (!assetsRoot || !fs.existsSync(assetsRoot)) {
|
|
96
|
+
console.error(`未找到自定义组件资源目录: ${assetsRoot}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const widgetName = _.kebabCase(cmpType);
|
|
100
|
+
const modelFile = path.join(assetsRoot, `${widgetName}Model.js`);
|
|
101
|
+
|
|
102
|
+
let ModelClass = null;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// 加载自定义组件模型资源文件
|
|
106
|
+
if (fs.existsSync(modelFile)) {
|
|
107
|
+
// 清除 require 缓存,确保每次都加载最新内容
|
|
108
|
+
const resolvedPath = require.resolve(modelFile);
|
|
109
|
+
if (require.cache[resolvedPath]) {
|
|
110
|
+
delete require.cache[resolvedPath];
|
|
111
|
+
}
|
|
112
|
+
const modelModule = require(modelFile);
|
|
113
|
+
// 获取导出的模型类(可能是 default 导出或命名导出)
|
|
114
|
+
ModelClass = modelModule.default || modelModule;
|
|
115
|
+
// 如果是命名导出,尝试查找类名(例如 EntityCardListModel)
|
|
116
|
+
if (typeof ModelClass !== 'function' && typeof modelModule === 'object') {
|
|
117
|
+
// 查找所有导出的类
|
|
118
|
+
const exportedClasses = Object.values(modelModule).filter(
|
|
119
|
+
(item) => typeof item === 'function'
|
|
120
|
+
);
|
|
121
|
+
if (exportedClasses.length > 0) {
|
|
122
|
+
ModelClass = exportedClasses[0];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 如果资源文件不存在,报错
|
|
127
|
+
else {
|
|
128
|
+
console.error(`未找到自定义组件模型文件,请检查以下路径是否存在:`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 如果 ModelClass 是函数(类),则实例化
|
|
133
|
+
if (typeof ModelClass !== 'function') {
|
|
134
|
+
console.error(`模型文件 ${modelFile} 未导出有效的模型类`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 实例化模型类
|
|
139
|
+
const modelInstance = new ModelClass();
|
|
140
|
+
|
|
141
|
+
// 构建组件数据,合并模型实例的信息
|
|
142
|
+
const curCmpInfo = {
|
|
143
|
+
...cmpInfo,
|
|
144
|
+
version: currentPackageJson.version || '1.0.0',
|
|
145
|
+
framework: currentPackageJson.framework ? getFramework(currentPackageJson.framework) : '0', // 0: React, 1: vue2, 2: jQuery, 3: vue3
|
|
146
|
+
// 从模型实例中提取并设置组件信息
|
|
147
|
+
label: modelInstance.label || cmpType,
|
|
148
|
+
description: modelInstance.description || '',
|
|
149
|
+
componentCategory: modelInstance.tags || [],
|
|
150
|
+
icon: modelInstance.iconSrc,
|
|
151
|
+
defaultProps: modelInstance.defaultComProps || {},
|
|
152
|
+
previewProps: modelInstance.previewComProps || {},
|
|
153
|
+
propsSchema: modelInstance.propsSchema || [],
|
|
154
|
+
events: modelInstance.events || [],
|
|
155
|
+
actions: modelInstance.actions || [],
|
|
156
|
+
// 如果模型实例中有其他属性,也可以添加
|
|
157
|
+
exposedToDesigner: modelInstance.exposedToDesigner !== undefined ? modelInstance.exposedToDesigner : true,
|
|
158
|
+
namespace: modelInstance.namespace || 'neo-cmp-cli',
|
|
159
|
+
enableDuplicate: modelInstance.enableDuplicate !== undefined ? modelInstance.enableDuplicate : true
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return curCmpInfo;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(`自定义组件模型文件解析失败 (${modelFile || '未知路径'}):`, error.message);
|
|
165
|
+
console.error(error.stack);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 发布组件到 NeoCRM
|
|
172
|
+
* @param {object} config 配置信息
|
|
173
|
+
* @param {string} assetsRoot 构建产物的目录
|
|
174
|
+
*/
|
|
175
|
+
const publishCmp = async (config, cmpType) => {
|
|
176
|
+
const {
|
|
177
|
+
authorization: credentials
|
|
178
|
+
} = config;
|
|
179
|
+
|
|
180
|
+
if (!credentials) {
|
|
181
|
+
console.error('未找到 NeoCRM 平台授权配置(neo.config.js / publishCmpConfig / credentials)。');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const spinner = ora('正在发布组件...').start();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// 步骤 1: 初始化 NeoService
|
|
189
|
+
spinner.text = '发布自定义组件:初始化 NeoService...';
|
|
190
|
+
const neoService = new NeoService(config);
|
|
191
|
+
|
|
192
|
+
// 步骤 2: 上传构建后资源文件
|
|
193
|
+
spinner.text = '发布自定义组件:上传自定义组件构建产物到 OSS...';
|
|
194
|
+
const cmpInfo = await neoService.publish2oss(cmpType);
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
// 步骤 3: 打包文件(打包单个自定义组件源码)
|
|
198
|
+
spinner.text = '发布自定义组件:打包文件(打包单个自定义组件源码)...';
|
|
199
|
+
const zipPath = await createZipPackage(assetsRoot);
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
// 步骤 4: 构建组件数据
|
|
203
|
+
spinner.text = '发布自定义组件:构建组件数据...';
|
|
204
|
+
const componentInfo = buildComponentData(config.assetsRoot, cmpInfo);
|
|
205
|
+
if (!componentInfo) {
|
|
206
|
+
throw new Error('构建组件数据失败,无法继续发布');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 步骤 5: 保存组件信息
|
|
210
|
+
spinner.text = '发布自定义组件:保存组件信息...';
|
|
211
|
+
await neoService.updateCustomComponent(componentInfo);
|
|
212
|
+
|
|
213
|
+
spinner.succeed('自定义组件发布成功!\n', componentInfo);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
spinner.fail(`自定义组件发布失败: ${error.message}`);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
module.exports = publishCmp;
|
|
221
|
+
|
package/src/module/index.js
CHANGED
|
@@ -81,11 +81,14 @@ yargs
|
|
|
81
81
|
value: 'neo',
|
|
82
82
|
short: 'neo'
|
|
83
83
|
},
|
|
84
|
+
/*
|
|
85
|
+
// 暂不提供 react js 模板(react js 模板已废弃)
|
|
84
86
|
{
|
|
85
87
|
name: 'react 自定义组件',
|
|
86
88
|
value: 'react',
|
|
87
89
|
short: 'react'
|
|
88
90
|
},
|
|
91
|
+
*/
|
|
89
92
|
{
|
|
90
93
|
name: 'vue2 自定义组件',
|
|
91
94
|
value: 'vue2',
|
|
@@ -169,11 +172,14 @@ yargs
|
|
|
169
172
|
{
|
|
170
173
|
name: 'cmpType',
|
|
171
174
|
type: 'input',
|
|
172
|
-
message: '
|
|
173
|
-
default: 'info-card'
|
|
175
|
+
message: '请输入要预览的自定义组件名称:',
|
|
174
176
|
}
|
|
175
177
|
];
|
|
176
178
|
inquirer.prompt(questions).then((ans) => {
|
|
179
|
+
if (!ans.cmpType) {
|
|
180
|
+
console.error('自定义组件名称不能为空。');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
177
183
|
mainAction.previewCmp(ans.cmpType);
|
|
178
184
|
});
|
|
179
185
|
}
|
|
@@ -248,6 +254,40 @@ yargs
|
|
|
248
254
|
mainAction.publish2oss(argv.cmpType); // 构建并发布脚本到oss
|
|
249
255
|
}
|
|
250
256
|
)
|
|
257
|
+
.command(
|
|
258
|
+
'publishCmp [options]',
|
|
259
|
+
'发布组件到 NeoCRM 平台',
|
|
260
|
+
(yargs) => {
|
|
261
|
+
yargs
|
|
262
|
+
.reset()
|
|
263
|
+
.usage(titleTip('Usage') + ': $0 publishCmp [options]')
|
|
264
|
+
.option('cmpType', {
|
|
265
|
+
alias: 't',
|
|
266
|
+
describe: '自定义组件名称'
|
|
267
|
+
})
|
|
268
|
+
.alias('h', 'help');
|
|
269
|
+
},
|
|
270
|
+
(argv) => {
|
|
271
|
+
if (argv.cmpType) {
|
|
272
|
+
mainAction.publishCmp(argv.cmpType); // 构建并发布组件到 NeoCRM
|
|
273
|
+
} else {
|
|
274
|
+
const questions = [
|
|
275
|
+
{
|
|
276
|
+
name: 'cmpType',
|
|
277
|
+
type: 'input',
|
|
278
|
+
message: '请输入要发布的自定义组件名称:'
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
inquirer.prompt(questions).then((ans) => {
|
|
282
|
+
if (!ans.cmpType) {
|
|
283
|
+
console.error('自定义组件名称不能为空。');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
mainAction.publishCmp(ans.cmpType);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
)
|
|
251
291
|
.command(
|
|
252
292
|
'build2esm',
|
|
253
293
|
'构建esm模块',
|
package/src/module/main.js
CHANGED
|
@@ -7,6 +7,7 @@ const neoConfigInit = require('../utils/neoConfigInit.js');
|
|
|
7
7
|
const { consoleTag } = require('../utils/neoParams');
|
|
8
8
|
const curConfig = require('../config/index'); // 获取当前项目根目录下的配置文件
|
|
9
9
|
const publish2oss = require('../oss/publish2oss');
|
|
10
|
+
const publishCmp = require('../cmpUtils/publishCmp');
|
|
10
11
|
const getEntries = require('../cmpUtils/getEntries');
|
|
11
12
|
const getEntriesWithAutoRegister = require('../cmpUtils/getEntriesWithAutoRegister');
|
|
12
13
|
const previewCmp = require('./previewCmp');
|
|
@@ -270,5 +271,93 @@ module.exports = {
|
|
|
270
271
|
);
|
|
271
272
|
});
|
|
272
273
|
},
|
|
274
|
+
// 发布组件到 NeoCRM 平台
|
|
275
|
+
publishCmp: (cmpType) => {
|
|
276
|
+
// 将 publishCmp 相关配置设置给 build2lib
|
|
277
|
+
const publishCmpConfig = curConfig.publishCmp;
|
|
278
|
+
curConfig.build2lib = Object.assign(curConfig.build2lib, publishCmpConfig);
|
|
279
|
+
|
|
280
|
+
const curEntry = curConfig.build2lib.entry;
|
|
281
|
+
let curCmpTypes = [];
|
|
282
|
+
|
|
283
|
+
if (!curEntry || Object.keys(curEntry).length === 0) {
|
|
284
|
+
// 如果未配置 entry,则自动生成 entry
|
|
285
|
+
let entries = {};
|
|
286
|
+
if (curConfig.build2lib.disableAutoRegister) {
|
|
287
|
+
// disableAutoRegister 为 true 时,仅自动生成入口文件(不自动注册)
|
|
288
|
+
const { widgetEntries, cmpTypes } = getEntries(curConfig.componentsDir, cmpType);
|
|
289
|
+
entries = widgetEntries;
|
|
290
|
+
curCmpTypes = cmpTypes;
|
|
291
|
+
} else {
|
|
292
|
+
// 自动生成入口文件(并自动创建对应的注册文件)
|
|
293
|
+
const { widgetEntries, cmpTypes } = getEntriesWithAutoRegister(
|
|
294
|
+
curConfig.componentsDir,
|
|
295
|
+
cmpType
|
|
296
|
+
);
|
|
297
|
+
entries = widgetEntries;
|
|
298
|
+
curCmpTypes = cmpTypes;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 注入 webpack/entry
|
|
302
|
+
if (entries && Object.keys(entries).length > 0) {
|
|
303
|
+
curConfig.build2lib.entry = entries;
|
|
304
|
+
console.info('已自动生成 entry 入口配置:', entries);
|
|
305
|
+
} else {
|
|
306
|
+
console.error(
|
|
307
|
+
`未识别到自定义组件,请检查 ${
|
|
308
|
+
curConfig.componentsDir || './src/components'
|
|
309
|
+
} 目录下是否存在自定义组件。`
|
|
310
|
+
);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
// 说明:自定义组件和平台模块联邦使用异常,所以暂时注释掉
|
|
317
|
+
// 添加模块联邦插件
|
|
318
|
+
if (curConfig.publishCmp.enableMF) {
|
|
319
|
+
curConfig.webpack.plugins.push(...MFPlugins);
|
|
320
|
+
}
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
// 添加自定义 webpack 插件: 用于实现和 Neo 平台共享依赖
|
|
324
|
+
if (
|
|
325
|
+
curConfig.webpack &&
|
|
326
|
+
curConfig.webpack.plugins &&
|
|
327
|
+
Array.isArray(curConfig.webpack.plugins)
|
|
328
|
+
) {
|
|
329
|
+
curConfig.webpack.plugins.push(new AddNeoRequirePlugin({ verbose: true }));
|
|
330
|
+
} else {
|
|
331
|
+
curConfig.webpack.plugins = [new AddNeoRequirePlugin({ verbose: true })];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 添加 内置 Neo 的 externals 配置
|
|
335
|
+
const neoExternals = getExternalsByNeoCommonModules(cmpNeoExternals);
|
|
336
|
+
if (curConfig.build2lib.externals && _.isPlainObject(curConfig.build2lib.externals)) {
|
|
337
|
+
curConfig.build2lib.externals = Object.assign(curConfig.build2lib.externals, neoExternals);
|
|
338
|
+
} else {
|
|
339
|
+
curConfig.build2lib.externals = neoExternals;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 写入自定义组件的共享模块和远程模块相关信息
|
|
343
|
+
const commonModulesFilePath = createCommonModulesCode(neoCommonModule, curCmpTypes);
|
|
344
|
+
|
|
345
|
+
// 所有入口文件添加 commonModulesFile
|
|
346
|
+
if (commonModulesFilePath && curConfig.build2lib.entry) {
|
|
347
|
+
Object.keys(curConfig.build2lib.entry).forEach((name) => {
|
|
348
|
+
// 判断不是以Model结尾的文件
|
|
349
|
+
if (!name.endsWith('Model')) {
|
|
350
|
+
curConfig.build2lib.entry[name] = [commonModulesFilePath].concat(
|
|
351
|
+
curConfig.build2lib.entry[name]
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
akfun.build('lib', curConfig, consoleTag, () => {
|
|
358
|
+
// 构建完成后,执行 publishCmp
|
|
359
|
+
publishCmp(publishCmpConfig);
|
|
360
|
+
});
|
|
361
|
+
},
|
|
273
362
|
build2esm: (fileName) => akfun.build2esm(fileName, curConfig, consoleTag) // 构建esm输出模块
|
|
274
363
|
};
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const FormData = require('form-data');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
const updatePublishLog = require('../cmpUtils/updatePublishLog');
|
|
7
|
+
|
|
8
|
+
// NeoCRM 平台默认 API 配置
|
|
9
|
+
const NeoCrmAPI = {
|
|
10
|
+
neoBaseURL: 'https://crm.xiaoshouyi.com', // 平台根地址
|
|
11
|
+
tokenAPI: 'https://login.crm.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址
|
|
12
|
+
uploadAPI: '/rest/metadata/v3.0/ui/customComponents/actions/upload', // 文件上传接口地址
|
|
13
|
+
saveAPI: '/rest/metadata/v3.0/ui/customComponents/actions/saveComponent' // 组件信息保存接口地址
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Neo 平台服务类
|
|
18
|
+
* 提供 token 管理、文件上传、组件更新等功能
|
|
19
|
+
*/
|
|
20
|
+
class NeoService {
|
|
21
|
+
/**
|
|
22
|
+
* 初始化 Neo 服务
|
|
23
|
+
* @param {object} config 配置信息
|
|
24
|
+
* @param {string} config.neoBaseURL Neo 平台根地址
|
|
25
|
+
* @param {string} config.tokenAPI Token 获取接口地址
|
|
26
|
+
* @param {object} config.authorization 授权信息
|
|
27
|
+
* @param {string} config.authorization.client_id 客户端 ID
|
|
28
|
+
* @param {string} config.authorization.client_secret 客户端密钥
|
|
29
|
+
* @param {string} config.authorization.username 用户名
|
|
30
|
+
* @param {string} config.authorization.password 密码
|
|
31
|
+
*/
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
const { assetsRoot, neoBaseURL, tokenAPI, authorization } = config;
|
|
34
|
+
if (!authorization) {
|
|
35
|
+
throw new Error('authorization 不能为空');
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
!authorization.client_id ||
|
|
39
|
+
!authorization.client_secret ||
|
|
40
|
+
!authorization.username ||
|
|
41
|
+
!authorization.password
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'authorization 配置不完整,需要包含 client_id、client_secret、username、password'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.assetsRoot = assetsRoot;
|
|
49
|
+
this.neoBaseURL = neoBaseURL || NeoCrmAPI.neoBaseURL;
|
|
50
|
+
this.tokenAPI = tokenAPI || NeoCrmAPI.tokenAPI;
|
|
51
|
+
this.authorization = authorization;
|
|
52
|
+
|
|
53
|
+
// Token 缓存
|
|
54
|
+
this.tokenCache = {
|
|
55
|
+
token: null,
|
|
56
|
+
expiresAt: null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 构建完整的 API URL
|
|
62
|
+
* @param {string} url 相对或绝对 URL
|
|
63
|
+
* @returns {string} 完整的 URL
|
|
64
|
+
*/
|
|
65
|
+
buildFullUrl(url) {
|
|
66
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
return `${this.neoBaseURL}${url}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 获取文件上传接口地址
|
|
73
|
+
uploadAPI() {
|
|
74
|
+
return this.buildFullUrl(NeoCrmAPI.uploadAPI);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 获取组件信息保存接口地址
|
|
78
|
+
saveAPI() {
|
|
79
|
+
return this.buildFullUrl(NeoCrmAPI.saveAPI);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 检查 token 是否过期
|
|
84
|
+
* @returns {boolean} true 表示已过期,false 表示未过期
|
|
85
|
+
*/
|
|
86
|
+
isTokenExpired() {
|
|
87
|
+
if (!this.tokenCache.token || !this.tokenCache.expiresAt) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// 提前 60 秒判断为过期,避免边缘情况
|
|
91
|
+
return Date.now() >= this.tokenCache.expiresAt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取 token(含授权信息、租户信息)
|
|
96
|
+
* @returns {Promise<string>} token
|
|
97
|
+
*/
|
|
98
|
+
async getToken() {
|
|
99
|
+
// 检查缓存是否有效
|
|
100
|
+
if (!this.isTokenExpired()) {
|
|
101
|
+
console.info('使用缓存的 token');
|
|
102
|
+
return this.tokenCache.token;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.info('正在获取 token...');
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// 构建请求参数
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
params.append('grant_type', 'password');
|
|
111
|
+
params.append('client_id', this.authorization.client_id);
|
|
112
|
+
params.append('client_secret', this.authorization.client_secret);
|
|
113
|
+
params.append('username', this.authorization.username);
|
|
114
|
+
params.append('password', this.authorization.password);
|
|
115
|
+
|
|
116
|
+
const tokenUrl = this.buildFullUrl(this.tokenAPI);
|
|
117
|
+
const response = await axios.post(tokenUrl, params, {
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { access_token, expires_in } = response.data;
|
|
124
|
+
|
|
125
|
+
if (!access_token) {
|
|
126
|
+
throw new Error('获取 token 失败:响应中未包含 access_token');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 缓存 token(提前 60 秒过期,避免边缘情况)
|
|
130
|
+
const expiresIn = parseInt(expires_in) || 3600; // 默认 1 小时
|
|
131
|
+
this.tokenCache = {
|
|
132
|
+
token: access_token,
|
|
133
|
+
expiresAt: Date.now() + (expiresIn - 60) * 1000
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
console.info('token 获取成功');
|
|
137
|
+
return access_token;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('获取 token 失败:', error.message);
|
|
140
|
+
if (error.response) {
|
|
141
|
+
console.error('响应数据:', error.response.data);
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 刷新 token
|
|
149
|
+
* @returns {Promise<string>} 新的 token
|
|
150
|
+
*/
|
|
151
|
+
async refreshToken() {
|
|
152
|
+
// 清除缓存,强制获取新 token
|
|
153
|
+
this.tokenCache = {
|
|
154
|
+
token: null,
|
|
155
|
+
expiresAt: null
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return await this.getToken();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 确保 token 有效,如果过期则自动刷新
|
|
163
|
+
* @returns {Promise<string>} 有效的 token
|
|
164
|
+
*/
|
|
165
|
+
async ensureValidToken() {
|
|
166
|
+
if (this.isTokenExpired()) {
|
|
167
|
+
console.info('token 已过期,正在刷新...');
|
|
168
|
+
return await this.refreshToken();
|
|
169
|
+
}
|
|
170
|
+
return this.tokenCache.token;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 上传文件到 Neo 平台
|
|
175
|
+
* @param {string} filePath 文件路径
|
|
176
|
+
* @param {object} options 可选配置
|
|
177
|
+
* @param {string} options.fieldName 表单字段名,默认为 'file'
|
|
178
|
+
* @returns {Promise<string>} CDN 地址或文件 URL
|
|
179
|
+
*/
|
|
180
|
+
async uploadFile(filePath, options = {}) {
|
|
181
|
+
// 确保 token 有效
|
|
182
|
+
const token = await this.ensureValidToken();
|
|
183
|
+
|
|
184
|
+
if (!fs.existsSync(filePath)) {
|
|
185
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.info('正在上传文件...');
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const formData = new FormData();
|
|
192
|
+
const fieldName = options.fieldName || 'customComponentCode';
|
|
193
|
+
formData.append(fieldName, fs.createReadStream(filePath));
|
|
194
|
+
|
|
195
|
+
const fullUploadAPI = this.uploadAPI();
|
|
196
|
+
const response = await axios.post(fullUploadAPI, formData, {
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${token}`,
|
|
199
|
+
...formData.getHeaders()
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 处理不同的响应格式
|
|
204
|
+
let resultData;
|
|
205
|
+
if (typeof response.data === 'string') {
|
|
206
|
+
resultData = response.data;
|
|
207
|
+
} else if (response.data && response.data.data) {
|
|
208
|
+
resultData = response.data.data;
|
|
209
|
+
} else if (response.data && response.data.code === 200) {
|
|
210
|
+
resultData = response.data.data;
|
|
211
|
+
} else {
|
|
212
|
+
resultData = response.data;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (response.data && response.data.code && response.data.code !== 200) {
|
|
216
|
+
throw new Error(`上传失败: ${response.data.message || '未知错误'}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!resultData || (typeof resultData !== 'string' && !resultData.url)) {
|
|
220
|
+
throw new Error(`返回的文件地址格式不正确: ${JSON.stringify(resultData)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const fileUrl = typeof resultData === 'string' ? resultData : resultData.url;
|
|
224
|
+
return fileUrl;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('上传文件失败:', error.message);
|
|
227
|
+
if (error.response) {
|
|
228
|
+
console.error('响应数据:', error.response.data);
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 将构建产物上传到 NeoCRM 平台端
|
|
236
|
+
*
|
|
237
|
+
* @param {object} cmpType 自定义组件名称
|
|
238
|
+
* @param {array} fileExtensions 需要上传的文件类型,默认 ['.js', '.css']
|
|
239
|
+
*/
|
|
240
|
+
async publish2oss(cmpType, fileExtensions = ['.js', '.css']) {
|
|
241
|
+
if (!fs.existsSync(cmpType)) {
|
|
242
|
+
console.error(`自定义组件名称不能为空: ${cmpType}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!fs.existsSync(this.assetsRoot)) {
|
|
246
|
+
console.error(`未找到自定义组件资源目录: ${this.assetsRoot}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// 当前组件信息
|
|
250
|
+
const curCmpInfo = {
|
|
251
|
+
cmpType
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const files = fs.readdirSync(this.assetsRoot); // 读取构建目录下的所有文件
|
|
255
|
+
|
|
256
|
+
// 并行上传所有指定类型的文件
|
|
257
|
+
const uploadPromises = files.map(async (file) => {
|
|
258
|
+
const filePath = path.join(this.assetsRoot, file);
|
|
259
|
+
// 获取文件状态
|
|
260
|
+
const fileStat = fs.statSync(filePath);
|
|
261
|
+
// 检查文件扩展名
|
|
262
|
+
// const fileExt = path.extname(file);
|
|
263
|
+
const fileInfo = path.parse(filePath);
|
|
264
|
+
if (fileStat.isFile() && fileExtensions.includes(fileInfo.ext)) {
|
|
265
|
+
let widgetName = _.kebabCase(cmpType);
|
|
266
|
+
|
|
267
|
+
if (file.indexOf(widgetName) < 0) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// 上传文件
|
|
273
|
+
const fileUrl = await this.uploadFile(filePath);
|
|
274
|
+
|
|
275
|
+
if (file.indexOf('Model') > -1) {
|
|
276
|
+
curCmpInfo.modelAsset = fileUrl;
|
|
277
|
+
} else if (file.endsWith('.css')) {
|
|
278
|
+
curCmpInfo.cssAsset = fileUrl;
|
|
279
|
+
} else {
|
|
280
|
+
curCmpInfo.asset = fileUrl;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error(`文件上传失败(${file}):\n`, error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await Promise.all(uploadPromises);
|
|
290
|
+
|
|
291
|
+
if (curCmpInfo && curCmpInfo.cmpType) {
|
|
292
|
+
console.info('上传至 OSS 的文件信息:\n', curCmpInfo);
|
|
293
|
+
// 更新发布日志
|
|
294
|
+
updatePublishLog(curCmpInfo);
|
|
295
|
+
}
|
|
296
|
+
return curCmpInfo;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 更新自定义组件
|
|
301
|
+
* @param {string} updateAPI 更新接口地址(相对或绝对路径)
|
|
302
|
+
* @param {object} componentData 组件数据
|
|
303
|
+
* @returns {Promise<object>} 更新结果
|
|
304
|
+
*/
|
|
305
|
+
async updateCustomComponent(componentData) {
|
|
306
|
+
// 确保 token 有效
|
|
307
|
+
const token = await this.ensureValidToken();
|
|
308
|
+
|
|
309
|
+
if (!componentData) {
|
|
310
|
+
throw new Error('componentData 不能为空');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.info('正在更新自定义组件...');
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const fullUpdateAPI = this.updateAPI();
|
|
317
|
+
const response = await axios.post(fullUpdateAPI, componentData, {
|
|
318
|
+
headers: {
|
|
319
|
+
Authorization: `Bearer ${token}`,
|
|
320
|
+
'Content-Type': 'application/json'
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (response.data && response.data.code && response.data.code !== 200) {
|
|
325
|
+
throw new Error(`更新组件失败: ${response.data.message || '未知错误'}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.info('组件更新成功:', response.data);
|
|
329
|
+
return response.data;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('更新组件失败:', error.message);
|
|
332
|
+
if (error.response) {
|
|
333
|
+
console.error('响应数据:', error.response.data);
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 通用请求方法(确保 token 有效)
|
|
341
|
+
* @param {string} method HTTP 方法
|
|
342
|
+
* @param {string} api API 地址(相对或绝对路径)
|
|
343
|
+
* @param {object} options 请求选项
|
|
344
|
+
* @param {object} options.data 请求数据
|
|
345
|
+
* @param {object} options.headers 额外的请求头
|
|
346
|
+
* @param {object} options.params 查询参数
|
|
347
|
+
* @returns {Promise<object>} 响应数据
|
|
348
|
+
*/
|
|
349
|
+
async request(method, api, options = {}) {
|
|
350
|
+
// 确保 token 有效
|
|
351
|
+
const token = await this.ensureValidToken();
|
|
352
|
+
|
|
353
|
+
const { data, headers = {}, params } = options;
|
|
354
|
+
const fullAPI = this.buildFullUrl(api);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const response = await axios({
|
|
358
|
+
method,
|
|
359
|
+
url: fullAPI,
|
|
360
|
+
data,
|
|
361
|
+
params,
|
|
362
|
+
headers: {
|
|
363
|
+
Authorization: `Bearer ${token}`,
|
|
364
|
+
...headers
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (response.data && response.data.code && response.data.code !== 200) {
|
|
369
|
+
throw new Error(`请求失败: ${response.data.message || '未知错误'}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return response.data;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error(`请求失败 [${method} ${api}]:`, error.message);
|
|
375
|
+
if (error.response) {
|
|
376
|
+
console.error('响应数据:', error.response.data);
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* GET 请求
|
|
384
|
+
* @param {string} api API 地址
|
|
385
|
+
* @param {object} options 请求选项
|
|
386
|
+
* @returns {Promise<object>} 响应数据
|
|
387
|
+
*/
|
|
388
|
+
async get(api, options = {}) {
|
|
389
|
+
return await this.request('GET', api, options);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* POST 请求
|
|
394
|
+
* @param {string} api API 地址
|
|
395
|
+
* @param {object} options 请求选项
|
|
396
|
+
* @returns {Promise<object>} 响应数据
|
|
397
|
+
*/
|
|
398
|
+
async post(api, options = {}) {
|
|
399
|
+
return await this.request('POST', api, options);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* PUT 请求
|
|
404
|
+
* @param {string} api API 地址
|
|
405
|
+
* @param {object} options 请求选项
|
|
406
|
+
* @returns {Promise<object>} 响应数据
|
|
407
|
+
*/
|
|
408
|
+
async put(api, options = {}) {
|
|
409
|
+
return await this.request('PUT', api, options);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* DELETE 请求
|
|
414
|
+
* @param {string} api API 地址
|
|
415
|
+
* @param {object} options 请求选项
|
|
416
|
+
* @returns {Promise<object>} 响应数据
|
|
417
|
+
*/
|
|
418
|
+
async delete(api, options = {}) {
|
|
419
|
+
return await this.request('DELETE', api, options);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = NeoService;
|
package/src/oss/publish2oss.js
CHANGED
|
@@ -10,101 +10,6 @@ const updatePublishLog = require('../cmpUtils/updatePublishLog');
|
|
|
10
10
|
const currentPackageJsonDir = catchCurPackageJson();
|
|
11
11
|
const currentPackageJson = getConfigObj(currentPackageJsonDir);
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* 将构建产物上传到指定 oss 存储桶
|
|
15
|
-
*
|
|
16
|
-
* @param {string} ossType oss 类型:baidu、ali
|
|
17
|
-
* @param {object} ossConfig oss 配置
|
|
18
|
-
* @param {string} assetsRoot 构建产物的目录
|
|
19
|
-
* @param {array} fileExtensions 需要上传的文件类型,默认 ['.js', '.css']
|
|
20
|
-
*/
|
|
21
|
-
const publish2oss = (ossType, ossConfig, assetsRoot, fileExtensions = ['.js', '.css']) => {
|
|
22
|
-
if (ossType !== 'baidu' && ossType !== 'ali') {
|
|
23
|
-
console.error(`不支持的oss类型: ${ossType}`);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const bosClient = getBosClient(ossType, ossConfig);
|
|
27
|
-
if (!assetsRoot) {
|
|
28
|
-
console.error('assetsRoot 不能为空');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (!fs.existsSync(assetsRoot)) {
|
|
32
|
-
console.error(`assetsRoot 不存在: ${assetsRoot}`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const files = fs.readdirSync(assetsRoot); // 读取构建目录下的所有文件
|
|
36
|
-
|
|
37
|
-
// 并行上传所有指定类型的文件
|
|
38
|
-
const uploadPromises = files.map(async (file) => {
|
|
39
|
-
const filePath = path.join(assetsRoot, file);
|
|
40
|
-
// 获取文件状态
|
|
41
|
-
const fileStat = fs.statSync(filePath);
|
|
42
|
-
// 检查文件扩展名
|
|
43
|
-
// const fileExt = path.extname(file);
|
|
44
|
-
const fileInfo = path.parse(filePath);
|
|
45
|
-
if (fileStat.isFile() && fileExtensions.includes(fileInfo.ext)) {
|
|
46
|
-
const objectKey = addVersionToFilename(file);
|
|
47
|
-
try {
|
|
48
|
-
// 判断线上是否存在重名文件,避免被覆盖
|
|
49
|
-
const historyResult = await bosClient.get(objectKey);
|
|
50
|
-
if (historyResult && historyResult.url) {
|
|
51
|
-
return {
|
|
52
|
-
success: false,
|
|
53
|
-
status: '文件上传失败',
|
|
54
|
-
widgetName: fileInfo.name,
|
|
55
|
-
fileName: file,
|
|
56
|
-
filepath: filePath,
|
|
57
|
-
ossPath: historyResult.url,
|
|
58
|
-
error: '线上存在重名文件。'
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// 上传文件
|
|
62
|
-
const result = await bosClient.upload(objectKey, filePath);
|
|
63
|
-
return {
|
|
64
|
-
success: true,
|
|
65
|
-
status: '文件上传成功',
|
|
66
|
-
fileName: file,
|
|
67
|
-
widgetName: fileInfo.name,
|
|
68
|
-
filepath: filePath,
|
|
69
|
-
ossPath: getFilePath(ossType, ossConfig.bucket, objectKey),
|
|
70
|
-
resultMsg: result
|
|
71
|
-
};
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return {
|
|
74
|
-
success: false,
|
|
75
|
-
status: '文件上传失败',
|
|
76
|
-
widgetName: fileInfo.name,
|
|
77
|
-
fileName: file,
|
|
78
|
-
filepath: filePath,
|
|
79
|
-
error: error.message
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
Promise.all(uploadPromises)
|
|
86
|
-
.then((results) => {
|
|
87
|
-
/*
|
|
88
|
-
const { succeedFiles, errorFiles } = getResultFiles(results);
|
|
89
|
-
if (succeedFiles.length > 0) {
|
|
90
|
-
console.info('已成功上传如下文件:\n', succeedFiles);
|
|
91
|
-
}
|
|
92
|
-
if (errorFiles.length > 0) {
|
|
93
|
-
console.info('上传失败的文件有:\n', errorFiles);
|
|
94
|
-
}
|
|
95
|
-
*/
|
|
96
|
-
const widgetFilesMap = getResultFilesByWidgetName(results);
|
|
97
|
-
if (widgetFilesMap) {
|
|
98
|
-
console.info('上传至 OSS 的文件信息:\n', widgetFilesMap);
|
|
99
|
-
// 更新发布日志
|
|
100
|
-
updatePublishLog(widgetFilesMap);
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
.catch((error) => {
|
|
104
|
-
console.error('批量上传文件异常:\n', error);
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
13
|
// 根据 ossType 获取对应的 BosClient
|
|
109
14
|
const getBosClient = (ossType, ossConfig) => {
|
|
110
15
|
if (ossType === 'baidu') {
|
|
@@ -241,4 +146,99 @@ function addVersionToFilename(
|
|
|
241
146
|
return `${projectName}/${baseName}-${version}${extension}`;
|
|
242
147
|
}
|
|
243
148
|
|
|
149
|
+
/**
|
|
150
|
+
* 将构建产物上传到指定 oss 存储桶
|
|
151
|
+
*
|
|
152
|
+
* @param {string} ossType oss 类型:baidu、ali
|
|
153
|
+
* @param {object} ossConfig oss 配置
|
|
154
|
+
* @param {string} assetsRoot 构建产物的目录
|
|
155
|
+
* @param {array} fileExtensions 需要上传的文件类型,默认 ['.js', '.css']
|
|
156
|
+
*/
|
|
157
|
+
const publish2oss = (ossType, ossConfig, assetsRoot, fileExtensions = ['.js', '.css']) => {
|
|
158
|
+
if (ossType !== 'baidu' && ossType !== 'ali') {
|
|
159
|
+
console.error(`不支持的oss类型: ${ossType}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const bosClient = getBosClient(ossType, ossConfig);
|
|
163
|
+
if (!assetsRoot) {
|
|
164
|
+
console.error('assetsRoot 不能为空');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!fs.existsSync(assetsRoot)) {
|
|
168
|
+
console.error(`assetsRoot 不存在: ${assetsRoot}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
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);
|
|
183
|
+
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
|
+
};
|
|
196
|
+
}
|
|
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
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
status: '文件上传失败',
|
|
212
|
+
widgetName: fileInfo.name,
|
|
213
|
+
fileName: file,
|
|
214
|
+
filepath: filePath,
|
|
215
|
+
error: error.message
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
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);
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
244
|
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/entity-table/index.tsx: 自定义组件的内容文件;
|
|
6
|
+
- src/components/entity-table/model.ts: 自定义组件的模型文件,用于对接页面设计器;
|
|
7
7
|
- neo.config.js: neo-cmp-cli 配置文件。
|
|
8
8
|
|
|
9
9
|
### 组件开发规范
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- 自定义组件最外层请设置一个唯一的 ClassName(比如 xx-cmpType-container),所有内容样式请放在该 ClassName 中,避免自定义组件样式相互干扰;
|
|
14
14
|
- 默认开启代码规范检测(含样式内容),如需关闭,请调整 neo.config.js 相关配置;
|
|
15
15
|
- 请使用 react 16版本;
|
|
16
|
-
- 支持在自定义组件中使用 Open API,详细见[使用说明](
|
|
16
|
+
- 支持在自定义组件中使用 Open API,详细见[使用说明](https://www.npmjs.com/package/neo-open-api)。
|
|
17
17
|
|
|
18
18
|
### 自定义组件注册器使用说明
|
|
19
19
|
- [neo-register 使用说明](https://www.npmjs.com/package/neo-register?activeTab=readme)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// NeoCRM 授权配置
|
|
3
|
+
module.exports = {
|
|
4
|
+
client_id: 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
5
|
+
client_secret: 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
|
|
6
|
+
username: 'xx', // 用户在销售易系统中的用户名
|
|
7
|
+
/**
|
|
8
|
+
* password 为 用户在销售易系统中的账号密码加上 8 位安全令牌。
|
|
9
|
+
* 例如,用户密码为 123456,安全令牌为 ABCDEFGH,则 password 的值应为 123456ABCDEFGH。
|
|
10
|
+
*/
|
|
11
|
+
password: 'xx xx', // 用户账户密码 + 8 位安全令牌
|
|
12
|
+
};
|
|
@@ -6,6 +6,12 @@ function resolve(dir) {
|
|
|
6
6
|
return path.resolve(__dirname, dir);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
let authConfig = {}; // NeoCRM 授权配置
|
|
10
|
+
if (fs.existsSync('./auth.config.js')) {
|
|
11
|
+
// 加载 NeoCRM 授权配置
|
|
12
|
+
authConfig = require('./auth.config');
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
// 包括生产和开发的环境配置信息
|
|
10
16
|
module.exports = {
|
|
11
17
|
settings: {
|
|
@@ -121,15 +127,18 @@ module.exports = {
|
|
|
121
127
|
},
|
|
122
128
|
publishCmp: {
|
|
123
129
|
// 用于构建并发布至 NeoCRM 的相关配置
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
|
|
131
|
+
tokenAPI: 'https://login.crm-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.crm.xiaoshouyi.com/auc/oauth2/token)
|
|
132
|
+
// NeoCRM 授权配置
|
|
133
|
+
authorization: {
|
|
134
|
+
client_id: authConfig.client_id || 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
135
|
+
client_secret: authConfig.client_secret || 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
|
|
136
|
+
username: authConfig.username || 'xx', // 用户在销售易系统中的用户名
|
|
128
137
|
/**
|
|
129
138
|
* password 为 用户在销售易系统中的账号密码加上 8 位安全令牌。
|
|
130
139
|
* 例如,用户密码为 123456,安全令牌为 ABCDEFGH,则 password 的值应为 123456ABCDEFGH。
|
|
131
140
|
*/
|
|
132
|
-
password: 'xx xx' // 用户账户密码 + 8 位安全令牌
|
|
141
|
+
password: authConfig.password || 'xx xx' // 用户账户密码 + 8 位安全令牌
|
|
133
142
|
},
|
|
134
143
|
/*
|
|
135
144
|
【特别说明】以下配置项都自带默认值,非必填。如需自定义请自行配置。
|
|
@@ -6,7 +6,7 @@ module.exports = {
|
|
|
6
6
|
semi: true, // Semicolons 分号,默认需要分号
|
|
7
7
|
tabWidth: 2, // 空格,默认 2,
|
|
8
8
|
useTabs: false,
|
|
9
|
-
singleQuote: true, // 单引号还是双引号,默认为false 双引号
|
|
9
|
+
singleQuote: true, // 单引号还是双引号,默认为 false 双引号
|
|
10
10
|
trailingComma: 'all', // 逗号
|
|
11
11
|
jsxBracketSameLine: false, // 默认为false,Put the > of a multi-line JSX element at the end of the last line instead of being alone on the next line (does not apply to self closing elements).
|
|
12
12
|
};
|