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.
Files changed (51) hide show
  1. package/README.md +5 -1
  2. package/bin/yida.js +7 -1
  3. package/lib/app/app-list.js +20 -1
  4. package/lib/app/check-page.js +2 -2
  5. package/lib/app/compile.js +3 -2
  6. package/lib/app/externalize-form.js +642 -0
  7. package/lib/app/import-app.js +39 -11
  8. package/lib/app/page-compat.js +258 -2
  9. package/lib/app/page-compiler.js +4 -1
  10. package/lib/app/page-linter.js +271 -0
  11. package/lib/app/publish.js +3 -2
  12. package/lib/auth/cdp-browser-login.js +7 -3
  13. package/lib/auth/login.js +2 -3
  14. package/lib/core/command-manifest.js +3 -0
  15. package/lib/core/copy.js +50 -8
  16. package/lib/core/env-manager.js +24 -16
  17. package/lib/core/locales/ar.js +7 -0
  18. package/lib/core/locales/de.js +7 -0
  19. package/lib/core/locales/en.js +7 -0
  20. package/lib/core/locales/es.js +7 -0
  21. package/lib/core/locales/fr.js +7 -0
  22. package/lib/core/locales/hi.js +7 -0
  23. package/lib/core/locales/ja.js +7 -0
  24. package/lib/core/locales/ko.js +7 -0
  25. package/lib/core/locales/pt.js +7 -0
  26. package/lib/core/locales/vi.js +7 -0
  27. package/lib/core/locales/zh-HK.js +7 -0
  28. package/lib/core/locales/zh.js +7 -0
  29. package/lib/core/utils.js +2 -2
  30. package/lib/process/configure-process.js +552 -20
  31. package/package.json +1 -1
  32. package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
  33. package/scripts/e2e-real/full-runner.js +257 -8
  34. package/scripts/e2e-real/skill-coverage.js +2 -2
  35. package/yida-skills/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-chart/SKILL.md +1 -1
  37. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  38. package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
  39. package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
  40. package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
  41. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
  42. package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
  43. package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
  44. package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
  45. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
  46. package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
  47. package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
  48. package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
  49. package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
  50. package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
  51. 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(function(file) {
121
- return this.uploadSingleAttachment(file);
122
- }.bind(this))).then(function(uploaded) {
121
+ Promise.all(files.map((file) => {
122
+ return self.uploadSingleAttachment(file);
123
+ })).then(function(uploaded) {
123
124
  var next = (_customState.attachments || []).concat(uploaded);
124
- this.setCustomState({ attachments: next, uploading: false });
125
- this.utils.toast({ title: '附件上传成功', type: 'success' });
126
- }.bind(this)).catch(function(error) {
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
- this.setCustomState({ uploading: false });
129
- this.utils.toast({ title: message, type: 'error' });
130
- }.bind(this));
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) => { this.handleAttachmentChange(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) => { this.removeAttachment(item.fileUuid); }}>删除</button>
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) => { this.submitForm(); }}>提交</button>
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) => { this.handleAttachmentChange(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、类组件绑定、无 `import/require`、通过 `this.utils.yida.*` 调用数据 API。
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
- this._customState = this._customState || {};
25
- this._customState.draft = this._customState.draft || {};
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={this._customState.keyword || ''}
35
- onCompositionStart={() => { this._isComposing = true; }}
35
+ defaultValue={_customState.keyword || ''}
36
+ onCompositionStart={(e) => { self._isComposing = true; }}
36
37
  onCompositionEnd={(e) => {
37
- this._isComposing = false;
38
- this._customState.keyword = e.target.value;
38
+ self._isComposing = false;
39
+ _customState.keyword = e.target.value;
39
40
  }}
40
41
  onChange={(e) => {
41
- if (this._isComposing) { return; }
42
- this._customState.keyword = e.target.value;
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) => { this.setDraftField(FIELDS.name, e.target.value); }}
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) => { this.setDraftField(FIELDS.remark, e.target.value); }}
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
- const timestamp = new Date(`${value}T00:00:00`).getTime();
220
- return Number.isNaN(timestamp) ? '' : timestamp;
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={this.formatDateInput(record.formData && record.formData[FIELDS.planDate])}
228
- onChange={(e) => { this.setDraftField(FIELDS.planDate, this.dateInputToTimestamp(e.target.value)); }}
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
- const raw = e.target.value;
243
- this.setDraftField(FIELDS.amount, raw === '' ? '' : Number(raw));
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={(this._customState.filters && this._customState.filters.keyword) || ''}
326
+ defaultValue={(_customState.filters && _customState.filters.keyword) || ''}
318
327
  placeholder="搜索关键词"
