h3factory 0.0.0-stage → 0.2.2

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 (37) hide show
  1. package/README.md +241 -2
  2. package/bin/h3factory.js +87 -0
  3. package/cli.py +951 -0
  4. package/config.example.json +8 -0
  5. package/docs/platform_gotchas.md +118 -0
  6. package/docs/requirement_checklist.md +54 -0
  7. package/docs/schema_doc.md +216 -0
  8. package/docs/workflow.md +146 -0
  9. package/h3design/__init__.py +0 -0
  10. package/h3design/controls.py +414 -0
  11. package/h3design/dsl.py +553 -0
  12. package/h3design/newbuilder.py +852 -0
  13. package/h3platform/__init__.py +0 -0
  14. package/h3platform/api.py +214 -0
  15. package/h3platform/h3console.py +99 -0
  16. package/h3platform/settings.py +74 -0
  17. package/h3verify/__init__.py +0 -0
  18. package/h3verify/verify.py +217 -0
  19. package/package.json +51 -4
  20. package/projects/demo/dicts.json +10 -0
  21. package/projects/demo/generated/product.cs +82 -0
  22. package/projects/demo/groups.json +3 -0
  23. package/projects/demo/sheets/product.json +24 -0
  24. package/scripts/postinstall.js +109 -0
  25. package/skills/h3factory/SKILL.md +137 -0
  26. package/skills/h3yun-dev-assistant/BACKEND_GUIDELINES.md +120 -0
  27. package/skills/h3yun-dev-assistant/DEV_DOC_ROUTING.md +39 -0
  28. package/skills/h3yun-dev-assistant/FORM_ACTIONS_GUIDELINES.md +116 -0
  29. package/skills/h3yun-dev-assistant/FRONTEND_GUIDELINES.md +47 -0
  30. package/skills/h3yun-dev-assistant/OPENAPI_GUIDELINES.md +8 -0
  31. package/skills/h3yun-dev-assistant/PLATFORM_CONSTRAINTS.md +160 -0
  32. package/skills/h3yun-dev-assistant/POST_GUIDELINES.md +47 -0
  33. package/skills/h3yun-dev-assistant/PRACTICE_NOTES.md +626 -0
  34. package/skills/h3yun-dev-assistant/SKILL.md +70 -0
  35. package/skills/h3yun-dev-assistant/SQL_GUIDELINES.md +9 -0
  36. package/skills/h3yun-dev-assistant/TOOL_FUNCTIONS.md +96 -0
  37. package/templates/controller.cs.tpl +78 -0
