1.1 前提条件#
1.2 应用场景示例——在列表页无缝创建生产单#
想象一下,您正在系统核心页面(如生产单列表)中进行操作。此时,一个紧急的新建任务需求出现,您是否必须中断当前工作,去复杂的导航中寻找创建入口?新核云Web Widget提供了完美的解决方案。通过 Top Bar 插件,您可以在任何页面的顶部导航栏嵌入一个轻量化的功能入口。例如,您可以将“快速建单”功能封装为一个 Widget,当用户点击时,一个创建面板会直接在当前页面弹出,用户完成操作后,面板关闭,用户仍停留在原页面,实现真正的 “无感操作” 和 “流程无缝融合”。维持上下文:用户无需离开当前页面,即可完成额外操作。
极致聚焦:为单一功能提供最简化的操作界面,排除所有无关信息干扰。
提升效率:将多步骤、跨页面的操作缩短为“点击-输入-提交”三步。
开发者价值:
您无需从零开始构建权限认证和页面布局。只需使用我们的 Widget 模板,开发一个简单的表单组件,并通过几行配置将其挂载到 Top Bar,即可为您的用户提供流畅的体验。 1.3 快速开始(Quickstart)#
以下是快速实现一个简单 Web Widget 的构建步骤:1
克隆项目模板
git clone https://gitee.com/newcore2014/newcore-widget-template.git
2
安装依赖并启动开发环境
# 安装依赖
pnpm install
# 启动开发服务器(含热更新)
pnpm run dev
pnpm run dev命令启用了热模块替换(HMR),您对代码的修改将立即生效而无需手动刷新页面,极大提升开发体验。
3
配置应用清单
项目模板中的原始 manifest.json 文件可以在快速开始中不经修改直接使用
若需要修改配置,打开/src/manifest.json文件进行编辑(参考3.1 manifest.json配置指南)
修改 name 为你的应用名(如 "Hello World Widget")
修改 author.name 为你自己的名字或团队名。
检查 location.topBar 的尺寸是否合适。 4
修改代码(实时生效)
打开 /src/app/locations/TopBarPlugin.tsx 目录中的文件进行编辑。
修改组件代码(例如 TopBarPlugin.tsx):
<span style=
{{
padding: '4px',
backgroundColor: 'green',
color: '#fff',
borderRadius: '8px'
}}>
{t('app.hello xhy')}
</span>
5
构建生产版本
产物生成在/dist目录, 将/dist目录压缩 zip 文件如:dist.zip
将dist.zip上传至新核云插件平台(参考4.2 上传应用) 并开启应用状态(如已开启请忽略) 2. 核心架构与概念#
2.1 项目结构详解#
以下是
项目模板的典型结构:
newcore-widget-template/
├── doc/ # 文档目录
│ └── i18n.md # 国际化文档
├── rollup/ # Rollup插件目录
│ ├── modifiers/ # 修改器
│ │ ├── manifest.ts # 清单插件
│ │ └── translations.ts # 翻译插件
│ ├── static-copy-plugin.ts # 静态资源复制插件
│ └── translations-loader-plugin.ts # 翻译加载插件
├── spec/ # 测试目录
│ ├── app.test.jsx # 应用测试
│ └── i18n.test.js # 国际化测试
├── src/ # 源代码目录
│ ├── app/ # 应用代码
│ │ ├── hooks/ # 自定义Hook
│ │ │ └──useI18n.tsx # 国际化Hook
│ │ ├── locations/ # 位置组件
│ │ │ └── BackgroundPlugin.tsx # 后台代码插件
│ │ │ └── NavBarPlugin.tsx # 菜单栏插件
│ │ │ └── SideBarPlugin.tsx # 侧边栏插件
│ │ │ └── TopBarPlugin.tsx # 顶部栏插件
│ │ types/ # 数据类型定义
│ │ │ └──global.ts # 全局参数类型定义
│ │ ├── app-store.ts # 主应用状态管理
│ │ ├── App.tsx # 主应用组件
│ │ ├── hero.ts # HeroUI(https://www.heroui.com) 组件声明
│ │ ├── index.css # 全局样式
│ │ └── index.tsx # 应用入口
│ ├── assets/ # 静态资源
│ │ ├── logo-small.png
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ └── robot_logo_default.png
│ ├── translations/ # 国际化文件
│ │ ├── en-US.json
│ │ └── zh-CN.json
│ ├── i18n.ts # 国际化配置
│ ├── index.html # HTML模板
│ ├── manifest.json # 应用清单
│ └── vite-env.d.ts # Vite类型定义
├── .env.development # 开发环境变量配置
├── .env.production # 生产环境变量配置
├── .eslintrc # ESLint 代码检查配置
├── .gitignore # Git 忽略规则配置
├── .npmrc # npm 配置
├── .prettierignore # Prettier 忽略规则配置
├── .prettierrc # Prettier 代码格式化配置
├── LICENSE # Apache 2.0 许可证文件
├── README.md # 项目说明文档
├── custom.d.ts # 自定义 TypeScript 类型声明
├── package.json # 项目依赖和脚本配置
├── pnpm-lock.yaml # pnpm 依赖锁文件
├── react-app-env.d.ts # React 应用环境类型声明
├── tsconfig.json # TypeScript 编译配置
├── types.d.ts # 全局类型定义文件
└── vite.config.ts # Vite 配置文件
2.2 理解 SDK (@newcoretech/widget-react)#
核心交互功能#
| 功能模块 | 作用描述 | 使用场景示例 |
|---|
| 数据操作 | 安全的平台数据读写 | 获取工单数据、提交表单 |
| 事件系统 | 跨组件/应用通信 | 通知其他组件数据更新 |
| 跨域请求 | 代理三方 api 请求 | 请求三方服务系统数据 |
SDK 架构设计#
典型工作流#
SDK 使用详见 README#
3. 详细配置与开发指南#
3.1 manifest.json 配置指南#
{
"name": "应用名称",
"author": {
"name": "作者名称"
},
"location": {
"topBar": {
"size": {
"width": "220px",
"height": "100px"
}
},
"navBar": {
},
"sideBar": {
"paths": ["/workOrder"],
"size": {
"width": "__px",
"height": "__px"
}
},
"background": {
"paths": ["/workOrder"],
}
},
"version": "应用版本",
"frameworkVersion": "框架版本要求"
}
| 字段 | 类型 | 必填 | 说明 |
|---|
| name | string | 是 | 应用名称 |
| author | object | 是 | 应用作者信息 |
| author.name | string | 是 | 作者名称 |
| location | object | 是 | 应用显示位置配置 |
| location.topBar | object | 否 | 顶部栏位置配置 |
| location.navBar | object | 否 | 导航栏位置配置 |
| location.sideBar | object | 否 | 侧边栏位置配置 |
| location.background | object | 否 | 后台配置 |
| paths | array | 否 | widget可部署页面的路由地址 |
| size | object | 是 | 容器尺寸配置 |
| size.width | string | 是 | 容器宽度(如"220px") |
| size.height | string | 是 | 容器高度(如"100px") |
| version | string | 是 | 应用版本号 |
| frameworkVersion | string | 是 | 所需框架最低版本 |
3.2 Location组件开发#
3.2.1 组件代码逻辑#
i. 核心功能架构#
const XINHE_CONFIG = {
appkey: 'cbd489e2-c888-4477-95a4-4b0d4b2ff935',
appsecret: 'Zu.1I1&+q7q7<0uC',
tokenUrl: 'https://c2.xinheyun.com/api/open/v2/token',
refreshUrl: 'https://c2.xinheyun.com/api/open/v2/token'
};
// Token管理类(封装认证逻辑)
class TokenManager {
// Token获取、刷新和清理实现
static async getAccessToken(): Promise<string> { /* ... */ }
}
// 生产单快速创建组件
const QuickOrderPage: React.FC = () => {
// 国际化支持
const { t } = useI18n();
// 全局参数获取
const { globalParams } = useGlobalParams();
// Ant Design消息组件
const { message } = App.useApp();
// 表单控制
const [form] = Form.useForm<QuickOrderForm>();
// 状态管理
const [materials, setMaterials] = useState<Material[]>([]);
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
// 搜索与分页状态
const [searchKeyword, setSearchKeyword] = useState<string>('');
const [currentPage, setCurrentPage] = useState<number>(0);
const [hasMore, setHasMore] = useState<boolean>(true);
// API交互方法
const fetchMaterials = async () => { /* ... */ };
const handleSubmit = async (values: QuickOrderForm) => { /* ... */ };
// UI渲染
return ( /* ... */ );
};
认证层:TokenManager类封装OAuth2.0认证流程
业务层:fetchMaterials和handleSubmit实现核心业务逻辑
ii. 核心功能实现#
// 物料获取逻辑
const fetchMaterials = async (page: number = 0, keyword: string = '', append: boolean = false) => {
setLoading(true);
try {
// 获取access_token
const accessToken = await TokenManager.getAccessToken();
// API调用(带认证头)
const response = await request.post('https://c2.xinheyun.com/api/open/v3/items/page/query', {
body: { /* 查询参数 */ }
}, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// 数据处理与状态更新
setMaterials(/* ... */);
} catch (error) {
// 错误处理与降级方案
if (error.response?.status === 401) {
TokenManager.clearTokens(); // 认证失败清理token
}
// 使用模拟数据降级
setMaterials(/* 模拟数据 */);
} finally {
setLoading(false);
}
};
// 生产单提交
const handleSubmit = async (values: QuickOrderForm) => {
setSubmitting(true);
try {
const accessToken = await TokenManager.getAccessToken();
// 构造生产单数据
const productionOrder = {
code: `WO${Date.now()}`,
itemCode: /* 物料编码 */,
planQty: values.plannedQuantity,
endTime: new Date(values.plannedEndDate).getTime(),
// ...其他字段
};
// 提交到新核云API
const resp = await request.post('https://c2.xinheyun.com/api/open/v4/workOrder/create', {
body: productionOrder
}, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// 成功反馈
message.success(t('app.quickOrder.submitSuccess'));
form.resetFields();
} catch (error) {
// 错误处理
message.error(t('app.quickOrder.submitError'));
} finally {
setSubmitting(false);
}
};
1.
认证流程:通过TokenManager处理OAuth2.0令牌获取与刷新
2.
API交互:使用标准化的request库发起API请求
iii. UI交互实现#
// 表单渲染
<Form form={form} onFinish={handleSubmit}>
{/* 物料选择(带搜索和分页) */}
<Form.Item name="materialId">
<Select
showSearch
onSearch={handleSearch}
options={materials.map(m => ({
value: m.id,
label: `${m.name} (${m.code}) - ${m.unit}`
}))}
dropdownRender={(menu) => (
<div onScroll={handleScroll}> {/* 无限滚动容器 */}
{menu}
{loading && <div>加载中...</div>}
</div>
)}
/>
</Form.Item>
{/* 数量输入 */}
<Form.Item name="plannedQuantity">
<InputNumber min={1} precision={0} />
</Form.Item>
{/* 日期选择 */}
<Form.Item name="plannedEndDate">
<DatePicker disabledDate={current => current < moment().startOf('day')} />
</Form.Item>
{/* 操作按钮 */}
<Button type="primary" htmlType="submit" loading={submitting}>
{t('app.quickOrder.submit')}
</Button>
</Form>
搜索防抖:debouncedSearch减少API请求频率
示例中的硬编码密钥仅用于演示,实际生产环境必须使用环境变量存储敏感信息
3.2.2 环境变量与安全配置#
安全配置最佳实践#
# .env.production - 生产环境配置
VITE_XHY_APP_KEY=your_actual_app_key
VITE_XHY_APP_SECRET=your_actual_app_secret
VITE_XHY_API_BASE=https://api.xinheyun.com
VITE_XHY_TOKEN_PATH=/api/open/v2/token
代码中安全使用环境变量#
// 从环境变量获取敏感信息
const XINHE_CONFIG = {
appkey: import.meta.env.VITE_XHY_APP_KEY,
appsecret: import.meta.env.VITE_XHY_APP_SECRET,
tokenUrl: `${import.meta.env.VITE_XHY_API_BASE}${import.meta.env.VITE_XHY_TOKEN_PATH}`
};
// API调用示例
const fetchMaterials = async () => {
const response = await request.post(
`${import.meta.env.VITE_XHY_API_BASE}/api/open/v3/items/page/query`,
{ /* ... */ }
);
};
安全配置要点#
1.
禁止硬编码:API密钥、访问令牌等敏感信息严禁直接写入源代码
2.
开发/生产环境使用不同变量文件(.env.development/.env.production)
安全审计清单#
| 检查项 | 安全要求 | 检查方法 |
|---|
| 密钥存储 | 使用环境变量 | 代码扫描 |
| 版本控制 | 排除.env文件 | 检查.gitignore |
| API权限 | 最小必要权限 | 开放平台审计 |
| 错误处理 | 不泄露敏感信息 | 测试错误响应 |
| 传输安全 | 全程HTTPS | 网络抓包检查 |
| 令牌管理 | 定期刷新机制 | TokenManager测试 |
生产环境强制要求:所有接入新核云API的Web Widget必须通过安全评审,未使用环境变量管理密钥的应用将不予上架
3.3 SDK 使用详解#
1
安装SDK(必需)
pnpm add @newcoretech/widget-react
2
SDK导入
在应用入口文件(如index.tsx)中添加导入语句:
import { useClient } from "@newcoretech/widget-react";
4
核心功能使用
// 获取数据
client.get("staff.email").then((res) => {
if (typeof res.data === "string") {
setEmailValue(res.data);
}
});
// 设置数据
client.set("staff.email", "test@example.com");
3.4 样式与UI开发#
3.4.1 核心原则#
样式隔离:使用CSS Modules避免全局污染
响应式设计:确保在不同位置(topBar/SideBar)自适应
平台一致性:遵循新核云设计规范
主题适配:支持亮色/暗色双模式
3.4.2 样式实现方式#
// 推荐使用CSS Modules(示例:TopBarWidget.module.css)
.widgetContainer {
padding: 12px;
border-radius: 8px;
background-color: var(--xhy-bg-primary); /* 使用设计系统变量 */
}
.flexLayout {
display: flex;
gap: 8px;
align-items: center;
}
// 暗色模式适配
@media (prefers-color-scheme: dark) {
.widgetContainer {
border: 1px solid var(--xhy-border-dark);
}
}
3.4.3 伪代码示例#
响应式布局技巧#
使用设计系统组件(补充)#
动画交互最佳实践#
// React动画组件
import { motion } from 'framer-motion';
function AnimatedCard() {
return (
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
transition={{ duration: 0.2 }}
whileHover={{ boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
>
<CardContent />
</motion.div>
);
}
3.5 国际化 (i18n)#
3.5.1 实现原理#
Webwidget 应用的国际化基于 JSON 文件翻译机制,通过/src/lib/i18n.ts模块提供的t函数实现多语言支持。系统会自动检测用户的语言偏好并加载对应的翻译文件。
3.5.2 核心流程#
3.5.3 语言文件配置#
文件位置:/src/translations/目录
文件命名规范: // zh-CN.json
{
"app": {
"title": "员工管理系统",
"welcome": "欢迎回来, {name}",
"buttons": {
"submit": "提交",
"cancel": "取消"
}
},
"errors": {
"required": "{field} 是必填项"
}
}
// en-US.json
{
"app": {
"title": "Employee Management",
"welcome": "Welcome back, {name}",
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
}
},
"errors": {
"required": "{field} is required"
}
}
3.5.4 在代码中使用翻译#
import { useI18n } from "../hooks/useI18n";
// 在组件中使用
function WelcomeBanner() {
const { t } = useI18n();
const { user } = useUser(); // 假设从上下文中获取用户信息
return (
<div className="welcome-container">
<h1>{t('app.title')}</h1>
{/* 带参数的翻译 */}
<p>{t('app.welcome', { name: user.firstName })}</p>
{/* 嵌套属性访问 */}
<div className="action-buttons">
<button>{t('app.buttons.submit')}</button>
<button>{t('app.buttons.cancel')}</button>
</div>
</div>
);
}
3.5.5 高级用法#
复数形式处理:#
// 在语言文件中
{
"notifications": {
"count": "您有 {count} 条通知 | 您有 {count} 条通知"
}
}
// 在组件中
const notificationCount = 3;
const text = t('notifications.count', { count: notificationCount });
// 根据数量自动选择单复数形式
格式化日期/数字:#
// 语言文件中定义格式
{
"formats": {
"date": "YYYY年MM月DD日",
"currency": "¥{amount}"
}
}
// 在组件中使用
const today = new Date();
const formattedDate = t('formats.date', { value: today });
const price = 99.9;
const formattedPrice = t('formats.currency', { amount: price.toFixed(2) });
3.6.1 通用 API#
client.get(path)#
/**
* 获取字段数据
* @param path 字段路径(如 "staff.email")
*/
get(path: string): Promise<{
success: boolean;
message?: string;
data?: unknown;
}>;
const res = await client.get("staff.email");
if (res.success && typeof res.data === "string") {
setEmailValue(res.data);
}
client.set(path, value)#
/**
* 设置字段数据
* @param path 字段路径
* @param value 字段值
*/
set(path: string, value: unknown): Promise<{
success: boolean;
message?: string;
}>;
await client.set("staff.email", "test@example.com");
client.trigger(eventName, data)#
/**
* 触发事件(向主页面或其他组件广播)
* @param eventName 事件名称
* @param data 事件数据
*/
trigger(eventName: string, data: unknown): void;
说明:主动广播事件;可用于通知页面执行某些动作或刷新数据。
client.trigger("refreshWorkOrders", { source: "quickCreate" });
client.invoke(path, ...args)#
/**
* 调用方法(请求-响应式)
* @param path 方法路径(如 "workOrder.create")
* @param args 方法参数
*/
invoke(path: string, ...args: unknown[]): Promise<{
success: boolean;
message?: string;
data: unknown;
}>;
const resp = await client.invoke("workOrder.create", {
code: "WO20251016",
itemCode: "STEEL_A",
planQty: 100
});
if (resp.success) {
// 后续处理
}
client.on(eventName, handler)#
/**
* 订阅事件(监听)
* @param eventName 事件名称
* @param handler 事件处理函数 (payload: unknown) => void
*/
on(eventName: string, handler: (payload: unknown) => void): void;