e10-ebuilder-prototype 0.5.0

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 (54) hide show
  1. package/README.md +113 -0
  2. package/dist/api.d.ts +12 -0
  3. package/dist/api.js +125 -0
  4. package/dist/application.d.ts +7 -0
  5. package/dist/application.js +16 -0
  6. package/dist/archive.d.ts +130 -0
  7. package/dist/archive.js +151 -0
  8. package/dist/capture.d.ts +15 -0
  9. package/dist/capture.js +440 -0
  10. package/dist/common.d.ts +20 -0
  11. package/dist/common.js +87 -0
  12. package/dist/dom.d.mts +1 -0
  13. package/dist/dom.mjs +58 -0
  14. package/dist/form-context.d.ts +3 -0
  15. package/dist/form-context.js +58 -0
  16. package/dist/form-runtime.d.mts +2 -0
  17. package/dist/form-runtime.mjs +149 -0
  18. package/dist/forms.d.ts +51 -0
  19. package/dist/forms.js +603 -0
  20. package/dist/html.d.ts +22 -0
  21. package/dist/html.js +427 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +370 -0
  24. package/dist/menus.d.ts +32 -0
  25. package/dist/menus.js +330 -0
  26. package/dist/model.d.ts +164 -0
  27. package/dist/model.js +8 -0
  28. package/dist/offline-store.d.mts +5 -0
  29. package/dist/offline-store.mjs +80 -0
  30. package/dist/platform.d.ts +10 -0
  31. package/dist/platform.js +123 -0
  32. package/dist/readiness.d.ts +124 -0
  33. package/dist/readiness.js +529 -0
  34. package/dist/runtime-support.d.mts +52 -0
  35. package/dist/runtime-support.mjs +279 -0
  36. package/dist/site.d.ts +34 -0
  37. package/dist/site.js +195 -0
  38. package/dist/store.d.ts +90 -0
  39. package/dist/store.js +296 -0
  40. package/dist/templates/form-guide.md +539 -0
  41. package/dist/templates/index.html +803 -0
  42. package/dist/templates/placeholder.html +143 -0
  43. package/dist/templates/workflow-guide.md +95 -0
  44. package/dist/templates/workflow-presets.json +89 -0
  45. package/dist/temporary-records.d.ts +15 -0
  46. package/dist/temporary-records.js +286 -0
  47. package/dist/vendor/environment-auth.d.ts +61 -0
  48. package/dist/vendor/environment-auth.js +455 -0
  49. package/dist/workflow-runtime.d.mts +2 -0
  50. package/dist/workflow-runtime.mjs +298 -0
  51. package/dist/workflows.d.ts +28 -0
  52. package/dist/workflows.js +90 -0
  53. package/docs/PROTOCOL.md +299 -0
  54. package/package.json +45 -0