@@ -0,0 +1,852 @@
1
+ # -*- coding: utf-8 -*-
2
+ """新引擎 SaveForm 载荷构造:从设计态控件列表生成 SchemaStr/BizSheetStr。
3
+ 模板逐字对照 UI 真实保存载荷(logs/ui_save_types_payload.json,7 类控件全覆盖)。
4
+ 字段编码用自定义语义短码(rulename/seqlen 等,已实证可持久化)。"""
5
+ import datetime
6
+ import json
7
+ import uuid
8
+
9
+ from . import controls as C
10
+
11
+ # ---- SchemaStr.Properties 的 ControlKey 数值(权威:UI 保存载荷逐字)----
12
+ NEW_CTL = {
13
+ "FormTextBox": 14,
14
+ "FormNumber": 7,
15
+ "FormTextArea": 13,
16
+ "FormDateTime": 5,
17
+ "FormDropDownList": 14, # 下拉/单选/复选与文本框同为字符串存储
18
+ "FormRadioButtonList": 14,
19
+ "FormCheckboxList": 14,
20
+ "FormCheckbox": 1, # 开关(是/否)布尔
21
+ "FormUser": 26, # 26=单选(人员/部门同值,v1 读回实证)
22
+ "FormSeqNo": 14, # 流水号:字符串存储,线上样本 F=14(2026-09-11)
23
+ "FormPhoto": 23, # 图片(类型试验线上样本逐字)
24
+ "FormAttachment": 24, # 附件
25
+ "FormMap": 55, # 位置
26
+ "FormAreaSelect": 56, # 地址
27
+ "FormQuery": 50,
28
+ "FormMultiQuery": 51,
29
+ "FormMultiUser": 27, # 27=多选(人员/部门同值)
30
+ "FormDepartment": 26,
31
+ "FormMultiDepartment": 27,
32
+ "FormGridView": 41, # 子表主属性(key=child schema 编码;子列=标准存储类型)
33
+ }
34
+
35
+ # SchemaStr.Properties 每字段的公共空键(含类型字段名,类型特有键在 EXTRA/下方映射)
36
+ EMPTY_STD = ["AssociationSchemaCode", "IsRelatedMember", "DataDictItemName",
37
+ "OptionalValues", "DateTimeMode", "ComputationRule", "DisplayRule",
38
+ "AssociationFilter", "UnitSelectionRange", "DefaultValue",
39
+ "MappingField", "IsFormula", "IsRollup", "LocationRangeLimit",
40
+ "Summary"]
41
+
42
+ EXTRA_BY_TYPE = {
43
+ "FormTextBox": {"NoRepeat": False},
44
+ "FormNumber": {"ShowMode": "0", "RollupSettings": [], "DecimalPlaces": -1,
45
+ "DataFormatType": "0", "ShowBarChart": False},
46
+ "FormCheckbox": {"DefaultValue": "false"},
47
+ "FormDropDownList": {"SelectShowMode": 0},
48
+ "FormRadioButtonList": {"SelectShowMode": 0},
49
+ "FormCheckboxList": {"SelectShowMode": "0"},
50
+ }
51
+
52
+ # 新控件(流水号/图片/附件/位置/地址)的 Properties **少几个公共键** ——
53
+ # 逐字对照线上样本(logs/types7_ui_save_payload.json,2026-09-11 类型试验)
54
+ PROP_DROP_KEYS = {
55
+ "FormSeqNo": ("ComputationRule", "DisplayRule"),
56
+ "FormPhoto": ("ComputationRule",),
57
+ "FormAttachment": ("ComputationRule",),
58
+ "FormMap": ("ComputationRule", "LocationRangeLimit"),
59
+ "FormAreaSelect": ("ComputationRule",),
60
+ }
61
+ # 这些类型 DefaultValue 线上是空串(文本框/选项类的 None 语义不适用)
62
+ PROP_EMPTY_DEFAULT = ("FormSeqNo", "FormPhoto", "FormAttachment", "FormMap")
63
+
64
+
65
+ def is_field(ctl):
66
+ """是否为**字段控件**:布局项(分组标题 101 / 描述 103)不进 SchemaStr.Properties。
67
+ 以 ControlKey 判定(分组标题线上根本不带 ControlKey)。"""
68
+ ck = (ctl.get("Options") or {}).get("ControlKey")
69
+ return ck not in ("FormGroupTitle", "FormDescription")
70
+
71
+
72
+ def _items_json(opt):
73
+ items = opt.get("DefaultItems") or []
74
+ return json.dumps(items, ensure_ascii=False) if items else ""
75
+
76
+
77
+ def prop_entry(ctl, assoc_schema_code="", assoc_schema_info=None, assoc_fields=None,
78
+ app_package=""):
79
+ """SchemaStr.Properties[code] 条目。ctl 为设计态控件 {Key, Options{...}}"""
80
+ opt = ctl.get("Options") or {}
81
+ ckey = opt.get("ControlKey")
82
+ e = {"ControlKey": NEW_CTL.get(ckey, 14),
83
+ "DisplayName": opt.get("DisplayName", "")}
84
+ for k in EMPTY_STD:
85
+ e[k] = ""
86
+ e["IsRelatedMember"] = False
87
+ e["IsFormula"] = False
88
+ e["IsRollup"] = False
89
+ e["LocationRangeLimit"] = None
90
+ e.update(EXTRA_BY_TYPE.get(ckey, {}))
91
+ if ckey == "FormTextBox" and opt.get("NoRepeat") is not None:
92
+ e["NoRepeat"] = opt["NoRepeat"]
93
+ if ckey == "FormDateTime":
94
+ e["DateTimeMode"] = opt.get("DateTimeMode", "")
95
+ if ckey == "FormCheckbox":
96
+ # UI 样本:无勾选发 "false";勾选态发 "true"
97
+ e["DefaultValue"] = opt.get("DefaultValue") or "false"
98
+ if ckey in ("FormDropDownList", "FormRadioButtonList", "FormCheckboxList"):
99
+ if ckey == "FormDropDownList" and opt.get("DataSource") == "Association":
100
+ # 联动下拉(选项来自另一张表):Properties 只落关联三键 + 干净空选项。
101
+ # 逐字对照线上样本 F0000021 —— 无 DataSource/BOSchemaInfo/AssociationFields 键;
102
+ # AssociationFilter 在此侧是纯文本(FormLayout options 侧才是 {Rule:..} 字典)
103
+ e["AssociationSchemaCode"] = opt.get("BOSchemaCode", "")
104
+ e["MappingField"] = opt.get("MappingField", "")
105
+ af = opt.get("AssociationFilter") or {}
106
+ e["AssociationFilter"] = (af.get("Rule", "") if isinstance(af, dict)
107
+ else (str(af) if af else ""))
108
+ e["OptionalValues"] = "" # 占位选项只发生在 UI 改型,工厂直建不带
109
+ e["DefaultValue"] = None
110
+ else:
111
+ e["OptionalValues"] = _items_json(opt)
112
+ dv = opt.get("DefaultValue")
113
+ # UI 语义:复选无默认值=""/null,下拉/单选默认 null
114
+ e["DefaultValue"] = dv if dv else ("" if ckey == "FormCheckboxList" else None)
115
+ if ckey in PROP_EMPTY_DEFAULT:
116
+ e["DefaultValue"] = ""
117
+ if ckey == "FormAreaSelect":
118
+ # 地址:Properties 侧 DefaultValue 是 JSON 串 + 多一个 AreaMode 键
119
+ dv = opt.get("DefaultValue")
120
+ e["DefaultValue"] = json.dumps(dv, ensure_ascii=False,
121
+ separators=(",", ":")) \
122
+ if isinstance(dv, dict) else (dv or "")
123
+ e["AreaMode"] = opt.get("AreaMode", "P-C-T")
124
+ if ckey == "FormQuery" and assoc_schema_code:
125
+ e["AssociationSchemaCode"] = assoc_schema_code
126
+ e["AssociationSchemaInfo"] = assoc_schema_info or json.dumps(
127
+ {"AppPackage": app_package, "AppGroup": "",
128
+ "AppMenu": assoc_schema_code, "IsChildSchema": False},
129
+ ensure_ascii=False, separators=(",", ":"))
130
+ e["AssociationFields"] = assoc_fields or json.dumps(
131
+ {"isDefault": True, "fieldSetting": []}, ensure_ascii=False,
132
+ separators=(",", ":"))
133
+ rule = opt.get("DisplayRule")
134
+ if isinstance(rule, dict) and rule.get("Rule"):
135
+ # Properties.DisplayRule = 规则文本(条件串),如 {probe}=="xxx"
136
+ e["DisplayRule"] = rule["Rule"]
137
+ for k in PROP_DROP_KEYS.get(ckey, ()): # 键集差异:线上样本没有这些键
138
+ e.pop(k, None)
139
+ return e
140
+
141
+
142
+ def _sub_prop(sub):
143
+ """子表主条目(ControlKey 41)。逐字对照 UI 保存样本:
144
+ {'ControlKey':41,'DisplayName':...,AssociationSchemaCode/IsRelatedMember/
145
+ DataDictItemName/OptionalValues/DateTimeMode/DisplayRule/AssociationFilter/
146
+ UnitSelectionRange/NameItems/MappingField/ShowFixedCol/FixedColNum/
147
+ IsFormula/Summary} —— 无 ComputationRule 等普通字段键。"""
148
+ return {
149
+ "ControlKey": 41,
150
+ "DisplayName": sub.get("label", ""),
151
+ "AssociationSchemaCode": "",
152
+ "IsRelatedMember": False,
153
+ "DataDictItemName": "",
154
+ "OptionalValues": "",
155
+ "DateTimeMode": "",
156
+ "DisplayRule": "",
157
+ "AssociationFilter": "",
158
+ "UnitSelectionRange": "",
159
+ "NameItems": "",
160
+ "MappingField": "",
161
+ "ShowFixedCol": False,
162
+ "FixedColNum": sub.get("fixed", 1),
163
+ "IsFormula": False,
164
+ "Summary": "",
165
+ }
166
+
167
+
168
+ def seqno_settings(controls):
169
+ """表单级流水号设置(SchemaStr 顶层)—— 由 FormSeqNo 控件推导。
170
+ 线上样本(类型试验 2026-09-11):EnableSeqNo=true、SeqNoDateFormat=YYYY、
171
+ SeqNoPrefix=""、SeqNoLength=12(= 前缀长 + 日期段长 + 序号位数)、
172
+ SeqNoStructure 是 JSON **字符串**:[{Type:1,Value:"YYYY"},{Type:2,IncreNum:8,Value:"1"}]。
173
+ 无流水号控件 → 全空(平台按未开启处理)。"""
174
+ for c in controls:
175
+ o = c.get("Options") or {}
176
+ if o.get("ControlKey") != "FormSeqNo":
177
+ continue
178
+ mode = str(o.get("DateTimeMode") or "")
179
+ prefix = str(o.get("Prefix") or "")
180
+ try:
181
+ inc = int(o.get("IncrementNum") or 0)
182
+ except (TypeError, ValueError):
183
+ inc = 0
184
+ struct = json.dumps(o.get("SeqNoStructure") or [], ensure_ascii=False,
185
+ separators=(",", ":"))
186
+ return True, mode, prefix, len(prefix) + len(mode) + inc, struct
187
+ return False, "", "", "", ""
188
+
189
+
190
+ def build_schema_str(controls, name_schema, enable_data_acl=True,
191
+ enable_seqno=None, enable_form_sns=True, enable_task=False,
192
+ enable_log=False, assoc=None, app_package="", subs=None):
193
+ """enable_seqno=None(缺省)→ 按 controls 里的流水号控件自动推导;显式 True/False 覆盖。"""
194
+ props = {}
195
+ for c in controls:
196
+ if not is_field(c): # 分组标题/描述:只占版面,不进 Properties
197
+ continue
198
+ a = (assoc or {}).get(c["Key"], {})
199
+ props[c["Key"]] = prop_entry(c, app_package=app_package, **a)
200
+ for s in subs or []:
201
+ # 子表主条目:key = child schema 编码(40 位:appCode前7位+F+32hex)。
202
+ # 键集逐字对照 UI 保存样本(无 ComputationRule/DefaultValue/IsRollup/LocationRangeLimit)
203
+ code = s.get("code")
204
+ if not code:
205
+ raise ValueError("子表 %r 缺注册编码(registry subs 未预注册,先 build)"
206
+ % s.get("key"))
207
+ props[code] = _sub_prop(s)
208
+ # 子列条目:key = <childcode>.<列编码>,标准字段键集 + ParentKey
209
+ for col in s["cols"]:
210
+ k = "%s.%s" % (code, col["Key"])
211
+ e = prop_entry(col, app_package=app_package)
212
+ e["ParentKey"] = code
213
+ props[k] = e
214
+ seq_on, seq_mode, seq_prefix, seq_len, seq_struct = seqno_settings(controls)
215
+ if enable_seqno is not None:
216
+ seq_on = bool(enable_seqno)
217
+ return {
218
+ "RemovedControls": [],
219
+ "NameSchema": name_schema,
220
+ "EnableDataAcl": enable_data_acl,
221
+ "EnableSeqNo": seq_on,
222
+ "EnableFormSNS": enable_form_sns,
223
+ "EnableTask": enable_task,
224
+ "EnableLog": enable_log,
225
+ "SeqNoDateFormat": seq_mode,
226
+ "SeqNoPrefix": seq_prefix,
227
+ "SeqNoLength": seq_len,
228
+ "DataAclInheritedFrom": "",
229
+ "AssociationMappings": {},
230
+ "SeqNoStructure": seq_struct,
231
+ "Properties": props,
232
+ }
233
+
234
+
235
+ def build_control_settings(controls):
236
+ """载荷顶层 ControlSettingsStr(**独立于 SchemaStr/BizSheetStr 的第三段**)。
237
+
238
+ 只有位置控件(FormMap)在这里落配置:Meters/LocationPCEnabled/LocationEditable
239
+ 在 FormLayout 与 Properties 里都没有键位。Controls 里 control["ControlSettings"]
240
+ 存这三键(controls.location() 生成)。样本逐字对照:
241
+ [{"key":"F0000024","dataField":"F0000024","options":{"LocationPCEnabled":"true",
242
+ "Meters":"500","LocationEditable":"false"}}](值全是字符串)。"""
243
+ out = []
244
+ for c in controls:
245
+ cs = c.get("ControlSettings")
246
+ if cs:
247
+ out.append({"key": c["Key"], "dataField": c["Key"],
248
+ "options": dict(cs)})
249
+ return json.dumps(out, ensure_ascii=False, separators=(",", ":"))
250
+
251
+
252
+ # =====================================================================
253
+ # HTML(DesignModeContent / RuntimeContent)
254
+ # =====================================================================
255
+ def hq(s):
256
+ """HTML 属性值转义:双引号 → &quot;(curl 样本风格)"""
257
+ return json.dumps(s, ensure_ascii=False)[1:-1].replace('"', '&quot;')
258
+
259
+
260
+ RULE = '{&quot;Rule&quot;:&quot;&quot;}'
261
+
262
+
263
+ def _esc(v):
264
+ return hq(v)
265
+
266
+
267
+ def _escj(v):
268
+ """HTML 属性里的 **JSON 串**转义:只把 & 和 " 变成实体,**不留反斜杠**
269
+ (对照线上:data-seqnostructure="[{&quot;Type&quot;:1,…}]")。
270
+ hq() 那种 json.dumps 内部转义会多出 \\&quot;,属性值会被读成字面反斜杠。"""
271
+ return str(v).replace("&", "&amp;").replace('"', "&quot;")
272
+
273
+
274
+ def _attrs(opt, key, extra_attrs="", rule=RULE, datafield=None, node_id=None):
275
+ """设计 HTML 通用头属性(类型特有属性由调用方附在 extra_attrs)。
276
+ node_id/datafield 供子表列使用:id=childcode-colcode(-),data-datafield=dotted
277
+ rule=None 表示**不带** data-displayrule(流水号线上就没有)。"""
278
+ idk = node_id if node_id is not None else key
279
+ dfk = datafield if datafield is not None else key
280
+ base = ('id="{i}" style="background-color: rgb(241, 250, 255);" '
281
+ 'data-datafield="{d}" data-displayname="{dn}" data-description="{de}" '
282
+ 'data-controlkey="{ck}" data-summary="{su}"').format(
283
+ i=idk, d=dfk, dn=_esc(opt.get("DisplayName", "")),
284
+ de=_esc(opt.get("Description", "")), ck=opt.get("ControlKey", ""),
285
+ su=_esc(opt.get("Summary", "")))
286
+ if rule is not None: # rule=None → 整个 data-displayrule 属性都不出现
287
+ base += ' data-displayrule="%s"' % rule
288
+ return base + extra_attrs
289
+
290
+
291
+ def _row_open():
292
+ return '<div class="row sheet-control" '
293
+
294
+
295
+ def _row_close(label, inner, remove=True, span_cls="col-sm-2", box_cls="col-sm-10"):
296
+ rm = (' <button class="btn btn-default btn-xs remove" style="display: none;" '
297
+ 'type="button" data-buttontype="remomvecontrol">'
298
+ '<span class="icon-sheet icon-delete"></span>\n </button>') if remove else ""
299
+ return (' <span class="%s">%s</span>\n'
300
+ ' <div class="%s">\n%s\n </div>\n%s'
301
+ '</div>') % (span_cls, label, box_cls, inner, rm)
302
+
303
+
304
+ def _chk_items(ctl, tag):
305
+ """单选/复选/开关的选项 HTML;id 用 uuid(与 UI 一致)。tag: radio/checkbox"""
306
+ opt = ctl["Options"]
307
+ parts = []
308
+ for it in (opt.get("DefaultItems") or []):
309
+ uid = str(uuid.uuid4())
310
+ parts.append('<input type="%s" id="%s" disabled="disabled" data-itemid="%s">'
311
+ '<label for="%s">%s</label>'
312
+ % (tag, uid, uid, uid, _esc(it)))
313
+ return '<div class="row notdrag">%s</div>' % "".join(parts)
314
+
315
+
316
+ def _rule_attr(opt):
317
+ """data-displayrule 属性值:空规则用 RULE 常量,带条件则 JSON 转义"""
318
+ rule = opt.get("DisplayRule")
319
+ if isinstance(rule, dict) and rule.get("Rule"):
320
+ return hq(json.dumps({"Rule": rule["Rule"]}, ensure_ascii=False))
321
+ return RULE
322
+
323
+
324
+ def _bool(v):
325
+ """HTML 属性里的布尔值(线上写小写 true/false)"""
326
+ return "true" if v else "false"
327
+
328
+
329
+ # 布局项(分组标题/描述)的删除按钮:分组标题用 icon-remove-block,描述用 icon-delete
330
+ _BTN_GROUP = ('<button type="button" data-buttontype="remomvecontrol" '
331
+ 'class="btn btn-default btn-xs remove" style="display: none;">'
332
+ '<span class="icon-sheet icon-remove-block"></span></button>')
333
+ _BTN_DESC = ('<button type="button" data-buttontype="remomvecontrol" '
334
+ 'class="btn btn-default btn-xs remove" style="display: none;">'
335
+ '<span class="icon-sheet icon-delete"></span></button>')
336
+
337
+
338
+ def seqno_preview(opt):
339
+ """流水号输入框的占位预览(设计器同款):前缀 + 当前日期段 + 补零后的序号。
340
+ 线上样本 YYYY/8 位 → "202600000001"(2026 + 00000001)。"""
341
+ prefix = opt.get("Prefix") or ""
342
+ mode = (opt.get("DateTimeMode") or "").upper()
343
+ now = datetime.datetime.now()
344
+ part = ""
345
+ for tok, fmt in (("YYYY", "%Y"), ("MM", "%m"), ("DD", "%d"),
346
+ ("HH", "%H"), ("MI", "%M"), ("SS", "%S")):
347
+ if tok in mode:
348
+ part += now.strftime(fmt)
349
+ try:
350
+ inc = int(opt.get("IncrementNum") or 0)
351
+ except (TypeError, ValueError):
352
+ inc = 0
353
+ return prefix + part + str(1).zfill(inc)
354
+
355
+
356
+ def _group_title_html(control, runtime=False):
357
+ """分组标题(type 101):设计态 class="row page-header",运行态去掉 row(线上同款)。
358
+ 注意整段**没有** data-datafield 等属性——线上就是纯 div + strong。"""
359
+ opt = control["Options"]
360
+ cls = " page-header" if runtime else "row page-header"
361
+ return ('<div class="{cls}" id="{k}" style="position: relative; left: 0px; '
362
+ 'top: 0px;background-color: rgb(241, 250, 255); text-align:{al};">'
363
+ '<strong>{t}</strong>{btn}</div>').format(
364
+ cls=cls, k=control["Key"], al=_esc(opt.get("Alignment", "left")),
365
+ t=_esc(opt.get("Title", "")), btn=_BTN_GROUP)
366
+
367
+
368
+ def _description_html(control, runtime=False):
369
+ """描述/说明文字(type 103):Content 是富文本 HTML,原样嵌入。"""
370
+ opt = control["Options"]
371
+ cls = " page-header page-describle" if runtime \
372
+ else "row page-header page-describle"
373
+ return ('<div class="{cls}" id="{k}" '
374
+ 'style="background-color: rgb(241, 250, 255);"><div>{c}</div>{btn}'
375
+ '</div>').format(cls=cls, k=control["Key"],
376
+ c=opt.get("Content", ""), btn=_BTN_DESC)
377
+
378
+
379
+ def _new_runtime_attrs(control, ck):
380
+ """新控件(流水号/图片/附件/位置/地址)运行态 HTML 的 data-* 属性串,
381
+ 用线上那套 `data-x = "v"` 写法(设计态见 design_html 里同名分支)。
382
+ 流水号的 SeqNoStructure 线上运行态写的是 JS 串 "[object Object],…"(设计器
383
+ 的字符串拼接瑕疵)——这里发真正的 JSON:运行态 HTML 是渲染模板,两处都只是
384
+ 装饰(回读的结构以 FormLayout 为准)。"""
385
+ opt = control["Options"]
386
+ sp = " "
387
+ eq = " = "
388
+ j = lambda k, v: "%sdata-%s%s\"%s\"%s" % (sp, k, eq, v, sp) # noqa: E731
389
+ out = ""
390
+ if ck == "FormSeqNo":
391
+ # 结构串运行态线上是 JS 拼接瑕疵 "[object Object],[object Object]"
392
+ # (设计态才是真 JSON)—— 运行态 HTML 只是渲染模板,逐字照抄线上
393
+ n_seg = len(opt.get("SeqNoStructure") or [])
394
+ out = (j("prefix", _esc(opt.get("Prefix", "")))
395
+ + j("datetimemode", _esc(opt.get("DateTimeMode", "")))
396
+ + j("seqnostructure", ",".join(["[object Object]"] * n_seg))
397
+ + j("incrementnum", opt.get("IncrementNum", 0)).rstrip())
398
+ elif ck == "FormPhoto":
399
+ out = (j("uploadmultiple", _bool(opt.get("UploadMultiple")))
400
+ + j("cameraonly", _bool(opt.get("CameraOnly")))
401
+ + j("haswatermark", _bool(opt.get("HasWatermark")))
402
+ + j("compression", _bool(opt.get("Compression")))).rstrip()
403
+ elif ck == "FormAttachment":
404
+ out = j("maxuploadsize", opt.get("MaxUploadSize", 10)).rstrip()
405
+ elif ck == "FormMap":
406
+ cs = control.get("ControlSettings") or {}
407
+ out = (j("range", _esc(opt.get("Range", "0")))
408
+ + j("meters", _esc(cs.get("Meters", "500")))
409
+ + j("locationrangelimit", "undefined")
410
+ + j("locationpcenabled", _esc(cs.get("LocationPCEnabled", "true")))
411
+ + j("locationeditable", _esc(cs.get("LocationEditable", "false")))
412
+ ).rstrip()
413
+ elif ck == "FormAreaSelect":
414
+ # 注意 defaultvalue 线上**没有** = 两侧空格(设计器这一条是拼串加的),照抄
415
+ out = (j("areamode", _esc(opt.get("AreaMode", "P-C-T")))
416
+ + j("showdetailaddr", _bool(opt.get("ShowDetailAddr")))
417
+ + 'data-defaultvalue="%s"' % _escj(json.dumps(
418
+ opt.get("DefaultValue") or {}, ensure_ascii=False,
419
+ separators=(",", ":")))).rstrip()
420
+ return (" " + out) if out else ""
421
+
422
+
423
+ def _member_dept_html(control):
424
+ """成员/部门(单/多选)。对照 UI 真实载荷 logs/loadform_userdept.json"""
425
+ opt = control["Options"]
426
+ ck = opt.get("ControlKey")
427
+ key = control["Key"]
428
+ label = opt.get("DisplayName", "")
429
+ is_user = ck in ("FormUser", "FormMultiUser")
430
+ is_dept = not is_user
431
+ ex = (' data-unitselectionrange="" '
432
+ 'data-isrelatedmember="false" data-showunactive="false" '
433
+ 'data-usedatacache="true" data-currentuserid="false" '
434
+ 'data-mappingcontrols="" data-orgunitvisible="{ov}" '
435
+ 'data-uservisible="{uv}" data-owndepvisible="{od}" '
436
+ 'data-owndepusersvisble="{odu}" data-ismultiple="{im}"').format(
437
+ ov="true" if is_dept else "false",
438
+ uv="false" if is_dept else "true",
439
+ od="true" if is_dept else "false",
440
+ odu="false" if is_dept else "true",
441
+ im="true" if opt.get("IsMultiple") else "false")
442
+ if is_user:
443
+ ex += ' data-showcuruser="false"'
444
+ ex += (' data-defaultvaluerule="" data-datalinkschemacode="" '
445
+ 'data-datalinkschema="{{}}" data-datalinkcondition="[]" '
446
+ 'data-datalinkresult=""')
447
+ ph = "点击选择人员" if is_user else "点击选择部门"
448
+ inner = ('<input class="notdrag" style="width: 100%%;" type="text" '
449
+ 'readonly="" placeholder="%s">') % ph
450
+ return (_row_open() + _attrs(opt, key, ex, rule=_rule_attr(opt))
451
+ + " >\n" + _row_close(label, inner))
452
+
453
+
454
+ def design_html(control, node_id=None, datafield=None, wide=False):
455
+ """逐类型复刻 UI 设计 HTML(见 logs/ui_save_types_payload.json BizSheetStr)。
456
+ node_id/datafield/wide:子表列渲染用 —— id=childcode-colcode、datafield=dotted、
457
+ 标签与输入区全宽(col-sm-12,UI 子表单元格样本)。"""
458
+ opt = control["Options"]
459
+ ck = opt.get("ControlKey")
460
+ key = node_id or control["Key"]
461
+ df = datafield or control["Key"]
462
+ label = opt.get("DisplayName", "")
463
+ di = _esc(json.dumps(opt.get("DefaultItems", []), ensure_ascii=False))
464
+
465
+ if control.get("Type") == C.T_GROUP_TITLE: # 布局项:自带 HTML,无 data-* 头
466
+ return _group_title_html(control)
467
+ if control.get("Type") == C.T_DESCRIPTION:
468
+ return _description_html(control)
469
+ if ck in ("FormUser", "FormMultiUser", "FormDepartment",
470
+ "FormMultiDepartment"):
471
+ return _member_dept_html(control)
472
+ if ck == "FormTextBox":
473
+ ex = (' data-placeholder="{ph}" data-computationrule="{cr}" '
474
+ 'data-defaultvalueruletype="2" data-datalinkschemacode="" '
475
+ 'data-datalinkschema="{{}}" data-datalinkcondition="[]" '
476
+ 'data-datalinkresult="" data-datalinkresultfunction="" '
477
+ 'data-mode="Normal" data-inputbyscan="false" '
478
+ 'data-scanupdateenable="false" data-norepeat="false" '
479
+ 'data-norepeatrange="0" data-norepeattipmessage="{tip}" '
480
+ 'data-defaultitems="{di}" data-voiceinput="false"').format(
481
+ ph=_esc(opt.get("PlaceHolder", "")), cr=RULE,
482
+ tip=_esc(opt.get("NoRepeatTipMessage", "")), di=di)
483
+ inner = ('<input type="text" readonly="" class="notdrag" '
484
+ 'style="width: 100%%;" placeholder="%s">') % _esc(opt.get("PlaceHolder", ""))
485
+ elif ck == "FormNumber":
486
+ ex = (' data-placeholder="" data-computationrule="{cr}" '
487
+ 'data-defaultvalueruletype="2" data-datalinkschemacode="" '
488
+ 'data-datalinkschema="{{}}" data-datalinkcondition="[]" '
489
+ 'data-datalinkresult="" data-datalinkresultfunction="" '
490
+ 'data-decimalplaces="{dp}" data-showmode="0" data-percentage="false" '
491
+ 'data-showbarchart="false" data-dataformattype="0" '
492
+ 'data-rollupsettings=""').format(cr=RULE,
493
+ dp=opt.get("DecimalPlaces", 0))
494
+ inner = ('<input readonly="" class="notdrag" style="width: 100%;" '
495
+ 'type="text">')
496
+ elif ck == "FormTextArea":
497
+ ex = (' data-placeholder="{ph}" data-computationrule="{cr}" '
498
+ 'data-defaultvalueruletype="2" data-datalinkschemacode="" '
499
+ 'data-datalinkschema="{{}}" data-datalinkcondition="[]" '
500
+ 'data-datalinkresult="" data-datalinkresultfunction="" '
501
+ 'data-rows="{rows}" data-openrichtext="false" '
502
+ 'data-voiceinput="false"').format(
503
+ ph=_esc(opt.get("PlaceHolder", "")), cr=RULE,
504
+ rows=opt.get("Rows", 3))
505
+ inner = ('<textarea class="notdrag" rows="%s" style="resize:none; '
506
+ 'width:100%%;border:1px solid #ddd; " placeholder="%s">'
507
+ '</textarea>') % (opt.get("Rows", 3), _esc(opt.get("PlaceHolder", "")))
508
+ elif ck == "FormDateTime":
509
+ ex = (' data-width="100%" data-datetimemode="{dm}"').format(
510
+ dm=opt.get("DateTimeMode", "yyyy-mm-dd"))
511
+ ph = "年-月-日 时:分" if "hh:mm" in (opt.get("DateTimeMode", "") or "").lower() \
512
+ else "年-月-日"
513
+ inner = ('<input readonly="" class="notdrag" placeholder="%s" '
514
+ 'style="width: 100%%;" type="text">') % ph
515
+ elif ck == "FormDropDownList":
516
+ if opt.get("DataSource") == "Association":
517
+ # 联动下拉设计 HTML(对照线上样本 F0000021):datasource 切 Association、
518
+ # 关联键填真值;associationfilter 双层转义同 _rule_attr
519
+ af = opt.get("AssociationFilter") or {}
520
+ rule_txt = (af.get("Rule", "") if isinstance(af, dict) else (af or ""))
521
+ ex = (' data-transferitems="FormDropDownList" data-selectshowmode="0" '
522
+ 'data-datadictitemname="" data-defaultitems="{di}" '
523
+ 'data-defaultvalue="" data-colorswitch="false" '
524
+ 'data-itemcolors="" data-datasource="Association" '
525
+ 'data-boschemacode="{bsc}" data-boschemainfo="{info}" '
526
+ 'data-mappingfield="{mf}" data-associationfilter="{af}" '
527
+ 'data-mappingcontrols="" data-associationfields="{aaf}"').format(
528
+ di=di, bsc=hq(opt.get("BOSchemaCode", "")),
529
+ info=hq(opt.get("BOSchemaInfo", "")),
530
+ mf=hq(opt.get("MappingField", "")),
531
+ af=hq(json.dumps({"Rule": rule_txt}, ensure_ascii=False)),
532
+ aaf=hq(opt.get("AssociationFields", "")))
533
+ else:
534
+ dv = opt.get("DefaultValue") or ""
535
+ ex = (' data-transferitems="FormDropDownList" data-selectshowmode="0" '
536
+ 'data-datadictitemname="" data-defaultitems="{di}" '
537
+ 'data-defaultvalue="{dv}" data-colorswitch="false" '
538
+ 'data-itemcolors="" data-datasource="Custom" '
539
+ 'data-boschemacode="" data-boschemainfo="" data-mappingfield="" '
540
+ 'data-associationfilter="{af}"').format(
541
+ di=di, dv=_esc(dv), af=RULE)
542
+ inner = ('<select class="form-control form-group-margin notdrag" '
543
+ 'style="width: 100%%;"></select>')
544
+ elif ck == "FormRadioButtonList":
545
+ ex = (' data-transferitems="FormRadioButtonList" data-selectshowmode="0" '
546
+ 'data-datadictitemname="" data-defaultitems="{di}" '
547
+ 'data-defaultvalue="" data-colorswitch="false" '
548
+ 'data-itemcolors=""').format(di=di)
549
+ inner = _chk_items(control, "radio")
550
+ elif ck == "FormCheckboxList":
551
+ dv = opt.get("DefaultValue")
552
+ dj = _esc(json.dumps([dv] if dv else [""], ensure_ascii=False))
553
+ ex = (' data-transferitems="FormCheckboxList" data-selectshowmode="0" '
554
+ 'data-datadictitemname="" data-defaultitems="{di}" '
555
+ 'data-defaultvalue="{dj}" data-colorswitch="false" '
556
+ 'data-itemcolors="" data-ischeckbox="false" '
557
+ 'data-boschemacode="" data-mappingfield="" '
558
+ 'data-associationfilter="{af}"').format(di=di, dj=dj, af=RULE)
559
+ inner = _chk_items(control, "checkbox")
560
+ elif ck == "FormCheckbox": # 开关(是/否)
561
+ dv = "true" if opt.get("Checked") else "false"
562
+ ex = (' data-defaultitems="[{i}]" data-defaultvalue="{dv}" '
563
+ 'data-ischeckbox="true" data-datadictitemname=""').format(
564
+ i=hq("是/否"), dv=dv)
565
+ uid = str(uuid.uuid4())
566
+ inner = ('<div class="row notdrag"><input disabled="disabled" id="{u}" '
567
+ 'type="checkbox" data-itemid="{u}"><label for="{u}">是/否</label>'
568
+ '</div>').format(u=uid)
569
+ elif ck == "FormQuery":
570
+ bsc = opt.get("BOSchemaCode", "")
571
+ info = opt.get("BOSchemaInfo", "")
572
+ af = _esc(json.dumps(opt.get("AssociationFields") or
573
+ {"isDefault": True, "fieldSetting": []},
574
+ ensure_ascii=False))
575
+ ex = (' data-width="100%" data-displayrule="{ru}" '
576
+ 'data-boschemacode="{bsc}" data-associationfilter="{r2}" '
577
+ 'data-boschemaname="{bsn}" data-islistview="{lv}" '
578
+ 'data-boschemainfo="{info}" data-bofilter="" '
579
+ 'data-associationfields="{af}" data-inputbyscan="false" '
580
+ 'data-scanupdateenable="false" data-datarule="" '
581
+ 'data-mappingcontrols="" data-mappingproperties="{{}}" '
582
+ 'data-ismultiple="false" data-mappingfield=""').format(
583
+ ru=RULE, bsc=_esc(bsc), r2=RULE,
584
+ bsn=_esc(opt.get("BOSchemaName", "")), lv=_esc(opt.get("IsListView", "")),
585
+ info=_esc(info), af=af)
586
+ inner = ('<div class="form-query-add notdrag" style="border: 1px solid '
587
+ 'rgb(221, 221, 221); border-image: none; height: 42px;"></div>')
588
+ elif ck == "FormSeqNo": # 流水号:线上没有 data-displayrule
589
+ ex = (' data-prefix="{pf}" data-datetimemode="{dm}" '
590
+ 'data-seqnostructure="{st}" data-incrementnum="{inc}"').format(
591
+ pf=_esc(opt.get("Prefix", "")),
592
+ dm=_esc(opt.get("DateTimeMode", "")),
593
+ st=_escj(json.dumps(opt.get("SeqNoStructure") or [],
594
+ ensure_ascii=False, separators=(",", ":"))),
595
+ inc=opt.get("IncrementNum", 0))
596
+ inner = ('<input readonly="" class="notdrag" placeholder="%s" '
597
+ 'style="width: 100%%;" type="text">') % _esc(seqno_preview(opt))
598
+ return (_row_open() + _attrs(opt, key, ex, rule=None) + " >\n"
599
+ + _row_close(label, inner))
600
+ elif ck == "FormPhoto":
601
+ ex = (' data-uploadmultiple="{um}" data-cameraonly="{co}" '
602
+ 'data-haswatermark="{wm}" data-compression="{cp}"').format(
603
+ um=_bool(opt.get("UploadMultiple")), co=_bool(opt.get("CameraOnly")),
604
+ wm=_bool(opt.get("HasWatermark")), cp=_bool(opt.get("Compression")))
605
+ inner = ('<img class="notdrag" '
606
+ 'src="/Content/images/svg/icon-pictureupload.svg">')
607
+ elif ck == "FormAttachment":
608
+ mb = opt.get("MaxUploadSize", 10)
609
+ ex = ' data-maxuploadsize="%s"' % mb
610
+ inner = ('<input class="notdrag" type="file" readonly="" '
611
+ 'placeholder="%sMB">') % mb
612
+ elif ck == "FormMap":
613
+ cs = control.get("ControlSettings") or {}
614
+ ex = (' data-range="{rg}" data-meters="{m}" '
615
+ 'data-locationrangelimit="undefined" data-locationpcenabled="{pc}" '
616
+ 'data-locationeditable="{ed}"').format(
617
+ rg=_esc(opt.get("Range", "0")), m=_esc(cs.get("Meters", "500")),
618
+ pc=_esc(cs.get("LocationPCEnabled", "true")),
619
+ ed=_esc(cs.get("LocationEditable", "false")))
620
+ inner = '<i class="fa icon-dizhi notdrag">读取地理位置</i>'
621
+ elif ck == "FormAreaSelect":
622
+ ex = (' data-areamode="{am}" data-showdetailaddr="{sd}" '
623
+ 'data-defaultvalue="{dv}"').format(
624
+ am=_esc(opt.get("AreaMode", "P-C-T")),
625
+ sd=_bool(opt.get("ShowDetailAddr")),
626
+ dv=_escj(json.dumps(opt.get("DefaultValue") or {},
627
+ ensure_ascii=False, separators=(",", ":"))))
628
+ inner = ('<div class="notdrag" style="width: 100%;">'
629
+ '<select class="form-control notdrag" style="width: 33%; '
630
+ 'display: inline-block;"><option>省</option></select>'
631
+ '<select class="form-control notdrag" style="width: 33%; '
632
+ 'display: inline-block;"><option>市</option></select>'
633
+ '<select class="form-control notdrag" style="width: 34%; '
634
+ 'display: inline-block;"><option>县</option></select></div>')
635
+ else:
636
+ ex = ' data-placeholder="" data-computationrule="{}"'.format(RULE)
637
+ inner = ""
638
+ if wide:
639
+ return (_row_open() + _attrs(opt, df, ex, node_id=key) + " >\n"
640
+ + _row_close(label, inner, span_cls="col-sm-12", box_cls="col-sm-12"))
641
+ return _row_open() + _attrs(opt, df, ex, node_id=key) + " >\n" + _row_close(label, inner)
642
+
643
+
644
+ def runtime_html(control):
645
+ """运行时 HTML(单 div;服务器对简化结构宽容,多类型保存已验证)"""
646
+ opt = control["Options"]
647
+ ck = opt.get("ControlKey")
648
+ if control.get("Type") == C.T_GROUP_TITLE:
649
+ return _group_title_html(control, runtime=True)
650
+ if control.get("Type") == C.T_DESCRIPTION:
651
+ return _description_html(control, runtime=True)
652
+ key = control["Key"]
653
+ base = ('data-datafield = "{k}" data-displayname = "{dn}" '
654
+ 'data-description = "{de}" data-controlkey = "{ck}" '
655
+ 'data-summary = "{su}"').format(
656
+ k=key, dn=_esc(opt.get("DisplayName", "")),
657
+ de=_esc(opt.get("Description", "")), ck=ck,
658
+ su=_esc(opt.get("Summary", "")))
659
+ if opt.get("DisplayRule") is not None: # DisplayRule 键都没有(流水号)→ 属性不出现
660
+ base += ' data-displayrule="{ru}"'.format(ru=_rule_attr(opt))
661
+ return "<div class='row sheet-control' {attrs}{ex} > </div>".format(
662
+ attrs=base, ex=_new_runtime_attrs(control, ck))
663
+
664
+
665
+ def _child_entry(control, parent_key=""):
666
+ """FormLayout 子条目(单控件,或一行多列的列内控件):
667
+ 旧枚举 type + Options 全量透传。
668
+ 成员/部门四类(type 11-14)UI 样本用 uuid key + DataFieldEditable=true"""
669
+ opt = dict(control.get("Options") or {})
670
+ if control.get("Type") not in (C.T_GROUP_TITLE, C.T_DESCRIPTION):
671
+ # 布局项(分组标题/描述)在 FormLayout 里**没有** DataField(线上样本同款)
672
+ opt.setdefault("DataField", control["Key"])
673
+ opt.setdefault("Summary", "")
674
+ opt.setdefault("Description", "")
675
+ if control.get("Type") is None:
676
+ raise ValueError("控件缺 Type(老枚举):%s" % control["Key"])
677
+ if control["Type"] in (11, 12, 13, 14):
678
+ return {"key": str(uuid.uuid4()), "type": control["Type"],
679
+ "parentKey": parent_key, "options": opt, "DataFieldEditable": True}
680
+ return {"key": control["Key"], "type": control["Type"],
681
+ "parentKey": parent_key, "options": opt}
682
+
683
+
684
+ def layout_entry(control):
685
+ """FormLayout 条目:旧枚举 type + Options 全量透传。"""
686
+ return _child_entry(control, parent_key=control.get("ParentKey", ""))
687
+
688
+
689
+ LAYOUT_ROW_OPTS = {"Layout": "fourCols", "PercentageBlock": "fourCols-1111",
690
+ "DisplayName": "一行多列"}
691
+ ROW_HTML_KEY = "layout_fourcols"
692
+ ROW_COL_CLASS = "col-sm-3 col-xs-3"
693
+
694
+
695
+ def layout_row_entry(controls):
696
+ """一行多列(type 102)FormLayout 条目。columns 与 childControls 双份列出子控件
697
+ (对照用户保存样本 D001599e98...:key 用字段编码、parentKey 指向行 key)。"""
698
+ rowkey = str(uuid.uuid4())
699
+ children = [_child_entry(c, rowkey) for c in controls]
700
+ return {"key": rowkey, "type": 102, "parentKey": "",
701
+ "options": dict(LAYOUT_ROW_OPTS),
702
+ "columns": children, "childControls": children}
703
+
704
+
705
+ def design_row_html(controls):
706
+ """设计态一行多列 HTML:布局行 div 内按列包各自控件 html"""
707
+ cols = []
708
+ for i, c in enumerate(controls):
709
+ cols.append('<div class="%s layoutrow-item ui-sortable ui-droppable" '
710
+ 'data-layoutitem="Col%d" placeholder="将左侧控件拖入此处">\n%s\n'
711
+ '</div>' % (ROW_COL_CLASS, i + 1, design_html(c)))
712
+ return ('<div class="row layoutrow" data-controlkey="%s" '
713
+ 'style="background-color: rgb(241, 250, 255);">\n%s\n</div>'
714
+ % (ROW_HTML_KEY, "\n".join(cols)))
715
+
716
+
717
+ # =====================================================================
718
+ # 子表(FormGridView,type 104 / ControlKey 41)
719
+ # 逐字对照 UI 真实保存样本(logs/child_ref_payload.json:行 GUID key、列 key 全 GUID、
720
+ # columns 与 childControls 双份同列表节点;列 DataField = <childcode>.<列编码>)
721
+ # =====================================================================
722
+ GRID_ROW_ATTRS = ('data-nameitems="" data-gridviewmode="" data-gridviewfields="" '
723
+ 'data-displayfields="" data-fixedcolnum="{n}" '
724
+ 'data-showfixedcol="false" data-mobilegridviewmode="0"')
725
+
726
+
727
+ def grid_layout_entry(sub):
728
+ """子表 type 104 FormLayout 条目。sub = {code,label,cols,fixed}(dsl 产物)"""
729
+ code = sub["code"]
730
+ rowkey = str(uuid.uuid4())
731
+
732
+ def col_entry(col):
733
+ opt = dict(col.get("Options") or {})
734
+ opt["DataField"] = "%s.%s" % (code, col["Key"])
735
+ return {"key": str(uuid.uuid4()), "type": col["Type"],
736
+ "parentKey": rowkey, "options": opt,
737
+ "DataFieldEditable": True, "removable": True}
738
+
739
+ children = [col_entry(c) for c in sub["cols"]]
740
+ return {"key": rowkey, "type": 104, "parentKey": "",
741
+ "options": {"DataField": code, "DisplayName": sub.get("label", ""),
742
+ "Description": "", "ControlKey": "FormGridView",
743
+ "Summary": "", "NameItems": "",
744
+ "DisplayRule": {"Rule": "", "RuleText": ""},
745
+ "GridViewMode": "", "GridViewFields": "",
746
+ "DisplayFields": [], "FixedColNum": sub.get("fixed", 1),
747
+ "ShowFixedCol": False, "MobileGridViewMode": "0"},
748
+ "DataFieldEditable": True,
749
+ "columns": children, "childControls": children}
750
+
751
+
752
+ def design_grid_html(sub):
753
+ """子表设计态 HTML(SheetGridView):主 div + 列单元格 td(列控件整行全宽版)
754
+ + 尾随占位 td + 编辑/加列/删除按钮。逐字对照 UI 样本。"""
755
+ code = sub["code"]
756
+ label = _esc(sub.get("label", ""))
757
+ attrs = GRID_ROW_ATTRS.format(n=sub.get("fixed", 1))
758
+ cells = []
759
+ for col in sub["cols"]:
760
+ dotted = "%s.%s" % (code, col["Key"])
761
+ cells.append('<td class="SheetGridView_td ui-sortable ui-droppable" '
762
+ 'style="display: table-cell; min-width: 185px;">\n%s\n</td>'
763
+ % design_html(col, node_id=dotted.replace(".", "-"),
764
+ datafield=dotted, wide=True))
765
+ cells.append('<td class="SheetGridView_td ui-sortable ui-droppable" '
766
+ 'style="min-width: 183px;"></td>')
767
+ return ('<div class="row sheet-control SheetGridView" id="{code}" '
768
+ 'data-datafield="{code}" data-displayname="{label}" data-description="" '
769
+ 'data-controlkey="FormGridView" data-summary="" {attrs} >'
770
+ '<span class="col-sm-12" style="cursor:pointer">{label}</span>'
771
+ '<div class="col-sm-12 SheetGridView_wrap" style="-ms-overflow-x: auto;">'
772
+ '<table class="table table-bordered">'
773
+ '<tr class="SheetGridView_tr ui-sortable">{cells}</tr></table></div>'
774
+ '<button class="btn btn-default btn-xs editcontrol" style="display: none;" '
775
+ 'type="button" data-buttontype="editcontrol">'
776
+ '<span class="glyphicon glyphicon-edit"></span>编辑子表</button>'
777
+ '<button class="btn btn-default btn-xs addtd" type="button" '
778
+ 'data-buttontype="addTd"><span class="glyphicon glyphicon-plus"></span>'
779
+ '添加列</button>'
780
+ '<button class="btn btn-default btn-xs remove btn-sheet-del" '
781
+ 'style="display: none;" type="button" data-buttontype="remomvecontrol">'
782
+ '<span class="icon-sheet-del icon-remove-block"></span>删除子表</button>'
783
+ '</div>').format(code=code, label=label, attrs=attrs, cells="".join(cells))
784
+
785
+
786
+ def runtime_grid_html(sub):
787
+ """子表运行态 HTML:主 div + table thead 各列一个 th(div.table_th)。
788
+ 列属性从列 Options 简取(服务器宽容,类同其它类型简化版)。"""
789
+ code = sub["code"]
790
+ label = _esc(sub.get("label", ""))
791
+ attrs = GRID_ROW_ATTRS.format(n=sub.get("fixed", 1))
792
+ ths = []
793
+ for col in sub["cols"]:
794
+ o = col.get("Options") or {}
795
+ ck = o.get("ControlKey", "")
796
+ clabel = _esc(o.get("DisplayName", col["Key"]))
797
+ ths.append('<th><div class=\'table_th sheet-control\' '
798
+ 'data-datafield = "{d}" data-displayname = "{dn}" '
799
+ 'data-description = "" data-controlkey = "{ck}" '
800
+ 'data-summary = "" data-displayrule="{ru}" >{dn}</div></th>'
801
+ .format(d="%s.%s" % (code, col["Key"]), dn=clabel, ck=ck, ru=RULE))
802
+ return ('<div class="row sheet-control" data-datafield = "{code}" '
803
+ 'data-displayname = "{label}" data-description = "" '
804
+ 'data-controlkey = "FormGridView" data-summary = "" {attrs} >'
805
+ '<table class="table table-bordered table-hover table-condensed">'
806
+ '<thead><tr>{ths}</tr></thead></table></div>'
807
+ ).format(code=code, label=label, attrs=attrs, ths="".join(ths))
808
+
809
+
810
+ def runtime_row_html(controls):
811
+ """运行态一行多列:外层 row + 等宽列包各控件 html"""
812
+ cols = "".join('<div class="col-md-3 col-sm-3 col-xs-3">%s</div>'
813
+ % runtime_html(c) for c in controls)
814
+ return "<div class='row'>%s</div>" % cols
815
+
816
+
817
+ def build_biz_sheet(sheet_code, controls, layout=None, enable_form_sns=True,
818
+ enable_task=False, enable_log=False, enable_data_acl=True,
819
+ subs=None):
820
+ """BizSheetStr。layout 缺省 = 全 flat;给出时按序混排:单控件条目 或 行组(4 列)。
821
+ subs:子表(自动排在表单底部整行,不参与一行多列打包)。"""
822
+ seq = layout if layout is not None else list(controls)
823
+ dm = []
824
+ rt = []
825
+ fl = []
826
+ for item in seq:
827
+ if isinstance(item, (list, tuple)):
828
+ dm.append(design_row_html(item))
829
+ rt.append(runtime_row_html(item))
830
+ fl.append(layout_row_entry(item))
831
+ else:
832
+ dm.append(design_html(item))
833
+ rt.append(runtime_html(item))
834
+ fl.append(layout_entry(item))
835
+ for s in subs or []:
836
+ dm.append(design_grid_html(s))
837
+ rt.append(runtime_grid_html(s))
838
+ fl.append(grid_layout_entry(s))
839
+ return {
840
+ "SheetCode": sheet_code,
841
+ "Javascript": "",
842
+ "NewJsCode": "",
843
+ "BehindCode": "",
844
+ "EnableFormSNS": enable_form_sns,
845
+ "EnableTask": enable_task,
846
+ "EnableLog": enable_log,
847
+ "EnableDataAcl": enable_data_acl,
848
+ "DataAclInheritedFrom": "",
849
+ "DesignModeContent": "".join(dm),
850
+ "RuntimeContent": "".join(rt),
851
+ "FormLayout": fl,
852
+ }