neo-cmp-cli 1.5.0-beta.2 → 1.5.0-beta.5
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 +47 -10
- package/package.json +1 -1
- package/src/cmpUtils/getCmpTypeByDir.js +41 -0
- package/src/cmpUtils/publishCmp.js +45 -35
- package/src/config/default.config.js +2 -2
- package/src/module/index.js +55 -7
- package/src/neo/NeoUMDContent.js +29 -0
- package/src/neo/neoService.js +150 -52
- package/src/neo/wrapperContent.js +2 -1
- package/src/plugins/AddNeoRequirePlugin.js +5 -2
- package/src/template/neo-custom-cmp-template/neo.config.js +1 -1
- package/src/template/neo-custom-cmp-template/package.json +1 -1
- package/src/template/neo-custom-cmp-template/src/components/entity-detail/index.tsx +8 -0
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
## Neo 自定义组件开发工具
|
|
2
|
-
neo-cmp-cli 是 Neo 自定义组件开发工具,基于 [AKFun](https://github.com/wibetter/akfun) 的工程能力,提供
|
|
2
|
+
neo-cmp-cli 是 Neo 自定义组件开发工具,基于 [AKFun](https://github.com/wibetter/akfun) 的工程能力,提供 初始化、编译构建、预览调试、热更新、多技术栈支持和发布等功能。
|
|
3
3
|
|
|
4
4
|
### 主要特性
|
|
5
5
|
- **零配置**: 内置默认配置,开箱可用;
|
|
6
6
|
- **多技术栈**: 支持 Vue2、React、React+TypeScript 自定义组件的调试、构建与发布;
|
|
7
|
-
- **多构建场景**: 本地预览(含热更新/代理)、外链调试、库构建(UMD/ESM
|
|
7
|
+
- **多构建场景**: 本地预览(含热更新/代理)、外链调试、库构建(UMD/ESM)、部署&发布;
|
|
8
8
|
- **灵活可配**: 支持 构建入口、别名、代理、SASS 注入、ESLint/StyleLint、Babel/Loader/Plugin 扩展等配置;
|
|
9
9
|
- **样式与规范**: 内置 Autoprefixer、Sass、PostCSS、ESLint、StyleLint;
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- **发布至 CDN**: 内置发布到对象存储(OSS)的能力,支持自定义对象存储配置;
|
|
11
|
+
- **发布至 NeoCRM 平台**: 支持一键发布到NeoCRM 平台的能力,需自行补充授权配置;
|
|
12
12
|
|
|
13
13
|
### 内置的自定义组件模板
|
|
14
14
|
创建自定义组件时(执行初始化命令 neo init)可选用。
|
|
@@ -40,8 +40,8 @@ neo preview
|
|
|
40
40
|
# 外链调试(在平台线上预览与调试)
|
|
41
41
|
neo linkDebug
|
|
42
42
|
|
|
43
|
-
# 构建并发布到
|
|
44
|
-
neo
|
|
43
|
+
# 构建并发布到 NeoCRM(需自行添加授权配置,并确保 package.json 的 name 唯一、version 不重复)
|
|
44
|
+
neo publish2neo
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
### 方法二:在现有业务项目中使用自定义组件开发工具
|
|
@@ -58,7 +58,7 @@ npm i neo-cmp-cli --save-dev
|
|
|
58
58
|
```bash
|
|
59
59
|
"preview": "neo preview",
|
|
60
60
|
"linkDebug": "neo linkDebug",
|
|
61
|
-
"
|
|
61
|
+
"publish2neo": "neo publish2neo"
|
|
62
62
|
```
|
|
63
63
|
##### 3) 初始化配置文件
|
|
64
64
|
```bash
|
|
@@ -76,6 +76,7 @@ npm run publish2oss
|
|
|
76
76
|
- **neo preview**: 本地预览自定义组件内容,默认支持热更新与接口代理。
|
|
77
77
|
- **neo linkDebug**: 外链调试模式,在平台端页面设计器中调试自定义组件。
|
|
78
78
|
- **neo publish2oss**: 构建并上传到对象存储(可自定义配置对象存储)。
|
|
79
|
+
- **neo publish2neo**: 构建并发布到NeoCRM平台(需自行添加授权配置)。
|
|
79
80
|
|
|
80
81
|
## 开发须知
|
|
81
82
|
#### 1)默认自动识别自定义组件
|
|
@@ -128,17 +129,50 @@ neo linkDebug
|
|
|
128
129
|
##### 3. 页面设计器开启 debug 模式后,左侧会展示 外部链接 管理面板
|
|
129
130
|
将第 1 步生成的「外链脚本地址」添加进来,即可在此页面设计器 / 组件物料面板中看到对应自定义组件。
|
|
130
131
|
|
|
131
|
-
#### 6
|
|
132
|
-
执行 `neo
|
|
132
|
+
#### 6)发布自定义组件至 NeoCRM
|
|
133
|
+
执行 `neo publish2neo` 即可构建并发布自定义组件至 NeoCRM 平台,其构建后资源也会上传到 NeoCRM 平台端提供的 CDN 中。
|
|
133
134
|
|
|
134
135
|
##### 发布前请确保
|
|
135
136
|
- **package.json 的 name 唯一**
|
|
136
137
|
- **version 不重复**
|
|
137
|
-
|
|
138
|
+
|
|
139
|
+
##### 需自行添加授权配置
|
|
140
|
+
```javascript
|
|
141
|
+
module.exports = {
|
|
142
|
+
publishCmp: {
|
|
143
|
+
neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
|
|
144
|
+
tokenAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.xiaoshouyi.com/auc/oauth2/token)
|
|
145
|
+
// NeoCRM 授权配置
|
|
146
|
+
authorization: {
|
|
147
|
+
/**
|
|
148
|
+
* 客户端 ID 和 客户端秘钥 需通过 创建连接器 获取,
|
|
149
|
+
* 详细见:https://doc.xiaoshouyi.com / 创建连接器。
|
|
150
|
+
*/
|
|
151
|
+
client_id: 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
152
|
+
client_secret: 'xxx', // 客户端秘钥,从创建连接器的客户端信息中获取(Client_Secret)
|
|
153
|
+
username: 'xx', // 用户在销售易系统中的用户名
|
|
154
|
+
/**
|
|
155
|
+
* password 为 用户在销售易系统中的账号密码加上 8 位安全令牌。
|
|
156
|
+
* 例如,用户密码为 123456,安全令牌为 ABCDEFGH,则 password 的值应为 123456ABCDEFGH。
|
|
157
|
+
* 如何获取 安全令牌请见:https://doc.xiaoshouyi.com / OAuth安全认证 / 密码模式 / 获取令牌。
|
|
158
|
+
*/
|
|
159
|
+
password: 'xx xx' // 用户账户密码 + 8 位安全令牌
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
```
|
|
138
164
|
|
|
139
165
|
##### 支持发布指定自定义组件
|
|
140
166
|
执行 `neo publish2oss --cmpType=xxCmp`
|
|
141
167
|
|
|
168
|
+
#### 7)发布自定义组件至CDN
|
|
169
|
+
执行 `neo publish2oss` 即可构建对应自定义组件,并自动将构建后资源上传到对象存储(OSS)中。
|
|
170
|
+
|
|
171
|
+
##### 发布前请确保
|
|
172
|
+
- **package.json 的 name 唯一**
|
|
173
|
+
- **version 不重复**
|
|
174
|
+
- 可按需配置对象存储参数(支持自定义),默认使用内置对象存储配置。
|
|
175
|
+
|
|
142
176
|
##### 支持自定义对象存储配置
|
|
143
177
|
```javascript
|
|
144
178
|
module.exports = {
|
|
@@ -159,6 +193,9 @@ module.exports = {
|
|
|
159
193
|
}
|
|
160
194
|
```
|
|
161
195
|
|
|
196
|
+
##### 支持发布指定自定义组件
|
|
197
|
+
执行 `neo publish2oss --cmpType=xxCmp`
|
|
198
|
+
|
|
162
199
|
## 项目工程配置说明(neo.config.js)
|
|
163
200
|
neo-cmp-cli 默认提供完整配置;
|
|
164
201
|
如需自定义,使用 `neo config init` 生成 `neo.config.js` 并按需修改。
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { resolveToCurrentRoot } = require('../utils/pathUtils');
|
|
4
|
+
/**
|
|
5
|
+
* 根据当前组件目录,获取所有组件类型
|
|
6
|
+
* @param {*} componentsBaseDir 自定义组件目录
|
|
7
|
+
* @returns 组件类型列表
|
|
8
|
+
*/
|
|
9
|
+
const getCmpTypeByDir = (componentsBaseDir = './src/components') => {
|
|
10
|
+
const componentsDir = resolveToCurrentRoot(componentsBaseDir);
|
|
11
|
+
if (!fs.existsSync(componentsDir)) {
|
|
12
|
+
console.error(`未找到自定义组件目录,请检查 ${componentsDir} 目录是否存在。`);
|
|
13
|
+
// 退出进程
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// 读取组件目录下的所有子目录
|
|
19
|
+
const dirs = fs.readdirSync(componentsDir);
|
|
20
|
+
const cmpTypes = [];
|
|
21
|
+
|
|
22
|
+
// 遍历所有目录,过滤出有效的组件类型
|
|
23
|
+
dirs.forEach((dir) => {
|
|
24
|
+
const dirPath = path.join(componentsDir, dir);
|
|
25
|
+
const stat = fs.statSync(dirPath);
|
|
26
|
+
|
|
27
|
+
// 只处理目录,过滤掉隐藏目录和 node_modules
|
|
28
|
+
if (stat.isDirectory() && !dir.startsWith('.') && dir !== 'node_modules') {
|
|
29
|
+
cmpTypes.push(dir);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return cmpTypes;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('获取组件类型失败(getCmpTypeByDir):', error);
|
|
36
|
+
// 退出进程
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = getCmpTypeByDir;
|
|
@@ -83,9 +83,9 @@ function getFramework(_framework) {
|
|
|
83
83
|
* 构建组件数据映射
|
|
84
84
|
* @param {string} assetsRoot 构建产物的目录
|
|
85
85
|
* @param {object} cmpInfo 自定义组件信息
|
|
86
|
-
* @returns {object} 自定义组件数据(含自定义组件模型信息)
|
|
86
|
+
* @returns {Promise<object|null>} 自定义组件数据(含自定义组件模型信息)
|
|
87
87
|
*/
|
|
88
|
-
const buildComponentData = (assetsRoot, cmpInfo) => {
|
|
88
|
+
const buildComponentData = async (assetsRoot, cmpInfo) => {
|
|
89
89
|
if (!cmpInfo || !cmpInfo.cmpType) {
|
|
90
90
|
console.error('自定义组件信息或组件名称不能为空');
|
|
91
91
|
return null;
|
|
@@ -94,64 +94,67 @@ const buildComponentData = (assetsRoot, cmpInfo) => {
|
|
|
94
94
|
const { cmpType } = cmpInfo;
|
|
95
95
|
|
|
96
96
|
if (!assetsRoot || !fs.existsSync(assetsRoot)) {
|
|
97
|
-
console.error(
|
|
97
|
+
console.error(`未找到自定义组件目录: ${assetsRoot}`);
|
|
98
98
|
return null;
|
|
99
99
|
}
|
|
100
100
|
const widgetName = _.camelCase(cmpType);
|
|
101
101
|
const modelFile = path.join(assetsRoot, `${widgetName}Model.js`);
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
|
|
103
|
+
// 为 Node.js 环境设置全局 window 对象(模型文件可能需要)
|
|
104
|
+
// 使用 globalThis 以确保在 Node.js 和浏览器环境中都能工作
|
|
105
|
+
const originalWindow = globalThis.window;
|
|
106
|
+
if (!globalThis.window) {
|
|
107
|
+
globalThis.window = {
|
|
108
|
+
console: console,
|
|
109
|
+
neoRequire: () => {},
|
|
110
|
+
postMessage: () => {},
|
|
111
|
+
// 可以添加其他常用的 window 属性
|
|
112
|
+
};
|
|
113
|
+
}
|
|
104
114
|
|
|
105
115
|
try {
|
|
106
116
|
// 加载自定义组件模型资源文件
|
|
107
117
|
if (fs.existsSync(modelFile)) {
|
|
108
|
-
|
|
109
|
-
const resolvedPath = require.resolve(modelFile);
|
|
110
|
-
if (require.cache[resolvedPath]) {
|
|
111
|
-
delete require.cache[resolvedPath];
|
|
112
|
-
}
|
|
113
|
-
const modelModule = require(modelFile);
|
|
118
|
+
let modelModule = require(modelFile);
|
|
114
119
|
// 获取导出的模型类(可能是 default 导出或命名导出)
|
|
115
|
-
|
|
116
|
-
// 如果是命名导出,尝试查找类名(例如 EntityCardListModel)
|
|
117
|
-
if (typeof ModelClass !== 'function' && typeof modelModule === 'object') {
|
|
118
|
-
// 查找所有导出的类
|
|
119
|
-
const exportedClasses = Object.values(modelModule).filter(
|
|
120
|
-
(item) => typeof item === 'function'
|
|
121
|
-
);
|
|
122
|
-
if (exportedClasses.length > 0) {
|
|
123
|
-
ModelClass = exportedClasses[0];
|
|
124
|
-
}
|
|
125
|
-
}
|
|
120
|
+
CatchCustomCmpModelClass = modelModule.default || modelModule;
|
|
126
121
|
}
|
|
127
|
-
// 如果资源文件不存在,报错
|
|
128
122
|
else {
|
|
129
123
|
console.error(`未找到自定义组件模型文件,请检查以下路径是否存在:`, modelFile);
|
|
130
124
|
return null;
|
|
131
125
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (typeof ModelClass !== 'function') {
|
|
135
|
-
console.error(`模型文件 ${modelFile} 未导出有效的模型类`);
|
|
126
|
+
if (!window.NEOEditorCustomModels) {
|
|
127
|
+
console.error(`模型文件未导出有效模型方法(CatchCustomCmpModelClass),模型文件地址: ${modelFile} `);
|
|
136
128
|
return null;
|
|
137
129
|
}
|
|
138
130
|
|
|
131
|
+
const ModelClass = window.NEOEditorCustomModels[cmpType];
|
|
132
|
+
if (!ModelClass) {
|
|
133
|
+
console.error(`未找到自定义组件模型类(${cmpType}),模型文件地址: ${modelFile} `);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
139
136
|
// 实例化模型类
|
|
140
137
|
const modelInstance = new ModelClass();
|
|
141
138
|
|
|
139
|
+
if (!modelInstance) {
|
|
140
|
+
console.error(`未找到自定义组件模型信息(${cmpType}),模型文件地址: ${modelFile} `);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
142
144
|
// 构建组件数据,合并模型实例的信息
|
|
143
145
|
const curCmpInfo = {
|
|
144
146
|
...cmpInfo,
|
|
147
|
+
plugin: cmpInfo.modelAsset,
|
|
145
148
|
version: currentPackageJson.version || '1.0.0',
|
|
146
149
|
framework: currentPackageJson.framework ? getFramework(currentPackageJson.framework) : '0', // 0: React, 1: vue2, 2: jQuery, 3: vue3
|
|
147
150
|
// 从模型实例中提取并设置组件信息
|
|
148
151
|
label: modelInstance.label || cmpType,
|
|
149
152
|
description: modelInstance.description || '',
|
|
150
|
-
componentCategory: modelInstance.tags || [],
|
|
153
|
+
componentCategory: (modelInstance.tags || []).join(','),
|
|
151
154
|
icon: modelInstance.iconSrc,
|
|
152
|
-
defaultProps: modelInstance.defaultComProps || {},
|
|
153
|
-
previewProps: modelInstance.previewComProps || {},
|
|
154
|
-
propsSchema: modelInstance.propsSchema || [],
|
|
155
|
+
defaultProps: JSON.stringify(modelInstance.defaultComProps || {}),
|
|
156
|
+
previewProps: JSON.stringify(modelInstance.previewComProps || {}),
|
|
157
|
+
propsSchema: JSON.stringify(modelInstance.propsSchema || []),
|
|
155
158
|
events: modelInstance.events || [],
|
|
156
159
|
actions: modelInstance.actions || [],
|
|
157
160
|
// 如果模型实例中有其他属性,也可以添加
|
|
@@ -160,11 +163,19 @@ const buildComponentData = (assetsRoot, cmpInfo) => {
|
|
|
160
163
|
enableDuplicate: modelInstance.enableDuplicate !== undefined ? modelInstance.enableDuplicate : true
|
|
161
164
|
};
|
|
162
165
|
|
|
166
|
+
console.log(`自定义组件模型信息(${cmpType}):`, curCmpInfo);
|
|
163
167
|
return curCmpInfo;
|
|
164
168
|
} catch (error) {
|
|
165
169
|
console.error(`自定义组件模型文件解析失败 (${modelFile || '未知路径'}):`, error.message);
|
|
166
170
|
console.error(error.stack);
|
|
167
171
|
return null;
|
|
172
|
+
} finally {
|
|
173
|
+
// 恢复原始的 window 对象(如果之前存在)
|
|
174
|
+
if (originalWindow === undefined) {
|
|
175
|
+
delete globalThis.window;
|
|
176
|
+
} else {
|
|
177
|
+
globalThis.window = originalWindow;
|
|
178
|
+
}
|
|
168
179
|
}
|
|
169
180
|
};
|
|
170
181
|
|
|
@@ -202,9 +213,9 @@ const publishCmp = async (config, cmpType) => {
|
|
|
202
213
|
|
|
203
214
|
// 步骤 4: 构建组件数据
|
|
204
215
|
spinner.text = '发布自定义组件:构建组件数据...';
|
|
205
|
-
const componentInfo = buildComponentData(config.assetsRoot, cmpInfo);
|
|
216
|
+
const componentInfo = await buildComponentData(config.assetsRoot, cmpInfo);
|
|
206
217
|
if (!componentInfo) {
|
|
207
|
-
throw new Error(
|
|
218
|
+
throw new Error(`构建组件数据失败,未获取到自定义组件模型信息。(${cmpType})`);
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
// 步骤 5: 保存组件信息
|
|
@@ -218,5 +229,4 @@ const publishCmp = async (config, cmpType) => {
|
|
|
218
229
|
}
|
|
219
230
|
};
|
|
220
231
|
|
|
221
|
-
module.exports = publishCmp;
|
|
222
|
-
|
|
232
|
+
module.exports = publishCmp;
|
|
@@ -34,8 +34,8 @@ const defaultNEOConfig = {
|
|
|
34
34
|
template: resolveByDirname('../initData/defaultTemplate.html'), // 默认使用neo-widget提供的页面模板(会启动页面设计器)
|
|
35
35
|
sassResources: [],
|
|
36
36
|
babelPlugins: [
|
|
37
|
-
['import', { libraryName: 'antd', style: 'css' }]
|
|
38
|
-
]
|
|
37
|
+
['import', { libraryName: 'antd', style: 'css' }] // 配置 antd 的样式按需引入
|
|
38
|
+
]
|
|
39
39
|
},
|
|
40
40
|
envParams: {
|
|
41
41
|
// 项目系统环境变量
|
package/src/module/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const inspect = require('./inspect.js'); // 输出当前项目配置文件
|
|
|
9
9
|
const neoConfigInit = require('../utils/neoConfigInit.js');
|
|
10
10
|
const { validateProjectName } = require('../utils/projectNameValidator.js');
|
|
11
11
|
const mainAction = require('./main.js'); // 入口文件
|
|
12
|
+
const getCmpTypeByDir = require('../cmpUtils/getCmpTypeByDir.js');
|
|
12
13
|
|
|
13
14
|
// neo 的 package 文件
|
|
14
15
|
const neoPackage = require('../../package.json');
|
|
@@ -168,16 +169,26 @@ yargs
|
|
|
168
169
|
if (argv.cmpType) {
|
|
169
170
|
mainAction.previewCmp(argv.cmpType);
|
|
170
171
|
} else {
|
|
172
|
+
const cmpTypes = getCmpTypeByDir();
|
|
173
|
+
if (!cmpTypes.lenth || cmpTypes.length === 0) {
|
|
174
|
+
console.error('当前自定义组件目录中未找到自定义组件。(./src/components 目录下)');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
const cmpTypeChoices = cmpTypes.map((cmpType) => ({
|
|
178
|
+
name: cmpType,
|
|
179
|
+
value: cmpType
|
|
180
|
+
}));
|
|
171
181
|
const questions = [
|
|
172
182
|
{
|
|
173
183
|
name: 'cmpType',
|
|
174
|
-
type: '
|
|
175
|
-
message: '
|
|
184
|
+
type: 'list',
|
|
185
|
+
message: '请选择要预览的自定义组件:',
|
|
186
|
+
choices: cmpTypeChoices
|
|
176
187
|
}
|
|
177
188
|
];
|
|
178
189
|
inquirer.prompt(questions).then((ans) => {
|
|
179
190
|
if (!ans.cmpType) {
|
|
180
|
-
console.error('
|
|
191
|
+
console.error('未选择要预览的自定义组件。');
|
|
181
192
|
process.exit(1);
|
|
182
193
|
}
|
|
183
194
|
mainAction.previewCmp(ans.cmpType);
|
|
@@ -251,7 +262,34 @@ yargs
|
|
|
251
262
|
.alias('h', 'help');
|
|
252
263
|
},
|
|
253
264
|
(argv) => {
|
|
254
|
-
|
|
265
|
+
if (argv.cmpType) {
|
|
266
|
+
mainAction.publish2oss(argv.cmpType); // 构建并发布脚本到oss
|
|
267
|
+
} else {
|
|
268
|
+
const cmpTypes = getCmpTypeByDir();
|
|
269
|
+
if (!cmpTypes.lenth || cmpTypes.length === 0) {
|
|
270
|
+
console.error('当前自定义组件目录中未找到自定义组件。(./src/components 目录下)');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const cmpTypeChoices = cmpTypes.map((cmpType) => ({
|
|
274
|
+
name: cmpType,
|
|
275
|
+
value: cmpType
|
|
276
|
+
}));
|
|
277
|
+
const questions = [
|
|
278
|
+
{
|
|
279
|
+
name: 'cmpType',
|
|
280
|
+
type: 'list',
|
|
281
|
+
message: '请选择要发布的自定义组件:',
|
|
282
|
+
choices: cmpTypeChoices
|
|
283
|
+
}
|
|
284
|
+
];
|
|
285
|
+
inquirer.prompt(questions).then((ans) => {
|
|
286
|
+
if (!ans.cmpType) {
|
|
287
|
+
console.error('未选择要发布的自定义组件。');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
mainAction.publish2oss(ans.cmpType);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
255
293
|
}
|
|
256
294
|
)
|
|
257
295
|
.command(
|
|
@@ -271,16 +309,26 @@ yargs
|
|
|
271
309
|
if (argv.cmpType) {
|
|
272
310
|
mainAction.publishCmp(argv.cmpType); // 构建并发布组件到 NeoCRM
|
|
273
311
|
} else {
|
|
312
|
+
const cmpTypes = getCmpTypeByDir();
|
|
313
|
+
if (!cmpTypes.lenth || cmpTypes.length === 0) {
|
|
314
|
+
console.error('当前自定义组件目录中未找到自定义组件。(./src/components 目录下)');
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const cmpTypeChoices = cmpTypes.map((cmpType) => ({
|
|
318
|
+
name: cmpType,
|
|
319
|
+
value: cmpType
|
|
320
|
+
}));
|
|
274
321
|
const questions = [
|
|
275
322
|
{
|
|
276
323
|
name: 'cmpType',
|
|
277
|
-
type: '
|
|
278
|
-
message: '
|
|
324
|
+
type: 'list',
|
|
325
|
+
message: '请选择要发布的自定义组件:',
|
|
326
|
+
choices: cmpTypeChoices
|
|
279
327
|
}
|
|
280
328
|
];
|
|
281
329
|
inquirer.prompt(questions).then((ans) => {
|
|
282
330
|
if (!ans.cmpType) {
|
|
283
|
-
console.error('
|
|
331
|
+
console.error('未选择要发布的自定义组件。');
|
|
284
332
|
process.exit(1);
|
|
285
333
|
}
|
|
286
334
|
mainAction.publishCmp(ans.cmpType);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置 UMD 模块,用于在 Node cli 端加载自定义组件模型
|
|
3
|
+
* 特别说明:这个文件内容会通过 AddNeoRequirePlugin 插入的构建代码中。
|
|
4
|
+
*/
|
|
5
|
+
(function (root, factory) {
|
|
6
|
+
// AMD 环境检测
|
|
7
|
+
if (typeof define === 'function' && define.amd) {
|
|
8
|
+
define([], factory);
|
|
9
|
+
}
|
|
10
|
+
// CommonJS 环境检测
|
|
11
|
+
else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
|
|
12
|
+
module.exports = factory();
|
|
13
|
+
}
|
|
14
|
+
// 浏览器全局变量
|
|
15
|
+
else {
|
|
16
|
+
root.NeoCustomCmpModel = factory();
|
|
17
|
+
}
|
|
18
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const getCustomModels = (cmpType) => {
|
|
22
|
+
// 自定义组件模型列表
|
|
23
|
+
const NEOEditorCustomModels = window.NEOEditorCustomModels || {};
|
|
24
|
+
return NEOEditorCustomModels[cmpType] || {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 导出模块
|
|
28
|
+
return getCustomModels;
|
|
29
|
+
}));
|
package/src/neo/neoService.js
CHANGED
|
@@ -8,9 +8,9 @@ const updatePublishLog = require('../cmpUtils/updatePublishLog');
|
|
|
8
8
|
// NeoCRM 平台默认 API 配置
|
|
9
9
|
const NeoCrmAPI = {
|
|
10
10
|
neoBaseURL: 'https://crm.xiaoshouyi.com', // 平台根地址
|
|
11
|
-
tokenAPI: 'https://login.
|
|
11
|
+
tokenAPI: 'https://login.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址
|
|
12
12
|
uploadAPI: '/rest/metadata/v3.0/ui/customComponents/actions/upload', // 文件上传接口地址
|
|
13
|
-
saveAPI: '/rest/metadata/v3.0/ui/customComponents/actions/
|
|
13
|
+
saveAPI: '/rest/metadata/v3.0/ui/customComponents/actions/saveOrUpdateComponent' // 创建或者保存接口地址
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -101,29 +101,29 @@ class NeoService {
|
|
|
101
101
|
console.info('使用缓存的 token');
|
|
102
102
|
return this.tokenCache.token;
|
|
103
103
|
}
|
|
104
|
+
console.info('获取 token...');
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
// 构建表单数据格式的请求参数
|
|
107
|
+
const formData = new URLSearchParams();
|
|
108
|
+
formData.append('grant_type', 'password');
|
|
109
|
+
formData.append('client_id', this.authorization.client_id);
|
|
110
|
+
formData.append('client_secret', this.authorization.client_secret);
|
|
111
|
+
formData.append('username', this.authorization.username);
|
|
112
|
+
formData.append('password', this.authorization.password);
|
|
113
|
+
|
|
114
|
+
const tokenUrl = this.buildFullUrl(this.tokenAPI);
|
|
106
115
|
|
|
107
116
|
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, {
|
|
117
|
+
const response = await axios.post(tokenUrl, formData.toString(), {
|
|
118
118
|
headers: {
|
|
119
119
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
const { access_token, expires_in } = response.data;
|
|
123
|
+
const { access_token, expires_in } = response.data || {};
|
|
124
124
|
|
|
125
125
|
if (!access_token) {
|
|
126
|
-
console.error('获取 token 失败:响应中未包含 access_token', response.data);
|
|
126
|
+
console.error('\n获取 token 失败:响应中未包含 access_token', response.data);
|
|
127
127
|
process.exit(1);
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -133,11 +133,11 @@ class NeoService {
|
|
|
133
133
|
token: access_token,
|
|
134
134
|
expiresAt: Date.now() + (expiresIn - 60) * 1000
|
|
135
135
|
};
|
|
136
|
-
|
|
137
|
-
console.info('token 获取成功');
|
|
138
136
|
return access_token;
|
|
139
137
|
} catch (error) {
|
|
140
|
-
console.error('获取 token 失败:', error.message);
|
|
138
|
+
console.error('\n获取 token 失败:', error.message);
|
|
139
|
+
console.error('\ntoken 授权地址:', tokenUrl);
|
|
140
|
+
console.error('\ntoken 请求参数:', formData);
|
|
141
141
|
if (error.response) {
|
|
142
142
|
console.error('响应数据:', error.response.data);
|
|
143
143
|
}
|
|
@@ -177,59 +177,155 @@ class NeoService {
|
|
|
177
177
|
* 上传文件到 Neo 平台
|
|
178
178
|
* @param {string} filePath 文件路径
|
|
179
179
|
* @param {object} options 可选配置
|
|
180
|
-
* @param {string} options.fieldName 表单字段名,默认为 '
|
|
180
|
+
* @param {string} options.fieldName 表单字段名,默认为 'customComponentCode'
|
|
181
|
+
* @param {number} options.maxSize 最大文件大小(字节),默认 50MB
|
|
182
|
+
* @param {number} options.timeout 超时时间(毫秒),默认 60000
|
|
181
183
|
* @returns {Promise<string>} CDN 地址或文件 URL
|
|
182
184
|
*/
|
|
183
185
|
async uploadFile(filePath, options = {}) {
|
|
184
186
|
// 确保 token 有效
|
|
185
187
|
const token = await this.ensureValidToken();
|
|
186
188
|
|
|
189
|
+
// 验证文件路径
|
|
190
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
191
|
+
throw new Error(`文件路径无效: ${filePath}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 检查文件是否存在
|
|
187
195
|
if (!fs.existsSync(filePath)) {
|
|
188
196
|
throw new Error(`文件不存在: ${filePath}`);
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
|
|
199
|
+
// 检查文件状态
|
|
200
|
+
const fileStat = fs.statSync(filePath);
|
|
201
|
+
if (!fileStat.isFile()) {
|
|
202
|
+
throw new Error(`路径不是文件: ${filePath}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 检查文件大小
|
|
206
|
+
const maxSize = options.maxSize || 50 * 1024 * 1024; // 默认 50MB
|
|
207
|
+
if (fileStat.size > maxSize) {
|
|
208
|
+
const sizeMB = (fileStat.size / 1024 / 1024).toFixed(2);
|
|
209
|
+
const maxSizeMB = (maxSize / 1024 / 1024).toFixed(2);
|
|
210
|
+
throw new Error(`文件大小超过限制: ${sizeMB}MB > ${maxSizeMB}MB`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (fileStat.size === 0) {
|
|
214
|
+
throw new Error(`文件为空: ${filePath}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const fileName = path.basename(filePath);
|
|
218
|
+
const fileSizeKB = (fileStat.size / 1024).toFixed(2);
|
|
219
|
+
console.info(`正在上传文件: ${fileName} (${fileSizeKB}KB)...`);
|
|
192
220
|
|
|
193
221
|
try {
|
|
222
|
+
// 创建 FormData
|
|
194
223
|
const formData = new FormData();
|
|
195
224
|
const fieldName = options.fieldName || 'customComponentCode';
|
|
196
|
-
|
|
225
|
+
|
|
226
|
+
// 使用文件流而不是读取整个文件到内存(对大文件更友好)
|
|
227
|
+
const fileContent = fs.createReadStream(filePath);
|
|
228
|
+
|
|
229
|
+
// 追加文件到 FormData,第三个参数指定文件名
|
|
230
|
+
formData.append(fieldName, fileContent, fileName);
|
|
197
231
|
|
|
232
|
+
// 构建完整的上传 API 地址
|
|
198
233
|
const fullUploadAPI = this.uploadAPI();
|
|
199
|
-
|
|
234
|
+
|
|
235
|
+
// 配置请求选项
|
|
236
|
+
const timeout = options.timeout || 60000; // 默认 60 秒
|
|
237
|
+
const requestConfig = {
|
|
200
238
|
headers: {
|
|
201
239
|
Authorization: `Bearer ${token}`,
|
|
240
|
+
'xsy-inner-source': 'bff',
|
|
241
|
+
// 无需手动设置 Content-Type,formData.getHeaders() 会自动设置正确的 multipart/form-data 和 boundary
|
|
202
242
|
...formData.getHeaders()
|
|
203
|
-
}
|
|
204
|
-
|
|
243
|
+
},
|
|
244
|
+
timeout,
|
|
245
|
+
// 确保 axios 正确处理大文件和流
|
|
246
|
+
maxContentLength: Infinity, // 不限制响应内容长度
|
|
247
|
+
maxBodyLength: Infinity // 不限制请求体长度(适用于文件上传)
|
|
248
|
+
};
|
|
205
249
|
|
|
206
|
-
//
|
|
250
|
+
// 发送上传请求
|
|
251
|
+
const response = await axios.post(fullUploadAPI, formData, requestConfig);
|
|
252
|
+
|
|
253
|
+
// 处理响应数据
|
|
207
254
|
let resultData;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
resultData = response.data.data;
|
|
214
|
-
} else {
|
|
215
|
-
resultData = response.data;
|
|
255
|
+
const responseData = response.data;
|
|
256
|
+
|
|
257
|
+
// 检查响应状态码
|
|
258
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
259
|
+
throw new Error(`上传失败: HTTP ${response.status}`);
|
|
216
260
|
}
|
|
217
261
|
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
// 处理不同的响应格式
|
|
263
|
+
if (typeof responseData === 'string') {
|
|
264
|
+
// 如果响应是字符串,直接使用
|
|
265
|
+
resultData = responseData.trim();
|
|
266
|
+
} else if (responseData && typeof responseData === 'object') {
|
|
267
|
+
// 检查是否有错误码
|
|
268
|
+
if (responseData.code !== undefined && responseData.code !== 200 && responseData.code !== 0) {
|
|
269
|
+
const errorMsg = responseData.message || responseData.msg || '未知错误';
|
|
270
|
+
throw new Error(`上传失败: ${errorMsg} (code: ${responseData.code})`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 提取数据
|
|
274
|
+
if (responseData.data !== undefined) {
|
|
275
|
+
resultData = responseData.data;
|
|
276
|
+
} else if (responseData.url !== undefined) {
|
|
277
|
+
resultData = responseData.url;
|
|
278
|
+
} else if (responseData.fileUrl !== undefined) {
|
|
279
|
+
resultData = responseData.fileUrl;
|
|
280
|
+
} else {
|
|
281
|
+
resultData = responseData;
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error(`响应数据格式不正确: ${typeof responseData}`);
|
|
220
285
|
}
|
|
221
286
|
|
|
222
|
-
|
|
223
|
-
|
|
287
|
+
// 验证返回的文件地址
|
|
288
|
+
if (!resultData) {
|
|
289
|
+
throw new Error(`返回的文件地址为空`);
|
|
224
290
|
}
|
|
225
291
|
|
|
226
|
-
|
|
292
|
+
// 格式化文件 URL
|
|
293
|
+
let fileUrl;
|
|
294
|
+
if (typeof resultData === 'string') {
|
|
295
|
+
fileUrl = resultData;
|
|
296
|
+
} else if (resultData && typeof resultData === 'object' && resultData.url) {
|
|
297
|
+
fileUrl = resultData.url;
|
|
298
|
+
}
|
|
299
|
+
console.info(`\n文件上传成功: ${fileName} -> ${fileUrl}`);
|
|
227
300
|
return fileUrl;
|
|
228
301
|
} catch (error) {
|
|
229
|
-
console.error(
|
|
302
|
+
console.error(`\n上传文件失败: ${error.message},`);
|
|
303
|
+
console.error(`文件路径: ${filePath}。\n`);
|
|
304
|
+
|
|
305
|
+
// 输出详细的错误信息
|
|
230
306
|
if (error.response) {
|
|
231
|
-
|
|
307
|
+
const status = error.response.status;
|
|
308
|
+
const statusText = error.response.statusText;
|
|
309
|
+
const responseData = error.response.data;
|
|
310
|
+
const requestUrl = error.config?.url || this.uploadAPI();
|
|
311
|
+
|
|
312
|
+
console.error(`\n========== 上传请求详情 ==========`);
|
|
313
|
+
console.error(`请求 URL: ${requestUrl}`);
|
|
314
|
+
console.error(`HTTP 状态码: ${status} ${statusText}`);
|
|
315
|
+
console.error(`响应数据:`, responseData);
|
|
316
|
+
console.error(`==================================\n`);
|
|
317
|
+
|
|
318
|
+
if (status === 404) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`上传 API 不存在 (404): ${requestUrl}\n` +
|
|
321
|
+
`请检查 neo.config.js 中的 neoBaseURL 配置是否正确,或者 API 路径是否存在。\n` +
|
|
322
|
+
`当前配置的 API 路径: ${NeoCrmAPI.uploadAPI}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
} else if (error.request) {
|
|
326
|
+
console.error('请求已发送但未收到响应,请检查网络连接或代理配置。');
|
|
232
327
|
}
|
|
328
|
+
|
|
233
329
|
throw error;
|
|
234
330
|
}
|
|
235
331
|
}
|
|
@@ -274,7 +370,7 @@ class NeoService {
|
|
|
274
370
|
try {
|
|
275
371
|
// 上传文件
|
|
276
372
|
const fileUrl = await this.uploadFile(filePath);
|
|
277
|
-
|
|
373
|
+
|
|
278
374
|
if (file.indexOf('Model') > -1) {
|
|
279
375
|
curCmpInfo.modelAsset = fileUrl;
|
|
280
376
|
} else if (file.endsWith('.css')) {
|
|
@@ -282,16 +378,15 @@ class NeoService {
|
|
|
282
378
|
} else {
|
|
283
379
|
curCmpInfo.asset = fileUrl;
|
|
284
380
|
}
|
|
285
|
-
|
|
286
381
|
} catch (error) {
|
|
287
|
-
console.error(`文件上传失败(${file}):\n
|
|
382
|
+
console.error(`文件上传失败(${file}):\n`);
|
|
288
383
|
process.exit(1);
|
|
289
384
|
}
|
|
290
385
|
}
|
|
291
386
|
});
|
|
292
387
|
|
|
293
388
|
await Promise.all(uploadPromises);
|
|
294
|
-
|
|
389
|
+
|
|
295
390
|
if (curCmpInfo && curCmpInfo.cmpType) {
|
|
296
391
|
console.info('上传至 OSS 的文件信息:\n', curCmpInfo);
|
|
297
392
|
// 更新发布日志
|
|
@@ -302,7 +397,6 @@ class NeoService {
|
|
|
302
397
|
|
|
303
398
|
/**
|
|
304
399
|
* 更新自定义组件
|
|
305
|
-
* @param {string} updateAPI 更新接口地址(相对或绝对路径)
|
|
306
400
|
* @param {object} componentData 组件数据
|
|
307
401
|
* @returns {Promise<object>} 更新结果
|
|
308
402
|
*/
|
|
@@ -317,24 +411,27 @@ class NeoService {
|
|
|
317
411
|
console.info('正在更新自定义组件...');
|
|
318
412
|
|
|
319
413
|
try {
|
|
320
|
-
const fullUpdateAPI = this.
|
|
414
|
+
const fullUpdateAPI = this.saveAPI();
|
|
321
415
|
const response = await axios.post(fullUpdateAPI, componentData, {
|
|
322
416
|
headers: {
|
|
323
417
|
Authorization: `Bearer ${token}`,
|
|
418
|
+
'xsy-inner-source': 'bff',
|
|
324
419
|
'Content-Type': 'application/json'
|
|
325
420
|
}
|
|
326
421
|
});
|
|
422
|
+
const {code, message} = response.data || {};
|
|
327
423
|
|
|
328
|
-
if (
|
|
424
|
+
if (code && code !== 200) {
|
|
329
425
|
throw new Error(`更新组件失败: ${response.data.message || '未知错误'}`);
|
|
330
426
|
}
|
|
427
|
+
|
|
331
428
|
|
|
332
|
-
console.info('
|
|
333
|
-
return response.data;
|
|
429
|
+
console.info(message ? `组件更新成功: ${message}。` : '组件更新成功。');
|
|
334
430
|
} catch (error) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
431
|
+
if (error.message) {
|
|
432
|
+
console.error('更新组件失败:', error.message);
|
|
433
|
+
} else {
|
|
434
|
+
console.error('响应数据:', error);
|
|
338
435
|
}
|
|
339
436
|
process.exit(1);
|
|
340
437
|
}
|
|
@@ -365,6 +462,7 @@ class NeoService {
|
|
|
365
462
|
params,
|
|
366
463
|
headers: {
|
|
367
464
|
Authorization: `Bearer ${token}`,
|
|
465
|
+
'xsy-inner-source': 'bff',
|
|
368
466
|
...headers
|
|
369
467
|
}
|
|
370
468
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 注入 neoRequire 函数
|
|
3
3
|
* 备注:用于实现和 Neo 平台共享依赖
|
|
4
|
+
* 特别说明:这个文件只是用于说明 AddNeoRequirePlugin 中会插入的代码内容,实际内容内置在 AddNeoRequirePlugin 方法中。
|
|
4
5
|
*/
|
|
5
6
|
(function(NeoCustomCmpFileFactory) {
|
|
6
7
|
if (!window.neoRequire) {
|
|
@@ -12,4 +13,4 @@
|
|
|
12
13
|
* 这里放自定义组件相关内容代码
|
|
13
14
|
* 备注: 自定义组件代码中的 require 函数 已被替换成 neoRequire 函数(require === neoRequire)。
|
|
14
15
|
*/
|
|
15
|
-
});
|
|
16
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { ConcatSource } = require('webpack-sources');
|
|
2
|
-
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
3
4
|
/**
|
|
4
5
|
* 注入 neoRequire 函数
|
|
5
6
|
* 备注:用于实现和 Neo 平台共享依赖
|
|
@@ -163,7 +164,9 @@ class AddNeoRequirePlugin {
|
|
|
163
164
|
})(function(require) {
|
|
164
165
|
`;
|
|
165
166
|
|
|
166
|
-
const
|
|
167
|
+
// const NeoUMDContent = fs.readFileSync(path.join(__dirname, '../neo/NeoUMDContent.js'), 'utf8');
|
|
168
|
+
// const Footer = `}); \n${NeoUMDContent}`;
|
|
169
|
+
const Footer = `});`;
|
|
167
170
|
|
|
168
171
|
// 创建新的资源
|
|
169
172
|
const newSource = new ConcatSource(Header, content, Footer);
|
|
@@ -129,7 +129,7 @@ module.exports = {
|
|
|
129
129
|
publishCmp: {
|
|
130
130
|
// 用于构建并发布至 NeoCRM 的相关配置
|
|
131
131
|
neoBaseURL: 'https://crm-cd.xiaoshouyi.com', // 平台根地址(默认:https://crm.xiaoshouyi.com)
|
|
132
|
-
tokenAPI: 'https://login
|
|
132
|
+
tokenAPI: 'https://login-cd.xiaoshouyi.com/auc/oauth2/token', // Token 获取接口地址(默认:https://login.xiaoshouyi.com/auc/oauth2/token)
|
|
133
133
|
// NeoCRM 授权配置
|
|
134
134
|
authorization: {
|
|
135
135
|
client_id: authConfig.client_id || 'xx', // 客户端 ID,从创建连接器的客户端信息中获取(Client_Id)
|
|
@@ -278,6 +278,14 @@ export default class EntityDetail extends React.PureComponent<
|
|
|
278
278
|
|
|
279
279
|
return (
|
|
280
280
|
<div className="entity-detail-container">
|
|
281
|
+
<img
|
|
282
|
+
src="https://custom-widget2.oss-cn-beijing.aliyuncs.com/img.png"
|
|
283
|
+
alt="logo"
|
|
284
|
+
/>
|
|
285
|
+
<img
|
|
286
|
+
src="https://publicfront-1253467224.cos.ap-beijing.myqcloud.com/customComponent/crm-cd/tenant/3392331536784858/1761882606360/img.png"
|
|
287
|
+
alt="logo"
|
|
288
|
+
/>
|
|
281
289
|
{showTitle && (
|
|
282
290
|
<div className="detail-header">
|
|
283
291
|
<div className="header-content">
|