openyida 2026.5.21 → 2026.5.25
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 +5 -1
- package/bin/yida.js +7 -1
- package/lib/app/app-list.js +20 -1
- package/lib/app/check-page.js +2 -2
- package/lib/app/compile.js +3 -2
- package/lib/app/externalize-form.js +642 -0
- package/lib/app/import-app.js +39 -11
- package/lib/app/page-compat.js +258 -2
- package/lib/app/page-compiler.js +4 -1
- package/lib/app/page-linter.js +271 -0
- package/lib/app/publish.js +3 -2
- package/lib/auth/cdp-browser-login.js +7 -3
- package/lib/auth/login.js +2 -3
- package/lib/core/command-manifest.js +3 -0
- package/lib/core/copy.js +50 -8
- package/lib/core/env-manager.js +24 -16
- package/lib/core/locales/ar.js +7 -0
- package/lib/core/locales/de.js +7 -0
- package/lib/core/locales/en.js +7 -0
- package/lib/core/locales/es.js +7 -0
- package/lib/core/locales/fr.js +7 -0
- package/lib/core/locales/hi.js +7 -0
- package/lib/core/locales/ja.js +7 -0
- package/lib/core/locales/ko.js +7 -0
- package/lib/core/locales/pt.js +7 -0
- package/lib/core/locales/vi.js +7 -0
- package/lib/core/locales/zh-HK.js +7 -0
- package/lib/core/locales/zh.js +7 -0
- package/lib/core/utils.js +2 -2
- package/lib/process/configure-process.js +552 -20
- package/package.json +1 -1
- package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
- package/scripts/e2e-real/full-runner.js +257 -8
- package/scripts/e2e-real/skill-coverage.js +2 -2
- package/yida-skills/SKILL.md +1 -1
- package/yida-skills/skills/yida-chart/SKILL.md +1 -1
- package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
- package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
- package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
- package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
- package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
- package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
- package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
- package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
- package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
- package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
- package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
- package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
- package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
- package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
- package/yida-skills/skills/yida-publish-page/SKILL.md +6 -4
|
@@ -110,6 +110,7 @@ export function uploadSingleAttachment(file) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export function handleAttachmentChange(e) {
|
|
113
|
+
var self = this;
|
|
113
114
|
var files = Array.prototype.slice.call(e.target.files || []);
|
|
114
115
|
if (!files.length) {
|
|
115
116
|
return;
|
|
@@ -117,17 +118,17 @@ export function handleAttachmentChange(e) {
|
|
|
117
118
|
|
|
118
119
|
this.setCustomState({ uploading: true });
|
|
119
120
|
|
|
120
|
-
Promise.all(files.map(
|
|
121
|
-
return
|
|
122
|
-
}
|
|
121
|
+
Promise.all(files.map((file) => {
|
|
122
|
+
return self.uploadSingleAttachment(file);
|
|
123
|
+
})).then(function(uploaded) {
|
|
123
124
|
var next = (_customState.attachments || []).concat(uploaded);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
125
|
+
self.setCustomState({ attachments: next, uploading: false });
|
|
126
|
+
self.utils.toast({ title: '附件上传成功', type: 'success' });
|
|
127
|
+
}).catch(function(error) {
|
|
127
128
|
var message = error && error.message ? error.message : '附件上传失败';
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
129
|
+
self.setCustomState({ uploading: false });
|
|
130
|
+
self.utils.toast({ title: message, type: 'error' });
|
|
131
|
+
});
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
export function removeAttachment(fileUuid) {
|
|
@@ -190,6 +191,7 @@ var styles = {
|
|
|
190
191
|
|
|
191
192
|
export function renderJsx() {
|
|
192
193
|
var state = this.getCustomState();
|
|
194
|
+
var self = this;
|
|
193
195
|
|
|
194
196
|
return (
|
|
195
197
|
<div style={styles.page}>
|
|
@@ -202,7 +204,7 @@ export function renderJsx() {
|
|
|
202
204
|
multiple={true}
|
|
203
205
|
style={{ display: 'none' }}
|
|
204
206
|
disabled={state.uploading}
|
|
205
|
-
onChange={(e) => {
|
|
207
|
+
onChange={(e) => { self.handleAttachmentChange(e); }}
|
|
206
208
|
/>
|
|
207
209
|
</label>
|
|
208
210
|
|
|
@@ -210,13 +212,13 @@ export function renderJsx() {
|
|
|
210
212
|
return (
|
|
211
213
|
<div key={item.fileUuid} style={styles.item}>
|
|
212
214
|
<span>{item.name}</span>
|
|
213
|
-
<button style={styles.btn} onClick={(e) => {
|
|
215
|
+
<button style={styles.btn} onClick={(e) => { self.removeAttachment(item.fileUuid); }}>删除</button>
|
|
214
216
|
</div>
|
|
215
217
|
);
|
|
216
218
|
})}
|
|
217
219
|
|
|
218
220
|
<div style={{ marginTop: '16px' }}>
|
|
219
|
-
<button style={styles.btn} onClick={(e) => {
|
|
221
|
+
<button style={styles.btn} onClick={(e) => { self.submitForm(); }}>提交</button>
|
|
220
222
|
</div>
|
|
221
223
|
</div>
|
|
222
224
|
);
|
|
@@ -380,13 +380,15 @@ export function submitForm() {
|
|
|
380
380
|
配套渲染片段:
|
|
381
381
|
|
|
382
382
|
```jsx
|
|
383
|
+
var self = this;
|
|
384
|
+
|
|
383
385
|
<label>
|
|
384
386
|
选择附件
|
|
385
387
|
<input
|
|
386
388
|
type="file"
|
|
387
389
|
multiple={true}
|
|
388
390
|
style={{ display: 'none' }}
|
|
389
|
-
onChange={(e) => {
|
|
391
|
+
onChange={(e) => { self.handleAttachmentChange(e); }}
|
|
390
392
|
/>
|
|
391
393
|
</label>
|
|
392
394
|
```
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
| **三方包引入** | 禁止使用 `import/require` 语法,如需使用第三方库,必须通过 `this.utils.loadScript` 加载 CDN 脚本,参考 [yida-api.md](../../../references/yida-api.md) 的「工具类 API」章节。Tailwind 属于默认视觉层,按下方「Tailwind 引入规范」处理 |
|
|
14
14
|
| **内置 lodash** | 宜搭页面运行时已全局加载 **lodash 4.6.1**(`window._`),可直接使用 `_.get`、`_.groupBy`、`_.cloneDeep` 等,无需 `loadScript`。详见下方「内置 lodash 使用指引」 |
|
|
15
15
|
| **函数导出格式** | 原生写法使用 `export function xxx() {}`;现代 authoring 写法使用 `export default function Page()`,由 OpenYida 编译为原生导出函数 |
|
|
16
|
+
| **生命周期名称** | 只允许 `didMount` / `didUnmount`,大小写敏感;不要写 `didmount`、`componentDidMount`、`componentWillUnmount` |
|
|
16
17
|
| **样式** | 默认使用 Tailwind utility `className` 组织视觉层;关键尺寸、容器兜底和 Tailwind 加载失败兜底可继续使用 `style` 对象。禁止 `import` CSS、CSS Modules 或构建期样式能力 |
|
|
17
18
|
| **`this` 上下文** | 所有导出函数中的 `this` 指向宜搭页面的 React 类实例 |
|
|
19
|
+
| **按钮交互** | 可见 `<button>` 必须有 `onClick`/`onMouseDown`/`onKeyDown` 或明确 `disabled`;静态标签、状态徽标、截图标记用 `span`/`div` |
|
|
18
20
|
| **禁止使用 `this.setState` 管理业务状态** | `this.setState` 已被覆盖,仅用于 `forceUpdate`(通过更新 `timestamp`) |
|
|
19
21
|
| **JavaScript 版本** | 使用 ES2015 (ES6) 语法,不能高于 ES2015 版本。**注意**:即使是 ES6 语法,部分特性也会导致静默失败,详见下方「JS 引擎兼容性限制」 |
|
|
20
22
|
| **必须定义页面入口** | 原生写法必须定义 `renderJsx`;`.oyd.jsx` authoring 写法必须定义 `export default function Page()` |
|
|
@@ -228,6 +230,8 @@ this.setCustomState(nextState);
|
|
|
228
230
|
- didUnmount 函数
|
|
229
231
|
- renderJsx 函数
|
|
230
232
|
|
|
233
|
+
OpenYida 编译器会在发布前为极简页面补齐缺失的空 `didMount` / `didUnmount` 和基础状态函数,避免 Schema 中 actionRef 找不到函数;但交付给 AI/IDE 的源码仍必须按下面结构生成,便于人审、二次修改和 `check-page` 精准定位问题。
|
|
234
|
+
|
|
231
235
|
```jsx
|
|
232
236
|
// ── 状态管理 ──────────────────────────────────────────
|
|
233
237
|
var _customState = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 自定义页面 JSX 组件指南
|
|
2
2
|
|
|
3
|
-
> 适用于宜搭自定义页面运行时:React 16
|
|
3
|
+
> 适用于宜搭自定义页面运行时:React 16、宜搭原生 `export function` 页面模式、无 `import/require`、通过 `this.utils.yida.*` 调用数据 API。
|
|
4
4
|
|
|
5
5
|
## 先说清楚边界
|
|
6
6
|
|
|
@@ -21,25 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
```javascript
|
|
23
23
|
export function setDraftField(key, value) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this._customState.draft[key] = value;
|
|
24
|
+
_customState.draft = _customState.draft || {};
|
|
25
|
+
_customState.draft[key] = value;
|
|
27
26
|
}
|
|
28
27
|
```
|
|
29
28
|
|
|
30
29
|
带输入法组合输入的文本输入:
|
|
31
30
|
|
|
32
31
|
```jsx
|
|
32
|
+
var self = this;
|
|
33
|
+
|
|
33
34
|
<input
|
|
34
|
-
defaultValue={
|
|
35
|
-
onCompositionStart={() => {
|
|
35
|
+
defaultValue={_customState.keyword || ''}
|
|
36
|
+
onCompositionStart={(e) => { self._isComposing = true; }}
|
|
36
37
|
onCompositionEnd={(e) => {
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
self._isComposing = false;
|
|
39
|
+
_customState.keyword = e.target.value;
|
|
39
40
|
}}
|
|
40
41
|
onChange={(e) => {
|
|
41
|
-
if (
|
|
42
|
-
|
|
42
|
+
if (self._isComposing) { return; }
|
|
43
|
+
_customState.keyword = e.target.value;
|
|
43
44
|
}}
|
|
44
45
|
style={styles.input}
|
|
45
46
|
/>
|
|
@@ -48,17 +49,19 @@ export function setDraftField(key, value) {
|
|
|
48
49
|
## TextField / TextareaField
|
|
49
50
|
|
|
50
51
|
```jsx
|
|
52
|
+
var self = this;
|
|
53
|
+
|
|
51
54
|
<input
|
|
52
55
|
defaultValue={(record.formData && record.formData[FIELDS.name]) || ''}
|
|
53
56
|
placeholder="请输入"
|
|
54
|
-
onChange={(e) => {
|
|
57
|
+
onChange={(e) => { self.setDraftField(FIELDS.name, e.target.value); }}
|
|
55
58
|
style={styles.input}
|
|
56
59
|
/>
|
|
57
60
|
|
|
58
61
|
<textarea
|
|
59
62
|
defaultValue={(record.formData && record.formData[FIELDS.remark]) || ''}
|
|
60
63
|
placeholder="请输入备注"
|
|
61
|
-
onChange={(e) => {
|
|
64
|
+
onChange={(e) => { self.setDraftField(FIELDS.remark, e.target.value); }}
|
|
62
65
|
style={styles.textarea}
|
|
63
66
|
/>
|
|
64
67
|
```
|
|
@@ -216,16 +219,18 @@ var styles = {
|
|
|
216
219
|
```javascript
|
|
217
220
|
export function dateInputToTimestamp(value) {
|
|
218
221
|
if (!value) { return ''; }
|
|
219
|
-
|
|
220
|
-
return
|
|
222
|
+
var timestamp = new Date(value + 'T00:00:00').getTime();
|
|
223
|
+
return isNaN(timestamp) ? '' : timestamp;
|
|
221
224
|
}
|
|
222
225
|
```
|
|
223
226
|
|
|
224
227
|
```jsx
|
|
228
|
+
var self = this;
|
|
229
|
+
|
|
225
230
|
<input
|
|
226
231
|
type="date"
|
|
227
|
-
defaultValue={
|
|
228
|
-
onChange={(e) => {
|
|
232
|
+
defaultValue={self.formatDateInput(record.formData && record.formData[FIELDS.planDate])}
|
|
233
|
+
onChange={(e) => { self.setDraftField(FIELDS.planDate, self.dateInputToTimestamp(e.target.value)); }}
|
|
229
234
|
style={styles.input}
|
|
230
235
|
/>
|
|
231
236
|
```
|
|
@@ -235,12 +240,14 @@ export function dateInputToTimestamp(value) {
|
|
|
235
240
|
保持空值为空字符串;有值时再转数字,避免把未填项误写成 `0`。
|
|
236
241
|
|
|
237
242
|
```jsx
|
|
243
|
+
var self = this;
|
|
244
|
+
|
|
238
245
|
<input
|
|
239
246
|
type="number"
|
|
240
247
|
defaultValue={(record.formData && record.formData[FIELDS.amount]) || ''}
|
|
241
248
|
onChange={(e) => {
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
var raw = e.target.value;
|
|
250
|
+
self.setDraftField(FIELDS.amount, raw === '' ? '' : Number(raw));
|
|
244
251
|
}}
|
|
245
252
|
style={styles.input}
|
|
246
253
|
/>
|
|
@@ -312,19 +319,21 @@ export function dateInputToTimestamp(value) {
|
|
|
312
319
|
筛选栏建议由关键词、状态、日期范围和按钮组成;点击查询时统一读取 `_customState.filters`,再调用 `this.utils.yida.searchFormDatas`。
|
|
313
320
|
|
|
314
321
|
```jsx
|
|
322
|
+
var self = this;
|
|
323
|
+
|
|
315
324
|
<div style={styles.filterBar}>
|
|
316
325
|
<input
|
|
317
|
-
defaultValue={(
|
|
326
|
+
defaultValue={(_customState.filters && _customState.filters.keyword) || ''}
|
|
318
327
|
placeholder="搜索关键词"
|
|
319
328
|
onChange={(e) => {
|
|
320
|
-
|
|
321
|
-
|
|
329
|
+
_customState.filters = _customState.filters || {};
|
|
330
|
+
_customState.filters.keyword = e.target.value;
|
|
322
331
|
}}
|
|
323
332
|
style={styles.input}
|
|
324
333
|
/>
|
|
325
334
|
<button
|
|
326
335
|
type="button"
|
|
327
|
-
onClick={() => {
|
|
336
|
+
onClick={(e) => { self.loadRecords({ page: 1 }); }}
|
|
328
337
|
style={styles.primaryButton}
|
|
329
338
|
>
|
|
330
339
|
查询
|
|
@@ -54,7 +54,7 @@ metadata:
|
|
|
54
54
|
2. **真实数据接入**:KPI 走 `getDataAsync.json`,明细走 `searchFormDatas`,禁止前端聚合
|
|
55
55
|
3. **视觉主题选型**:深色紫蓝科技风 / 金蓝奢华风 / 白底商务风(见 `references/theme-presets.md`)
|
|
56
56
|
4. **每元素可派单闭环**:任何 KPI/图表/风险/动作项都能一键触发看板「派单触发表」的 saveFormData,由预先配置好的集成自动化自动调用「待办2.0 / 创建待办任务」连接器生成真实钉钉待办。前端只管写表,鉴权和连接器调用全部交给集成自动化在后端托管。不得把「工作通知」包装成待办(见 `references/interaction-patterns.md`)
|
|
57
|
-
5. **卡片截图分享**:html2canvas 1.4.1
|
|
57
|
+
5. **卡片截图分享**:html2canvas 1.4.1,每个核心卡片右上角有可点击"截图"按钮,必须绑定 `onClick` 调用 `captureCard`;如果只是视觉标记或演示标签,用 `span/div`,不要写成无事件 `<button>`
|
|
58
58
|
6. **多端响应**:PC 三列 / 平板两列 / 手机一列,断点 768 / 1024
|
|
59
59
|
7. **组织内短链 + 隐藏导航**:发布后配置 `isRenderNav=false` + 组织内分享 URL
|
|
60
60
|
8. **AI 快讯 marquee**(可选):底部滚动字幕展示最新动态
|
|
@@ -70,9 +70,9 @@ metadata:
|
|
|
70
70
|
- 有原生报表 → 记录 REPORT-xxx + 提取 cid/componentClassName
|
|
71
71
|
- 没有原生报表 → 先调用 yida-report 技能创建数据源
|
|
72
72
|
- 需要派单闭环 → **固定双件套**:
|
|
73
|
-
(a) 用 `yida-create-form-page` 建一张「看板派单触发表」(TodoTrigger),最少
|
|
73
|
+
(a) 用 `yida-create-form-page` 建一张「看板派单触发表」(TodoTrigger),最少 6 个字段:
|
|
74
74
|
subject(TextField) / executor(EmployeeField) / description(TextareaField) /
|
|
75
|
-
dueTime(DateField) / priority(
|
|
75
|
+
dueTime(DateField) / priority(SelectField 展示审计) / priorityNum(NumberField 透传 10/20/30/40)
|
|
76
76
|
(b) 用 `openyida integration create <appType> <formUuid> "<看板名>-派单" --events create
|
|
77
77
|
--connector-id G-CONN-1016B8AEBED50B01B8D00009
|
|
78
78
|
--action-id G-ACT-1016B8B1911A0B01B8D0000I
|
|
@@ -82,7 +82,7 @@ metadata:
|
|
|
82
82
|
--connector-assignment creatorId:processVar:form_inst_modifier
|
|
83
83
|
--connector-assignment description:processVar:<description字段ID>
|
|
84
84
|
--connector-assignment dueTime:processVar:<dueTime字段ID>
|
|
85
|
-
--connector-assignment priority:
|
|
85
|
+
--connector-assignment priority:processVar:<priorityNum字段ID> // ⚠ 必须 NumberField,禁止透传 SelectField/RadioField
|
|
86
86
|
--connector-assignment executorIds:processVar:<executor字段ID>
|
|
87
87
|
--publish` 一键生成并发布
|
|
88
88
|
- 需要审计台账 → 可以复用上面这张「派单触发表」,它本身就是最小审计表;也可以再加一张独立审计表记录 dingTodoId 回写
|
|
@@ -124,9 +124,9 @@ metadata:
|
|
|
124
124
|
6. **禁止把「工作通知」当作钉钉待办**:集成自动化里不允许只配「发送钉钉工作通知」节点就声称已打通待办;派单链路必须有一个 **ConnectorCall 节点**调用「待办2.0 / 创建待办任务」连接器。工作通知只能作为降级提醒,交付时必须写明"非真实待办"
|
|
125
125
|
7. **禁止在看板内直接 fetch 钉钉 OpenAPI 或写 accessToken**:必须通过集成自动化后端托管连接器鉴权
|
|
126
126
|
8. **禁止 cdnjs.cloudflare.com**:宜搭环境被拦截,ECharts / html2canvas 统一用 `g.alicdn.com`
|
|
127
|
-
9. **禁止缺少 `.sl-no-capture`
|
|
127
|
+
9. **禁止缺少 `.sl-no-capture` 标记或写无事件截图按钮**:截图按钮本身要被 html2canvas 排除,且必须有真实 `onClick`;静态状态胶囊、筛选展示项、截图占位标签一律用 `span/div`
|
|
128
128
|
10. **禁止 2300 行一口气写到单个 create_file**:超过 1000 行用 `large-file-write` 技能或分批 append
|
|
129
|
-
11. **禁止集成自动化 priority 入参用 processVar 透传 RadioField/SelectField 的选项值**:连接器要求 Number(10/20/30/40)
|
|
129
|
+
11. **禁止集成自动化 priority 入参用 processVar 透传 RadioField/SelectField 的选项值**:连接器要求 Number(10/20/30/40),正常方案必须 `priority:processVar:<priorityNum NumberField 字段 ID>`;`literal:10` 只能作为临时回滚止血方案,否则 UI 优先级选项会失效
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -136,12 +136,13 @@ metadata:
|
|
|
136
136
|
- `project/pages/src/supply-chain-dashboard.js`(深色紫蓝标杆,2356 行)
|
|
137
137
|
- `project/pages/src/shangri-la-executive-dashboard.js`(金蓝奢华样本,1796 行)
|
|
138
138
|
- 读取关键段学结构,不要凭空设计
|
|
139
|
-
2. **审计表单字段 ID 在 FORM_CONFIG 常量集中声明**,便于一次性替换;`FORM_CONFIG.todoTrigger` 必须标注出 subject/executor/description/dueTime/priority
|
|
139
|
+
2. **审计表单字段 ID 在 FORM_CONFIG 常量集中声明**,便于一次性替换;`FORM_CONFIG.todoTrigger` 必须标注出 subject/executor/description/dueTime/priority/priorityNum 6 个字段 ID 与集成自动化 `--connector-assignment` 的映射完全一致
|
|
140
140
|
3. **前端派单统一走 `self.utils.yida.saveFormData`** 写入派单触发表,禁止引用已不存在的 `this.dataSourceMap.createDingTodo`。没有配置集成自动化时前端要 toast "派单触发表未配置集成自动化",不要静默降级
|
|
141
141
|
4. **每次数据请求必写 catch + 降级渲染**,不要静默失败
|
|
142
142
|
5. **发布前用 `openyida check-page` 跑一次规范扫描**
|
|
143
143
|
6. **首次发布必带 `--health-check`** 做首屏 HTTP 健康检查
|
|
144
144
|
7. **交付物验收必须包含组织内短链 URL**,纯 aliwork.com 链接不算交付
|
|
145
|
+
8. **所有可见 `<button>` 必须可用**:每个 button 要么有真实 `onClick/onMouseDown/onKeyDown`,要么显式 `disabled`;`openyida check-page` 不能出现 `button-missing-handler`
|
|
145
146
|
|
|
146
147
|
---
|
|
147
148
|
|
|
@@ -194,14 +195,14 @@ metadata:
|
|
|
194
195
|
|
|
195
196
|
核对五件事(按优先级):
|
|
196
197
|
1. 派单触发表是否挂了集成自动化:`openyida integration list <appType>` / 或直接去设计器看流程状态 `y`
|
|
197
|
-
2. 集成自动化执行记录是否有"执行异常 / 一方连接器异常:接口参数异常":若是 →
|
|
198
|
+
2. 集成自动化执行记录是否有"执行异常 / 一方连接器异常:接口参数异常":若是 → **优先检查 priority 是否透传了 SelectField/RadioField 字符串**,必须改为 `--connector-assignment priority:processVar:<priorityNum NumberField 字段 ID>`;前端要同时写入展示字段 `priority` 和数字字段 `priorityNum`
|
|
198
199
|
3. `FORM_CONFIG.todoTrigger.fields` 的字段 ID 是否与集成自动化 `--connector-assignment <column>:processVar:<字段ID>` 完全一致
|
|
199
200
|
4. EmployeeField 在 saveFormData 里是否传成 `[String(userId)]` 数组(连接器会从数组自动取 unionId)
|
|
200
201
|
5. 钉钉应用 / 宜搭版本是否具备「待办2.0」连接器权限;无权限须向用户说明只能降级为工作通知
|
|
201
202
|
|
|
202
203
|
**Q:卡片截图里把"截图"按钮也拍进去了?**
|
|
203
204
|
|
|
204
|
-
给截图按钮加 class `sl-no-capture
|
|
205
|
+
给截图按钮加 class `sl-no-capture`,按钮本身还必须绑定 `onClick={function(e){ e.stopPropagation(); self.captureCard(...); }}`;并在 `html2canvas` 调用时传 `ignoreElements: function(el) { return el.classList && el.classList.contains('sl-no-capture'); }`。
|
|
205
206
|
|
|
206
207
|
**Q:看板要支持 3 个品牌/产品线/BU 切换视图,怎么做?**
|
|
207
208
|
|
|
@@ -575,6 +575,8 @@ export function captureCard(domId, fileName) {
|
|
|
575
575
|
|
|
576
576
|
### 3.3 截图按钮渲染组件
|
|
577
577
|
|
|
578
|
+
截图控件是一个真实可点击按钮,不能只因为要加 `.sl-no-capture` 就写成静态 `<button>`。所有可见 `<button>` 必须绑定真实事件;如果只是展示"截图"状态、演示占位或不需要点击的标签,用 `span/div`。
|
|
579
|
+
|
|
578
580
|
```javascript
|
|
579
581
|
var renderCaptureButton = function(self, domId, fileName, s) {
|
|
580
582
|
return (
|
|
@@ -197,11 +197,19 @@ fetch('/dingtalk/web/' + appType + '/.../saveFormData.json');
|
|
|
197
197
|
|
|
198
198
|
**现象**:用户分享到群里,截图右上角赫然一个"截图"按钮。
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
**正确做法**:截图按钮要同时满足两件事:有真实 `onClick`,并带 `sl-no-capture` 让 html2canvas 排除。如果只是视觉标签或占位,不要用 `<button>`,改用 `span/div`。
|
|
201
201
|
|
|
202
202
|
```javascript
|
|
203
|
-
// 按钮加 class
|
|
204
|
-
<button
|
|
203
|
+
// 按钮加 class,且必须有真实点击事件
|
|
204
|
+
<button
|
|
205
|
+
className="sl-no-capture"
|
|
206
|
+
onClick={function(e) {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
self.captureCard('chart-region', '区域营收');
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
截图
|
|
212
|
+
</button>
|
|
205
213
|
|
|
206
214
|
// 截图时排除
|
|
207
215
|
window.html2canvas(el, {
|
|
@@ -505,7 +513,8 @@ self.utils.toast({ title: '已创建待办', type: 'success' });
|
|
|
505
513
|
- [ ] 派单 toast 文案是"派单已发起,将在 30 秒内收到钉钉待办",**不是**"已创建待办"
|
|
506
514
|
- [ ] 页面源码没有直接 `fetch` 钉钉 OpenAPI 或保存 accessToken/appSecret
|
|
507
515
|
- [ ] ECharts / html2canvas 用 `g.alicdn.com`
|
|
508
|
-
- [ ]
|
|
516
|
+
- [ ] 截图按钮有真实 `onClick` + `sl-no-capture` class + `ignoreElements` 已配
|
|
517
|
+
- [ ] 所有可见 `<button>` 都有真实事件或显式 `disabled`,静态标签/状态胶囊不用 button
|
|
509
518
|
- [ ] 超过 1000 行用分批写入
|
|
510
519
|
- [ ] 每个 `renderJsx` 的 return 分支都有 `timestamp` 隐藏 div
|
|
511
520
|
- [ ] 纯工具函数用 `var`,组件方法用 `export function`
|
|
@@ -337,6 +337,6 @@ var KPI_CARDS = [
|
|
|
337
337
|
- [ ] 筛选任意维度,6 图表和 5 KPI 都实时刷新(不闪烁)
|
|
338
338
|
- [ ] 每个 KPI / 模块 / 动作 / 风险点击都能弹出派单弹窗
|
|
339
339
|
- [ ] 派单后钉钉客户端出现真实待办;不是仅收到工作通知(见 `interaction-patterns.md` 联调步骤)
|
|
340
|
-
- [ ] 每个卡片右上角"截图"
|
|
340
|
+
- [ ] 每个卡片右上角"截图"按钮可用,有真实 `onClick`,截出来不包含按钮本身
|
|
341
341
|
- [ ] marquee 在底部平滑滚动不卡顿
|
|
342
342
|
- [ ] 组织内短链打开 → 无平台导航 → 看板顶满一屏
|
|
@@ -7,21 +7,23 @@ description: "宜搭自定义页面 PPT 幻灯片开发指南。用于在宜搭
|
|
|
7
7
|
|
|
8
8
|
## 严格禁止 (NEVER DO)
|
|
9
9
|
|
|
10
|
-
- 不要使用 React Hooks(`useState`、`useEffect
|
|
11
|
-
-
|
|
10
|
+
- 不要使用 React Hooks(`useState`、`useEffect`),必须使用宜搭原生 `export function` + `_customState` 模式
|
|
11
|
+
- 不要写会在渲染期执行或无效的事件绑定:禁止 `onClick={foo()}`、`onClick={handleDotClick(i)}`、`onClick={(e) => self.foo}`;推荐在 `renderJsx` 顶部定义 handler 后引用,传参用 `data-*` 或函数包装调用
|
|
12
12
|
- 不要使用 `import/require` 引入第三方库,必须通过 CDN 或内联代码
|
|
13
|
-
- 不要在 `
|
|
13
|
+
- 不要在 `didUnmount` 中遗漏清理键盘/触摸事件监听,否则内存泄漏
|
|
14
14
|
- 不要使用 `objectFit: 'cover'` 裁剪图片,必须用 `contain` 确保完整显示
|
|
15
15
|
- 不要将幻灯片数据硬编码在 `renderJsx` 中,必须定义为顶层 `SLIDES` 数组
|
|
16
16
|
|
|
17
17
|
## 严格要求 (MUST DO)
|
|
18
18
|
|
|
19
19
|
- **发布前必须确认**:执行发布操作前,必须向用户展示幻灯片配置摘要(页数、标题列表),获得用户明确同意后再发布
|
|
20
|
-
- 必须在 `
|
|
21
|
-
- 必须在 `
|
|
20
|
+
- 必须在 `didMount` 中注册键盘事件(含 `PageDown`/`PageUp` 演讲笔支持)
|
|
21
|
+
- 必须在 `didUnmount` 中清理所有事件监听
|
|
22
22
|
- 必须使用 `this.utils.isMobile()` 判断设备类型并适配移动端样式
|
|
23
23
|
- 必须用 `position: fixed; top:0; left:0; right:0; bottom:0` 覆盖宜搭默认容器样式
|
|
24
24
|
- 状态变更必须通过 `_customState.xxx = value; this.forceUpdate()` 触发重渲染
|
|
25
|
+
- 所有可见 `<button>` 必须有真实 `onClick/onMouseDown/onKeyDown` 或显式 `disabled`,发布前 `openyida check-page` 不能出现 `button-missing-handler`
|
|
26
|
+
- 发布前必须执行 `openyida check-page <源文件>`;首次发布建议带 `--health-check`
|
|
25
27
|
- 本技能不读写 memory,所有状态仅在当前页面会话内有效,不跨会话持久化
|
|
26
28
|
|
|
27
29
|
## 适用场景
|
|
@@ -48,14 +50,15 @@ description: "宜搭自定义页面 PPT 幻灯片开发指南。用于在宜搭
|
|
|
48
50
|
|
|
49
51
|
| 异常场景 | 处理方式 |
|
|
50
52
|
|---------|----------|
|
|
51
|
-
| 键盘翻页无响应 | 确认在 `
|
|
52
|
-
| 内存泄漏(切换页面后事件仍触发) | 在 `
|
|
53
|
+
| 键盘翻页无响应 | 确认在 `didMount` 中注册了键盘事件,检查 `PageDown`/`PageUp` 支持 |
|
|
54
|
+
| 内存泄漏(切换页面后事件仍触发) | 在 `didUnmount` 中清理所有键盘/触摸事件监听 |
|
|
53
55
|
| 图片显示不完整 | 使用 `objectFit: 'contain'` 而非 `cover`,确保完整显示 |
|
|
54
56
|
| 幻灯片数据难以维护 | 将幻灯片数据定义为顶层 `SLIDES` 数组,不得硬编码在 `renderJsx` 中 |
|
|
55
57
|
| 移动端布局异常 | 使用 `this.utils.isMobile()` 判断设备类型并适配移动端样式 |
|
|
56
58
|
| 数字键翻页跳到错误页 | 检查 300ms 延迟缓冲逻辑,确保 `numBuffer` 在跳转后清空 |
|
|
57
59
|
| 导航栏不显示 | 导航默认隐藏,鼠标移到底部 80px 区域才会显示;移动端通过触摸底部触发 |
|
|
58
60
|
| 全屏按钮无效 | 部分浏览器限制 Fullscreen API 必须由用户手势触发,确保在 `onClick` 中调用 |
|
|
61
|
+
| 按钮点不了 | 检查是否写了 `onClick={foo()}`、JSX 小写 `onclick`、只引用不调用的箭头函数,或可见 `<button>` 没有事件;运行 `openyida check-page <file>` 查看具体行号 |
|
|
59
62
|
| 中英文切换后内容未更新 | 确保 `forceUpdate()` 被调用,且 UI 文案从 `I18N[state.lang]` 动态读取 |
|
|
60
63
|
|
|
61
64
|
---
|
|
@@ -156,16 +159,18 @@ npm install -g openyida@latest
|
|
|
156
159
|
↓
|
|
157
160
|
[Step 4] 编写幻灯片代码 → 参考本技能规范 → pages/src/<文件名>.js
|
|
158
161
|
↓
|
|
159
|
-
[Step 5]
|
|
162
|
+
[Step 5] 本地校验 → openyida check-page <文件>
|
|
160
163
|
↓
|
|
161
|
-
[Step 6]
|
|
164
|
+
[Step 6] 发布页面 → openyida publish <文件> <appType> <formUuid> --health-check
|
|
165
|
+
↓
|
|
166
|
+
[Step 7] 配置公开访问(可选)→ openyida save-share-config / update-form-config --is-render-nav false
|
|
162
167
|
```
|
|
163
168
|
|
|
164
169
|
---
|
|
165
170
|
|
|
166
171
|
## 技术栈
|
|
167
172
|
|
|
168
|
-
- **框架**:React 16
|
|
173
|
+
- **框架**:React 16(宜搭原生 `export function` 页面模式,禁止使用 Hooks)
|
|
169
174
|
- **样式**:内联 style(宜搭自定义页面限制)
|
|
170
175
|
- **状态管理**:全局变量 `_customState` + `this.setState({ timestamp: Date.now() })`
|
|
171
176
|
- **导出格式**:`export function`(非 `export default`)
|
|
@@ -258,40 +263,42 @@ export function renderJsx() {
|
|
|
258
263
|
### 1. 生命周期方法
|
|
259
264
|
|
|
260
265
|
```javascript
|
|
261
|
-
// ✅ 使用
|
|
262
|
-
|
|
266
|
+
// ✅ 使用 didMount 初始化事件监听
|
|
267
|
+
export function didMount() {
|
|
268
|
+
var self = this;
|
|
269
|
+
|
|
263
270
|
// 初始化幻灯片总数
|
|
264
271
|
_customState.total = SLIDES.length;
|
|
265
272
|
|
|
266
273
|
// 键盘翻页事件(支持演讲笔的 PageDown/PageUp)
|
|
267
274
|
this._handleKeyDown = function(e) {
|
|
268
275
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
|
|
269
|
-
|
|
276
|
+
self.handleNext();
|
|
270
277
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
|
|
271
|
-
|
|
278
|
+
self.handlePrev();
|
|
272
279
|
}
|
|
273
|
-
}
|
|
280
|
+
};
|
|
274
281
|
|
|
275
282
|
document.addEventListener('keydown', this._handleKeyDown);
|
|
276
283
|
|
|
277
284
|
// 触摸滑动支持(移动端)
|
|
278
285
|
this._touchStartX = 0;
|
|
279
286
|
this._handleTouchStart = function(e) {
|
|
280
|
-
|
|
281
|
-
}
|
|
287
|
+
self._touchStartX = e.changedTouches[0].screenX;
|
|
288
|
+
};
|
|
282
289
|
|
|
283
290
|
this._handleTouchEnd = function(e) {
|
|
284
291
|
var touchEndX = e.changedTouches[0].screenX;
|
|
285
|
-
if (
|
|
286
|
-
if (touchEndX -
|
|
287
|
-
}
|
|
292
|
+
if (self._touchStartX - touchEndX > 50) { self.handleNext(); }
|
|
293
|
+
if (touchEndX - self._touchStartX > 50) { self.handlePrev(); }
|
|
294
|
+
};
|
|
288
295
|
|
|
289
296
|
document.addEventListener('touchstart', this._handleTouchStart);
|
|
290
297
|
document.addEventListener('touchend', this._handleTouchEnd);
|
|
291
|
-
}
|
|
298
|
+
}
|
|
292
299
|
|
|
293
|
-
// ✅ 使用
|
|
294
|
-
|
|
300
|
+
// ✅ 使用 didUnmount 清理事件监听,防止内存泄漏
|
|
301
|
+
export function didUnmount() {
|
|
295
302
|
document.removeEventListener('keydown', this._handleKeyDown);
|
|
296
303
|
document.removeEventListener('touchstart', this._handleTouchStart);
|
|
297
304
|
document.removeEventListener('touchend', this._handleTouchEnd);
|
|
@@ -302,21 +309,21 @@ componentWillUnmount: function() {
|
|
|
302
309
|
|
|
303
310
|
```javascript
|
|
304
311
|
// ✅ 正确:直接修改 _customState,然后调用 forceUpdate()
|
|
305
|
-
|
|
312
|
+
export function handleNext() {
|
|
306
313
|
if (_customState.currentIndex < SLIDES.length - 1) {
|
|
307
314
|
_customState.currentIndex++;
|
|
308
315
|
this.forceUpdate(); // 触发重渲染
|
|
309
316
|
}
|
|
310
|
-
}
|
|
317
|
+
}
|
|
311
318
|
|
|
312
|
-
|
|
319
|
+
export function handlePrev() {
|
|
313
320
|
if (_customState.currentIndex > 0) {
|
|
314
321
|
_customState.currentIndex--;
|
|
315
322
|
this.forceUpdate();
|
|
316
323
|
}
|
|
317
|
-
}
|
|
324
|
+
}
|
|
318
325
|
|
|
319
|
-
|
|
326
|
+
export function handleGoTo(index) {
|
|
320
327
|
_customState.currentIndex = index;
|
|
321
328
|
this.forceUpdate();
|
|
322
329
|
}
|
|
@@ -328,10 +335,12 @@ handleGoTo: function(index) {
|
|
|
328
335
|
### 3. 分页导航(精简版)
|
|
329
336
|
|
|
330
337
|
```javascript
|
|
331
|
-
// 在 renderJsx
|
|
332
|
-
var
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
// 在 renderJsx 顶部定义,JSX 中直接引用,避免 onClick={handleDotClick(i)} 渲染期调用
|
|
339
|
+
var self = this;
|
|
340
|
+
var handleDotClick = function(e) {
|
|
341
|
+
var index = Number(e.currentTarget.getAttribute('data-index'));
|
|
342
|
+
self.handleGoTo(index);
|
|
343
|
+
};
|
|
335
344
|
|
|
336
345
|
// 精简分页点(最多显示5个)
|
|
337
346
|
var dots = [];
|
|
@@ -352,7 +361,8 @@ for (var i = dotStart; i < dotEnd; i++) {
|
|
|
352
361
|
transition: 'all 0.3s ease',
|
|
353
362
|
cursor: 'pointer',
|
|
354
363
|
}}
|
|
355
|
-
|
|
364
|
+
data-index={i}
|
|
365
|
+
onClick={handleDotClick}
|
|
356
366
|
/>
|
|
357
367
|
);
|
|
358
368
|
}
|
|
@@ -632,10 +642,10 @@ var handleLangSwitch = function() {
|
|
|
632
642
|
{lang.langSwitch}
|
|
633
643
|
</div>
|
|
634
644
|
|
|
635
|
-
// 导航按钮使用 lang
|
|
636
|
-
<button
|
|
645
|
+
// 导航按钮使用 lang 对象,button 必须绑定真实事件
|
|
646
|
+
<button onClick={handlePrev}>{lang.prev}</button>
|
|
637
647
|
<span ...>{lang.pageOf(state.currentIndex + 1, SLIDES.length)}</span>
|
|
638
|
-
<button
|
|
648
|
+
<button onClick={handleNext}>{lang.next}</button>
|
|
639
649
|
```
|
|
640
650
|
|
|
641
651
|
> **提示**:如果幻灯片内容本身也需要中英文,可以在 `SLIDES` 数组中为每个 slide 提供 `title_en`、`subtitle_en` 等字段,在 `renderSlideContent` 中根据 `state.lang` 选择对应文案。
|
|
@@ -1348,7 +1358,7 @@ export function renderJsx() {
|
|
|
1348
1358
|
#### dark-tech 注意事项
|
|
1349
1359
|
|
|
1350
1360
|
- 🚨 **禁止 `import`/`require`**:文件顶部不能有任何 import 语句,宜搭沙箱不支持
|
|
1351
|
-
-
|
|
1361
|
+
- **事件绑定必须是真实函数**:可以用 `onClick={handleNext}` 或 `onClick={function() { self.changeSlide(1); }}`;禁止 `onClick={self.changeSlide(1)}` 这种渲染期调用,禁止 JSX 小写 `onclick`(ECharts `graphic.onclick` 不是 JSX 属性,可以保留)
|
|
1352
1362
|
- **禁止 ES6 计算属性名**:`{ [key]: value }` 改为 `var obj = {}; obj[key] = value;`
|
|
1353
1363
|
- **Canvas 初始化延迟**:`setTimeout(() => { self.initParticles(); }, 500)` 确保 DOM 就绪
|
|
1354
1364
|
- **`WebkitBackdropFilter`** 必须与 `backdropFilter` 同时写,兼容 Safari
|
|
@@ -23,8 +23,9 @@ openyida create-page APP_DEMO123 "产品路演2026"
|
|
|
23
23
|
# Step 4:编写幻灯片代码(见下方代码示例)
|
|
24
24
|
# 输出到 project/pages/src/product-ppt.js
|
|
25
25
|
|
|
26
|
-
# Step 5
|
|
27
|
-
openyida
|
|
26
|
+
# Step 5:校验并发布页面
|
|
27
|
+
openyida check-page project/pages/src/product-ppt.js
|
|
28
|
+
openyida publish project/pages/src/product-ppt.js APP_DEMO123 FORM-PPT001 --health-check
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
### 输出
|
|
@@ -147,7 +148,7 @@ var THEME_CONFIG = {
|
|
|
147
148
|
### 生命周期与事件绑定
|
|
148
149
|
|
|
149
150
|
```javascript
|
|
150
|
-
// ✅ 使用 didMount
|
|
151
|
+
// ✅ 使用 didMount 注册键盘事件(宜搭运行时会在页面挂载后调用)
|
|
151
152
|
export function didMount() {
|
|
152
153
|
var self = this;
|
|
153
154
|
_customState.total = SLIDES.length;
|
|
@@ -368,7 +369,7 @@ export function renderJsx() {
|
|
|
368
369
|
|
|
369
370
|
### 注意事项
|
|
370
371
|
|
|
371
|
-
-
|
|
372
|
+
- 事件处理函数推荐在 `renderJsx` 顶部定义;JSX 中可以直接引用 handler,或用不依赖 `this` 的小包装函数调用 handler。禁止 `onClick={foo()}` 渲染期调用、禁止 JSX 小写 `onclick`
|
|
372
373
|
- 必须在 `didUnmount` 中清理所有事件监听(键盘、鼠标移动、全屏变化、hash 变化、数字键定时器),防止内存泄漏
|
|
373
374
|
- 使用 `position: fixed` 覆盖宜搭默认容器样式
|
|
374
375
|
- 图片使用 `objectFit: 'contain'` 确保完整显示
|