@@ -0,0 +1,803 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>__APP_TITLE__</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ font-family: Inter, 'PingFang SC', 'Microsoft YaHei', sans-serif;
11
+ color: #263149;
12
+ background: #f3f5f8;
13
+ font-synthesis: none;
14
+ }
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+ body {
19
+ margin: 0;
20
+ }
21
+ button,
22
+ input {
23
+ font: inherit;
24
+ }
25
+ button,
26
+ a,
27
+ input,
28
+ summary {
29
+ -webkit-tap-highlight-color: transparent;
30
+ }
31
+ button {
32
+ cursor: pointer;
33
+ }
34
+ a {
35
+ color: inherit;
36
+ text-decoration: none;
37
+ }
38
+ button:focus-visible,
39
+ a:focus-visible,
40
+ input:focus-visible,
41
+ summary:focus-visible {
42
+ outline: 3px solid #9ec5ff;
43
+ outline-offset: 3px;
44
+ }
45
+ svg {
46
+ width: 20px;
47
+ height: 20px;
48
+ flex-shrink: 0;
49
+ fill: none;
50
+ stroke: currentColor;
51
+ stroke-width: 1.7;
52
+ stroke-linecap: round;
53
+ stroke-linejoin: round;
54
+ }
55
+ .symbols {
56
+ position: absolute;
57
+ width: 0;
58
+ height: 0;
59
+ overflow: hidden;
60
+ }
61
+ [hidden] {
62
+ display: none !important;
63
+ }
64
+ .app {
65
+ display: grid;
66
+ grid-template-columns: 260px minmax(0, 1fr);
67
+ height: 100vh;
68
+ height: 100dvh;
69
+ }
70
+ .sidebar {
71
+ display: flex;
72
+ flex-direction: column;
73
+ min-height: 0;
74
+ padding: 26px 16px 18px;
75
+ background: #fff;
76
+ border-right: 1px solid #e6e9f0;
77
+ }
78
+ .brand {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 12px;
82
+ padding: 0 10px 25px;
83
+ }
84
+ .brand-mark {
85
+ display: grid;
86
+ place-items: center;
87
+ width: 40px;
88
+ height: 40px;
89
+ flex: none;
90
+ border-radius: 12px;
91
+ color: #fff;
92
+ background: #2563eb;
93
+ box-shadow: 0 4px 12px #2563eb1a;
94
+ }
95
+ .brand-mark svg {
96
+ width: 23px;
97
+ height: 23px;
98
+ }
99
+ .brand-name {
100
+ font-size: 16px;
101
+ font-weight: 650;
102
+ line-height: 1.5;
103
+ margin: 0;
104
+ overflow-wrap: anywhere;
105
+ letter-spacing: -0.15px;
106
+ }
107
+ .search {
108
+ position: relative;
109
+ margin: 0 4px 27px;
110
+ }
111
+ .search svg {
112
+ position: absolute;
113
+ left: 12px;
114
+ top: 12px;
115
+ width: 16px;
116
+ height: 16px;
117
+ color: #959bad;
118
+ }
119
+ .search input {
120
+ width: 100%;
121
+ height: 40px;
122
+ border: 1px solid #eceef4;
123
+ border-radius: 9px;
124
+ background: #f7f8fb;
125
+ color: #38435a;
126
+ padding: 0 12px 0 37px;
127
+ font-size: 12px;
128
+ }
129
+ .search input::placeholder {
130
+ color: #a0a6b5;
131
+ }
132
+ .menu {
133
+ flex: 1;
134
+ min-height: 0;
135
+ overflow: auto;
136
+ padding: 0 4px;
137
+ scrollbar-width: thin;
138
+ scrollbar-color: #d9dce7 transparent;
139
+ }
140
+ .section {
141
+ margin-bottom: 25px;
142
+ }
143
+ .section-title {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ list-style: none;
148
+ cursor: pointer;
149
+ user-select: none;
150
+ padding: 0 10px;
151
+ margin-bottom: 10px;
152
+ font-size: 12px;
153
+ font-weight: 550;
154
+ color: #929aae;
155
+ border-radius: 4px;
156
+ }
157
+ .section-title::-webkit-details-marker {
158
+ display: none;
159
+ }
160
+ .section-name {
161
+ display: inline-flex;
162
+ align-items: center;
163
+ gap: 7px;
164
+ }
165
+ .section-title svg {
166
+ width: 12px;
167
+ height: 12px;
168
+ transition: transform 0.15s;
169
+ }
170
+ .section:not([open]) .section-title svg {
171
+ transform: rotate(-90deg);
172
+ }
173
+ .section-count {
174
+ min-width: 20px;
175
+ line-height: 20px;
176
+ text-align: center;
177
+ border-radius: 5px;
178
+ background: #f2f4f8;
179
+ font-size: 10px;
180
+ font-weight: 400;
181
+ color: #8d96a8;
182
+ }
183
+ .nav-item {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 10px;
187
+ position: relative;
188
+ min-height: 45px;
189
+ padding: 11px 10px;
190
+ margin: 4px 0;
191
+ border-radius: 9px;
192
+ color: #616c82;
193
+ font-size: 13px;
194
+ line-height: 1.55;
195
+ transition:
196
+ background 0.15s,
197
+ color 0.15s;
198
+ }
199
+ .nav-item:hover {
200
+ background: #f5f6fa;
201
+ color: #303d55;
202
+ }
203
+ .nav-item svg {
204
+ width: 17px;
205
+ height: 17px;
206
+ color: #909cb5;
207
+ }
208
+ .nav-item .label {
209
+ flex: 1;
210
+ min-width: 0;
211
+ overflow-wrap: anywhere;
212
+ }
213
+ .nav-item[aria-current='page'] {
214
+ background: #eaf2ff;
215
+ color: #245dcc;
216
+ font-weight: 600;
217
+ }
218
+ .nav-item[aria-current='page'] svg {
219
+ color: #3975dc;
220
+ }
221
+ .nav-item[aria-current='page']:before {
222
+ content: '';
223
+ position: absolute;
224
+ left: -3px;
225
+ top: 15px;
226
+ bottom: 15px;
227
+ width: 3px;
228
+ border-radius: 3px;
229
+ background: #3b82f6;
230
+ }
231
+ .nav-item[data-kind='form'] svg {
232
+ color: #c29861;
233
+ }
234
+ .nav-item[data-kind='form'][aria-current='page'] {
235
+ background: #fbf3e8;
236
+ color: #99743d;
237
+ }
238
+ .nav-item[data-kind='form'][aria-current='page']:before {
239
+ background: #cea160;
240
+ }
241
+ .tag {
242
+ font-size: 9px;
243
+ line-height: 1.5;
244
+ border: 1px solid #eee0ca;
245
+ border-radius: 4px;
246
+ padding: 2px 4px;
247
+ color: #b18b56;
248
+ white-space: nowrap;
249
+ font-weight: 400;
250
+ }
251
+ .no-results {
252
+ padding: 28px 8px;
253
+ text-align: center;
254
+ font-size: 12px;
255
+ color: #949caf;
256
+ }
257
+ .main {
258
+ display: flex;
259
+ flex-direction: column;
260
+ min-width: 0;
261
+ min-height: 0;
262
+ }
263
+ .content-area {
264
+ display: flex;
265
+ flex-direction: column;
266
+ flex: 1;
267
+ min-width: 0;
268
+ min-height: 0;
269
+ padding: 20px;
270
+ }
271
+ .content-panel {
272
+ display: flex;
273
+ flex-direction: column;
274
+ flex: 1;
275
+ min-width: 0;
276
+ min-height: 0;
277
+ background: #fff;
278
+ border: 1px solid #dfe4ed;
279
+ border-radius: 12px;
280
+ overflow: hidden;
281
+ box-shadow: 0 3px 16px #26385607;
282
+ }
283
+
284
+ .menu-children {
285
+ padding-left: 10px;
286
+ }
287
+ .combined-tabs {
288
+ display: flex;
289
+ flex: none;
290
+ gap: 6px;
291
+ padding: 12px 16px 0;
292
+ border-bottom: 1px solid #e8ecf2;
293
+ overflow-x: auto;
294
+ }
295
+ .combined-tabs button {
296
+ padding: 10px 14px;
297
+ border: 0;
298
+ border-bottom: 2px solid transparent;
299
+ background: transparent;
300
+ color: #69748a;
301
+ white-space: nowrap;
302
+ }
303
+ .combined-tabs button[aria-selected='true'] {
304
+ color: #2563eb;
305
+ border-bottom-color: #2563eb;
306
+ }
307
+
308
+ .page-content {
309
+ display: block;
310
+ flex: 1;
311
+ min-height: 0;
312
+ width: 100%;
313
+ height: 100%;
314
+ border: 0;
315
+ background: #f3f5f8;
316
+ }
317
+ .empty {
318
+ margin: auto;
319
+ padding: 40px;
320
+ text-align: center;
321
+ font-size: 14px;
322
+ color: #969fb0;
323
+ }
324
+ .mobile-bar,
325
+ .shade {
326
+ display: none;
327
+ }
328
+ .mobile-menu {
329
+ display: grid;
330
+ place-items: center;
331
+ width: 34px;
332
+ height: 34px;
333
+ padding: 5px;
334
+ background: transparent;
335
+ border: 0;
336
+ border-radius: 6px;
337
+ color: #6b7590;
338
+ }
339
+ @media (max-width: 1050px) {
340
+ .content-area {
341
+ padding: 16px;
342
+ }
343
+ .app {
344
+ grid-template-columns: 232px minmax(0, 1fr);
345
+ }
346
+ .sidebar {
347
+ padding: 24px 12px 16px;
348
+ }
349
+ .brand {
350
+ padding-left: 8px;
351
+ gap: 10px;
352
+ }
353
+ .brand-name {
354
+ font-size: 15px;
355
+ }
356
+ }
357
+ @media (max-width: 700px) {
358
+ .content-panel {
359
+ border-radius: 10px;
360
+ }
361
+ .content-actions {
362
+ padding: 10px 12px;
363
+ }
364
+ .content-area {
365
+ padding: 12px;
366
+ }
367
+ .app {
368
+ display: block;
369
+ }
370
+ .main {
371
+ height: 100vh;
372
+ height: 100dvh;
373
+ }
374
+ .sidebar {
375
+ position: fixed;
376
+ left: 0;
377
+ top: 0;
378
+ bottom: 0;
379
+ width: 268px;
380
+ z-index: 5;
381
+ transform: translateX(-100%);
382
+ transition: transform 0.2s;
383
+ }
384
+ .app.menu-open .sidebar {
385
+ transform: translateX(0);
386
+ }
387
+ .mobile-bar {
388
+ height: 52px;
389
+ flex: none;
390
+ display: flex;
391
+ align-items: center;
392
+ gap: 12px;
393
+ padding: 0 14px;
394
+ border-bottom: 1px solid #e8ebf1;
395
+ background: #fff;
396
+ }
397
+ .mobile-title {
398
+ font-size: 14px;
399
+ font-weight: 550;
400
+ white-space: nowrap;
401
+ overflow: hidden;
402
+ text-overflow: ellipsis;
403
+ }
404
+ .app.menu-open .shade {
405
+ display: block;
406
+ position: fixed;
407
+ inset: 0;
408
+ border: 0;
409
+ padding: 0;
410
+ background: #19264055;
411
+ z-index: 4;
412
+ }
413
+ }
414
+ @media (prefers-reduced-motion: reduce) {
415
+ * {
416
+ transition: none !important;
417
+ }
418
+ }
419
+ </style>
420
+ </head>
421
+ <body>
422
+ <svg class="symbols" aria-hidden="true">
423
+ <defs>
424
+ <symbol id="i-grid" viewBox="0 0 24 24">
425
+ <rect x="3" y="3" width="7" height="7" rx="1.5" />
426
+ <rect x="14" y="3" width="7" height="7" rx="1.5" />
427
+ <rect x="3" y="14" width="7" height="7" rx="1.5" />
428
+ <rect x="14" y="14" width="7" height="7" rx="1.5" />
429
+ </symbol>
430
+ <symbol id="i-page" viewBox="0 0 24 24">
431
+ <rect x="3" y="4" width="18" height="16" rx="2" />
432
+ <path d="M3 9h18M8 9v11" />
433
+ </symbol>
434
+ <symbol id="i-form" viewBox="0 0 24 24">
435
+ <path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9Z" />
436
+ <path d="M14 3v6h6M8 13h8M8 17h5" />
437
+ </symbol>
438
+ <symbol id="i-search" viewBox="0 0 24 24">
439
+ <circle cx="10.5" cy="10.5" r="6.5" />
440
+ <path d="m16 16 4.5 4.5" />
441
+ </symbol>
442
+ <symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" /></symbol>
443
+ </defs>
444
+ </svg>
445
+ <div class="app" id="app">
446
+ <button class="shade" id="shade" aria-label="关闭目录"></button>
447
+ <aside class="sidebar" aria-label="应用目录">
448
+ <div class="brand">
449
+ <div class="brand-mark">
450
+ <svg><use href="#i-grid" /></svg>
451
+ </div>
452
+ <h1 class="brand-name" id="app-name"></h1>
453
+ </div>
454
+ <label class="search"
455
+ ><svg><use href="#i-search" /></svg
456
+ ><input id="search" placeholder="搜索菜单" aria-label="搜索菜单" autocomplete="off"
457
+ /></label>
458
+ <nav class="menu" id="menu" aria-label="应用菜单">
459
+ <div class="no-results" id="no-results" hidden>没有找到匹配的菜单</div>
460
+ </nav>
461
+ </aside>
462
+ <main class="main">
463
+ <header class="mobile-bar">
464
+ <button class="mobile-menu" id="menu-toggle" aria-label="打开目录" aria-expanded="false">
465
+ <svg><use href="#i-menu" /></svg></button
466
+ ><span class="mobile-title" id="current-title"></span>
467
+ </header>
468
+ <div class="content-area">
469
+ <div class="content-panel">
470
+ <div class="combined-tabs" id="combined-tabs" role="tablist" hidden></div>
471
+ <iframe class="page-content" id="page-content" title="页面内容" hidden></iframe>
472
+ <div class="empty" id="empty">此应用暂无已发布的业务菜单</div>
473
+ </div>
474
+ </div>
475
+ </main>
476
+ </div>
477
+ <script type="application/json" id="catalog">
478
+ __CATALOG_JSON__
479
+ </script>
480
+ <script>
481
+ ;(() => {
482
+ 'use strict'
483
+ const data = JSON.parse(document.getElementById('catalog').textContent)
484
+ const items = data.items,
485
+ $ = (id) => document.getElementById(id)
486
+ const links = new Map(),
487
+ routes = new Map(),
488
+ groups = []
489
+ const itemMap = new Map(items.map((item) => [item.kind + '/' + item.id, item]))
490
+ let currentItem,
491
+ searchExpansion = null
492
+ const tabMemory = new Map()
493
+ $('app-name').textContent = data.appName
494
+ document.title = data.appName
495
+ const menus =
496
+ data.menus ||
497
+ ['page', 'form']
498
+ .map((kind) => ({
499
+ id: kind + '-section',
500
+ name: kind === 'page' ? '页面' : '表单',
501
+ legacy: kind,
502
+ children: items
503
+ .filter((item) => item.kind === kind)
504
+ .map((item) => ({
505
+ id: kind + '/' + item.id,
506
+ name: item.name,
507
+ target: { kind: item.kind, id: item.id },
508
+ children: [],
509
+ })),
510
+ }))
511
+ .filter((group) => group.children.length)
512
+ const routeKey = (node) => (data.menus ? 'menu/' + node.id : node.id)
513
+ const flatten = (nodes) => nodes.flatMap((node) => [node, ...flatten(node.children || [])])
514
+ function link(node) {
515
+ const a = document.createElement('a'),
516
+ target = node.target
517
+ const key = routeKey(node),
518
+ item = itemMap.get(target.kind + '/' + target.id)
519
+ a.className = 'nav-item'
520
+ a.dataset.kind = target.kind
521
+ a.href = '#' + key
522
+ a.title = node.name
523
+ a.addEventListener('click', closeMenu)
524
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
525
+ const use = document.createElementNS('http://www.w3.org/2000/svg', 'use')
526
+ use.setAttribute('href', '#i-' + (target.kind === 'combined' ? 'grid' : target.kind))
527
+ icon.append(use)
528
+ a.append(icon)
529
+ const label = document.createElement('span')
530
+ label.className = 'label'
531
+ label.textContent = node.name
532
+ a.append(label)
533
+ if (item && !item.available) {
534
+ const tag = document.createElement('span')
535
+ tag.className = 'tag'
536
+ tag.textContent = data.menus
537
+ ? '暂不可用'
538
+ : target.kind === 'form'
539
+ ? '未对接'
540
+ : '暂不可用'
541
+ a.append(tag)
542
+ }
543
+ links.set(key, a)
544
+ routes.set(key, node)
545
+ return a
546
+ }
547
+ function render(nodes, parent) {
548
+ for (const node of nodes) {
549
+ const children = node.children || []
550
+ if (children.length) {
551
+ const details = document.createElement('details')
552
+ details.className = 'section'
553
+ details.open = true
554
+ details.id = node.legacy ? node.id : 'group-' + node.id
555
+ const summary = document.createElement('summary')
556
+ summary.className = 'section-title'
557
+ const label = document.createElement('span')
558
+ label.className = 'section-name'
559
+ label.textContent = node.name
560
+ summary.append(label)
561
+ const count = document.createElement('span')
562
+ count.className = 'section-count'
563
+ count.textContent = flatten(children).filter((child) => child.target).length
564
+ if (node.legacy) count.id = node.legacy + '-badge'
565
+ summary.append(count)
566
+ details.append(summary)
567
+ const body = document.createElement('div')
568
+ body.className = 'menu-children'
569
+ if (node.legacy) body.id = node.legacy + '-menu'
570
+ if (node.target) body.append(link(node))
571
+ render(children, body)
572
+ details.append(body)
573
+ parent.append(details)
574
+ groups.push(details)
575
+ } else if (node.target) parent.append(link(node))
576
+ }
577
+ }
578
+ const fragment = document.createDocumentFragment()
579
+ render(menus, fragment)
580
+ $('menu').insertBefore(fragment, $('no-results'))
581
+ function closeMenu() {
582
+ $('app').classList.remove('menu-open')
583
+ $('menu-toggle').setAttribute('aria-expanded', 'false')
584
+ }
585
+ const firstRoute =
586
+ [...routes].find(([, node]) => node.default)?.[0] || routes.keys().next().value
587
+ function select() {
588
+ const [requested, query = ''] = location.hash.slice(1).split('?')
589
+ const key = routes.has(requested) ? requested : firstRoute
590
+ const node = routes.get(key)
591
+ if (!node) return
592
+ for (const [route, a] of links) {
593
+ if (route === key) a.setAttribute('aria-current', 'page')
594
+ else a.removeAttribute('aria-current')
595
+ }
596
+ let active = node,
597
+ selected = key
598
+ const tabs = $('combined-tabs')
599
+ tabs.replaceChildren()
600
+ tabs.hidden = node.target.kind !== 'combined'
601
+ if (node.target.kind === 'combined') {
602
+ const children = flatten(node.tabs || []).filter((child) => child.target)
603
+ const requestedTab = new URLSearchParams(query).get('tab') || tabMemory.get(key)
604
+ active =
605
+ children.find((child) => child.id === requestedTab) ||
606
+ children.find((child) => child.default) ||
607
+ children[0]
608
+ if (!active) {
609
+ $('empty').hidden = false
610
+ $('empty').textContent = '此菜单暂无可用内容'
611
+ $('page-content').hidden = true
612
+ currentItem = null
613
+ return
614
+ }
615
+ tabMemory.set(key, active.id)
616
+ selected += '?tab=' + active.id
617
+ for (const child of children) {
618
+ const button = document.createElement('button')
619
+ button.type = 'button'
620
+ button.role = 'tab'
621
+ button.textContent = child.name
622
+ button.setAttribute('aria-selected', String(child.id === active.id))
623
+ button.addEventListener('click', () => {
624
+ location.hash = key + '?tab=' + child.id
625
+ })
626
+ tabs.append(button)
627
+ }
628
+ }
629
+ const item = itemMap.get(active.target.kind + '/' + active.target.id)
630
+ if (!item) return
631
+ currentItem = item
632
+ $('current-title').textContent = active.name
633
+ document.title = active.name + ' · ' + data.appName
634
+ $('empty').hidden = true
635
+ const frame = $('page-content')
636
+ if (
637
+ frame.dataset.selected !==
638
+ selected + (new URLSearchParams(query).get('params') || '')
639
+ ) {
640
+ const next = document.createElement('iframe')
641
+ next.id = 'page-content'
642
+ next.className = 'page-content'
643
+ next.title = active.name
644
+ const params = new URLSearchParams(query).get('params')
645
+ next.dataset.selected = selected + (params || '')
646
+ next.src = item.html + (params ? '#params=' + encodeURIComponent(params) : '')
647
+ frame.replaceWith(next)
648
+ } else frame.hidden = false
649
+ closeMenu()
650
+ }
651
+ $('search').addEventListener('input', () => {
652
+ const query = $('search').value.trim().toLocaleLowerCase()
653
+ if (query && !searchExpansion)
654
+ searchExpansion = new Map(groups.map((group) => [group, group.open]))
655
+ let total = 0
656
+ for (const [key, a] of links) {
657
+ const node = routes.get(key)
658
+ const text = [node.name, ...flatten(node.tabs || []).map((child) => child.name)]
659
+ .join(' ')
660
+ .toLocaleLowerCase()
661
+ a.hidden = !text.includes(query)
662
+ if (!a.hidden) total++
663
+ }
664
+ for (const group of groups) {
665
+ const visible = [...group.querySelectorAll('a.nav-item')].some((a) => !a.hidden)
666
+ group.hidden = !visible
667
+ if (query) group.open = visible
668
+ else if (searchExpansion) group.open = searchExpansion.get(group)
669
+ }
670
+ $('no-results').hidden = !!total
671
+ if (!query) searchExpansion = null
672
+ })
673
+ // The active form can explicitly access published objects within this application.
674
+ const objectScopes = new Set(
675
+ items
676
+ .filter((item) => item.kind === 'form' && item.available)
677
+ .flatMap((item) =>
678
+ [item.objId, ...(item.objectIds || []), ...(item.workflowType ? ['workflow'] : [])]
679
+ .filter(Boolean)
680
+ .map((id) => data.appId + ':' + id),
681
+ ),
682
+ )
683
+ const offlineStorage = __OFFLINE_STORAGE__(String(data.appId))
684
+ addEventListener('message', (event) => {
685
+ const message = event.data
686
+ if (
687
+ event.source !== $('page-content').contentWindow ||
688
+ currentItem?.kind !== 'form' ||
689
+ !currentItem.available
690
+ )
691
+ return
692
+ if (message?.type === 'e10-form-navigation') {
693
+ let destination
694
+ const direct = 'menu/' + message.menuKey
695
+ if (routes.has(direct)) destination = direct
696
+ else
697
+ for (const [key, node] of routes)
698
+ if (
699
+ flatten(node.tabs || []).some((tab) => tab.id === message.menuKey && tab.target)
700
+ ) {
701
+ destination = key + '?tab=' + message.menuKey
702
+ break
703
+ }
704
+ const validParams =
705
+ message.params &&
706
+ typeof message.params === 'object' &&
707
+ !Array.isArray(message.params) &&
708
+ JSON.stringify(message.params).length <= 10000
709
+ event.source.postMessage(
710
+ {
711
+ type: 'e10-form-navigation-result',
712
+ requestId: message.requestId,
713
+ ok: !!destination && validParams,
714
+ },
715
+ '*',
716
+ )
717
+ if (destination && validParams) {
718
+ const query = new URLSearchParams(destination.split('?')[1] || '')
719
+ query.set('params', JSON.stringify(message.params))
720
+ location.hash = destination.split('?')[0] + '?' + query.toString()
721
+ }
722
+ return
723
+ }
724
+ if (message?.type === 'e10-form-commit') {
725
+ try {
726
+ if (
727
+ !Array.isArray(message.entries) ||
728
+ message.entries.some(
729
+ (e) =>
730
+ !objectScopes.has(e.scope) ||
731
+ (e.scope.endsWith(':workflow') && !currentItem.workflowType),
732
+ )
733
+ )
734
+ throw new Error('联合保存目标未完成生成或不在当前范围')
735
+ const value = offlineStorage.commit(message.entries)
736
+ event.source.postMessage(
737
+ { type: 'e10-form-commit-result', requestId: message.requestId, value },
738
+ '*',
739
+ )
740
+ } catch {
741
+ event.source.postMessage(
742
+ {
743
+ type: 'e10-form-commit-result',
744
+ requestId: message.requestId,
745
+ error: '联合保存失败,范围或数据无效',
746
+ },
747
+ '*',
748
+ )
749
+ }
750
+ return
751
+ }
752
+ if (message?.type !== 'e10-form-store') return
753
+ const scope = message.scope
754
+ if (
755
+ !['load', 'save', 'reset'].includes(message.operation) ||
756
+ typeof message.requestId !== 'string'
757
+ )
758
+ return
759
+ if (
760
+ !objectScopes.has(scope) ||
761
+ (scope.endsWith(':workflow') && !currentItem.workflowType)
762
+ ) {
763
+ event.source.postMessage(
764
+ {
765
+ type: 'e10-form-store-result',
766
+ requestId: message.requestId,
767
+ error: '目标表单未完成生成或不在当前应用中',
768
+ },
769
+ '*',
770
+ )
771
+ return
772
+ }
773
+ try {
774
+ const value = offlineStorage.operate(scope, message.operation, message.value)
775
+ event.source.postMessage(
776
+ { type: 'e10-form-store-result', requestId: message.requestId, value },
777
+ '*',
778
+ )
779
+ } catch {
780
+ event.source.postMessage(
781
+ {
782
+ type: 'e10-form-store-result',
783
+ requestId: message.requestId,
784
+ error: '本地数据保存失败',
785
+ },
786
+ '*',
787
+ )
788
+ }
789
+ })
790
+ $('menu-toggle').addEventListener('click', () => {
791
+ const open = $('app').classList.toggle('menu-open')
792
+ $('menu-toggle').setAttribute('aria-expanded', String(open))
793
+ })
794
+ $('shade').addEventListener('click', closeMenu)
795
+ document.addEventListener('keydown', (event) => {
796
+ if (event.key === 'Escape') closeMenu()
797
+ })
798
+ addEventListener('hashchange', select)
799
+ select()
800
+ })()
801
+ </script>
802
+ </body>
803
+ </html>