319
328
  onChange={(e) => {
320
- this._customState.filters = this._customState.filters || {};
321
- this._customState.filters.keyword = e.target.value;
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={() => { this.loadRecords({ page: 1 }); }}
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),最少 5 个字段:
73
+ (a) 用 `yida-create-form-page` 建一张「看板派单触发表」(TodoTrigger),最少 6 个字段:
74
74
  subject(TextField) / executor(EmployeeField) / description(TextareaField) /
75
- dueTime(DateField) / priority(RadioField 10/20/30/40 固定值)
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:literal:10 // ⚠ 必须 literal 数字
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` 标记**:截图时"截图"按钮本身要被 html2canvas 排除
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),必须 `priority:literal:10` 字面量赋值,否则执行记录会报"一方连接器异常:接口参数异常"
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 5 个字段 ID 与集成自动化 `--connector-assignment` 的映射完全一致
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. 集成自动化执行记录是否有"执行异常 / 一方连接器异常:接口参数异常":若是 → **95% priority 没用 literal**,必须 `--connector-assignment priority:literal:10`(10=较低/20=普通/30=较高/40=最高)
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`,并在 `html2canvas` 调用时传 `ignoreElements: el => el.classList.contains('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
- **正确做法**:给截图按钮加标记 class,传 `ignoreElements`:
200
+ **正确做法**:截图按钮要同时满足两件事:有真实 `onClick`,并带 `sl-no-capture` 让 html2canvas 排除。如果只是视觉标签或占位,不要用 `<button>`,改用 `span/div`。
201
201
 
202
202
  ```javascript
203
- // 按钮加 class
204
- <button className="sl-no-capture" onClick={() => captureCard('chart-region')}>📷 截图</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
- - [ ] 截图按钮有 `sl-no-capture` class + `ignoreElements` 已配
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
- - 不要在 `renderJsx` 内部创建内联事件处理函数,必须在顶部定义后引用
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
- - 不要在 `componentWillUnmount` 中遗漏清理键盘/触摸事件监听,否则内存泄漏
13
+ - 不要在 `didUnmount` 中遗漏清理键盘/触摸事件监听,否则内存泄漏
14
14
  - 不要使用 `objectFit: 'cover'` 裁剪图片,必须用 `contain` 确保完整显示
15
15
  - 不要将幻灯片数据硬编码在 `renderJsx` 中,必须定义为顶层 `SLIDES` 数组
16
16
 
17
17
  ## 严格要求 (MUST DO)
18
18
 
19
19
  - **发布前必须确认**:执行发布操作前,必须向用户展示幻灯片配置摘要(页数、标题列表),获得用户明确同意后再发布
20
- - 必须在 `componentDidMount` 中注册键盘事件(含 `PageDown`/`PageUp` 演讲笔支持)
21
- - 必须在 `componentWillUnmount` 中清理所有事件监听
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
- | 键盘翻页无响应 | 确认在 `componentDidMount` 中注册了键盘事件,检查 `PageDown`/`PageUp` 支持 |
52
- | 内存泄漏(切换页面后事件仍触发) | 在 `componentWillUnmount` 中清理所有键盘/触摸事件监听 |
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] 发布页面 → openyida publish <文件> <appType> <formUuid>
162
+ [Step 5] 本地校验 → openyida check-page <文件>
160
163
 
161
- [Step 6] 配置公开访问(可选)→ openyida save-share-config
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(类组件模式,禁止使用 Hooks)
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
- // ✅ 使用 componentDidMount 初始化事件监听
262
- componentDidMount: function() {
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
- this.handleNext();
276
+ self.handleNext();
270
277
  } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
271
- this.handlePrev();
278
+ self.handlePrev();
272
279
  }
273
- }.bind(this);
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
- this._touchStartX = e.changedTouches[0].screenX;
281
- }.bind(this);
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 (this._touchStartX - touchEndX > 50) this.handleNext();
286
- if (touchEndX - this._touchStartX > 50) this.handlePrev();
287
- }.bind(this);
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
- // ✅ 使用 componentWillUnmount 清理事件监听,防止内存泄漏
294
- componentWillUnmount: function() {
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
- handleNext: function() {
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
- handlePrev: function() {
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
- handleGoTo: function(index) {
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 handleDotClick = function(idx) {
333
- return function() { this.handleGoTo(idx); }.bind(this);
334
- }.bind(this);
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
- onClick={handleDotClick(i)}
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 ...>{lang.prev}</button>
645
+ // 导航按钮使用 lang 对象,button 必须绑定真实事件
646
+ <button onClick={handlePrev}>{lang.prev}</button>
637
647
  <span ...>{lang.pageOf(state.currentIndex + 1, SLIDES.length)}</span>
638
- <button ...>{lang.next}</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
- - **事件绑定用箭头函数**:`onClick={() => {}}` 不能用 `onClick={function() {}}`
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 publish project/pages/src/product-ppt.js APP_DEMO123 FORM-PPT001
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 注册键盘事件(由运行时映射到 React componentDidMount)
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
- - 事件处理函数必须在 `renderJsx` 顶部定义,不要在 JSX 内部创建内联函数
372
+ - 事件处理函数推荐在 `renderJsx` 顶部定义;JSX 中可以直接引用 handler,或用不依赖 `this` 的小包装函数调用 handler。禁止 `onClick={foo()}` 渲染期调用、禁止 JSX 小写 `onclick`
372
373
  - 必须在 `didUnmount` 中清理所有事件监听(键盘、鼠标移动、全屏变化、hash 变化、数字键定时器),防止内存泄漏
373
374
  - 使用 `position: fixed` 覆盖宜搭默认容器样式
374
375
  - 图片使用 `objectFit: 'contain'` 确保完整显示