wwt-platform 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/platform.html ADDED
@@ -0,0 +1,1986 @@
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.0"/>
6
+ <title>WWT 工作平台</title>
7
+ <style>
8
+ :root{
9
+ --brand:#1a6fd4;--brand-dark:#1560bd;--brand-soft:#eaf2fd;--brand-line:#c7ddf8;
10
+ --bg:#f0f3f8;--panel:#fff;--border:#e0e6ef;
11
+ --text:#1c2126;--text-2:#5b656f;--text-3:#9aa3ac;
12
+ --green:#14915b;--green-soft:#e8f6ef;
13
+ --red:#d92b2b;--red-soft:#fdecec;
14
+ --orange:#c47f00;--purple:#6b46d4;
15
+ --sh:0 1px 3px rgba(20,30,50,.07),0 4px 16px rgba(20,30,50,.05);
16
+ --sh-lg:0 4px 24px rgba(20,30,50,.12);
17
+ --r:10px;--nw:240px;--th:58px;
18
+ }
19
+ *{box-sizing:border-box;margin:0;padding:0}
20
+ body{font-family:"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
21
+ background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;
22
+ -webkit-font-smoothing:antialiased}
23
+ button{cursor:pointer;font-family:inherit}
24
+ ::-webkit-scrollbar{width:5px;height:5px}
25
+ ::-webkit-scrollbar-thumb{background:#c8d0db;border-radius:3px}
26
+
27
+ /* ── 顶栏 ── */
28
+ .topbar{position:fixed;top:0;left:0;right:0;height:var(--th);z-index:200;
29
+ background:var(--panel);border-bottom:1px solid var(--border);
30
+ display:flex;align-items:center;padding:0 20px;gap:14px;box-shadow:var(--sh)}
31
+ .logo-wrap{display:flex;align-items:center;gap:10px;text-decoration:none}
32
+ .logo-img{height:38px;width:38px;object-fit:contain;border-radius:8px}
33
+ .logo-text strong{font-size:17px;font-weight:700;color:var(--brand);letter-spacing:.3px;display:block;line-height:1.2}
34
+ .logo-text span{font-size:10px;color:var(--text-3);letter-spacing:1.5px}
35
+ .tsep{flex:1}
36
+ .tact{display:flex;align-items:center;gap:10px}
37
+ .btn{border:1px solid var(--border);background:var(--panel);color:var(--text);
38
+ padding:6px 14px;border-radius:7px;font-size:13px;transition:all .15s;white-space:nowrap}
39
+ .btn:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-soft)}
40
+ .btn.prim{background:var(--brand);border-color:var(--brand);color:#fff}
41
+ .btn.prim:hover{background:var(--brand-dark)}
42
+ .btn.sm{padding:4px 10px;font-size:12px;border-radius:6px}
43
+ .btn.danger{color:var(--red);border-color:var(--red)}
44
+ .btn.danger:hover{background:var(--red-soft)}
45
+ .ttag{background:var(--bg);border:1px solid var(--border);border-radius:20px;
46
+ padding:4px 12px;font-size:12px;color:var(--text-2)}
47
+
48
+ /* ── 左导航 ── */
49
+ .nav{position:fixed;top:var(--th);left:0;bottom:0;width:var(--nw);z-index:100;
50
+ background:var(--panel);border-right:1px solid var(--border);
51
+ display:flex;flex-direction:column}
52
+ .nav-scroll{flex:1;overflow-y:auto;padding:12px 0}
53
+ .nav-item{display:flex;align-items:center;gap:8px;padding:8px 16px;
54
+ font-size:13px;color:var(--text-2);cursor:pointer;transition:background .1s;
55
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;user-select:none}
56
+ .nav-item:hover{background:var(--bg);color:var(--text)}
57
+ .nav-item.active{background:var(--brand-soft);color:var(--brand);font-weight:600;
58
+ border-right:3px solid var(--brand)}
59
+ .nav-mod{font-weight:600;padding:9px 16px}
60
+ .nav-mod .chevron{margin-left:auto;font-size:11px;transition:transform .2s;color:var(--text-3)}
61
+ .nav-mod.open .chevron{transform:rotate(90deg)}
62
+ .nav-mod-icon{width:28px;height:28px;border-radius:7px;display:flex;align-items:center;
63
+ justify-content:center;font-size:14px;flex-shrink:0}
64
+ .nav-child{padding-left:52px}
65
+ .nav-children{overflow:hidden;transition:max-height .25s}
66
+ .nav-children.closed{max-height:0!important}
67
+ .nav-sep{height:1px;background:var(--border);margin:8px 12px}
68
+ .idot{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.6;flex-shrink:0}
69
+ .nav-bottom{padding:12px;border-top:1px solid var(--border)}
70
+ .pulse-dot{width:8px;height:8px;border-radius:50%;background:var(--green);
71
+ animation:pulse 1.8s ease infinite;flex-shrink:0}
72
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
73
+
74
+ /* ── 主区 ── */
75
+ .main{margin-left:var(--nw);margin-top:var(--th);padding:24px;min-height:calc(100vh - var(--th))}
76
+ .page{display:none}.page.on{display:block}
77
+ .fpage{display:none;flex-direction:column;height:calc(100vh - var(--th) - 48px)}
78
+ .fpage.on{display:flex}
79
+ .ph{margin-bottom:20px}
80
+ .ph h2{font-size:20px;font-weight:700}
81
+ .ph p{font-size:13px;color:var(--text-2);margin-top:4px}
82
+
83
+ /* ── 首页 ── */
84
+ .stat-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px}
85
+ .stat-c{background:var(--panel);border:1px solid var(--border);border-radius:var(--r);
86
+ padding:18px 20px;box-shadow:var(--sh)}
87
+ .stat-c .k{font-size:12px;color:var(--text-3);margin-bottom:4px}
88
+ .stat-c .v{font-size:28px;font-weight:700}
89
+ .stat-c.blue .v{color:var(--brand)}.stat-c.green .v{color:var(--green)}.stat-c.pur .v{color:var(--purple)}
90
+ .mod-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
91
+ .mod-c{background:var(--panel);border:1px solid var(--border);border-radius:var(--r);
92
+ padding:20px;box-shadow:var(--sh);cursor:pointer;transition:all .18s}
93
+ .mod-c:hover{border-color:var(--brand);box-shadow:var(--sh-lg);transform:translateY(-2px)}
94
+ .mod-c-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;
95
+ justify-content:center;font-size:22px;margin-bottom:12px}
96
+ .mod-c h3{font-size:15px;font-weight:700;margin-bottom:6px}
97
+ .mod-c p{font-size:13px;color:var(--text-2);line-height:1.5}
98
+ .mod-c-ft{display:flex;align-items:center;justify-content:space-between;margin-top:14px;font-size:12px;color:var(--text-3)}
99
+
100
+ /* ── 工具卡片 ── */
101
+ .tool-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:16px}
102
+ .tool-c{background:var(--panel);border:1px solid var(--border);border-radius:var(--r);
103
+ padding:18px 16px;display:flex;flex-direction:column;gap:10px;
104
+ box-shadow:var(--sh);transition:all .18s;position:relative}
105
+ .tool-c:hover{border-color:var(--brand);box-shadow:var(--sh-lg);transform:translateY(-2px)}
106
+ .tool-c-body{display:flex;flex-direction:column;gap:10px;flex:1;cursor:pointer}
107
+ .tool-c-ico{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px}
108
+ .tool-c h3{font-size:14px;font-weight:600}
109
+ .tool-c p{font-size:12px;color:var(--text-2);flex:1;line-height:1.5}
110
+ .tool-c-ft{display:flex;align-items:center;justify-content:space-between}
111
+ .ttool-tag{font-size:11px;padding:2px 8px;border-radius:20px}
112
+ /* 工具操作菜单 */
113
+ .tool-menu-wrap{position:absolute;top:10px;right:10px}
114
+ .tool-menu-btn{background:none;border:none;cursor:pointer;font-size:18px;color:var(--text-3);
115
+ width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;
116
+ opacity:0;transition:opacity .15s,background .15s}
117
+ .tool-c:hover .tool-menu-btn{opacity:1}
118
+ .tool-menu-btn:hover{background:var(--border);color:var(--text-1)}
119
+ .tool-menu{display:none;position:absolute;right:0;top:28px;background:var(--panel);
120
+ border:1px solid var(--border);border-radius:8px;box-shadow:var(--sh-lg);
121
+ min-width:130px;z-index:200;overflow:hidden}
122
+ .tool-menu.open{display:block}
123
+ .tool-menu-item{padding:9px 14px;font-size:13px;cursor:pointer;transition:background .12s;white-space:nowrap}
124
+ .tool-menu-item:hover{background:var(--bg)}
125
+ .tool-menu-item.danger{color:var(--red)}
126
+ .tool-menu-item.danger:hover{background:#fef2f2}
127
+ .add-c{background:transparent;border:1.5px dashed var(--border);border-radius:var(--r);
128
+ padding:18px 16px;display:flex;flex-direction:column;align-items:center;
129
+ justify-content:center;gap:8px;cursor:pointer;transition:all .18s;min-height:130px;color:var(--text-3)}
130
+ .add-c:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-soft)}
131
+
132
+ /* ── iframe 页 ── */
133
+ .frame-hd{display:flex;align-items:center;gap:10px;margin-bottom:12px;
134
+ background:var(--panel);border:1px solid var(--border);border-radius:var(--r);
135
+ padding:10px 16px;box-shadow:var(--sh)}
136
+ .back-btn{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:7px;
137
+ background:var(--bg);border:1px solid var(--border);font-size:13px;cursor:pointer;
138
+ color:var(--text-2);transition:all .15s}
139
+ .back-btn:hover{color:var(--brand);border-color:var(--brand)}
140
+ .frame-hd h3{font-size:14px;font-weight:600;flex:1}
141
+ .frame-box{flex:1;border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
142
+ .frame-box iframe{width:100%;height:100%;border:none}
143
+
144
+ /* ═══════════════════════════════════
145
+ 数据中心
146
+ ═══════════════════════════════════ */
147
+ /* 顶部操作栏 */
148
+ .dc-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:20px;flex-wrap:wrap}
149
+ .dc-toolbar .search{flex:1;min-width:180px;padding:8px 12px;border:1px solid var(--border);
150
+ border-radius:8px;font-size:13px;font-family:inherit}
151
+ .dc-toolbar .search:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)}
152
+
153
+ /* 文件列表 */
154
+ .dc-list{display:flex;flex-direction:column;gap:10px;margin-bottom:24px}
155
+ .dc-row{background:var(--panel);border:1px solid var(--border);border-radius:var(--r);
156
+ padding:14px 18px;display:flex;align-items:center;gap:14px;box-shadow:var(--sh);
157
+ transition:border-color .15s}
158
+ .dc-row:hover{border-color:var(--brand-line)}
159
+ .dc-row-ico{font-size:24px;flex-shrink:0;width:40px;text-align:center}
160
+ .dc-row-body{flex:1;min-width:0}
161
+ .dc-row-name{font-size:14px;font-weight:600;margin-bottom:2px;
162
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
163
+ .dc-row-meta{font-size:12px;color:var(--text-3);display:flex;gap:12px;flex-wrap:wrap}
164
+ .dc-row-meta span{display:flex;align-items:center;gap:4px}
165
+ .dc-row-status{font-size:12px;font-weight:600;padding:3px 10px;border-radius:20px;flex-shrink:0}
166
+ .st-ok{background:var(--green-soft);color:var(--green)}
167
+ .st-pending{background:#fff7e6;color:var(--orange)}
168
+ .st-empty{background:var(--bg);color:var(--text-3)}
169
+ .dc-row-acts{display:flex;gap:6px;flex-shrink:0}
170
+
171
+ /* 空态 */
172
+ .dc-empty{text-align:center;padding:60px 20px;color:var(--text-3)}
173
+ .dc-empty-ico{font-size:48px;margin-bottom:12px}
174
+ .dc-empty p{font-size:14px;margin-bottom:16px}
175
+
176
+ /* 分组标题 */
177
+ .dc-group-hd{font-size:12px;font-weight:600;color:var(--text-3);
178
+ text-transform:uppercase;letter-spacing:.8px;margin:16px 0 8px;padding:0 4px}
179
+
180
+ /* 统计栏 */
181
+ .dc-stats{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap}
182
+ .dc-stat{background:var(--panel);border:1px solid var(--border);border-radius:8px;
183
+ padding:12px 16px;flex:1;min-width:120px;box-shadow:var(--sh)}
184
+ .dc-stat .k{font-size:11px;color:var(--text-3);margin-bottom:2px}
185
+ .dc-stat .v{font-size:22px;font-weight:700;color:var(--brand)}
186
+
187
+ /* ── 弹窗 ── */
188
+ .mask{display:none;position:fixed;inset:0;background:rgba(10,20,40,.45);
189
+ z-index:1000;align-items:center;justify-content:center}
190
+ .mask.on{display:flex}
191
+ .modal{background:var(--panel);border-radius:14px;box-shadow:var(--sh-lg);
192
+ width:480px;max-width:calc(100vw - 32px);padding:28px;position:relative;
193
+ animation:mIn .2s ease}
194
+ .modal.wide{width:560px}
195
+ @keyframes mIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
196
+ .modal-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border-radius:50%;
197
+ display:flex;align-items:center;justify-content:center;background:var(--bg);
198
+ border:none;font-size:16px;cursor:pointer;color:var(--text-2)}
199
+ .modal-x:hover{background:var(--border)}
200
+ .modal h2{font-size:17px;font-weight:700;margin-bottom:18px}
201
+ .modal h2 small{font-size:12px;color:var(--text-3);font-weight:400;margin-left:8px}
202
+ .frow{margin-bottom:14px}
203
+ .frow label{display:block;font-size:13px;color:var(--text-2);margin-bottom:5px;font-weight:500}
204
+ .frow input,.frow select,.frow textarea{width:100%;padding:9px 12px;border:1px solid var(--border);
205
+ border-radius:8px;font-size:14px;background:#fff;color:var(--text);font-family:inherit;
206
+ transition:border-color .15s}
207
+ .frow textarea{resize:vertical;min-height:64px}
208
+ .frow input:focus,.frow select:focus,.frow textarea:focus{outline:none;border-color:var(--brand);
209
+ box-shadow:0 0 0 3px var(--brand-soft)}
210
+ .frow .hint{font-size:11px;color:var(--text-3);margin-top:4px}
211
+ .drop-zone{border:1.5px dashed var(--border);border-radius:var(--r);padding:28px;
212
+ text-align:center;background:#fafbfc;cursor:pointer;transition:all .15s}
213
+ .drop-zone:hover,.drop-zone.over{border-color:var(--brand);background:var(--brand-soft)}
214
+ .drop-zone .dz-ico{font-size:36px;margin-bottom:8px}
215
+ .drop-zone p{font-size:13px;color:var(--text-2)}
216
+ .drop-zone b{color:var(--brand)}
217
+ .drop-zone small{font-size:12px;color:var(--text-3);display:block;margin-top:4px}
218
+ .file-picked{margin-top:10px;padding:10px 14px;background:var(--brand-soft);
219
+ border:1px solid var(--brand-line);border-radius:8px;font-size:13px;color:var(--brand);
220
+ display:none;align-items:center;gap:8px}
221
+ .modal-ft{display:flex;gap:10px;justify-content:flex-end;margin-top:6px}
222
+
223
+ /* 图标选择 */
224
+ .icon-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
225
+ .icon-opt{width:36px;height:36px;border-radius:7px;display:flex;align-items:center;
226
+ justify-content:center;font-size:18px;border:2px solid var(--border);cursor:pointer;transition:all .12s}
227
+ .icon-opt:hover,.icon-opt.sel{border-color:var(--brand);background:var(--brand-soft)}
228
+ .color-row{display:flex;gap:8px;margin-top:6px}
229
+ .color-opt{width:26px;height:26px;border-radius:50%;cursor:pointer;border:2px solid transparent}
230
+ .color-opt.sel,.color-opt:hover{border-color:var(--text)}
231
+
232
+ /* ── Toast ── */
233
+ .toast{position:fixed;bottom:24px;right:24px;z-index:9999;background:var(--text);color:#fff;
234
+ padding:10px 18px;border-radius:8px;font-size:13px;box-shadow:var(--sh-lg);
235
+ opacity:0;transform:translateY(8px);transition:all .25s;pointer-events:none}
236
+ .toast.on{opacity:1;transform:translateY(0)}
237
+ .toast.ok{background:var(--green)}.toast.err{background:var(--red)}.toast.info{background:var(--brand)}
238
+ </style>
239
+ </head>
240
+ <body>
241
+
242
+ <!-- 顶栏 -->
243
+ <header class="topbar">
244
+ <a class="logo-wrap" href="#" onclick="nav('home');return false">
245
+ <img class="logo-img" id="logo-img" alt="WWT"/>
246
+ <div class="logo-text">
247
+ <strong>WWT 工作平台</strong>
248
+ <span>Work Management Platform</span>
249
+ </div>
250
+ </a>
251
+ <div class="tsep"></div>
252
+ <div class="tact">
253
+ <span class="ttag" id="ttag">首页</span>
254
+ <button class="btn" style="color:var(--green);border-color:var(--green)" onclick="nav('dc')">🗄 数据中心</button>
255
+ <button class="btn prim" onclick="openUploadTool()">+ 上传工具</button>
256
+ <button class="btn" onclick="openAddMod()">+ 新增模块</button>
257
+ <button class="btn" id="upd-btn" onclick="openUpdateModal()" style="position:relative">
258
+ ↑ 检查更新
259
+ <span id="upd-dot" style="display:none;position:absolute;top:4px;right:4px;
260
+ width:8px;height:8px;border-radius:50%;background:var(--red)"></span>
261
+ </button>
262
+ </div>
263
+ </header>
264
+
265
+ <!-- 左导航 -->
266
+ <nav class="nav">
267
+ <div class="nav-scroll" id="navtree"></div>
268
+ </nav>
269
+
270
+ <!-- 主区 -->
271
+ <main class="main" id="main">
272
+
273
+ <!-- 首页 -->
274
+ <div class="page" id="pg-home">
275
+ <div class="ph"><h2>欢迎使用 WWT 工作平台</h2><p>统一管理财务、业务、数据分析三大核心模块。</p></div>
276
+ <div class="stat-row">
277
+ <div class="stat-c blue"><div class="k">模块数量</div><div class="v" id="s-mods">3</div></div>
278
+ <div class="stat-c green"><div class="k">工具总数</div><div class="v" id="s-tools">4</div></div>
279
+ <div class="stat-c pur"><div class="k">数据文件</div><div class="v" id="s-files">0</div></div>
280
+ </div>
281
+ <div style="margin-bottom:12px"><strong style="font-size:16px">核心模块</strong></div>
282
+ <div class="mod-grid" id="home-mods"></div>
283
+ </div>
284
+
285
+ <!-- 数据中心 -->
286
+ <div class="page" id="pg-dc">
287
+ <div class="ph">
288
+ <div style="display:flex;align-items:center;gap:12px">
289
+ <div style="width:38px;height:38px;border-radius:10px;background:#14915b1a;color:#14915b;
290
+ display:flex;align-items:center;justify-content:center;font-size:20px">🗄</div>
291
+ <div><h2>数据中心</h2><p>统一管理所有上传的业务数据文件,各工具直接调用,无需重复上传。</p></div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- 统计 -->
296
+ <div class="dc-stats">
297
+ <div class="dc-stat"><div class="k">文件总数</div><div class="v" id="dc-s-total">0</div></div>
298
+ <div class="dc-stat"><div class="k">已入库</div><div class="v" id="dc-s-ok" style="color:var(--green)">0</div></div>
299
+ <div class="dc-stat"><div class="k">总行数</div><div class="v" id="dc-s-rows">0</div></div>
300
+ </div>
301
+
302
+ <!-- 操作栏 -->
303
+ <div class="dc-toolbar">
304
+ <input class="search" id="dc-search" placeholder="🔍 搜索文件名或备注…" oninput="renderDC()"/>
305
+ <button class="btn prim" onclick="openAddFile()">+ 添加文件</button>
306
+ <button class="btn" onclick="exportAll()">⬇ 导出全部</button>
307
+ </div>
308
+
309
+ <!-- 文件列表 -->
310
+ <div id="dc-list"></div>
311
+ </div>
312
+
313
+ </main>
314
+
315
+ <!-- ── 弹窗:添加文件 ── -->
316
+ <div class="mask" id="m-addfile">
317
+ <div class="modal wide">
318
+ <button class="modal-x" onclick="closeMask('m-addfile')">✕</button>
319
+ <h2>添加数据文件 <small>支持 .xls .xlsx .csv</small></h2>
320
+
321
+ <div class="frow">
322
+ <label>文件名称 <span style="color:var(--red)">*</span></label>
323
+ <input id="af-name" placeholder="例如:销售已确认、入库单2025年…"/>
324
+ <div class="hint">这个名字用于在数据中心识别该文件,可自由填写</div>
325
+ </div>
326
+
327
+ <div class="frow">
328
+ <label>文件分类</label>
329
+ <select id="af-cat">
330
+ <option value="销售">销售</option>
331
+ <option value="入库">入库</option>
332
+ <option value="出库">出库</option>
333
+ <option value="维修">维修</option>
334
+ <option value="调拨">调拨</option>
335
+ <option value="对账">对账</option>
336
+ <option value="往来账">往来账</option>
337
+ <option value="凭证">凭证</option>
338
+ <option value="其他">其他</option>
339
+ </select>
340
+ </div>
341
+
342
+ <div class="frow">
343
+ <label>备注说明</label>
344
+ <textarea id="af-note" placeholder="选填,例如:2025年7月结算数据"></textarea>
345
+ </div>
346
+
347
+ <div class="frow">
348
+ <label>选择文件 <span style="color:var(--red)">*</span></label>
349
+ <div class="drop-zone" id="af-dz"
350
+ ondragover="event.preventDefault();this.classList.add('over')"
351
+ ondragleave="this.classList.remove('over')"
352
+ ondrop="afDrop(event)"
353
+ onclick="document.getElementById('af-inp').click()">
354
+ <div class="dz-ico">📂</div>
355
+ <p>拖拽文件到此处,或 <b>点击选择</b></p>
356
+ <small>支持 Excel (.xls .xlsx) 和 CSV</small>
357
+ </div>
358
+ <input type="file" id="af-inp" accept=".xls,.xlsx,.csv" style="display:none" onchange="afPick(this)"/>
359
+ <div class="file-picked" id="af-picked">📄 <span id="af-picked-name"></span></div>
360
+ </div>
361
+
362
+ <div class="modal-ft">
363
+ <button class="btn" onclick="closeMask('m-addfile')">取消</button>
364
+ <button class="btn prim" onclick="doAddFile()">确认添加</button>
365
+ </div>
366
+ </div>
367
+ </div>
368
+
369
+ <!-- ── 弹窗:上传工具 ── -->
370
+ <div class="mask" id="m-tool">
371
+ <div class="modal">
372
+ <button class="modal-x" onclick="closeMask('m-tool')">✕</button>
373
+ <h2>上传工具程序</h2>
374
+ <div class="frow"><label>目标模块</label><select id="ut-mod"></select></div>
375
+ <div class="frow"><label>工具名称</label><input id="ut-name" placeholder="例如:往来款项两清工具"/></div>
376
+ <div class="frow"><label>工具描述</label><input id="ut-desc" placeholder="简要说明用途"/></div>
377
+ <div class="frow">
378
+ <label>工具类型</label>
379
+ <select id="ut-type" onchange="utTypeChange()">
380
+ <option value="html">HTML 单文件(直接在平台内打开)</option>
381
+ <option value="service">后端服务(需本机启动,如 Flask/Node 应用)</option>
382
+ </select>
383
+ </div>
384
+ <!-- HTML 类型:上传文件 -->
385
+ <div id="ut-file-area" class="frow">
386
+ <label>工具文件 (.html / .zip)</label>
387
+ <div class="drop-zone" id="ut-dz"
388
+ ondragover="event.preventDefault();this.classList.add('over')"
389
+ ondragleave="this.classList.remove('over')"
390
+ ondrop="utDrop(event)"
391
+ onclick="document.getElementById('ut-inp').click()">
392
+ <div class="dz-ico">📄</div>
393
+ <p>拖拽文件到此处,或 <b>点击选择</b></p>
394
+ </div>
395
+ <input type="file" id="ut-inp" accept=".html,.zip" style="display:none" onchange="utPick(this)"/>
396
+ <div class="file-picked" id="ut-picked">📄 <span id="ut-picked-name"></span></div>
397
+ </div>
398
+ <!-- 后端服务类型:填地址 -->
399
+ <div id="ut-svc-area" style="display:none">
400
+ <div class="frow">
401
+ <label>服务地址</label>
402
+ <input id="ut-svc-url" placeholder="例如:http://localhost:5000" value="http://localhost:5000"/>
403
+ <div class="hint">工具启动后在浏览器访问的地址</div>
404
+ </div>
405
+ <div class="frow">
406
+ <label>工具目录路径(可选)</label>
407
+ <input id="ut-svc-path" placeholder="例如:C:/MyTools/WWT凭证工具"/>
408
+ <div class="hint">填写后,未启动时会显示启动提示和路径</div>
409
+ </div>
410
+ <div class="frow">
411
+ <label>启动文件名(可选)</label>
412
+ <input id="ut-svc-bat" placeholder="例如:启动.bat" value="启动.bat"/>
413
+ </div>
414
+ </div>
415
+ <div class="modal-ft">
416
+ <button class="btn" onclick="closeMask('m-tool')">取消</button>
417
+ <button class="btn prim" onclick="doAddTool()">发布到平台</button>
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <!-- ── 弹窗:新增模块 ── -->
423
+ <div class="mask" id="m-mod">
424
+ <div class="modal">
425
+ <button class="modal-x" onclick="closeMask('m-mod')">✕</button>
426
+ <h2>新增模块</h2>
427
+ <div class="frow"><label>模块名称</label><input id="nm-name" placeholder="例如:人力资源"/></div>
428
+ <div class="frow"><label>模块描述</label><input id="nm-desc" placeholder="简要说明用途"/></div>
429
+ <div class="frow"><label>图标</label><div class="icon-grid" id="nm-icons"></div></div>
430
+ <div class="frow"><label>主题色</label><div class="color-row" id="nm-colors"></div></div>
431
+ <div class="modal-ft">
432
+ <button class="btn" onclick="closeMask('m-mod')">取消</button>
433
+ <button class="btn prim" onclick="doAddMod()">创建</button>
434
+ </div>
435
+ </div>
436
+ </div>
437
+
438
+ <!-- ── 弹窗:编辑工具 ── -->
439
+ <div class="mask" id="m-edit-tool">
440
+ <div class="modal" style="max-width:460px">
441
+ <button class="modal-x" onclick="closeMask('m-edit-tool')">✕</button>
442
+ <h2>编辑工具信息</h2>
443
+ <div class="frow"><label>工具名称</label><input id="et-name" placeholder="工具名称"/></div>
444
+ <div class="frow"><label>工具描述</label><input id="et-desc" placeholder="简要说明用途"/></div>
445
+ <div id="et-path-row" class="frow"><label>文件路径</label><input id="et-path" placeholder="本地文件路径"/></div>
446
+ <div id="et-svc-row" class="frow" style="display:none"><label>服务地址</label><input id="et-svcurl" placeholder="http://localhost:5000"/></div>
447
+ <div class="modal-ft">
448
+ <button class="btn" onclick="closeMask('m-edit-tool')">取消</button>
449
+ <button class="btn prim" onclick="doEditTool()">保存</button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ <!-- ── 弹窗:删除确认 ── -->
455
+ <div class="mask" id="m-del-tool">
456
+ <div class="modal" style="max-width:400px">
457
+ <button class="modal-x" onclick="closeMask('m-del-tool')">✕</button>
458
+ <h2>确认删除</h2>
459
+ <div style="padding:10px 0 20px;color:var(--text-2);line-height:1.7">
460
+ 确定要从平台删除工具 <strong id="del-tool-name" style="color:var(--text-1)"></strong> 吗?<br>
461
+ <span style="font-size:13px;color:var(--text-3)">此操作仅从平台移除该工具入口,不会删除本地文件。</span>
462
+ </div>
463
+ <div class="modal-ft">
464
+ <button class="btn" onclick="closeMask('m-del-tool')">取消</button>
465
+ <button class="btn" style="background:var(--red);color:#fff;border-color:var(--red)" onclick="doDelTool()">确认删除</button>
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ <!-- ── 弹窗:平台与工具更新 ── -->
471
+ <div class="mask" id="m-update">
472
+ <div class="modal" style="max-width:540px">
473
+ <button class="modal-x" onclick="closeMask('m-update')">✕</button>
474
+ <h2>检查更新</h2>
475
+ <div id="upd-body" style="margin:16px 0;max-height:55vh;overflow-y:auto">
476
+ <div style="text-align:center;padding:30px;color:var(--text-3)">正在检查平台和工具更新…</div>
477
+ </div>
478
+ <div class="modal-ft">
479
+ <button class="btn" onclick="openUpdRegModal()">⚙ 更新清单管理</button>
480
+ <button class="btn" onclick="markAllUpdated()">✓ 全部标记为已更新</button>
481
+ <button class="btn" onclick="openUpdateModal()">↻ 重新检查</button>
482
+ <button class="btn prim" onclick="closeMask('m-update')">关闭</button>
483
+ </div>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- ── 弹窗:更新清单管理 ── -->
488
+ <div class="mask" id="m-updreg">
489
+ <div class="modal" style="max-width:520px">
490
+ <button class="modal-x" onclick="closeMask('m-updreg')">✕</button>
491
+ <h2>更新清单管理</h2>
492
+ <div style="font-size:13px;color:var(--text-3);margin-bottom:12px">
493
+ 清单中每一项对应一个 Gitee 仓库。平台更新时会逐一检查并提示可更新对象。新增工具仓库时在这里添加即可,无需改代码。
494
+ </div>
495
+ <div id="updreg-list" style="max-height:50vh;overflow-y:auto"></div>
496
+ <div class="modal-ft"><button class="btn" onclick="closeMask('m-updreg')">关闭</button></div>
497
+ </div>
498
+ </div>
499
+
500
+ <!-- ── 弹窗:编辑/新增更新条目 ── -->
501
+ <div class="mask" id="m-updreg-item">
502
+ <div class="modal" style="max-width:480px">
503
+ <button class="modal-x" onclick="closeMask('m-updreg-item')">✕</button>
504
+ <h2 id="ur-title">新增更新条目</h2>
505
+ <div class="frow"><label>标识 key</label><input id="ur-key" placeholder="例如:erp-recon(唯一)"/></div>
506
+ <div class="frow"><label>显示名称</label><input id="ur-name" placeholder="例如:ERP 对账工具"/></div>
507
+ <div class="frow"><label>当前版本号</label><input id="ur-cur" placeholder="例如:V1.0.2(平台当前内置的版本)"/></div>
508
+ <div class="frow"><label>Gitee 仓库</label><input id="ur-repo" placeholder="格式:用户名/仓库名,如 wwtmrhu/erp-recon"/></div>
509
+ <div class="frow"><label>版本文件路径</label><input id="ur-ver" placeholder="version.js 或 dist/version.json"/></div>
510
+ <div class="frow"><label>更新包文件路径</label><input id="ur-app" placeholder="app.js 或 dist/app.js"/></div>
511
+ <div class="frow"><label>版本字段</label><input id="ur-pat" placeholder="version.js 中的变量名,如 __ERP_LATEST_VER;JSON 填 version"/></div>
512
+ <div class="modal-ft">
513
+ <button class="btn" onclick="closeMask('m-updreg-item')">取消</button>
514
+ <button class="btn prim" onclick="saveUpdRegItem()">保存</button>
515
+ </div>
516
+ </div>
517
+ </div>
518
+
519
+ <!-- ── 弹窗:查看数据 ── -->
520
+ <div class="mask" id="m-preview">
521
+ <div class="modal wide">
522
+ <button class="modal-x" onclick="closeMask('m-preview')">✕</button>
523
+ <h2 id="pv-title">数据预览</h2>
524
+ <div id="pv-body" style="max-height:380px;overflow-y:auto;font-size:13px"></div>
525
+ <div class="modal-ft"><button class="btn" onclick="closeMask('m-preview')">关闭</button></div>
526
+ </div>
527
+ </div>
528
+
529
+ <div class="toast" id="toast"></div>
530
+
531
+ <script>
532
+ // ═══════════════════════════════════════════════════════════
533
+ // IndexedDB
534
+ // ═══════════════════════════════════════════════════════════
535
+ const IDB_NAME='wwt_shared_db',IDB_VER=2,ST_TAB='tables',ST_KV='kv',ST_FILES='files';
536
+ let _idb=null;
537
+ function openDB(){
538
+ if(_idb)return Promise.resolve(_idb);
539
+ if(_idbPending)return _idbPending;
540
+ _idbPending=new Promise((res,rej)=>{
541
+ const r=indexedDB.open(IDB_NAME,IDB_VER);
542
+ r.onupgradeneeded=ev=>{const db=ev.target.result;
543
+ if(!db.objectStoreNames.contains(ST_TAB))db.createObjectStore(ST_TAB);
544
+ if(!db.objectStoreNames.contains(ST_KV))db.createObjectStore(ST_KV);
545
+ if(!db.objectStoreNames.contains(ST_FILES))db.createObjectStore(ST_FILES);};
546
+ r.onsuccess=ev=>{_idb=ev.target.result;_idbPending=null;res(_idb);};
547
+ r.onerror=ev=>{_idbPending=null;rej(ev.target.error);};
548
+ r.onblocked=()=>{_idbPending=null;rej(new Error('IndexedDB 被其他标签页占用,请关闭其他已打开的 WWT 平台窗口'));};
549
+ // 8 秒超时兜底:避免 onblocked 未触发时永久挂起,导致工具 init 卡在 await DB.probe()
550
+ setTimeout(()=>{if(_idbPending){_idbPending=null;try{r.abort();}catch(e){}
551
+ rej(new Error('IndexedDB 打开超时'));}},8000);
552
+ });
553
+ return _idbPending;
554
+ }
555
+ let _idbPending=null;
556
+ const iGet=(s,k)=>openDB().then(db=>new Promise((res,rej)=>{const r=db.transaction(s,'readonly').objectStore(s).get(k);r.onsuccess=()=>res(r.result);r.onerror=()=>rej(r.error);}));
557
+ const iPut=(s,k,v)=>openDB().then(db=>new Promise((res,rej)=>{const r=db.transaction(s,'readwrite').objectStore(s).put(v,k);r.onsuccess=()=>res();r.onerror=()=>rej(r.error);}));
558
+ const iDel=(s,k)=>openDB().then(db=>new Promise((res,rej)=>{const r=db.transaction(s,'readwrite').objectStore(s).delete(k);r.onsuccess=()=>res();r.onerror=()=>rej(r.error);}));
559
+ const iKeys=(s)=>openDB().then(db=>new Promise((res,rej)=>{const r=db.transaction(s,'readonly').objectStore(s).getAllKeys();r.onsuccess=()=>res(r.result);r.onerror=()=>rej(r.error);}));
560
+
561
+ // postMessage 路由(供子工具 SDK 调用)
562
+ const DB_ACTIONS={
563
+ async saveTable({table,rows}){await iPut(ST_TAB,table,rows);renderDC();return{count:rows.length};},
564
+ async getTable({table}){return(await iGet(ST_TAB,table))||[];},
565
+ async appendRows({table,rows}){const ex=(await iGet(ST_TAB,table))||[];const m=ex.concat(rows);await iPut(ST_TAB,table,m);renderDC();return{count:m.length};},
566
+ async dropTable({table}){await iDel(ST_TAB,table);renderDC();},
567
+ async listTables(){return iKeys(ST_TAB);},
568
+ async setKV({key,value}){await iPut(ST_KV,key,value);},
569
+ async getKV({key,defaultVal}){const v=await iGet(ST_KV,key);return v!==undefined?v:defaultVal;},
570
+ async delKV({key}){await iDel(ST_KV,key);},
571
+ async getPlatformInfo(){return{version:'1.0',platform:'WWT'};},
572
+ async notify({message,type}){toast(message,type);},
573
+ };
574
+ window.addEventListener('message',async function(ev){
575
+ const d=ev.data;
576
+ // ① 工具主动上报自己的版本号 → 平台自动识别,无需手工标记
577
+ // 工具里加一行:parent.postMessage({__wwt_tool_ver:true,ver:'P1.2.36',name:'WWT凭证工具'},'*')
578
+ if(d&&d.__wwt_tool_ver===true){handleToolVerReport(d);return;}
579
+ if(!d||d.__wwt_req!==true)return;
580
+ const h=DB_ACTIONS[d.action];
581
+ try{const result=h?await h(d.payload||{}):null;ev.source.postMessage({__wwt_sdk:true,seq:d.seq,result},'*');}
582
+ catch(e){ev.source.postMessage({__wwt_sdk:true,seq:d.seq,error:String(e)},'*');}
583
+ });
584
+
585
+ // ═══════════════════════════════════════════════════════════
586
+ // 应用状态
587
+ // ═══════════════════════════════════════════════════════════
588
+ const STORE='wwt_v4';
589
+ const ICONS=['💰','📋','📊','👥','🏭','📦','🚚','📞','🔧','📈','🏦','⚙️','🌐','🗂️','📣'];
590
+ const COLORS=['#1a6fd4','#14915b','#6b46d4','#c47f00','#d92b2b','#0891b2','#7c3aed','#b45309'];
591
+ const CAT_ICONS={销售:'💰',入库:'📦',出库:'🚛',维修:'🔧',调拨:'🔄',对账:'📋',往来账:'📒',凭证:'🧾',其他:'📂'};
592
+ const CAT_COLORS={销售:'#1a6fd4',入库:'#14915b',出库:'#0891b2',维修:'#c47f00',调拨:'#6b46d4',对账:'#7c3aed',往来账:'#b45309',凭证:'#d92b2b',其他:'#5b656f'};
593
+
594
+ function initST(){return{modules:[
595
+ {id:'fin',name:'财务',desc:'财务对账、核销、凭证管理等工具集',icon:'💰',color:'#1a6fd4',open:true,tools:[
596
+ {id:'t1',name:'往来款项两清工具',desc:'自动对账核销,支持 Excel 导入',tag:'HTML',path:'C:/Users/Administrator/Desktop/往来款项两清工具.html',local:true},
597
+ {id:'t2',name:'ERP对账工具',desc:'ERP 系统多模块对账比对',tag:'HTML',path:'C:/Users/Administrator/Desktop/ERP对账工具更新版.html',local:true},
598
+ {id:'t3',name:'ERP对账单生成匹配',desc:'对账单自动生成与匹配',tag:'HTML',path:'C:/Users/Administrator/Desktop/ERP对账单生成匹配工具更新版.html',local:true},
599
+ {id:'t4',name:'WWT快快对',desc:'往来账双向自动对账',tag:'HTML',path:'C:/Users/Administrator/WorkBuddy/2026-08-12-10-53-10/WWT快快对.html',local:true},
600
+ {id:'t5',name:'WWT凭证工具',desc:'自动生成记账凭证,支持规则配置、凭证审核、导出金蝶',tag:'服务',type:'service',serviceUrl:'http://localhost:5000',startPath:'C:/Users/Administrator/WorkBuddy/2026-08-12-10-01-47/WWT凭证工具/dist_release',startBat:'启动.bat'}
601
+ ]},
602
+ {id:'biz',name:'业务',desc:'业务流程管理工具集',icon:'📋',color:'#14915b',open:false,tools:[]},
603
+ {id:'ana',name:'数据分析',desc:'数据报表与可视化工具集',icon:'📊',color:'#6b46d4',open:false,tools:[]}
604
+ ],files:[]};}
605
+
606
+ let ST;
607
+ try{
608
+ const _raw=localStorage.getItem(STORE);
609
+ ST=_raw?JSON.parse(_raw):null;
610
+ // 校验结构完整性
611
+ if(!ST||!Array.isArray(ST.modules)||!ST.modules.length)ST=null;
612
+ }catch(e){ST=null;}
613
+ if(!ST)ST=initST();
614
+ if(!ST.files)ST.files=[];
615
+ // 迁移旧版数据(wwt_v3)里用户自己添加的模块和工具
616
+ (function migrateLegacy(){
617
+ try{
618
+ const old=JSON.parse(localStorage.getItem('wwt_v3')||'null');
619
+ if(!old||!old.modules)return;
620
+ const builtinIds=['fin','biz','ana'];
621
+ old.modules.forEach(function(om){
622
+ // 用户自添加的模块(非内置)
623
+ if(!builtinIds.includes(om.id)){
624
+ if(!ST.modules.find(function(m){return m.id===om.id;})){
625
+ ST.modules.push(om);
626
+ }
627
+ } else {
628
+ // 内置模块里用户上传的工具(blob 类型)
629
+ const builtinToolIds=['t1','t2','t3','t4','t5'];
630
+ const target=ST.modules.find(function(m){return m.id===om.id;});
631
+ if(target&&om.tools){
632
+ om.tools.forEach(function(ot){
633
+ if(!builtinToolIds.includes(ot.id)&&!target.tools.find(function(t){return t.id===ot.id;})){
634
+ target.tools.push(ot);
635
+ }
636
+ });
637
+ }
638
+ }
639
+ });
640
+ // 迁移数据文件列表
641
+ if(old.files&&old.files.length&&!ST.files.length){
642
+ ST.files=old.files;
643
+ }
644
+ }catch(e){}
645
+ })();
646
+ function save(){
647
+ try{localStorage.setItem(STORE,JSON.stringify(ST));}catch(e){}
648
+ }
649
+
650
+ // LOGO
651
+ document.getElementById('logo-img').src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAN4AAAC4CAYAAACMylLwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAG9bSURBVHhe7X0FYFVH9v7vv1vB3aVCu93tag2Ie3ArUFxLvVuhpVvX3TqS5OXFIGix4lKkUKRCWygUDe7uwSH2/b9v5t0QINCmTUICOXBy37vv3rkzZ84355yx+38ooiIqonynIuAVURFdBSoCXhEV0VWgIuAVURFdBSoCXhEV0VWgIuAVURFdBSoCXkGkDLH+pJPT+FWcas97KMvHX6AM/ks3bG7y3KhDmudYRPlPRcArgGTxYQGjf2mGnTOCoNiCyZ6zEM2Odd05pBjW1fpvmKTfi+jqUBHwChgJEwIa7dslnMa/KeRz5ihIWtDJcmXHDvDO8u9Z3pHCswZ8+sEDviK6OlQEvAJGwoNw4WDjAqb76fCVL/Sw53dzeea9+s2c4IciulpUBLyCSB5z5biaOqbyZFao6Ntpnj1J+3ea9szhUziTyWc8DqaSc0if7Tlavwxj/zK5iPKPioBXgEmw2HVqH9Ye3YRVyeux/Pgu8nYsP7kFP55chxdm9cNzc/vihTmf4kUPvzDnY8tffoxX5/bDiPXTseTkBqzkfYaPbWd6O7Dz5AHPU4roalAR8PKB1Bly1rE7Mi1yAbOaIZJOnyPvSTmC2Xt+wrS932P0jtl4YeYHaDPsKbQZ+RQajfo3+Wk0HPMEwsY8ior9AlE+IgiVBgQYrnwRV+0fgHsT2yJ8zGNoPOopNOW9LT97Bu2HPofnp/wPs/d/jy/2LMKMXT9gwYEV52PArHTBV5v5jAxm3rAtSgr5ouIU0S9QEfBylaSYFykuKSU93bh3JqySpnroGJ3BgziODad2YuyKGYhaNhrPf9kXf45/CLUTWuKWgU1RJToQ5SK9UNbljTJR9TO5LLlSjO8VuSK5fLS3udbhClFeqBzhjWquANwR34zcErfHtsK9Q7og9qfRGLViCtae2oJDOMrcnfXkNCuxHOlsStJSTVHV6XOGH86acgt+l5a/iC6lIuDlKknxzvN5FbRR2je7l2LEhhl4ePI7aDq6Nyq6w1E6KgglCYJicQH4Y3w93BRbl4ChxXIHEjx+ZG9yfVSMI2D4/Xez2xdVY31Rxe2DqnxONXcQqrqCUSUmBH/g82+M98XN/K2Yyw/VYhsiZORjeHr8uxi2dho2ndnLmFI9pIo8DQRZNH5iw4J0RY3qQ83SshTRZakIeLlJ0jnpYpr9cCrjHA6fO4Z5+5bAvXw0QuO64N6Ylqg5IBAVI71ROjYQZdz+KBNLjvND2XgvlIv1QsVYAo5cMdabIPRChVhaKnK2QMoxE3QxPqgi8PF7lRh/y8xDyfj6KEmAl4rzQUleUzI2AKXZKFShO3t3TDM8M+M9RC8dgcSln+PHI2twyjjHpDQWWgaQxS7qLP11VAS83CQqnWPvkumADV08Hq9N+gR+I3vRwoShgovuX7RcxAcIrHtRPToY1d3BqBkdhFrRAbiFVqZ2tD8BRxeRXCHWJwv7ZgOi38YWcJdyrWg/5sUfNZiXGsxTNeZNDcKNA+uhGK1uTVrnP0U1wt8GNEarMU9j9Ipp2HxqB91oOZxqcIzhK6JfQUXAy0U6RT5EK7A4eQMil43E36NbonpEMErSvStOl7Es3bhKtGJVacWqk6vSnRRXc8vlo8KTq5MFOst+ZH0PMJwdiHLOSifYw4Ee9icTdJ48WFae6O4yH2VpBUvH+6AsG4BSZJWlNO+5L6YtOk96BQPXTcGqU9utEIroV1ER8K5EjuvIg2PJ0uhLpcuf8vymtj7p7G4M3DQVdw1siuqRVGwXXUlar7JxUlof09FRlYpdhQC0yi9FD6AV88uWLwSKc70Fx+9n+2zLF6ZpY8rzXJEsS1iV14kVH8oNNvlUxw1BWF5HArVipC+aznwSiWvGY/upAzgtaUlGhvlHTIGpk+kM+Vzmj5Lu9UdFwLsCOWCTamT2SBJpAt85nt16cg8mrZuLJya8hbqxHUzPY1m3YjIprhNDZVX6a4OdcmWWT4B0+zAe9MK/Ylui18TXMHzdVHoAkh7JkV2Wj9cn3M5TEfCuQKfJGqMyoJNp8yjPcSpU0tldeHTqa7g3ugVq92Os5goj2GgVYmlJ3FYZs1Paa5f9aQX9TSxYyu2NvyS0wJAVk7GBjVNyxjmnG8YizmnRrmMqAt4VKBNrppnWFK00rDuzDyPXz8ET095Dabpaf4y/Fzcn1KWlk0sp100uWXaKea2zOoX8KAsf/GGgF0rw+33uNugx6VW8Oi8CE7cvNF6CoVQe2aLpm+fMdUdFwLsSqZuOYNuZKrBNw1/iW6NiRDAqRhFkbNVLxfubjoaSLh8T16lbPnulvD5YbmdtVwPcFtUE1d2hpkG6eeADuJFc3FUPfnEd8d6iaCSlbsFJRnrX89hDEfCuQCcyzmDF8S1UlkT83d0e5aLrUbnuQdm4f6BM3H2o1T8M98a3g+/wrrg7ttl1DzyxekKrR6s31Idy0tCJD2qYnltaQ8a9t8Y0RdfJr2Jc0izjRVyv4LuugWddHc0q0ZwL9ZrwhNgTy8VvmolmY17Ena5WqOgKoSt1H93If6BsTF2Uc/njyQlvY2zSHHydvAZRq0Z7XEx/KlmIafGrRwfxqG55y1XJ6k2sRK4Ya4cQzG/87lxjr3N6EnlkrGiO/K6eRn3WkIO6/HWt7R31QTmyxvocVo9kdsDIe2Zsa/JCL4Cgq8781XYFo6Y7yLjmGoaoOiAIIfHdsBeHraBZERK5R+yk899sHV17VAQ8/kunS5mazqBDdc2TmtC8N+0o6g3pisoDCDgCqALdygp0KytE1UediIYIjO+Kref2mDhQWN2edgiVozUVS4PPYeSGBngWXLIEYk+3vEAUS87aASPQerhSLMHEayqqt5C/CaQCXak4Cyrbm+hLsPnS8vJcHBWdRw28a9zNzEQxoPSkXQC4isYsma9yzGu5WG9UjwzCq99E4ds9K3CKUjSiz8SbbQ5F+pulLbxm6LoCnvYuOd9lQoRZ5Nmv5C2nd2PsxploP/4FVI1owFa6HqrSvSxDhSkbGYZnZn6IKTsXYlfqfmgzBTM2JbzysDPlAGpEWVezXCxBEu+H0mbguR6KDVSsU9eApLzmXjImrOTyRa3IENQisGtGnD/eOiAMQYk90Xnqa+g87TV0mvoquvDYZnwf3NK/IW4bQO4fjpq0GiXdmtblhwrRjDtdQQQpGwd+l2KXic+luZ25xGoI1CCUIfCKJ9RDSeavWkwI8x6GfyW2wyuz3mclqH4oT9aJ2CFP9Xh+vTbougKepvFa8LEaGVtohr2+azGMBnbf/OJT1Hc9hCp0K/+Y4Euw1EcpzWuMCkPgkEewL/0Ir7LVb/RCf/g1hcctqQdRPcpaGQGsGO+XRdJAcwkeb9YqgchA1HSF4y+xLXB3dHO8OncAPl48hDwYH/wwyPAn3w/BV9t+xM7UQwTzQTKP/LyRjcKn3w1Hv0XD0P+H4fiU9zQZ8jj+NaA17o5pjj+5G6NCJK2J2wvF4+vipvj7DMA1rCFreTEQ8pur0NpXodspl7gkY7/i8fWN9ZNbWiWyPm5lozM4aRp2pXjcT8r14oW6At+1QtcZ8BwHhtWYoTXdslup2IcjWHhoCf7RvzkqRwajhJmf6IUSdIf+HNsar3wbhbkHltja9xhMpaOvIi2e2URwWODJ/fNDCbqmcqdu7xeCO/o2wn3x7dByxL/x3uIEDN82A59tnoH9qcdxkikdZ6Lik8zRaUe9nMSzkKakneIPWoajVQLf7VmFCUlfYdSW6Ri2bSrCh3XDHVTgqi66qgSdcVHlvtKtzQ4M+cmydopJ1Qg4U+LkFtdx10dtcnE2cLfHtMP73yRgT+pellCN4YVgy0YkhZauM+BZtpRGBT6H3ecOIXLZcPgP7UQ3MAClEuhWapoXlaLNyBcQu3Qs9qYfNoqgWfjGDeLdaovNOX4XFrcTeDU1XUzA472laGn+Fd0K7UY/gxenfoDPN8zE6uRNOESIabqUo0SmU+9ijdIDnMw6zIdkXqYP9tHmUj1fA9TfHlqOt7+KQrdxLyFoYFeUdtUzU7oKgsVTXKt5qdVND6dlfa4RrbjYm9aP+YwKxj2uVnh9USTm7/8ZR9K0NFdyvvboGgaeYKHeSsv6rAq0ypqBYziBxondaR3o/kX6486oQNyQ8ADdID/UTeyI12e7TCoXk5OGIX7QBkKyeHvPnkLZCB/cEdsE7cc+h8ErJtA6ZYXYRXSZ05fSr7hQlygv9lsmTdnyPZ6f+QnqJ3ZgLCUAqrdRk66DqOzBVPpg0wFUjudk5bMHTO6z7fyxy5HUKIgzp5+RK7DRKhfli3sHt2UzRemqYGxd0ozrqabGnjrfiBY+usaBZ+Fmld9qpqyD1laP3zAbt0WGMIa7n26hF0qywqtGhaDV+H9j0s652JGyz6SSHXlSM5RBE6g0j6Sk4LGJbyFq6Uj8fGKjeYYhqydXhVKYy+0pezGZ5Xn0y7fM2rrisQEEWQDK8qjhDQ13aMaNVkFcDJCryYqNK0YF4fM1M9hEWlmaedYeyeuvgOfUQ2GjaxJ41gnM4qLog1wzWqcjrMQxG+YgNPFRs8yldMJ9+AMt3f9LCEb3z1/DN3Rx5IJaiGZfrY73JxLwdJXu2H8uGScy7Lx7c9K5MPtk8p60QJX/laOtaQdx//AeKOkORfE4f2PZtbpAnRvVqOh2TDB7EFwNNpY5JhjhCZ0xfM0E1tuxS9xyfZSsxVlOFwq6JoHnbIpnKEvtnCakJqyfhdBBBJ2rAW6Kr48KcfVQOao+6g7tgJXJWwzczD3ZBl+WhCVznYcEaAM2kT44YFMaV0gnz0l5UQ+FmDRi4xcIHNSdsVQolToQJRN8DPg0KK/Fr9kB4GqxxjFLxwWbYZeAQQ9h5LpJdDtPG1FaidrG1WnbMuVfSOiaBZ6pHh6k93K5DuEkwhK7olZEoBnnKhZfz4yHNR7YC5NXz0BK+glezCr0YOTy9s5Sdr+lahMgjzqcI8iNWlwl4OmJij3Nk02PkFVNKe/odV+gRUIvxlLa7sHbjK1lp/xXk+X+ajz05oFeZvHtLZFBCB7YEYd/0R8pHHSNuppZgMd/Rwk6xXSVXEEoRQtXcuD9tHT3o8nAhzGC55NVjbw8I4OmIcNTpVZPc0SKJw+dOYIdh3fibBrjEjXFsjZXSUP0WEW5aYRgKhVW2VHpDjFqGrZmEirSypWI9zFcNh87V34da1aPL+tLM3bkdqrDxRufr57D+tRAUNYqukoC/h10DQHPAzb99VgZWZwTOIXxm2YhbEgvtu7BKC5FS/BG1Yj6+Hz9dBPzSSEtKQ1POvyvT/rNOeuQMSB6hOdShzalbMf/FsTi2XHvYcPxrQZ0GanMh8nP1SFJQd0/tm+X35kV9fF+fXCV6dGsEFcX5eIeQDnKJXsAXC3W1Df1xGqqnu1s0XS5+we1R8KGSWw8rNupQgmGKl9homsEeEJAFueDB7X0ybR0E9fPRrPBvVAtKoTuZSDKxAShWmQwQoZ0NXtaGkBdBhc67XDWa6S8Zl4hSW7tGT5bs0uen/ke/hzzIP7iao24n0ZbpeDFmfFmQSCT93SsP7PPzBappo2XCDzN9cweAAWLtX+NV2JHjFw9FScz6HSyPNaqFwEvn8haNAs48gVWJYOu5ZcIUc+lqwFuTPDHTQPro0qkN891xTiC8RgrzQCI/1Rx2ZKSdJgkrDlv3jmEY/jvPDcaD3scf058CDfSeqiL/oYELxRnbNJ56n9MzhRaXZC1q03KCy1xKk32I9NfR62oQLpx/qhoNj/KXtkLEld0B/EYjNqM+UJo/Y6a+Twkydl+KhR0jQCPUk9Lpftn513KvWyW8AgqDQjDHwYFkH3MLlkhgzpg1ObptISKEEi8/Xzf2KWkihTY9ITjBOqkDfPQ//uh+PCnoejzdQTu7NcU1fqHoLxLE6LVQ+iNkrH3UInrofPkZ3kf776KMV62pLx4RPbSnE/MlhWlYgPpyhUO4FWmx1LODId4Ue5eGM5Y9ahnop1qUv8KAxVyV1NCtoJOz5AlSsXh9JMYt3YG7owIRxlanhsSfVHa7YPaA4Ixdv0007GgLQjM4nLWlkB3OeA5oNPw7U971yPY1QW39WvI+DAc5QdQAais2vW5KuOPGnyGtkyvGHM/ahJ4gQkPmQ1tTRecEiogZCSm/LDIL8/6mHFUOG6mIlcoYON4l2OtwNDyKHkwxeldhMZ1wvB1E3GY9WqKZSq24NO1EeNRm1IIPE0c/vnYBnQe8QLdJx8C734e6+OWAQFoMKi7cUvM21RZObbDw6OBl0MGL9FlslzLDq5Fnb5BKMGWtmxsXQb6XqY3UEqgSci1otUKB0M7QVfjM/8c2ZjKcDbTuhQUUvnN3ifM0yuzPqF8QumKay1f9ope0FiTq0t6Vn8Ui/fDLXSVvRNb4fOkqUhPY92m26mBBZ0KNfCMtfKMvR2jkg9OGg+v+Dao6QqkNdKmq164O7Yp3CtHmd7LnJMcFzs2J/pi+wL8a2h7lHHbeYUXTz7WJrCaXK3xpzpRDXAw47jFdUHSBOWFrYnWRfwlqhnKM2ZS46ElTFnLUpi4Ar2Nv8Q3xTs/xJjJOs5An51GUTBhWLgtnsyRpmwRF2tObcUTU96gdVMnhzctUiDq9A9HtzF9sOz4BlMXOSdVnAd4/L85dS8em/s+qrvDLlw97uGswLsjqiEOFVDgaZrbMcZFd7gas4EKNMCTJbm4PIWJq0QF4L7o1ibac1xpU9YCJfzzVLiBJ2NHIcu5eHXmp/hnZHMzG+PGQXVROyIMPT97Cd/uWc62Xbv7qzZySlmAlwKmk4bPdy7EPxJbmw1cL678wgI88aaTO1HbFY7SdJNLE3gFY+nQb+cyjFNruBtgWfJ6GTtLBRNzhgo38IgHbaeu8bh/9WuOSq5gBty+uDH+frQZ828s3L0sC9z0Kac1IcgJsryXtakKnbHzRzOIK/fm4sq/GHgK+Asi8JSd7yibKtEhRl4CnrMkp7By6ThyTAB6f/E/LDu6loGFhosKLvIKeYwHrDi0EQ9//hpKDPQnU+ljfHBPRHPGdHpDuHVDrbL9Fn9f9k7/NAXFvod85s7vUNcA78qu5p0Enhlj+i14z2NSA7LwwM8oFueDGxIfQOl4b1QrYJOkc8oVaPHKsu6rRwWj6Yhe+GLXAhzLOGkLXACp0ABPuuuwQ0dwFgOXjMf9A9rgD4PqsfX2Rh13MF6e/SFbvHS7cNJzg0CXY/3nDQJbCv9lMC29yn/6zm9wf2K7ywDPvkxSC0tvczXAznMHMoFfkEjAW3BgGYHnReDdT+B5FXrgaW1hVYYZ2kWgzoAwPDy6D9ac2HKJ6H+DFuQJFQ7gUVZaYaA5CqbXippzKiMFA7fPQfCIR3FXRCD+yLiujCsQTYc8gp8OrzLi/d1CJmj0XL0BVZ2nmhE4dfd3+Nfgttm6mnqrarXocFSICUMJ8sSt83kH8/A7s5HbpDzNP7DUvOWnTJw3ypO11+fF5SlMbDfSDUZFWr5qrlDcH9kGcavHm7pLTWVFGpdfQYOanatPhQh4dpmLWd2SmmF23Wo9/TVU1tZ40drJywd/j2+LAYsG42jaMd31+8k81457yXJpY6QxO+bi74NbZwu8SjFBZk/NSu5Q3BQbjOHrpxmLqXQKEjnAqxgdiIqUm7hC7LUBPE2k1musb4toiKC47jiccQopZlY7C14EvJyR0VsJzuhwGg6mH8A97rao7PJBmfj6Zvu8wMGP4Ytt35nLTOuWS2RdVP5jJn46vg7NRj6GKpHZb5NQga5bjWj7cslydEVHb5phrGSBIpZDwJu3/ydUpIJqx+drAXjaMFdcgvFqcbrOZWK92bD44Z2vI7A5dTcbTdNF5vl79anwAE8NFXVYHR0LDyxB+Sgqd3w9ukr1UComFIOSpuMIWzej57naqOnpdlLZwKQJuDehuQF8dpVfgS6bXmdcw63ubT+88mVf7Dq936RSYIjFUYnm77cW71oBnnbm1q7bAp64tFzoGB/UjX0QET8Oxd6UZFOHakQLAhUO4MncCEw87M04hv9842KLVpexyf3Q64qrRzTG1jNUcP4ulzC3epFNMvrDNFVpXSa+gmpRUtTsd2nW2rFbXHQ3afE0nazR0MdM13YuZSd3SDLiYb4s3jUEPE150xt4NTRSlp/N+yMIxnJsBDuO6Y2NJ3cXAS+nZKaFpWrdQQa+OrgK3p89jXKx91LR61N5QtBw3PM4m37OoCOFqNOmC7lBJhXrn5gY896odgR6PQK+XraVry3zbnGFmZ27bhjohfsHtsfnG2fjnFnVXnBIyvfTyfW0EEEGdNcC8DTzphStnHl9NOVfLVrxdhBujL8PYcMfxoLtP5k6LBiwK0yuJr306Xu/xV9GPUKBNjTLfIrTsrSZ8AJWntzM39We/T7/3a5UEEg86RDEGkxIJpRHrf2CLmYwbhykidG/7r0E5dw++NPgJhi27guz5FZjidqNxTzjammAisbHn8BZ/COhJS1EAL2HQCpssGcQ3VqKijxXKSaE37X+7dKyFRYuzzooHx0K/0E9MW/3IqrJOZZc72K/ulQogGcphQo8BTVjWqEyrZw2wLkrtiVcy0ebDXCsJv82bbZ36q9AZx0S859KeoLPnb79a7SmlS1PZdTg+K+Zya/xPG1ZUM3lj0cmvoqJW+Zh4oa5GJk0BV/vXWI2HbqYbD7ymAzwpHipeGLKW7jl0zCU0X6bCb6Mk7zNEqfqdM/UQVRTY2N0n7MrX2HhqrLmdKlvjWyGN+dHGwGoXq82FRrgHTx7EK9++SmqRzdBybggVIoOQKcxfbDy2FYDl9+jtnZWi6wQQWc2UzEnaZuAb/etQbvPnkfVqGBaWSmkdl8OzLaSs7Kshl65pR2T745sgLDB3XFPbAd+boXWI57GpI1f2udkIemDB/Z5SEzdEwRPX7cADyU+gzruJihGN60CgVdFoKNstd2fQGg3Hcq+jIWBa7i1jb0PyrjDUC+xB60dbV3eC/kXqdAAb86O7xAe15UWJBh6Ichf3c0wZNkEnNTOYKYFE3wEoJxL9Dzw1BqyVghAOYW7cAofLRqKOyObo1gCK4/KeQvdlpqMHbKr5AvYM7OlEq2jtk6vTKtyYyxdVVrN6hFhaDv0SdNg6Mkmy2Tpg87lvAQ5Ic/D+P94+lksOrQKb//kxt1xTVB1QCBq9AtGjf4hqOEKpRW0K/cvKVsh4loEXoXY+mYH7QruptiZcYhCp8JcZatXKICnKVv3DulBVy8INV11UZaBdMzSETiJY1aI1FZtdmOmdv0etTX6qM6ZFLqvp1B3UBtUiwykuxJGAIWYKUl6waTe2JpdJWfHmrupvSu1M7LuKx2vV3954ZaYRth+KtlMCkgzG7NkZiFPSek7z1CnlQaU5XaeYk72px/Bfka0S06sx0uzP0FZ5l8dFtmV67dy1nck5AeXj7WWuxLBV4INyUvzB+Agy5n3kr4yFUDgSfWzCiUD+88cRDlXQ9NFXz36flSM9MOq40lUGsVJVFqaCimP5lL+NovnqQb+0VqEvVS+xBUTUD6qPsrHMU6LDqaVa0A3U5ZOS2h+vfsli6e9Iavw3jrGfauHsu66KMnPy3dvNe5sqpmOk38ky2p9A3UxeGxsliyc4xWrTm5CdXoXOV0gq9jWYee7eVMtZSb3Wy/O1BCQjvpdW/g592q+pV5kohg6pyvis96TFdgl6WEoTq0ZU58Ndn34D+qBObs00eJimedcb34PFTjgye1TG2yMAGWhmR+zt32DipoWlvCA6VT5a2w7pGSc4NVSIUsWrr9NeFI/vVwSaWnYl3EYrk0T8HdaO1krAzJj5ezbbZwK/bUsxdNi03Km59DHxBvlCLrKMQ1w4NBB83yNU+bXEhZJTOU1xaWs9UJOM5/UkZ0u4EdNtaruakLrnLNeTXXna/t1wwSC3k8hF127VZeKD0Bxxsk3M5YszsZI72+vRutfhd81ubmWS6+wDjRjcSXjJftL078c62Wg8iz0Hgh1Dmlbet1fgh5GxTgbs2qLjtsjw/HK1/2xI3UfZW4bbRukmLUsVgb5QAUSeOaVhJSJ2qTt5w7ilVn9WDF6fe99DJID8eDYPvxFtuLiVuu3kVIR0M/Qgn62dhrqJnahhc2dbnRZPLk7ep2z3ldQPTIU97pb47FZb9uHi/IReCqrVTFJ2uLMSFGPF3sM4L6MY6gc3ZgNXc7G9zR2VovxYY1oueaBLL8HgBrcZuNVNSIId8c3wy2aTO7yQrVYL1QiMPUSFb02rKYZ1Lfvds8J8MwrqHlPdX7WzCFZUrM/y0CN7WlcL9hY3Kr0lkKGdMOcPd+yqCws/58HnpFEvlCBAp6te/5V72JaBvamH8Xjc/uyghri5oECni/8Yjti7s4feYmus/f9VspUdrPkAXjjm2jcFdcapWOD+Sx1KuTMzcqOZTU1rU1TyJqPfgFT9/4Ivf1VpKcaFvDyzd3kE4186Wzyo55qS2+PUr+tZ3bhg/kus7ypVMIvyMDEu+c9gWr8LNdOWx2W0RZ8LH/FKD/8JbYZRq+bic0n9zKaTMeBjON495sIhA/rzMbIvjqsEq3jLdG+qO2yrw+74Dm/wNXM+/5CedSLLgVmu/N0nX4N4TuoC0JH90KNuDAUY11UiQpC57HPYfXpTZk6YO2+I4m8pwIIPJHEkIrpGxbgXrZOf6QAb46vZ6Y4PTX1XWw5touK+vvEZJTdI/TUjBRsPb0bf3G3YvwVBL3RVftk5qTFvRwLeGXjqQhR/piyZxGOGNUm/bZNYHKB+HS61AKetkPcmXoEm87uw7Yze7H+7A7zNqUXp7wL36gHaUFoqegaZlcuw6bnVqA7z2VpvYrHP4CbEu5lfFUXlSL88PfopnhjUSROCeYqvMrOLBzECUzdORetRj1p3g1fkTFYTbc3akdronnOgFeV9SYrWyVGnUHOsEgwXprxIcZsmwnX2pG4J76lea99OV7vm9geozZNY3YcLTL9y/lGBcvVVMnJEoa2Rf94wSC6BmG4cZC3WWFe09UA0T+NZJ3RMRBwzA2/n47xaZ8uHo6SrOxyrHxtz2eD9ZzHdBezXM0ytJx3RDGmy0i2D5TBk493NUgik5lLT8XiwyvQZ+ZHaDe2N9qPfgbtxjyHejFtcEu/YLpmgShPYF2xk4O/mU6TaMWudO3IJd31cRMBV4yxVZ3IYDRLfBhRPw7DtvSDduBa5VZA7Qk0TxKF03YshF9CZ8ZiGsD3sjFaDoFXhe6kuHxcffPOP7mst/QLZ6iyl4BPwcrTG9Fj8otswNW76W+29H9ixtt0dozCmczkl88hKljAMwph62bd8V14dsr/UI2xVon4+6m8AfjnkI6YsmWB2WflHGvRdgr8DpLMmdbSo+vRbMKLZn5l5dj7UMdF8LHicwN46mwoFe0H/2FdbNV6OhJ/2+ZLuUASmXl0Kt6bF4E7+zdCiSgfFI96AOUi/U2nx41UzpsS6CaajpVLJwvIExCXj/JGBYJOm8xWjlXnBZWf7lzl2Aamo+RNupI/HF6N5DRN0lK9yo85zUbzOL+fMfmQ3u9OOYznaZkq0dpVIfDs2Kd9TfPFz74cKw/yVMrEPcDne+MWgr5+XHvzTNFRNnpvfvkRivO3kvEa0wuCb1xH+6syZ2sn36hAAU+LRgUqKWff5SNQJb4pBRRgpv3cO6QTJu76gdXG31VhvN7IKyfE++ToaVsItb4KqWccWoK/DWqDctFyUdTK5qzCf4kr0mrcGheO6NXDjJIp/3K59Hy7jZJKoSrXJ+Uuj02hHsdH6B1JH/0wBP+MbmWmtpVIuN/EZllZFqRMbDhKxTY0HSXlGLOVdnmj+oBgBA/uhvE75mPRvhU4lHpcNWfqTxCTJ6nxyQvocpVlYnX74penp/0Pdw5oSgDans3ScdblrBmtmI9xI89dzgJr9X9NupbV3XVRNtoXDT5/BstPbCPImRtPf8CGkztRiQ2FGpNyvL6KuyF+Pr7e5o2XZcgFv2xGc5cKGPCoiBYR6DDqBcZbIQzOg1BVgvzsMSw5ace9jAb/FgHxFllJjVMpiaOMMaKTPsef4prZ7fpiLfCyq9jfytp/s3ZsID5dpnmCGuK3O15LQa2rLBbwHNW1HS95Rp5HSg5Ljyah27DncFe/BozX1APJuJZWQz2CGnSuaXoH5X6rp5FApBWrH98BL878ANO3LWAD4smtUwySVFdG3XqSvxwMqB50lepk8fEkPDntbQO+yu5QPledNcwH61/rHGuSLzf4XpFgqkILXY3Auj06HO+vHMIYUk0cc+gB3p5zh3CH2SFA74APQoXIICw4sIRX8EdmXP0G5sJ8oAIFPL10RKwtFhoO74UKURIo3Rf66+98H4PtqYc9avnbBWS7ji3wVp/cgnaf96a/T5fKuJW5Czqx3KbSVIZmI3rx4cy9x0U2dk14yywGPxgt/G3l+rVkbaulE2nHMXPDPMZ5H+JvkS2Na63Bbc0MUve/XLZqMd4sgzfPBcArvisSV0/CntRDrCNPKjqoUlgg6a3KJtstz0UlEWd95sUkOeg3FVtb8i1OXoPHp7yDOgRfWQJEb+/V+scqzJPWOV4OeBqyKa/xuqhA1HU9iBEbpprYTvrkZERW9QFaeL1kRnG3VmT0/X4gDqQ7W4V4LswHKmCdK2qfUrE3PRl/jaabGaMWmIE2gTFhyxxFB6ZyBZ3fLiB7r+52LxmFf7qbs+JyH3AOa3zpBrpM9wztymfy2Wp9paiezgWHjGLqvG1Z8oz0HCm7xbd6Ns9i9ZmteGu+G9X6hzFuC0EJWo5ijIPU9V4h9n5an3p0N4MRtWIsDpmJC+ZWm4g8FLJdQCWrbUpiSI8QO+DKjjJ/M+mlEyzn8P3RtehF8NVwNUYJNgCKNzVXtizzlJ2MxXKXS1PWt0U2xGOTXsOqExtMbrLaXFm2NsOf4rWBKKm3OxGo3ca/iI0pu0w+8pMKXOeKXI5xmxcwtvNmK3YvWzsf3BLb1Lxkkj+amjKtrVOrOSBzuZSF6JWr13jUc2YaUal4KtdFFZl7LMtRl2UJRIMxz+HjxfFYemI51pzehA5TXsI/I1vBm0H+83M+MGX8Zefs95JUTFPrrJto2wDZqQyz7nDO7sV4eUEE7ktoT4vga6yd3kNRzd0Ex9OPU/hsPth4nDD5FFqUniclA0R+/NWkpxr7aFIy2POck1+w/NAa1He3QXVXkOkxVeyt2DM7OVfTRHSy95AemLxxDs6me94Ya1Lz+Bj8nrh8onH/i8fXM7NqbosIwvsL3ThGrcpR1n8nFTjgHc9IwX8XDWWle6Fkwn0M7r1xR1QztoSsWFkJyk8tl2mxcygp48vLZJqk0vGXyAfZotZH6fi6l1RkbrHipVtjHqDCaIZ8GG51N0DY2B5oNu5plIr0QRnGL6VdPqhOBRif9AWS0z0WJc9IQFHTpYl59pshq/UGklszDiJyxUjU6RuIqnLL6PLVHtAEJ9JPGdlpbqn6JS3p6PmsxPjR+eWXSVfaDiUHwuZeeQWmoyMNn2+YhrajnsCdESEEIMFl3M3zlk9T+bSfabVoTQ3zwYOf90HSqZ1MjykqUaaosprVJ0xywvq5+MfQNijFxqRUPOPvyGC8+U0kDpve1l+f899LBQt4lM1hxkFv/jSMgbQP/jCoPm4Y6IvQIb1Mb6Dp8fSQR09yROrN1DPU5aaK+EtMEzOYqvdrZwVLbrOdCKyYgs9y+6BcpJdhdYHXYvwq5SnF37t91gcbju2wmc1TkuQulp6+WxhKtrvTjuKx8a/hH1HNcVtUA4TH9sRRwtIBq8SYO5RdXkjKBB8myMzd/j3aD/83qg0IYF0x5nfTAlKO2tBIMZ+WaWlv0NJxXnj764HYnqrly5Zs6hbE+r/53AE8PvsDVHAFm8nrt0SGo8vkl7HkWBKBl3ul+iUqWMCjYPamncR/fkxAbQr2D4k+uInCeeTLd4yXKSfEod8CPDOmI9mSj9GG3h7fkO6LevHkpmQPmrxgOw4mMAZ4gOdn1r41H/wEVh/VNhZXgyRNysdqqpHvyhPrMTRpHGJWDMf0TV/xV6cG9FdX5CF58qHDcda+BtnbjXkGlSIDUI5WuHi8D24cWA/FBj6AEgn1cPMgb5RkA/bFzkWs2fO5s8nomwXeQTbhfWb1R5XIMJRljFcrMgT3R7bGyKTpVAtekE9UoICnTdI/XzcL/4jtgFpUxpviA1HGFY5np79vRJKeZT7jb6r2DJk6u+vJjN1LaGU0k/1+3MJ4MjuA5DVrBr/GqNSFfwOVqPGwR/Bz8jpPZvOXLJQ8wLLamkmZsvb8qOVEqqu8JeZGi5w9eVEe9HEpY+MBSz9Dx1Ev4N6IB1EnqiktcjPGai3QYNhTdIdP8mJ7g+dWspoMpaXOm3QsOrwajcY8YXrLK0f748+xLTBo42TTsZNfVKCAd46t0eD1k3H7wDaoTVezZEwQ/fHOGLnhC1aCREjZOUfzN2eUkU7BUvhyQ2btWcrYRfFBPTM/ULPo85vL0prXkOtEEGqRZtvhzyEpeavNbD6T5ClnwDRwnu/O0SHnnB1tdJy5vCHbBOhp5ktmRvTUo3z6quRtGLNiFoaunIohK6chcdUU1ukPzBlLoNvITt5tDKk71VxkYO2xbWgz6hnUYHynMKO6KxRPz/ifeXVZflGBAt7WE1vRaeLLKBYTihpRDJajgtFg9BNYfoJWIBeAp8nQulH3Ljm8HhVdgajAoLxStJfdjSqfuWSsl5kcUJmuUyla+IifRiM57be8ufb3k2RyMTvAy/rdspw3nck7cp6ZSc4X4coDKk/UoFOElaDFf9m8itkCj5Cjt6N+m23H9+Gpae/gdnpTxdWrTcsXHN8T3+9b4bkj76lAAW/l4TUIHtwLJeIa4C4K446IBnjws2ew8ljuuF9mfqSnSU/OOI27YprR4oTQ3QzkMZCWL+gCrpXHrBds3B4dgjquMNR2NcS3yWuNIl01kmwytTYrxKTM+u4h+1Oe08UAMvlRHg3JJjITukjnTN4z2LhaMGbNnoYTFGCkZ6TK4TEu5Ygt01FvcDs7PVB7syY+jh8OrvbckfdUwGI8O34jgTqDsdatyR11VGWonqwSqTLUDtpBe9PdfAHpvPJxnp3vWk+WG6xOAM1pdBRFLbLZvLeI8o0ke7EcUdVtflGBAp5pjpwmzOz25fmeS4tE5cRlKjpbR6ebwDSj6TxrfBg9V9XAq81nh/VbVr743OWuu5jPX2MbgCxsnq/yF1Gek+QsHTOyt82x/Z4/VMCAR1bZzZF/dHTkkgukrd3Vb6UkDTm9Xzryv9wU/e5Yt8znO3nI+vn3sJNO1pNGEXTU+SLKF5LMPWx2ANDnfKICBbxMOeizw55zDqlzxelgySmlGitD0h8DIKfFs6RvsnUO9h1O89SJ892QvjsncsKirJ9Jzlc91z77t5fxt9Dvkemvoculnx9lvNwzdNaogCdvYsn+YsqrPBYsi5cpjos5lwovycrPJJ3hvyXJ6+D6aQxentMPfRZ8jKcXfozn5/XD67OjMHrVDALVxpuKO61jaP/azobsqunXklPOIspKDgCuB7pqwFu9ZjXWrluLpLVJWEtOTbPjLJfjpPVJWL1uDa9fg207tvHcpRXkVFzy0WNYs2ENVm7iMzavw4kTnpfQK4zj4SAdyvE7F6DtZ0/jX5GtUSUyBCXc3rgp2hfFowNRPiIEdyW2wVfbF0FbE+ge/ZFLqnEgwU7P37hhHdatY/6Zr3XM3zodHdZ3D+v39Txu2rgeZ86oS0V0Yf6TjxzF+nXrsH7tWmzYsIF5/uU5m2fOnMGWzZuxZi3LaZ61zsh0HdMRS2ZrTf6SkMQ8bN+5nU89/1x9lrxS01Kxbds2rN1g62PHzh3m3MVAMNfznxqgPfv2MM0kbNm6GSkpduA567UibZ+n6w8eOWhkoHrevGUL822XyW7avIFprMYayYeydJ6XmpqKXbt3Mf+sb5ZBz3HKZsqicpnza0zZN27eiHOp0h9LWtNp0qL7qOPJUyexadMm83xbTzquZZ2sw4a167CRbGQvmXlkpeMa6lqK0csLy5UbdNWA9/wLz6FT147o0r0zOnTugJOnPeDIhs6knEWHHh3RoXtHPNS5LV5982WKQqYri1J4Kk20YO5CtO35EJr3bIXHez+JlcvXmPOKG1cmb8F7K0ei7rhHUbu/F6poknK8D8rGeqGCyxcVo7xRJqo+boi+ByEjHsGAlWOw/PhWa9/4x6ywVucI6YmnHkfHLizDw53RsXsHdGIeHe7cs9N57tbJlLNnrx6IiXNj+cqfkZqucSWPcvDf9z8sQvuu7Q0/8fSTWPrzsszyXI4E0N4v9EbHbh0omw7mGV27d0W3bt3IXS/Jz3MvPYcxE0fjq2++wsFjBwkfWnI+X4o+c9ZMtO/WHh34/A8/+RCHCJasMhUJcuoBXrtlLV55+xXmtR0e//djmLfwK5Yn5ZL8ai1cihq5aePRifnr2KU9+vznP9iz176ss88rvZnGQybvnZg/5UeUfPQoPvr0Q7Tr3s6Tf8qvZxeWrcsFZRIrzz0e647EEYMxa96XnjSYU3WWGY8FWMVG/tneHn2jrDqxjJ2Z5gV15OFO1DGH23V5CEdOHGGKtoc9N+mqAe/t995CaIMQNGgczmMwDhy+9M2pTjWqJQ5tGorwZuEIaRyCBx9qyZbIeUPQhSRFmjxxEgKbBSCwhT/6RvTF4f2HjcXadno3ek94H3/q39JMkq0a42PWZWm/x+pRYfAa1BGBg7rCf2AneCW0w13utrirf2v0nPoevj9J8GobAQFFnSCkdh0fQnjjMIQ3DUNok1D4hfrB3+Gw8xwcHoSg0EA0bNIAjZo2Qs9HemDu/DkmDWorS5GBBd/MRzDLFtQwCA91aI/vf/zhVwGvW4+uCAoPRAifr2cEhwQhNDSEx+DMvChffqG+vCYEDVs0QMt2LfHmf9/AuTRZHvuM3bQwQQ2YTqMQo6Dz5n+lrF2QB+typ2HclM9NmcObhSGMz33j7Tew/+A+I/usJKCuI0if6fMMQlm2Zi2bYtDgwbRAdpLAo0/0RDDLG6a0yI77npyczMb1VQQ3CTZ1HsbnhFBHVD7/UP/McgWE+xt5BTcORsPmDdG0dXOMHjcah45qo2Dl2+Zn2fKfWabOCGsYSn0Lpb4FISDkwjpy2MrKsk+wDw4dO2Qam2sGeOs3rcNjTzxKRWyIJs0bY8So4cZFEGVtaY8fO4a4hFg0bNkQjVqQWzZCo+YN6AqsyrzGuf7s2bOYPHmyAUSDVsF4+tlHsfrn5aYOtJWd95hHzF4bldx6EaOX2Tey1pAW+PeCT/HVriVmuCGreLX72Lwdi9Bn1seo7+6MvVoj4fS+kKSgjZs1ojI3ROsOD+L4iWNIPn70Ej6SfBgnTh7HK6+/jKYtmph71OBs377dJkRa+M0CozwNqGgdu3TCD4t/9PxyeRLwHnmsF+9pYAA1buLnSD6WbBT3GOXmPP/Y8WSjjP/74D082LalUebGrRrj4Ud6GhdOJPC3owUTAJTHjvRCzHnJ2CNn0c+rl6HrI11NmcWNWjQy9Zc4JNFzxXk6efo4Bg4biGZtmpm6e51g2kjX2KEnnn6Ezws1ddqE+RFQRcr/m+++ifAW4eY33T963KgLZCo+euwIVqxagegYFzrT2oeyEWzUrCEebNMKX86ZxZRUKtDDWIkevXqaRq9Vm+aYMm0i6+r4JeldwseOGqutxsamlHt01YC3c88OvPDSC0YJVdFSyu07rCJm3dx1y5bNbBkfI/AaGWVRRTRmJU6cPI76cGErJGV76623jPDDmgchYsAnOMfWVXqzO/UgyrpDUTpWa7rqowItXa2IBug69WUkpW2jS0Q0SbYClbxYJa3QhYLfn7IPUUvHYN6hjeYnpw46dulgGg4HeL9EI0YON2WVouq+LYx3HPr624VowAalIVnAW/zTEs8vl6eNGzei16MPZwLvi1nTPb9kTwcP7kdCYjza0l1XQ9aydQsMHTYUp0/bd/U9/OTD5vlNmjdBq9YtkWbWxLG4Ki//qNVf8N18tO7Y2lxnGkKyytPnpRfNtSKnIdx7YA8ef+ZxY9F03WejRzAW02QFS088/Sjzzt9Yp00fbGIUXHQx8Jq3JVimTza/ZUenT5/CtBnT0LVXdzShfAWw//7vXRw+fMA86+eVKy4A3tx5s+2NV5GuGvDOppzB5GmT2AK3MuBr1aYlJk2exED9fJCsyvvxxx/RvWd3NGxFS6eKZkXoGD8whkCz+1Q6FS1FfP753lbAdKdGj/wMqWftYo8pW74ym9xUjfZGDbJWBjT+7GnM2r6Q+DrFCiKkBDZdrCPTM+Op2gOSH/akH0W/b0Zjf9pRXmsV5NcCTwqr6Ohi4G3den5CdG4Ab9qMqeb8xS6fSPIRyUq8/f5bfBatHuX0zLP/xtq1a83v8UPj2bjZhrApwedY5HQzvphh3Ml3P3jX1oGH1RiqPK3btPZc6+nYIP+8Yhm60TqqXPJSZs+ZyTD7vO3ICfAmTZ2Y+btDznNE59JSMGLsSDbKNj8dOrTDArrLoouBN3vODHP+atJVA55o6/atdD9eo+/fBA2piL1f7I09e/Z4fgVOnTqJ/v37E5iN0bh1E1ZSuHGTmrIyHn60O5bTdzdE4at1HkmgtaECNGreFE8+8yS2bbYWRT2TcUnjzELUGnQztbd++ehA9F05hpBja5+eAi2C9IRuVAzBMJ2uptLlSbI2YHpx7P+w5NBPrH7bi/frgacUMzByzGdZgNcAWzz5E+UX8KS8X7LFV0dCEyppy1YtkJiYaNz0abOm0eWzrrDKlZSUZO5x7lVcqo4c1YHKrOc2am5dTaVz8uTJC8DQP7IfG0kL0Aa85wDjeMnBkbMBnqxhLgBPtG7LemPxmkhfmjTEm2++bs4XWbyLKI2W5ItZX7AyO5qKbEGhDBycgJNntKNHOoYMG2w6JJoSmE889zg2bNuAAe7+1nWhq/Te++/i6HH7rrMfFn9P4dLVoBKE87e58+YhI1V2Jg3zd/6IBjHdUTsiCNUjAlApMti8gnjdqR38lRVHPZVCmGpVPXrq0gTU+uyp78ELP0fiN+NxONV29atH0ygplaNV+1amW3xV0upLeO369fj6m+/QtUc3KgVjwiZhaNW2qelNdNzq3AHeFM8vV6bDhw7jRbr5DVryPsoqfmA8gcfGhGWNiooyoJPM//3iv7Fu8/pMkbz97lsm701ZVy1bN8fOnTvxzjvvmOvl3i9esdg0YJpdq/ozlpMsy/rE04+bZ8t6esSLR596gjEewZsN8N7SsxjzyoK1eLCF8Yaygiw7OnXmNOtDDbM6scIYylj3dymB15XAC2ceZaF7PN4DvV/qjd59nr8iP9v7GZxgnKqynM917tBVBZ6q88jRw3j3fVYeha/Ke//D/xp3aNfenaaiVXENWTn9Xf2QfPIovl38LYXYxVSKuunnfz2fAE7FjNlfmJhFQGjMVthprRW9rDm+HRE/DMc78yLw9rwBeH3+ALw5NxLHNTigulbPs4WZ1TCSAHmEVxxLP4mT6WdwNP0YNp7egQ3Hd+JUuh2HUueKsXhSEFZot56d0TUr97DcpXsXdOzcmUpLK9GYrTjL88prL5k0HGVa+O2CfAPeoYOHMoHXiK7lq6+/iu1bdxhLOX36dLTv2M7URROCYRjdY0ft1GnVlBZM4Ov1aA+cPXMW/SPkkSjuboQP+n2Aoyfkiqdjx67tphfTCSP6R/Qzz1Z5lZ4ot4GXQj3o9nBnWr3sgdeAeqFnqSc0MDTgF9k/yM/oolWKKz87p3T1gOcpx7mUs4iOc9n4jZWtXroflnyP777/Fm3atTYWTPzTMquIh48ewqtvvGIAqfP9I/sb4H3w0fto3qoZQdcYzehqHD6s8RdAtmnl4R2YtXURDhNIh3jmAJ3IAzymCHW6SO6kow6eP+oF/WjxQPxnxsd4c+YAvDLrY7w1qy8Gfz8Oh87Z2NLp1WysRkMVGuaHoHDLweH+CG0YiPDGwQhrGOYBXQM81KEt3nznDayhJcxK+Q08dWyF0ZVrSPC9QZds5za7CHT//v14/a3XDJDkWfSN7IvTZ8/gbOo5A6SmBEKjpuF4/sVnGPqmY/6C+aYBVN11ptLP/3Y+U8nAoh++M7JRR03b9m1Mr62Ao7ffpnk6xXIdeGkEnnGhKY8modlavKatm6JNpzbo3qsbuj98Ze7aowuOn0hmaa45i0d1p/IvXvajqWQJWZUVGR2BSVMm0oK1NN8FqOMn7aaj6rKPiXWjaTP1XjVkBb2JXXt2ohPdPn1XRT/8cA9zrbppNCy/YMNyfDw+luKzpGMmyFTXxv3hB33nj3KVjmacQu2YcJTv74fKkSEoH+WHvw5oSgD2xe5zh3Q3gdfJ5E9usir0lTdewqtv/ocNw0t45fU+6NC5LRWSrg/L1YStrcYuZ86egd17d5uqzEr56WoKeL379EaDVlL6hohLiMM5upoGGIyVZ305E+07tzPl6vloTyyn4m7astk0do2lvATl2HGjTFqHjxyml/I/NHuwmQHQqM8/o+U5h4mTJ3iA1xj/fvZprFy90lwvckpugGfyznqjx5AbrmYTys9YPKbb+8UXzPmsFq/FQy0wdNQQrN2QxNBAs2Yu4rXneXXSKoL5HBsK9WVf+dk5pasOPLUmJ8+cZADdjAKz1k2W5OlnnjJumb6/0OeFC4S+Y8cONGmi2IFBPd3LwUMTDejUE9fiQcaJQ4fYC9W6pqeY8bghy6eZTXMMZSYlwFnQGeAJkZ7xgu3ph3BbpN3GXPujaP/NRnHd8eOBJHsJSZ0rsrDqIWzZvqXn7Hla9MOPtOCPmUHbplTw1996HVu2a7qbx8hmoXwDHp87a8Ys01A1bEWFb90Eg4ckGuCZ3kvSgcP7McDV35SrGWO51954HW++/ZYBktzkQYPj7cwbz/XLli9F90e6GRC3eqgVnnnu32hHy65GsPVDD2LC5PF8rNPsnRd/bgNPlrmJGkFa8cZM93VP54oDvDA2GtKzGXO/MOetJbuYrS44rDzp/Plc5w5dfYtnCgW6lW1sC9mSgm7THC3btkAjCr4hhfXxpx+Z+MMRvIYcWrduTSWwcWHXhxnz8ajrH33qUaxkS2XkRNbUoZOM9BJ/noSpG+YThKftE40sJVqtTKZt5Gclr26BXXRKP105DP/oH4Y6kYEoH+uF0nH18cxnr2Prod26Wzcb4DVinqUcbTu29Zy1LJ0Uf/fdIjzyeC+jsM1bNcfnEz4381J1TVaSKybgqTydOnfCT0uW2oQuIJ04z+s3rEevxx42MbDKPvULB3i2OcmOjh49irf/+zZdvHAzk+Wp55/Eug1rzW9ZFXvy1ElWpqyPMF5rwM3vHWgJp06fbNx7h06cPGGsphoh5UOD8KpHlfmNN1/D3n27L8iP8+mxKwDvHQLPxJm0rurImUQLqtq6HKWkpmAy89WY4FdP6UOsj4XffW2eu2zlz+jWqwfzz4aaejX7Kw2uZ09Wcuf/maEgzf3kv9ykqw48h15++T/GusldEasyGtBNe6hLW0yaPtFz1XnlePXVV1m5dDd5rdO9rePzLz+P/QcEDnudBtnP8d83+5ai2/AXsPTMejO8YJIR3jRIrJ5Ffle17kw/ggE/D8c/ElrhrugGqOwOwE0DvVEixgtf71mCc4wjLHmAZ9xbAq+9BZ5DTj7Va/kd453Hn3rcuGo9e3XHvAVzzdzGrGRmrrAMpiHp0hUrfv7l/T/Wb1yHh+kKNmTsqDzIjb0SHdh3AK4YF1p1bIXwlrTSdLtGfT4SZ89ppaLNr0M/L1+WWR9ySdURI/m+9Eof7NkvINkG06GJkybx+qZshFh3VP5GtMCy9P0H9GVDqUkMF14vepzAs9Y6e+A5bm3LNs0wxejAhXl06MzpMwxNJqBt59YmraYPNsX7fT/E/uSDplw/r1xmerzVsGtWy9x5nul6v4IUj1rYZf/s30oFAnhS0okTJ+IhKq8E1/hBxnqsCMV9T/V+Etv3nJ9a5Sj0ELqTsjSZQKW10IyKMRPH0HU97hEWifWdxjjyOC1d98//g+dmvo8J2+ZgP7K8JJIX6rD2+A7ELR6L0OjOuIVxXWm6l6XjfVEuKhB3D2hmtji36qOUrww8USb4+O9rtr66Vu6zYr39h/aZ3xxygKdytO/SHnPmfYnd+3dl4d3YQ8vh8JFjh820O7maGnJRHkaNHYW9+/cwhuT15F1Z7t2xZwfefPcNuo5NEdY8DOHkwSMGZ/bamZd7ZCHNBlF+G9NyySUVC1SavHzMdDjYshliOTX1rBFdf2PpPMBrR6vz1Vdz2K4p7SzXe+jKwHvbuIsGeK2b4bPRw1i28+VxjosWL0K/iL54qFMbWvBgM2e298svYvPOrWxX5b9cCLxWBN74ieOw7+DeC+TjpLln397zvHevAZ6t7Uvz/3vo6sd4rDS5kadOncLHn3yE8EZUCrImULd+qDUWfD2fRT7fWjrX79u3D6+89gpCwoPRoBFdISk0K0vDEBKTfTsASbcKVfySlLIPL03ri/s/eRB/czdDQGJH9PnmEzz51X9RL7GTeTlKObcPStGtLD6wLkq76+Jv0U3x8oy+WHvMcTGVuhLNML11Tl7lRl5MDvAchXrz7TeMMgeHBZke2/NT4zIwf+E8082tSceaHOwX7gffMF/4hPlkckCIv2HfAB/894P3jMVTz5tkoHwo3cCQAAQGByAg2P+Ce8Vm0jEBpyljg4YN8jz68gqlcazgBlTmxiEIbRSKfv374vjx4+a3i29LpefQ5qE2Zr6kJrIHNwrBrDmzM+vLiR+zUq/HHkEQ0w9uZAHjuK9yh1959RUjV0cfVMYAlss3NItMQn3g18APgY0D+dxgPEg3cuq0yThy9AglKqmK6GquWGr6DZxJ+Zq07htyoWwdDqD8AkI0iToIfoH+bJg8DXQuU4GweOpJS6WP/vXXC/Hsc8+ie/fu6NGjO2JiY3DwoGaaX0iqTClAREQEunXvZq7t+XBPzJ492/jjEraAl+nM6RRZo2/rj+/CwBXj8eDof6PUAFo0be2n9wNE+KFYvJd9mYVexRxRH61HPoZBq8eagXbHwRQ5wHv62aeo+F0NP/bko/bHbEitpe5RLPXOe2+b7ncB5vDhw5lXqPtd3fEao9Sski49Oxvu7GF9dp7VuVtnJAyKx6YtG/FCn96e9OxvWdm5X8uWHH7vg3fM0MUxTy/xlUg9yyqXutbVGfP1118rq4az8RzZEL6KTt27oEuvbnxu18wOIgEvO3rl9VfN5AmVuwvL7QBPc2779uuLLlri1LO74a6sY3FmmTLLRXk93AkvvPQ863+muT8rSfKrklbiud7PmvHUTPn0lJxtOhfKWMMInmfx+QLepU3G76cC42qK5JLs2LHdzLoXO63rxRWn6wXW3bt3Y/369diwfgM2btho1nGZ39m6qi9KO3mpXyoTeZ509GLIlSe2odW45/GnmKao2NcPVQYE0qX0RuV+3rgvujk6jn4e32/7kc7lWaZxsatkgbR52yZjdQSoTVs2eH67lBQlOLM5FB85Xdly51QWcfKJo0janIS1m5KwcRPLs3Gj5U0e5uf1m9dbpospl/LMudM2D/ye+VsWzpqGlel6HDzsLJkRekz2LkMZJv0t2zebMooFCIlRfAHwPGXYyrpbQ1ms5bOTmKfjJ0/oJ8MXkySoBc1Jm9Zi9UYtWE4yUhKZut2zGxs3q2zM+2aV3bIjC3vcYDqYNlAH9vB6pXoxKU3NhNq6fYsth0c2GyRjpZGVjYyzMM+lUievKKbfSAWmc+VyZBQzGzflcqTrpRjyzO3WfapM2zVs1vQYTbDXrji9AwkrJuLNhS688W0UXvu+H976egASl3+OdaedXYV1vUfbHFKesn7/BTI9YyYPl5ZD+TWLNrP57Zcou/R+DSnvNu66El0mbRZb4sjIOh4ieUiu2ZARdzY/OUvAspKkdLl0fh1dmg9bT789Td1po7zcpQIFPGunpISqAJ2R0GQpfklJ8pNsVdjqcNj+c85fyvotOxbpqDTURNiSOr9czyR5CogW5VaGBpg6ly1nlauVafbnLdum0LnGaZyzBhR5SwUMeFbcBnUGfxKSFXfBIeVFmVMEKfZA5XdmUSVXamYJoDlzfZP0wEpE7PEIJJjcEk5mWg7oxAJh/lABA56HVH5r+Azlnzh+JalhyGBFmeXonlxnZj6npLR0YLWL7ZkikoyNO5xFEXJTMEbmzkf9Mx/yjQoU8CRiK2ZJQI6Adb10rkCRMsSMyTlU14t2HhME1Wv6SyyL5rTj56CBa34yk7VZZulXgWtlrhJJDpSzxuL07nt1lFmZZS/XnLK1c3qPgtL3dOvko+wLFPBWHt2AzsOfQ51+jVDC7Y9a/cLQdujTWHbUTmnKf8q+CZQCHOPf9iOfxR39G6BaVAgqR4XilsigK3JtcYTlWgMC0W5yH7w8+1PM3PwN9p49bOrdKEC2lI/NcQEglVYNWtfRfXB7v4aoShlXMzIOvkSuv4VvVR1EBTDNANzxaShaunrh231L7cPzgQoU8A6mJyPy5xH4c2wr/DGhLipF+qPp6Cfw46l1HmtgXYLLK2fukoYArN3NShm0U2lIOrODDUQ4SsfoBZreuDnOvqS//C+yTyZX6++HPw0IQUhcB7wx+yPTsptG15T1vHU0LzIxbq0l5cfkKfPDtUc2zM9Ah3EvslELonz1WrPs5JlzVj0Vj/VlevZVaaVdQQgZ2BOL9i/zPD3vqUABT27blE0LUD+mA0rG10dtCtxvYGcMWfeFVUa6Y9Kz/HI9BTvb80Xig20skI4DSMbQNRNR0xXKfPqieAIrMy7n71GvRPCVozJVjvLGXyPDMWb1dGw+vRdH005TFgI8/2ns0cR/55ubTLxlfrgGyVO2ruNfolUKQikCrzyBopd4ZifLnLBezVUqLgBl4/xRgQCsGNcYj835EFtPnZ+dlNdUwGK8NExdPRe+rg4oEeeFau5A1BnQBM9+8TF/ZS2ol5P/88vieere/FGPtlrgE7RLozdMQ8NBPVCR7nAJWrtSzGtlz/KhnHCZuCAUj9P7vH1Rnmn5ujri4Ulv4bmvIjB119d8MKMRrQVjuT0LmjLJ5OtaJo/w8wp4ZWL9zfaOlSj322NaIHbjZJw0/kX+UIECnrT7aMZxfPBDAoHnSytCwUQFovHop6nuZ/izXC7PpfaQ96QHCek8/nyaMehnvVGjXyBBQ1clvh7z6AW9w7waKzCnSnFLdKh5KWbFGF+m580WmADUq4Gj+bvLH/8a1B7v/zAQe1IPmucrK1kHiM13+/HaIxWMjV2XcX1QNTIwV4FnONaPjZ0XqrjC8NDoZ7Ho0HI2bh7vJh+oYAGP5T5JgPVdPJRK7Y/yHvftblq9ZP5igOcxd9bpzAe18yiAquTd7924M7YZW0tZKwFOimDzWDHG/8KK/RV8mysQtaL1HnY/VGU6FQlgpXcbQaxY5P9Y/lviWuH972Jx4Nwe2j/PcEMW8F2zpIKSBbxqUcG5Drxq0Ta9alEN8c43LhylL6ORvPyiggU80gmq1weLElGabkBFCqY6hfQXCucIfzGdDKa3IX+AZzuZ7TMUcd3vaoUbaJmKD/RFVTdBER2Amu4Aui7+KCnrfFHl/hJXYmBfiUDTfVWYRqWYQJ4PYrnpgtL9vHkQrSBBeU9MK7z+bT8cS001LqezZ8k1TZ4idhrbG5UiAnIdeDXd3mwsCbwBDfHu3Ei7zWPeqtMFVOCAp569qdu/YYsfgNKMnyrTHbjNFYIVyRtZGZSM7eajjCSlvJWUBrTtMzKw5fh21IwMxU3x9ekSam9Of9Qm8GoY4FExFC9kU8FXYq1qL0dlqkALKotZKSaYn0PoZodQ0RT416OiUdlcAagW0xBrju2kfNgEKF/qYeIhUwLns3qeCzN58v/pT0Nxd0ILlFZPcC4CrxYbTnkV9w3tjAkbvjTbEuZne1bAYjx72J9yFHfHNkYJKl4ZKnk5KveE7Qutl2mEoz95r1nmeXyMOvln7f7aWJ+qZjfqnLuVucH3u1rj00WM+dLt1uQSg47Kp3BoJz1p0kFBm2b3G0jZZ6G+O7IW4cMfQznKvBxd8aps8LKTTU65OuuxHMHcYfwL2Hn2gBFifrrwBQ94ZI2ThQ7tgvLGD/dDBVqU9xbF40Ba1jVkeQ8++3504Oi5Y4j+bohpdcvSBazEPGVXmXnNVaIC8U93a3ywZDBWn92VWXodJQ2BzfI1ADwRC/XdodVoNORRVHLZrv/cAp56odUrrS0cD6Xb5U7XN/AoALXcPce+gFtdQahAAZWND0J4Yg8s3P8TQam2XddK1fJWUGfl1/IRp1JPY+hPn5sBV20HUZEuSraVmcdcPIHuqTsQdzLm6zD+VSSbdz5kIYs+a/48YirUxLJ8f3ANWhB41aIIvDjJIXeAV0FuK9NccOAn1jKFpfAl/3BXsIBn4zZLCT+PxL+im5jNhsrFB6JOZDje+y4KB3Hc4k2TlI2W5R1pU1s9Qo+bv+tHKr0vysZ65ziWyy2+OcEXxcil1anTLxQJy0dj6ZE1ZqjFLtYlKbMC3cUDf4WRKPslB5PQKvERM71L3o86obKTTU5Y9VeKDWjNqBCsSd5CkTktVf4hr4ABj7KW/tDQJJ3Ygkemv4Iq0YG0Mr4o466HukPbYNrWhfY6AzyPsuURGVjrEeRj1OQ/xzdnfBeEKrTCqkAF+g5fXLl5wVXc4aY3tWrMA1See1FMg+8EYdXoYNSMCMGjM95B72kfI3rxWPyQvF65NyR55Z9K5R5pAfSOc/vw/qIo3BkbZsY4K+fQ1bSNpD9lp3FWe6/OlYirh38ltjV9dRZ02izkOgaeMfdkteF9FyaiFhVK+6Dc4mJ8FRmC7gs+YTC8FymsFM0kyXPy5EcdFl0m9EHliFDGBgGmAtWjebsrELewcajwG3o1c8pVYgINyDXmpzHEcrQAJeJ8cGNcXbrB9VGtvz9q9w/BndHNEDzxKaw8tsH0gqoBEeenYv1eUl4Vc2mbjlGbpuNfMS1QITrnMi7BxqmqOwS3RzXArdGhZrJDVXcwakc0xr/nfORpup1OqfyjghXjZaHTjOFmbl0E3yGdUCKmLupQ6CXcobhzaDd8vfsH89q6/BSU1Hfw6gl4YGAnVI4MMHFeVVq+210BjEU9048uqvS8ZoFQ44Ca9aKu9ups1avGBKMc48ASzNc7X0Vhd0qyGYUR5bVrnptk86oJ6ekYvW467nc/yLKGZCuHK3GxeAEvDHWiGuOWKMomwQeVXUEIGvQwxm2dxydY4ehpsnv5RQUOeBKDOIWt3e6MZLww70PUokWpTgUrHheMPw18CGPWTsu8Lj9pD47CtWos/hrThIruZeK96nT9arJRKH8VgOewtYJ+qBYTYFyqCuSysYFoNfQZLNr5s/UiqFm2x7NwkAM8TRYfk0TgRbVi2BGcbfmvxCUN8EJRy9WAcgpAyYR6bDh90f3zV7H+9G4+gsLhfws8+8z8oAIIPImctowykEXbenY/Xpv1KcpQkfQa5Sp07bziOuB0ev70HqgaTFV4PiQz1nMtH4a7oxuhWILmV9aj0nt5BsCzr/y8ZAGtuhomclXmQVZYsYw4YdM0nKDNQCoznma3W8pPL+H3kUBg3eRdOIT/fPMJNLc1p0M5csc1G6gKPYFScXTRXX4IHdod8/cspiwoDQlE4jGH6xh4tvgMeSkJrYiRqzFr67coFdvADHhWc3tReKE4lOa8tyxvSVWRqaxpUtwMLElehQ6jn0UZxlzl6OZVpdt5Na3deRYIA6igwbgjIhDrzu6ycbDZEcwCT+UpHCSpk5n1QziGN77tT5eewPOU+9eyxv5sbBxMLyAAfxrQCO8sjMYeoz+Uhv1vniYp5RcVSIuX5gGe6XKiVHacO4xy0U1QjYFyjZgH8Ee2XEmH9H65TEjkGdmeeSmsakjf0nGYihC95DNUZAus6V5VCDxNcM6u4vOaFeOVjfMycV5FKlZtgu6uqBC0HteLjRa9AmqThjwt6PJTtX4veYDHvB+kvF9b+Cnj6ZwDr7wHeFVjVFchaDz4UXy5a7GZcG6aIQpHT8rfwYQCa/EoCknB6rnp4fQe1gsVXL6oEF8PJeN8EL10OE7zn67TpbzMQ/qWeyJUurIa1umxyqCB9cVH15jtCDSlrbzWDnrcu+wqPy9ZsWXxeHXuEHiyvq4A3BfVAjErhjOvVjgKY5RzpyOhMJDyavLL/6co73GbZ8F/SAfKOns5XI4Vf1dmbFcmnt4JwxVNiN6fccI0puZfhl3qLM5P6RRA4GVH6Zi5dxG8h3ZFebVebl/cGtscEUvH4GC6ohiKTVIz9WT/5SVp8vTxjDN4IKaNyU/JeE1tuzoWryKVqjwVqjLd8NKxXrg9tgUil43CSTYPhZlUg2osHNp55iB6TniN8va5RAZX4lrRfijNGPxGNo63usOxN/2Q8bzzc0J0dlQ4gEdBbU09iD7zPkUdV0MKNIDWLxB+Ax/G8HUzcJRtomMd7XhM3gJPFkQt5hNT3sKtEQ1ND2KJBLu8J7vKz0vW+r2qsT5mCVXZAQFoPupZrDixmbnLWxnkNSn3pgSeDwdSjrGhHYGKnskLv5Yr0RMoQS7LGC9ocDf6SCm2I9MkfvWo0ADvFOGk8Zx6Ua1RxR2C0uqhighHl6lvYOWZjRZ4bCLzBXjpfAZrbsxauj9RnVA9IpTu79UBngbTa6jDiUfvob3M/jTH1ZOZ1VwUUjK1KISwQdWwwpxDS1AlWsuwspdFdlwqrp6Z+VQnuhneXRTDNJmeEs5bFflFKjTAS6Fv8O2upWa7v2pRDeiv10cptz/+Gt8aQ9Z7XlrI/3asKo+lSuAJfLvOHcanCxNxf2QbKkPQVQNeNbc3KkQH4sMlw3HQLBimpl5jwFO7On/PMlSPCsyRnEvE1aU3EIKGI57AV/uWmLSy7Bt11ahQAI+yMop+jm7CnEM/wW/kY6hC16pS7AOo6vKj4relutneKUsWeHkDPylCCjndzCuVyzl3/2K0HPO0Wbqi8byKqvRYO6hdkce8A6Q/ZcAYj3HMA8M60ys4ZzWUrB3bCjtl1iLLo9k3x/lBoUYFxvjqRdaYntZHSg5Gzp7vGih3VjGU5ee/RLfEp4tH4AjjclN9lI3tLLt6VCiAZ7p9TTVkmM2QPvouzuy7WZsVUI1u5w3uMMza9TXFScVTi2aEaxt9587cJZsXkVzOI2mnseroDtxLy1c2KhzF4wIIOi8T2BePD2AMmDu9nRWZjh2v82XDQ9C5g1AuOgS1o5vglXmfMDcqvM3atbQvi8qiDi2FEE2GP2HKrZ3dtDC2liuMsg5AiXhf8/beWq5ws4FRRcZ0mlBQLjIID476N344ssI03AWFCoermQU+aqm+P/wziru82cL5QktFbogLxIuzPsLu1APmGrkSHv3L5LyiNJo9GRlV6aCV49Bm6n+Yr4Zkf9Qk8MoQhOryzw5IOWfNjlFrbztTKsaEoAYtQI8Zb2LJCY1rSk4O5WWp85cc4Ile+qofarJxK8u619t7HeApxtbeptrrtGJMGIEZxKMv7o1vj/4/DcO+9INGdwoKFRLgkSQzj/e0N+0wHhjUDjfHy6LUpwXwQf1BXTBm6zz1WWUb3+SmGiotpwptZw5dTirGGf775sRKNJr4HMq6Qox1yh5Av43lQmnYQjucmY1wIwLxQNxDGLthlh0+8GRK+SvsvZpZKSvwBiwehnvcrcw6TTW6NWnxNX5qV6f7mZk72qz25nh/upl+eG9RArak7qdesHn0pFEQqHABT6aFR7kcc7d8h9ARj6JWf3/cHeWHGweF4I6Eh/DJtwnYfXqvuUXzEx39y02RKy1lR3xBuvR0FT18k7yacegTKBkbgOpslavRJc4OSDllzcCQe1VGk7Oj/fH+dzHYmLbXtjOeDOlgB/yzaX0KOUnWO1IO4NkJb+OOAeGoQMBpiY8aOO1rWtul+M4bpeXqRzdE6NCHcYSNoakjiYMfJJ+CQIUEeB711sGj7aco0Em0cK2GPo476HqUiPcye+zfG90aMcvGEQMX9m1m/ZxbpLr0ZMeS0fUMJB3ZhAYDHzPAkyJUJfiyA1JOWROiyxj32h+3RYThxwNJxsXNBB5JjdL5uRjXFknOJzLO4rPVU1A/vi1dTclF8zAZ6xOEWhZVmY1SJVcg/uVqi0GrJ1spaNJvCpkJqO3OC13IKRWazpVM/1wHI7l0OlepSFw+GXW0Viu2LmpHe9PFC0CLCa9qSJ1K6bmHlBfClsJ7GlJDJmsZ57Dr5Ha88MX7KM7Ys2KMxthyB3jav7N0nA/L6AffxE44rt5Vo0xyLK2Vs69ZYU4873u/lsiR8/cHf0ZAYkeUpuutmTsaUpHVMysX3N6oHRmKrhNex9pTe3gPa0grWdI0cJ7OBrlgOOGFBnjiC8jjr688sgXdKGQbT9VnkO2NWtFN8NOxzbSJUkBeZA6yBbkrcqXmsEjruTT37yyrd/z22Z7VCznrWNFQhJavKH6RG6VNbnVOwyc3a5/RyCAEx3fF8KSp9rmmbBZ4toGynwpSPJNbZErEPzvP7MZLsz+k1ae7aabLyaW3c2XLR/uixehH8M3+xQZk9i6rBLZpMtK56lR4YrzsSI0ZD7voyf/Npa0BAs1LRLROLozKOX3LAoKPovaAL++JD2Gtyv2btm2+WbhZLlYD678efAJZabNdgSZAB+DmgYEsjza9rYugwd0xbONk7MNRlkrPohIVhNHg/KRzsmFp2Jy6G8/Ne5/ylZzoBcTVhxYmPzvvQ6w5sR5p9Dw09lsQQJYdFW7gqaMqLcPMv3v+yw9wa1RDlIwPwA2DvHAb476uI5/DvJ0/4KR6PfKlBizwhPHZ275DjehQWrxA6wJdBLArsQaB9U4FuZU3EnTaWaxilBfGJn2B/Ug2PXR6fVdGGtnsDnUdkaqSMt7NxvalhZ9kgq5k/P0oE10f8/ZqgSsplaAzwCuY0Cv0Fs9KGZi6dQGC47qhSmQYisX7mpbwz/3C8fCoPvg5eb0wmg9kgaeq3kR36HZ3OMrTcmnsLTuAXY5ruO3LTErTbS5Gt7OCKxi+g9ojOUMLZGjBzZQwPcXha5uyTgbQx2PpZzFh0xw0HtKTnoD2m6lHOdfHP+Oa4lDKUSsSUxVFwMs78gj5QNpxuJeOhm9cV1QxL4z0QUUq7z3uFnAtH0FLkXUX6rwiZoT/RVoxcWd0OCq6cz55uhotnoYg5DpVcAfBd2BnjNgw1aRrXlSpZ2QauoKpWLlJAp7DhxhBj1k3C43d3VGnnxa3aiKBL/4a2xz9lg7WxZldlxZ0BVM+hRZ4Euo5SthsUCNFJGvEZumxjegw8kWUifNnfFQXJePq4ZYBQWgY0w0nM7Tu2EOmYqS/dipSrpDiLQGCeUmmA/wXV0O2xD6odJlxPAHSjkHppSTqFrexoNbXqXOlsssPfgmdMG3nt1nmXsq9lI9t+3l1tmCq1u8j1a+dmqCi8hPLeyLtDEISuqNyRAjdyyAUpwtebUAA/IZ2xsiNX5qtAJGmPl7tYXBJd1yBokJt8WyLdp70PYWCn7lpIW6JCKLieqEC/X8zvSrCH8NWT8JhM52axDrSUftO5tqEYrOpkP2YzGbg7simKBPjgwqXcTUVn2g+oWZf3OIKM+DTeW3hUJbAu9PdEC/N+Rg7Uw/ZzGZD1yLoROmmSdVGTSwjZXqKHxYcXY7b+tt9TbVtXzHKqHnCI5i0YY6N47NQQZdL4Xc1s5A6GyTxY6ymFkMeQZ0BIWYme+k4L5RO8EMY3dBRa6fRDTxppiCpcoSTXIIdn80UPTV+lHD+c2QzlNScQvLFoBOXj9NrpzTwG4ga0UHme2la6GIJ9c1A+cNTXsXiY2uoftQ+/r+uyMiS9cn/qp9tafvx7o8xqBodaGSj9xFWYOM6Zr1nIXQho2sKeFJ6Ewvw4+Qt89Eo8VFUiQpDiXh/lIqvixqRoQga1A3D10+m5fO8g0G16gHL7yebkIyTgHdXZHPzjvPLAU/zCzXRWzMvdCzBPN408D78IfF+xqd+GLtqJu0z3WkpoBTxeiIJ0c4PwN60I4j4YQjui3mQ9ciGVG54RACCB3ez6w91fSETz7UFPIfknrDi1PE+cdt8tBj2GG5nnKfxveIJjLlcgQhI7GzBR1IMkRuU1XKq//G+6LZ2EF2Kkg3wNMwg8OlFl2Xjaf3cfqga6Y/73C3x/nfRNiElKiXMPbtcYCldaxw1TMJGRnG39jDt//1g+Ed1YJweyridMosMQigBN2rzdPOWYAM4eQNGRoWHrkngGS9Fo9g8anHolC1fofmQR1HZrTEfL7M/SkW6LEPXTDTgy63OFVP3SoqsCKX9pBcY34Uwlst+AN0Bntn7kdfUGRCOupEPImJhInalHbS5UqLki+PZa5Y8lv0Ea27kpqn4e1RTMxGhNOWlDrOwwT3x2cbpBKXcS17L9sjUt7mr8NA1CTzb08mq0AsW+F8x39jtc3EHW0ttk6A3xWhQOjSuG4atnETonfbcmZVyXpXmDv1hC6yZJU/Pec+8TPJywwlaNe24mzXdwXho/POIWjkK+84lm2TMeJ0nHznPTWEhU1L70UNa4ziVbnbQ4PYoEeONGxM1kYDAc/uj7+Ih2McaOyfEeSyd6tvUeSGiaxJ4mpVnmkGBj6zv6vVqNKwHakZqWpY3W896/BwGH7otcWsmYEfKQVv/TmXSZDpensO/ROZJJh6z35+b+V9Uo1vrjDVVI9iqRWteoeXidDFLJASgrMsffx7QGEuS17Clz7Itn2n9bWKeJAs3sRBOiWxnvySsQM6CSNV1gDU1csN0NE7siVoRgfh/g73xf4leuDE+ENX6h2HpviRzV9Z9ZewM1V9TQwWHrs0YLwspXvB8oHtyEuM2zUTTxIdRUzuDxerdBz6sYH/89ZOGmL37Ryq+ZyWb6tFza1bK5lQm2cjEqIX5/vyM/xmgaRJvNXeQx8KdZ72CrNWoJzFh5zxsSz1I3dPok8dHvsZIJVKbJj4vIRLRZvfTScPYLTPQaHAP3BoRjqrRGuOsz7g3AHf1a4CeI1/E9/uW8QZe79RpIabrB3giftRK7Ylb5iN86GNmZki5uPtRLvZ+M9bWeMyzGL93EfbSNTV36Y/DhtS2qnXNPHEBXQ54eltNdXe4mfysvUG0TYF65tp+9hjm7FvEp0kd7f1pZhpA9ukXZlKJMtsy54uIn7WKYNGBpag3tDWqRvmZ3bA1gUA90XX6haHbZy8QdMspJcpeQGWdOlxY6ZoH3gXkMSbqLRu75SvUHhBMUHih1MC6uCmhHkq76XoOfRhvz3Uj6eiW8yCTD6QxQnPm1wPv2RlyNbVKOoQWLxzFtVWFYhW6TVVjwvHlngWMLj0TuM3kX1nbgrJiLLeJZZII1cZorJusyEyzTRYdXIGHx76A4tH3mfhb3kD5uCCUptXrMPpFfLl3CaM6TQy3yVwLdH0BjxWXoViCx2RavmaDHjEtajkG7cXifczk6spRwfj7p83w7JT3CArPwlJ1kXpWAVyp7i8PPHXqhJj5o2XiNWAejr/HPkhLp3XyTFG3KNRhC65OmWsTeJ5yevxNlTHp5GYMSpqEDp/3xp39tYtAPfNu8vJxAagZFQr/gV0x/8AK1pXWOPL2a0gs1xXwtL2b2RNM6KESHKHqLzi6Cv/9aQTudXdGOcYUJRL+hdLx90GvOu46ojeiFg3FT4dXsfJPmDRMq0uWDgg2gpoJ7PmDjlmB8978KNSJDiPobO9l5RhvVGErfqerCT5ZNsSDYt7F1kCvJrsm9EqFIOtgpJEhCVEmnvMq8r70Y3AtG4vqMU3MavqylHUZNXqRwfhbTGu0GvcCPts4i1buDK8medoyRYKFrRPlcnR9WTzVnuEL6WD6WQxbMRfV6P79Ie4+3DzwPn6uj+qRfvizqxFaMB4cnjTZXuy02Kx/rXA3y3QMmO1GEwKjpumKvj2wGMEjutLqaVW5P2pH+5qXbpSPaYxPftTbfLLSpfkqlCQhkFUa06DIxfBMHtdPO9MOou8PifhXdEvckOBlOp60DEq7p93jbom+P47A6uP76IJmR9eIjEjXGfCyJ1WnJkq/NPdT/MXVFKWifHHTIIHFG5XohtbuG4yGCd3xw97lvE6BIomKpFZc1k3/rOUTJm0UKC1bdmINGozphaouX7PlnIBXjlavbEyjbIB3bZA8Cts7SwGokRLaKA4ZrYmb56PP7E9RP7otavcPRMU4rR4JQMnYUDO0M3rDVOxLO6RkriWMZUtFwBMZ/zEVm0/swv++jEPA4F4oyZjspni6nrF66WOA2cOyy+hnMWHHLOxI2Wv1Qp0uWRRLH853yMCMy4V89jCqRvlfALxy1zTwtAWVtkr3AI906MwxfLntezyQ0BHVGVNXpmw1gaBWdH2U1iukIxrjiVkf8HLeow2cUslpVqLXKhUBj6T2OctKPaMAq48lmRXOmsZVwa29T3zYOvuggssflaMa4e8xHbDqzEbGIcm8Q0qiWE3rwIz6mM6SZUfXo8mwx1HzGgdeZve+0xCR5HKPWDsVHSb3Rs24YJR23Y+abm3G64MScX6o6ApGh0mvYME+jZ0e59W81/6n6Gyn1rVMRcAjyU20XRtSHH1TF0kaxm+YjdaDnsTtn4abl1+UosLoRfZaqFomOoBK9SLik8ZhxclNVB5FfAKwgMd0zgHrT+7AI5Nfx+3RwWYZyzVr8TyiEwlw607uxPA1U/DXhGYoE+llxy0FthhflNWiYHoPoYN7YEXyZiMvhyQ34dYuZb22qQh4IqM4+kP4aXcqneD/wziNL7Z/i2dnv49/xrUiYALpJgWhRowXamifjwGBuDu2HdqPew1RS8dh72kts/Xs5kmLd4z3D9k0BX9LaEqX1Z+ulYDnhfIxDfHx4ssDz2THfswX+qXnSR5GJr9Au4/ux9jl0/HIxFfpETTF/yXcY+bEVjY9u41xE+Plivys5TxjN86wD1W/FFEmoMnOOZbvVzyuUFMR8C4gQUZsa12ttwZ4t6cdhuuH0agcGYr/5yZwYr0JPm9aQB/zjr6KUaG4zdUCz834CKO3fIVtGUdNOy4rOmvTIvw5oS1KxgZBrxUrH1cXtdwN8N7SYbzCqlumlnkOyoFz1uHcJidd51nii59kf9dsGgGCdki9k7pDjZRHVLJReyif5ee2oe3kN3Bf5IP4c/8QVGcjUyL+fpSM90IFNliVIoLgldAFr34VxXjvRzOOmibX1ENK7teA+1qhIuBdiUw3ONVB23+TdmccZEzyA/p8E4EHhvVEJXcISpvhgbpUrvusNYvwxy39whHi7oLodWPx9Lef4vb4hmZJkvb8rESXtcngXpi1/jsqm+PU2tn1ijOty3sp5ZZKKp3Lp+VA0MLAkD46vZMkTSrQ+NrW1P14jJ6AT1wX3NWvEW7rH4RaLi82Rt7440Bv3Ej3snZEIwQP6oVXFkRi2u5v2JBpRqZkyoRM53CW51xnVAS8K5GAp2kujj56DvvoUM7c+SOem/0B6sW1Q82oEMYuPig28D7cOKgebmYrXy7Sh+fDCMhwVGNso5eXaJ8QgfXfM9/DnrNZus09umeGJ7LqYZbPeTIv0UnSPJd/HBw4nIVU7s3n9mDu/h/wybcD8ezM/6EKPYByUYx96UbLsmnigd7eVIxlrR7dAC/P7YeZOzT3VavrJEuy0lVifJaN5C560HVCRcC7AsmNMtHNxYro0ZftZ/Zj/JYFeOSL/+EeulGV6F4Vi6uH/yP4/pBYH8VjfXBDvN5W6oXqtIyltCrBHYwn57yLnTho0xI56StdMb/Yldie8zrkEvCcHkiTsD6rjHqWGhhZNs9jhEFlZWfKAXxzaCm+OvQDuk54EWEje6JORDhqRQYTXHrjqi9uTvDHjbTmxeO8UDMyCEEDO+E/cz7EjpR9TIMJZpaLpMfaA42enedzPVIR8K5Ajr5cYghsAGfoJM+sPbcfYzYuQEh0R9wW2QA3x/jhxoT6ZrJv6bhA04VuFr5SSSvw2GBcL0SvHIXxy7/AsoOrCcFjjHlOG3dTz1PyYkdXDeWWfmp8TdsDGqW33fZK2imrLNNh/t2No/hs1RS8Mu9TBAzuCJ9hHRnjBqK8pnixDNohrZbe1qNZJ1FBtH7h8EvojNfn9MPsLQuxK2U/UyNJeJoUnaUwFtRyqz2TDa5DKgLeL5IDN2mO0JZiYhWtKkiXEjs/kxSjbTy3FyO3zEDPyf/BnR8FoUb/UNwUG4Cb4rV1n7Z5eIBcF1XVwxnLmC/KG1Uj/BgPhcBvRHe0GPcsnhv/P4xa+wW+3rfULIdZdiAJe9KyWMjLkScfVyKVIOn0dqa9DPOPLsfwDV/gmYnvo9uo19B63Avwju2Iu/s2wh399bZZH5SL9qL76M/41I8W29dwCea7VJQX7nE/hB6T38TozV9i/Zld9gFZySMb4zU4Xwxf0pRdd1QEvNwk6RNJHQg7z+zBW/Mi0WXSK7grtgWBFkIlDqSrqQnT2na8LqrEaK6iOly8aQl9UMZFhY70pvUIwJ0JzfBAYkfUTegEn/jueOSrjzBs9XSMWDUdo1ZOw8iVU5G4ZhJ5CoYun4wZm76hjUrFxlP7MG3dfIzkdQN5/bBVU8214qG8Nmr1WHSZ/jp8B3ZD3cEd8KeYJgR+AGNSWjKtzo8OQBXNJnEHoabp/pcL6W06kSpH6uWPIbjdFYrmU57C2KQvse30fmOdiyhnVAS83CTHR/QA8BAd0dUnt2Lshun49Mc4+MR2QJ2oJijN+OjGaMZFcbSEcZrVQoXXviu0guqEuTHBC/8XXx83RPvgZgKhJJW9TFQ4qtLFq0HgajywFkFa3V3frHyoEtkY9wzugae/daHdF+/grzEdUdPVHFXcvqjBNGqRq/OzhkHkGt7aP8zEadXpOlZlPmSJK8r6xtSlFWZsyljtxoE++L+B3vgDG4gSrgcYzwWi7ZCnEP39MExYMQtJx7fSIfU4i47lL6JfTUXAy0XSRHzjkXo8KetIqfsilTHcMXyxYyFepRVsM7YPGo57DqUjwlAmMgQVooJR2RWEanTtqmpmS/wDKBt3H8pqkJ5AqEDLWIHntSt1WYJI7l+F6Pqo6K6HcuTiMfV5zg/VBwQTTMFMzxclea4M7yvj9mJM5oVSvP8mWi51+JQngBVrljWLcmnN+JwyBLqWRWlIpBRBWtLlj+K00nclPogHxz6GF+e+j5/2r8apjPN7whj3UYvkPGW25S2iX0NFwMtF0upxzcwXq9vivPnLRh0z0nCCFnFv+kHM2fkdBq0aj9eXxiH0sydwX2IX3DukC6rFNKPrGYpitEw3RvmjLOOqsgRfSVrI4vE+KBXvZbrxi5GLa6NXnpf11OdiBFSJhAdQPKG++ay3Dpm1b+og6R+ESv2Dcbe7JeoP6gyfQV3hF9cF/X4ejUErJ2PW9kXYk34YZ9MZtWbNvjmqIdE/21tiwWbPCZIqcRH9MhUBLxfJGQ53BsTPq6X9JFJPvlFmsb6TNTdRfZr7GaX9eHAlZu1dii/2L8PbiwbjoVEvo8XoF9BszPOoO7QVKvf1oaX0wU1RtGCu+riZxxIRgSgZEYxSEUGG9Vnnikfdi2JRdBUjCNh+PvAf0Qktxj6ODqOfR8fPeiNh1Tg+60d8tWcJ5u/6CdrmUM5jlg5Ij/usTNsSWJBZtrm3nU4qu3OmiH6ZioCXm5TFOmQqYJYP+lk6bPtGeUa/ifWD9DfzWksnUk5iz/E92HZiG7YyVpy+fQE+nBePt+bH4JUFbry0IBovL3DhzfmRPBeRhfU9Eq8viMDr86Pw1jw33p0biwV7FmPd2a1YfWoj1p3aZoYwLGQ8pLE80yroMyHG/MiSpfC67PeC0bUqicYLiNBsylBE2VMR8PKZpJcOZ1LWE5f8mI900bPtV/2TNfMA8gJybrjoxiL6RSoCXhEV0VWgIuAVURFdBSoCXhEV0VWgIuAVURFdBSoCXhEV0VWgIuAVURFdBSoCXhEV0VWgIuAVURFdBSoCXhEV0VWgIuAVURHlOwH/H1HdCx6vhispAAAAAElFTkSuQmCC';
652
+
653
+ // ═══════════════════════════════════════════════════════════
654
+ // 工具函数
655
+ // ═══════════════════════════════════════════════════════════
656
+ function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
657
+ function $id(id){return document.getElementById(id);}
658
+ function setTxt(id,v){const e=$id(id);if(e)e.textContent=v;}
659
+ function mk(t,c){const e=document.createElement(t);if(c)e.className=c;return e;}
660
+ function getM(id){return ST.modules.find(m=>m.id===id);}
661
+ let _tt;
662
+ function toast(msg,type){const el=$id('toast');el.textContent=msg;el.className='toast on'+(type?' '+type:'');clearTimeout(_tt);_tt=setTimeout(()=>el.classList.remove('on'),3000);}
663
+ function closeMask(id){$id(id).classList.remove('on');}
664
+ function openMask(id){$id(id).classList.add('on');}
665
+ function fmtSize(b){return b<1024?b+'B':b<1048576?(b/1024).toFixed(1)+'KB':(b/1048576).toFixed(1)+'MB';}
666
+ function fmtDate(d){const dt=new Date(d);return`${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')} ${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}`;}
667
+
668
+ // 隐藏所有页面(含 iframe 页)
669
+ function hideAll(){document.querySelectorAll('.page,.fpage').forEach(e=>e.classList.remove('on'));}
670
+
671
+ // ═══════════════════════════════════════════════════════════
672
+ // 导航
673
+ // ═══════════════════════════════════════════════════════════
674
+ let curPage='home';
675
+
676
+ function nav(p,extra){
677
+ curPage=p;
678
+ hideAll();
679
+ const pages={home:'pg-home',dc:'pg-dc'};
680
+ if(pages[p]){$id(pages[p]).classList.add('on');}
681
+ const labels={home:'首页',dc:'数据中心'};
682
+ setTxt('ttag',labels[p]||extra||p);
683
+ renderNav();
684
+ if(p==='dc')renderDC();
685
+ if(p==='home'){renderHomeMods();refreshStats();}
686
+ }
687
+
688
+ function showModPage(modId){
689
+ hideAll();curPage='mod-'+modId;
690
+ const mod=getM(modId);setTxt('ttag',mod.name+'模块');
691
+ let pg=$id('pg-mod-'+modId);
692
+ if(!pg){pg=mk('div','page');pg.id='pg-mod-'+modId;$id('main').appendChild(pg);}
693
+ const cards=mod.tools.map(t=>{
694
+ const isSvc=t.type==='service';
695
+ const ico=isSvc?'⚙️':'📄';
696
+ const tagStyle=isSvc?'background:#6b46d415;color:#6b46d4':'background:'+mod.color+'15;color:'+mod.color;
697
+ const hint=isSvc?'<div style="font-size:11px;color:var(--text-3);margin-top:3px">需先启动本地服务</div>':'';
698
+ return `<div class="tool-c" style="position:relative">
699
+ <div class="tool-c-body" onclick="openTool('${t.id}','${mod.id}')" style="cursor:pointer;flex:1">
700
+ <div class="tool-c-ico" style="background:${isSvc?'#6b46d418':mod.color+'18'};color:${isSvc?'#6b46d4':mod.color}">${ico}</div>
701
+ <h3>${esc(t.name)}</h3><p>${esc(t.desc)}</p>${hint}
702
+ <div class="tool-c-ft">
703
+ <span class="ttool-tag" style="${tagStyle}">${esc(t.tag)}</span>
704
+ <span style="font-size:12px;color:var(--brand)">打开 ›</span>
705
+ </div>
706
+ </div>
707
+ <div class="tool-menu-wrap">
708
+ <button class="tool-menu-btn" onclick="event.stopPropagation();toggleToolMenu('${t.id}')" title="更多操作">⋮</button>
709
+ <div class="tool-menu" id="tmenu-${t.id}">
710
+ <div class="tool-menu-item" onclick="event.stopPropagation();openEditTool('${t.id}','${mod.id}')">✏️ 编辑信息</div>
711
+ <div class="tool-menu-item danger" onclick="event.stopPropagation();confirmDelTool('${t.id}','${mod.id}')">🗑 删除工具</div>
712
+ </div>
713
+ </div>
714
+ </div>`;}).join('');
715
+ pg.innerHTML=`
716
+ <div class="ph">
717
+ <div style="display:flex;align-items:center;gap:10px">
718
+ <div class="tool-c-ico" style="background:${mod.color}18;color:${mod.color};width:38px;height:38px;border-radius:9px;font-size:20px">${mod.icon}</div>
719
+ <div><h2>${esc(mod.name)}模块</h2><p>${esc(mod.desc)}</p></div>
720
+ </div>
721
+ </div>
722
+ <div class="tool-grid">${cards}
723
+ <div class="add-c" onclick="openUploadTool('${mod.id}')"><div style="font-size:28px">+</div><span>上传新工具</span></div>
724
+ </div>`;
725
+ pg.classList.add('on');renderNav();
726
+ // 点击空白关闭所有工具菜单
727
+ pg.addEventListener('click',function(){closeAllToolMenus();},{once:false});
728
+ }
729
+
730
+ function openTool(tid,mid){
731
+ const mod=getM(mid);const tool=mod&&mod.tools.find(t=>t.id===tid);if(!tool)return;
732
+
733
+ // ── 后端服务类工具:先检测服务是否在运行 ──
734
+ if(tool.type==='service'){
735
+ openServiceTool(tool,mod);return;
736
+ }
737
+
738
+ // ── 本地/上传型工具多重开预警 ──
739
+ warnIfToolOpenInOtherTab(tool);markToolOpen(tool);
740
+
741
+ // ── 上传型工具:blob 地址每次打开时重新生成(旧地址刷新后失效)──
742
+ if(!tool.local&&tool.fileId){
743
+ openUploadedTool(tool,mod);return;
744
+ }
745
+
746
+ hideAll();curPage='tool-'+tid;setTxt('ttag',tool.name);
747
+ let fp=$id('fp-'+tid);
748
+ if(!fp){
749
+ fp=mk('div','fpage');fp.id='fp-'+tid;
750
+ const src=tool.local?tool.path.replace(/\\/g,'/'):tool.blobUrl||'';
751
+ fp.innerHTML=`
752
+ <div class="frame-hd">
753
+ <button class="back-btn" onclick="showModPage('${mid}')">← 返回${esc(mod.name)}模块</button>
754
+ <h3>${esc(tool.name)}</h3>
755
+ <div style="display:flex;gap:8px">
756
+ <button class="btn sm" onclick="(function(){const f=$id('ifr-${tid}');if(f.srcdoc){const sd=f.srcdoc;f.srcdoc='';f.srcdoc=sd;}else{const s=f.src;f.src='';f.src=s;}})()">↻ 刷新</button>
757
+ <button class="btn sm" onclick="window.open($id('ifr-${tid}').src,'_blank')">↗ 新窗口</button>
758
+ </div>
759
+ </div>
760
+ <div class="frame-box"><iframe id="ifr-${tid}" src="${src}" sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals allow-popups"></iframe></div>`;
761
+ $id('main').appendChild(fp);
762
+ }
763
+ fp.classList.add('on');renderNav();
764
+ }
765
+
766
+ // 多重开预警:工具内部使用 IndexedDB 时,多个标签页同时打开同一工具
767
+ // 会互相锁库导致工具卡在"检测中",这里打开前检查并提醒
768
+ function warnIfToolOpenInOtherTab(tool){
769
+ const key='wwt_open_'+tool.id;
770
+ try{
771
+ const rec=JSON.parse(localStorage.getItem(key)||'null');
772
+ if(rec&&rec.ts&&Date.now()-rec.ts<60000&&rec.ts!==_myOpenTs){
773
+ toast('⚠️ 该工具似乎已在其他标签页打开。部分工具(如凭证工具)不支持多标签页同时运行,会卡在"检测中"——请关闭其他标签页中的该工具,或点「↻ 刷新」重试','err');
774
+ }
775
+ }catch(e){}
776
+ }
777
+ let _myOpenTs=Date.now();
778
+ function markToolOpen(tool){
779
+ _myOpenTs=Date.now();
780
+ try{localStorage.setItem('wwt_open_'+tool.id,JSON.stringify({ts:_myOpenTs}));}catch(e){}
781
+ const onUnload=()=>{try{
782
+ const rec=JSON.parse(localStorage.getItem('wwt_open_'+tool.id)||'null');
783
+ if(rec&&rec.ts===_myOpenTs)localStorage.removeItem('wwt_open_'+tool.id);
784
+ }catch(e){}};
785
+ window.addEventListener('pagehide',onUnload);
786
+ }
787
+
788
+ // ── 上传型工具:从 IndexedDB 读回文件内容,动态生成 blob 地址 ────────────────
789
+ async function openUploadedTool(tool,mod){
790
+ warnIfToolOpenInOtherTab(tool);
791
+ markToolOpen(tool);
792
+ hideAll();curPage='tool-'+tool.id;setTxt('ttag',tool.name);
793
+ let fp=$id('fp-'+tool.id);
794
+ if(fp){fp.classList.add('on');renderNav();return;}
795
+ fp=mk('div','fpage');fp.id='fp-'+tool.id;
796
+ fp.innerHTML=`
797
+ <div class="frame-hd">
798
+ <button class="back-btn" onclick="showModPage('${mod.id}')">← 返回${esc(mod.name)}模块</button>
799
+ <h3>${esc(tool.name)}</h3>
800
+ <div style="display:flex;gap:8px">
801
+ <button class="btn sm" onclick="$id('fp-${tool.id}').remove();openTool('${tool.id}','${mod.id}')">↻ 刷新</button>
802
+ <button class="btn sm" onclick="window.open($id('ifr-${tool.id}').src,'_blank')">↗ 新窗口</button>
803
+ </div>
804
+ </div>
805
+ <div class="frame-box"><iframe id="ifr-${tool.id}"></iframe></div>`;
806
+ $id('main').appendChild(fp);
807
+ fp.classList.add('on');renderNav();
808
+
809
+ const ifr=$id('ifr-'+tool.id);
810
+ try{
811
+ const rec=await iGet(ST_FILES,tool.fileId);
812
+ if(!rec)throw new Error('文件数据不存在(可能删除过数据库或从未保存成功)');
813
+ if(rec.text!==undefined){
814
+ // 用 srcdoc 加载 HTML 工具——iframe 继承父页面 file:// origin,
815
+ // localStorage / IndexedDB 全部可用(blob:null origin 下这些 API 均被禁止)
816
+ ifr.removeAttribute('src');
817
+ ifr.srcdoc=rec.text;
818
+ }else if(rec.data){
819
+ const u=URL.createObjectURL(new Blob([new Uint8Array(rec.data)],{type:rec.mime||'application/octet-stream'}));
820
+ ifr.src=u;
821
+ if(tool.tag==='ZIP')toast('ZIP 压缩包无法直接在平台内打开,已在新标签加载','info');
822
+ }
823
+ }catch(e){
824
+ ifr.style.display='none';
825
+ const box=$id('fp-'+tool.id);
826
+ box.insertAdjacentHTML('beforeend',
827
+ '<div style="padding:30px;text-align:center;color:var(--red)">⚠️ 文件内容丢失:'+esc(e.message)+
828
+ '<div style="margin-top:14px"><button class="btn" onclick="document.getElementById(\'fp-'+tool.id+'\').remove()">关闭</button></div></div>');
829
+ }
830
+ }
831
+
832
+ // ── 后端服务工具专用打开逻辑 ────────────────────────────────────
833
+ async function openServiceTool(tool,mod){
834
+ hideAll();curPage='tool-'+tool.id;setTxt('ttag',tool.name);
835
+ let fp=$id('fp-'+tool.id);
836
+ if(!fp){
837
+ fp=mk('div','fpage');fp.id='fp-'+tool.id;
838
+ fp.innerHTML=`
839
+ <div class="frame-hd">
840
+ <button class="back-btn" onclick="showModPage('${mod.id}')">← 返回${esc(mod.name)}模块</button>
841
+ <h3>${esc(tool.name)}</h3>
842
+ <div style="display:flex;gap:8px">
843
+ <button class="btn sm" id="svc-reload-${tool.id}" onclick="reloadServiceFrame('${tool.id}')">↻ 刷新</button>
844
+ <button class="btn sm" onclick="window.open('${tool.serviceUrl}','_blank')">↗ 新窗口</button>
845
+ </div>
846
+ </div>
847
+ <div class="frame-box" id="svc-box-${tool.id}">
848
+ <div id="svc-checking-${tool.id}" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:16px;color:var(--text-2)">
849
+ <div style="font-size:36px">⏳</div>
850
+ <div style="font-size:15px;font-weight:600">正在检测服务状态…</div>
851
+ </div>
852
+ </div>`;
853
+ $id('main').appendChild(fp);
854
+ }
855
+ fp.classList.add('on');renderNav();
856
+ // 检测服务
857
+ await checkAndLoadService(tool);
858
+ }
859
+
860
+ async function checkAndLoadService(tool){
861
+ const boxId='svc-box-'+tool.id;
862
+ const checkId='svc-checking-'+tool.id;
863
+ const box=$id(boxId);if(!box)return;
864
+
865
+ // 尝试 fetch localhost(会因 CORS 报错但能判断是否通)
866
+ let alive=false;
867
+ try{
868
+ const resp=await fetch(tool.serviceUrl,{method:'HEAD',mode:'no-cors',signal:AbortSignal.timeout(2000)});
869
+ alive=true;
870
+ }catch(e){
871
+ // no-cors 模式下网络可达时也会"成功",不可达时抛 TypeError
872
+ alive=!(e instanceof TypeError&&e.message.includes('fetch'));
873
+ // 更直接:只要不是 AbortError/网络错误就认为可达
874
+ if(e.name==='AbortError') alive=false;
875
+ else if(e.name==='TypeError') alive=false;
876
+ else alive=true;
877
+ }
878
+
879
+ if(alive){
880
+ // 服务在线 → 显示 iframe
881
+ box.innerHTML=`<iframe id="ifr-${tool.id}" src="${tool.serviceUrl}"
882
+ style="width:100%;height:100%;border:none"
883
+ sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals allow-popups allow-top-navigation"></iframe>`;
884
+ } else {
885
+ // 服务离线 → 显示启动引导
886
+ const batPath=tool.startPath?tool.startPath.replace(/\//g,'\\')+'\\'+tool.startBat:'启动.bat';
887
+ box.innerHTML=`
888
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
889
+ height:100%;gap:20px;padding:40px;text-align:center">
890
+ <div style="font-size:52px">🔌</div>
891
+ <div>
892
+ <div style="font-size:18px;font-weight:700;margin-bottom:8px">${esc(tool.name)} 未运行</div>
893
+ <div style="font-size:14px;color:var(--text-2);line-height:1.8">
894
+ 该工具需要先在本机启动后端服务,才能在平台里使用。
895
+ </div>
896
+ </div>
897
+ <div style="background:#f7f9fc;border:1px solid var(--border);border-radius:var(--r);
898
+ padding:20px 24px;text-align:left;max-width:460px;width:100%">
899
+ <div style="font-size:13px;font-weight:600;margin-bottom:12px;color:var(--text)">启动步骤</div>
900
+ <div style="display:flex;flex-direction:column;gap:10px;font-size:13px;color:var(--text-2)">
901
+ <div style="display:flex;gap:10px;align-items:flex-start">
902
+ <span style="background:var(--brand);color:#fff;border-radius:50%;width:20px;height:20px;
903
+ display:flex;align-items:center;justify-content:center;font-size:11px;
904
+ flex-shrink:0;margin-top:1px">1</span>
905
+ <span>打开文件夹:<code style="background:#f0f4fa;padding:1px 6px;border-radius:4px;
906
+ font-size:12px">${esc(tool.startPath||'工具目录')}</code></span>
907
+ </div>
908
+ <div style="display:flex;gap:10px;align-items:flex-start">
909
+ <span style="background:var(--brand);color:#fff;border-radius:50%;width:20px;height:20px;
910
+ display:flex;align-items:center;justify-content:center;font-size:11px;
911
+ flex-shrink:0;margin-top:1px">2</span>
912
+ <span>双击运行 <strong>${esc(tool.startBat||'启动.bat')}</strong>,等待浏览器自动打开</span>
913
+ </div>
914
+ <div style="display:flex;gap:10px;align-items:flex-start">
915
+ <span style="background:var(--brand);color:#fff;border-radius:50%;width:20px;height:20px;
916
+ display:flex;align-items:center;justify-content:center;font-size:11px;
917
+ flex-shrink:0;margin-top:1px">3</span>
918
+ <span>服务启动后,回到这里点「重新检测」</span>
919
+ </div>
920
+ </div>
921
+ </div>
922
+ <div style="display:flex;gap:10px">
923
+ <button class="btn prim" onclick="checkAndLoadService(ST.modules.flatMap(m=>m.tools).find(t=>t.id==='${tool.id}'))">
924
+ ↻ 重新检测
925
+ </button>
926
+ <button class="btn" onclick="window.open('${tool.serviceUrl}','_blank')">
927
+ 直接访问 ${tool.serviceUrl}
928
+ </button>
929
+ </div>
930
+ <div style="font-size:12px;color:var(--text-3)">
931
+ 首次使用需安装依赖(需联网),之后可离线使用
932
+ </div>
933
+ </div>`;
934
+ }
935
+ }
936
+
937
+ function reloadServiceFrame(tid){
938
+ const f=$id('ifr-'+tid);
939
+ if(f){
940
+ if(f.srcdoc){const sd=f.srcdoc;f.srcdoc='';f.srcdoc=sd;}
941
+ else{const s=f.src;f.src='';f.src=s;}
942
+ }
943
+ else{
944
+ const tool=ST.modules.flatMap(function(m){return m.tools;}).find(function(t){return t.id===tid;});
945
+ if(tool)checkAndLoadService(tool);
946
+ }
947
+ }
948
+
949
+ function renderNav(){
950
+ const tree=$id('navtree');tree.innerHTML='';
951
+ function item(label,active,onclick,extra){
952
+ const d=mk('div','nav-item'+(active?' active':''));
953
+ d.innerHTML=`<span class="idot"></span>${label}`;
954
+ d.onclick=onclick;if(extra)Object.assign(d.style,extra);
955
+ return d;
956
+ }
957
+ tree.appendChild(item('首页',curPage==='home',()=>nav('home')));
958
+ // 数据中心
959
+ const dc=mk('div','nav-item'+(curPage==='dc'?' active':''));
960
+ dc.innerHTML='<div style="width:20px;height:20px;border-radius:5px;background:#14915b18;color:#14915b;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0">🗄</div> 数据中心';
961
+ dc.onclick=()=>nav('dc');tree.appendChild(dc);
962
+ tree.appendChild(Object.assign(mk('div','nav-sep')));
963
+
964
+ (ST.modules||[]).forEach(function(mod){
965
+ const color=mod.color||'#1a6fd4';
966
+ const icon=mod.icon||'📋';
967
+ const mh=mk('div','nav-item nav-mod'+(mod.open?' open':''));
968
+ mh.innerHTML='<div class="nav-mod-icon" style="background:'+color+'22;color:'+color+'">'+icon+'</div>'+esc(mod.name||'')+'<span class="chevron">▶</span>';
969
+ mh.onclick=function(){mod.open=!mod.open;save();renderNav();};
970
+ tree.appendChild(mh);
971
+ const ch=mk('div','nav-children'+(mod.open?'':' closed'));
972
+ ch.style.maxHeight=mod.open?'600px':'0';
973
+ const mhome=mk('div','nav-item nav-child'+(curPage==='mod-'+mod.id?' active':''));
974
+ mhome.innerHTML='<span class="idot"></span>模块总览';mhome.onclick=function(){showModPage(mod.id);};
975
+ ch.appendChild(mhome);
976
+ (Array.isArray(mod.tools)?mod.tools:[]).forEach(function(t){
977
+ const ti=mk('div','nav-item nav-child'+(curPage==='tool-'+t.id?' active':''));
978
+ ti.title=t.name||'';ti.innerHTML='<span class="idot"></span>'+esc(t.name||'');
979
+ ti.onclick=function(){openTool(t.id,mod.id);};ch.appendChild(ti);
980
+ });
981
+ tree.appendChild(ch);
982
+ });
983
+ }
984
+
985
+ // ═══════════════════════════════════════════════════════════
986
+ // 首页
987
+ // ═══════════════════════════════════════════════════════════
988
+ function renderHomeMods(){
989
+ const g=$id('home-mods');if(!g)return;g.innerHTML='';
990
+ if(!ST||!Array.isArray(ST.modules)){g.innerHTML='<div style="color:var(--text-3);padding:20px">模块数据加载失败,请刷新页面</div>';return;}
991
+ ST.modules.forEach(function(mod){
992
+ try{
993
+ const c=mk('div','mod-c');
994
+ const color=mod.color||'#1a6fd4';
995
+ const icon=mod.icon||'📋';
996
+ const tools=Array.isArray(mod.tools)?mod.tools:[];
997
+ c.innerHTML='<div class="mod-c-icon" style="background:'+color+'22;color:'+color+'">'+icon+'</div>'
998
+ +'<h3>'+esc(mod.name||'')+'模块</h3><p>'+esc(mod.desc||'')+'</p>'
999
+ +'<div class="mod-c-ft"><span>'+tools.length+' 个工具</span>'
1000
+ +'<span style="background:'+color+'18;color:'+color+';padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600">进入 ›</span></div>';
1001
+ c.onclick=function(){showModPage(mod.id);};g.appendChild(c);
1002
+ }catch(e){}
1003
+ });
1004
+ }
1005
+
1006
+ async function refreshStats(){
1007
+ setTxt('s-mods',ST.modules.length);
1008
+ setTxt('s-tools',ST.modules.reduce((s,m)=>s+m.tools.length,0));
1009
+ setTxt('s-files',ST.files.length);
1010
+ }
1011
+
1012
+ // ═══════════════════════════════════════════════════════════
1013
+ // 数据中心 — 核心
1014
+ // ═══════════════════════════════════════════════════════════
1015
+ let _XLSX=null;
1016
+ async function ensureXLSX(){
1017
+ if(_XLSX)return _XLSX;
1018
+ if(typeof XLSX!=='undefined'){_XLSX=XLSX;return XLSX;}
1019
+ return new Promise((res,rej)=>{
1020
+ const s=document.createElement('script');
1021
+ s.src='https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js';
1022
+ s.onload=()=>{_XLSX=XLSX;res(XLSX);};
1023
+ s.onerror=()=>rej(new Error('解析库加载失败,请检查网络连接'));
1024
+ document.head.appendChild(s);
1025
+ });
1026
+ }
1027
+
1028
+ function xlDate(n){
1029
+ if(!n||isNaN(n))return String(n||'');
1030
+ try{return new Date(Math.round((n-25569)*86400*1000)).toISOString().slice(0,10);}catch{return String(n);}
1031
+ }
1032
+
1033
+ // 添加文件弹窗
1034
+ let _afFile=null;
1035
+ function openAddFile(){
1036
+ _afFile=null;
1037
+ $id('af-name').value='';$id('af-note').value='';
1038
+ $id('af-picked').style.display='none';$id('af-inp').value='';
1039
+ openMask('m-addfile');
1040
+ }
1041
+ function afDrop(e){e.preventDefault();$id('af-dz').classList.remove('over');const f=e.dataTransfer.files[0];if(f)afSetFile(f);}
1042
+ function afPick(inp){if(inp.files[0])afSetFile(inp.files[0]);}
1043
+ function afSetFile(f){
1044
+ _afFile=f;
1045
+ $id('af-picked').style.display='flex';
1046
+ $id('af-picked-name').textContent=f.name+' ('+fmtSize(f.size)+')';
1047
+ if(!$id('af-name').value)$id('af-name').value=f.name.replace(/\.(xlsx?|csv)$/i,'');
1048
+ }
1049
+
1050
+ async function doAddFile(){
1051
+ const name=$id('af-name').value.trim();
1052
+ if(!name){toast('请填写文件名称','err');return;}
1053
+ if(!_afFile){toast('请选择文件','err');return;}
1054
+ const cat=$id('af-cat').value;
1055
+ const note=$id('af-note').value.trim();
1056
+
1057
+ // 解析
1058
+ toast('⏳ 解析中…');
1059
+ try{
1060
+ const XL=await ensureXLSX();
1061
+ const buf=await _afFile.arrayBuffer();
1062
+ const wb=XL.read(buf,{type:'array',cellDates:false});
1063
+ const sh=wb.Sheets[wb.SheetNames[0]];
1064
+ const rows=XL.utils.sheet_to_json(sh,{defval:''});
1065
+
1066
+ // 存 IndexedDB(表名 = 去掉特殊字符的文件名)
1067
+ const tableKey=name.replace(/[^a-zA-Z0-9一-龥_]/g,'_');
1068
+ await iPut(ST_TAB,tableKey,rows);
1069
+
1070
+ // 存元数据到 ST.files
1071
+ const entry={
1072
+ id:'f'+Date.now(),name,cat,note,
1073
+ tableKey,rows:rows.length,
1074
+ filename:_afFile.name,size:_afFile.size,
1075
+ addedAt:Date.now(),
1076
+ };
1077
+ // 同名则覆盖
1078
+ const idx=ST.files.findIndex(f=>f.name===name);
1079
+ if(idx>=0)ST.files[idx]=entry;else ST.files.push(entry);
1080
+ save();
1081
+
1082
+ closeMask('m-addfile');
1083
+ toast(`✓ 「${name}」已入库(${rows.length} 行)`,'ok');
1084
+ renderDC();refreshStats();
1085
+ }catch(e){toast('❌ '+e.message,'err');}
1086
+ }
1087
+
1088
+ // 渲染数据中心列表
1089
+ async function renderDC(){
1090
+ const q=($id('dc-search')||{}).value||'';
1091
+ const list=$id('dc-list');if(!list)return;
1092
+
1093
+ let files=[...ST.files];
1094
+ if(q)files=files.filter(f=>f.name.includes(q)||f.note.includes(q)||(f.cat||'').includes(q));
1095
+
1096
+ // 统计
1097
+ let totalRows=0;let okCount=0;
1098
+ for(const f of ST.files){
1099
+ const rows=await iGet(ST_TAB,f.tableKey);
1100
+ if(rows&&rows.length){totalRows+=rows.length;okCount++;}
1101
+ }
1102
+ setTxt('dc-s-total',ST.files.length);
1103
+ setTxt('dc-s-ok',okCount);
1104
+ setTxt('dc-s-rows',totalRows.toLocaleString());
1105
+ setTxt('s-files',ST.files.length);
1106
+
1107
+ if(!files.length){
1108
+ list.innerHTML=`<div class="dc-empty">
1109
+ <div class="dc-empty-ico">🗄</div>
1110
+ <p>${q?'没有匹配的文件':'还没有任何数据文件'}</p>
1111
+ <button class="btn prim" onclick="openAddFile()">+ 添加第一个文件</button>
1112
+ </div>`;return;
1113
+ }
1114
+
1115
+ // 按分类分组
1116
+ const groups={};
1117
+ files.forEach(f=>{const c=f.cat||'其他';if(!groups[c])groups[c]=[];groups[c].push(f);});
1118
+
1119
+ list.innerHTML='';
1120
+ for(const [cat,items] of Object.entries(groups)){
1121
+ const gh=mk('div','dc-group-hd');
1122
+ gh.textContent=(CAT_ICONS[cat]||'📂')+' '+cat;
1123
+ list.appendChild(gh);
1124
+
1125
+ for(const f of items){
1126
+ const ico=CAT_ICONS[f.cat]||'📂';
1127
+ const color=CAT_COLORS[f.cat]||'#5b656f';
1128
+ const rows=await iGet(ST_TAB,f.tableKey);
1129
+ const rowCount=rows&&rows.length?rows.length:0;
1130
+ const statusCls=rowCount?'st-ok':'st-empty';
1131
+ const statusTxt=rowCount?`已入库 ${rowCount.toLocaleString()} 行`:'未入库';
1132
+
1133
+ const row=mk('div','dc-row');
1134
+ row.innerHTML=`
1135
+ <div class="dc-row-ico" style="background:${color}18;border-radius:10px;padding:6px">${ico}</div>
1136
+ <div class="dc-row-body">
1137
+ <div class="dc-row-name">${esc(f.name)}</div>
1138
+ <div class="dc-row-meta">
1139
+ <span>📅 ${fmtDate(f.addedAt)}</span>
1140
+ <span>📄 ${esc(f.filename)}</span>
1141
+ ${f.note?`<span>💬 ${esc(f.note)}</span>`:''}
1142
+ </div>
1143
+ </div>
1144
+ <div class="dc-row-status ${statusCls}">${statusTxt}</div>
1145
+ <div class="dc-row-acts">
1146
+ ${rowCount?`<button class="btn sm" onclick="previewFile('${f.id}')">查看</button>`:''}
1147
+ <button class="btn sm" onclick="reuploadFile('${f.id}')">重新上传</button>
1148
+ <button class="btn sm danger" onclick="deleteFile('${f.id}')">删除</button>
1149
+ </div>`;
1150
+ list.appendChild(row);
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ async function previewFile(id){
1156
+ const f=ST.files.find(x=>x.id===id);if(!f)return;
1157
+ const rows=await iGet(ST_TAB,f.tableKey)||[];
1158
+ if(!rows.length){toast('暂无数据','err');return;}
1159
+ const sample=rows.slice(0,20);
1160
+ const cols=Object.keys(sample[0]||{}).slice(0,10);
1161
+ let html=`<p style="margin-bottom:10px;color:var(--text-3)">显示前 ${sample.length} 行 / 共 ${rows.length} 行(最多显示10列)</p>`;
1162
+ html+=`<table style="width:100%;border-collapse:collapse;font-size:12px">
1163
+ <tr>${cols.map(c=>`<th style="padding:6px 8px;background:#f0f4fb;border:1px solid var(--border);text-align:left;white-space:nowrap">${esc(c)}</th>`).join('')}</tr>
1164
+ ${sample.map(r=>`<tr>${cols.map(c=>`<td style="padding:5px 8px;border:1px solid var(--border);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(String(r[c]||''))}</td>`).join('')}</tr>`).join('')}
1165
+ </table>`;
1166
+ $id('pv-title').textContent=f.name+' — 数据预览';
1167
+ $id('pv-body').innerHTML=html;
1168
+ openMask('m-preview');
1169
+ }
1170
+
1171
+ function reuploadFile(id){
1172
+ const f=ST.files.find(x=>x.id===id);if(!f)return;
1173
+ _afFile=null;
1174
+ $id('af-name').value=f.name;$id('af-note').value=f.note||'';$id('af-cat').value=f.cat||'其他';
1175
+ $id('af-picked').style.display='none';$id('af-inp').value='';
1176
+ openMask('m-addfile');
1177
+ }
1178
+
1179
+ async function deleteFile(id){
1180
+ const f=ST.files.find(x=>x.id===id);if(!f)return;
1181
+ if(!confirm(`确定删除「${f.name}」及其全部数据?`))return;
1182
+ await iDel(ST_TAB,f.tableKey);
1183
+ ST.files=ST.files.filter(x=>x.id!==id);
1184
+ save();toast('已删除','ok');renderDC();refreshStats();
1185
+ }
1186
+
1187
+ async function exportAll(){
1188
+ if(!ST.files.length){toast('没有可导出的文件','err');return;}
1189
+ const data={files:[],data:{}};
1190
+ for(const f of ST.files){
1191
+ data.files.push({name:f.name,cat:f.cat,note:f.note,tableKey:f.tableKey,rows:f.rows,filename:f.filename,addedAt:f.addedAt});
1192
+ data.data[f.tableKey]=await iGet(ST_TAB,f.tableKey)||[];
1193
+ }
1194
+ const a=document.createElement('a');
1195
+ a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));
1196
+ a.download='wwt_data_'+new Date().toISOString().slice(0,10)+'.json';
1197
+ a.click();toast('✓ 已导出','ok');
1198
+ }
1199
+
1200
+ // ═══════════════════════════════════════════════════════════
1201
+ // 上传工具
1202
+ // ═══════════════════════════════════════════════════════════
1203
+ let _utFile=null;
1204
+ function openUploadTool(pre){
1205
+ _utFile=null;$id('ut-name').value='';$id('ut-desc').value='';
1206
+ $id('ut-picked').style.display='none';$id('ut-inp').value='';
1207
+ $id('ut-type').value='html';utTypeChange();
1208
+ $id('ut-svc-url').value='http://localhost:5000';
1209
+ $id('ut-svc-path').value='';$id('ut-svc-bat').value='启动.bat';
1210
+ $id('ut-mod').innerHTML=ST.modules.map(m=>`<option value="${m.id}"${m.id===pre?' selected':''}>${m.name}模块</option>`).join('');
1211
+ openMask('m-tool');
1212
+ }
1213
+ function utTypeChange(){
1214
+ const isSvc=$id('ut-type').value==='service';
1215
+ $id('ut-file-area').style.display=isSvc?'none':'';
1216
+ $id('ut-svc-area').style.display=isSvc?'':'none';
1217
+ }
1218
+ function utDrop(e){e.preventDefault();$id('ut-dz').classList.remove('over');const f=e.dataTransfer.files[0];if(f)utSet(f);}
1219
+ function utPick(inp){if(inp.files[0])utSet(inp.files[0]);}
1220
+ function utSet(f){_utFile=f;$id('ut-picked').style.display='flex';$id('ut-picked-name').textContent=f.name;if(!$id('ut-name').value)$id('ut-name').value=f.name.replace(/\.(html|zip)$/i,'');}
1221
+ async function doAddTool(){
1222
+ const mid=$id('ut-mod').value;
1223
+ const name=$id('ut-name').value.trim();
1224
+ const desc=$id('ut-desc').value.trim();
1225
+ const utType=$id('ut-type').value;
1226
+ if(!name){toast('请填写工具名称','err');return;}
1227
+ const mod=getM(mid);
1228
+
1229
+ if(utType==='service'){
1230
+ // 后端服务类型
1231
+ const svcUrl=$id('ut-svc-url').value.trim()||'http://localhost:5000';
1232
+ const svcPath=$id('ut-svc-path').value.trim();
1233
+ const svcBat=$id('ut-svc-bat').value.trim()||'启动.bat';
1234
+ mod.tools.push({id:'t'+Date.now(),name,desc:desc||name,tag:'服务',type:'service',
1235
+ serviceUrl:svcUrl,startPath:svcPath,startBat:svcBat});
1236
+ save();closeMask('m-tool');toast('✓ "'+name+'" 已添加到'+mod.name+'模块','ok');renderNav();showModPage(mid);
1237
+ return;
1238
+ }
1239
+
1240
+ if(!_utFile){toast('请选择文件','err');return;}
1241
+ const tid='t'+Date.now();
1242
+ const isZip=_utFile.name.endsWith('.zip');
1243
+ // 文件内容持久化到 IndexedDB(刷新后 blob 地址失效问题由此解决)
1244
+ try{
1245
+ if(isZip){
1246
+ const ab=await _utFile.arrayBuffer();
1247
+ await iPut(ST_FILES,tid,{name:_utFile.name,mime:'application/zip',data:new Uint8Array(ab)});
1248
+ }else{
1249
+ const text=await _utFile.text();
1250
+ await iPut(ST_FILES,tid,{name:_utFile.name,mime:'text/html',text});
1251
+ }
1252
+ }catch(e){
1253
+ toast('文件保存失败:'+e.message+'(刷新后该工具将无法打开)','err');return;
1254
+ }
1255
+ mod.tools.push({id:tid,name,desc:desc||name,tag:isZip?'ZIP':'HTML',fileId:tid,local:false});
1256
+ save();closeMask('m-tool');toast('✓ "'+name+'" 已发布到'+mod.name+'模块','ok');renderNav();showModPage(mid);
1257
+ }
1258
+
1259
+ // ═══════════════════════════════════════════════════════════
1260
+ // 工具 菜单 / 编辑 / 删除
1261
+ // ═══════════════════════════════════════════════════════════
1262
+ function closeAllToolMenus(){
1263
+ document.querySelectorAll('.tool-menu.open').forEach(function(m){m.classList.remove('open');});
1264
+ }
1265
+ function toggleToolMenu(tid){
1266
+ const m=$id('tmenu-'+tid);
1267
+ const wasOpen=m&&m.classList.contains('open');
1268
+ closeAllToolMenus();
1269
+ if(m&&!wasOpen)m.classList.add('open');
1270
+ }
1271
+ document.addEventListener('click',function(){closeAllToolMenus();});
1272
+
1273
+ let _editTid=null,_editMid=null;
1274
+ function openEditTool(tid,mid){
1275
+ closeAllToolMenus();
1276
+ const mod=getM(mid);
1277
+ const tool=mod&&mod.tools.find(function(t){return t.id===tid;});
1278
+ if(!tool)return;
1279
+ _editTid=tid;_editMid=mid;
1280
+ $id('et-name').value=tool.name||'';
1281
+ $id('et-desc').value=tool.desc||'';
1282
+ const isSvc=tool.type==='service';
1283
+ $id('et-path-row').style.display=isSvc?'none':'';
1284
+ $id('et-svc-row').style.display=isSvc?'':'none';
1285
+ if(isSvc){
1286
+ $id('et-svcurl').value=tool.serviceUrl||'';
1287
+ }else{
1288
+ $id('et-path').value=tool.path||tool.blobUrl||'';
1289
+ }
1290
+ openMask('m-edit-tool');
1291
+ }
1292
+ function doEditTool(){
1293
+ if(!_editTid||!_editMid)return;
1294
+ const mod=getM(_editMid);
1295
+ const tool=mod&&mod.tools.find(function(t){return t.id===_editTid;});
1296
+ if(!tool)return;
1297
+ const name=$id('et-name').value.trim();
1298
+ if(!name){toast('名称不能为空','err');return;}
1299
+ tool.name=name;
1300
+ tool.desc=$id('et-desc').value.trim()||name;
1301
+ if(tool.type==='service'){
1302
+ const u=$id('et-svcurl').value.trim();
1303
+ if(u)tool.serviceUrl=u;
1304
+ }else{
1305
+ const p=$id('et-path').value.trim();
1306
+ if(p){tool.path=p;tool.local=true;}
1307
+ }
1308
+ save();closeMask('m-edit-tool');
1309
+ toast('✓ 工具「'+name+'」已更新','ok');
1310
+ // 刷新:关闭已缓存的 iframe 页,重新生成模块页
1311
+ const fp=$id('fp-'+_editTid);if(fp)fp.remove();
1312
+ showModPage(_editMid);
1313
+ }
1314
+
1315
+ let _delTid=null,_delMid=null;
1316
+ function confirmDelTool(tid,mid){
1317
+ closeAllToolMenus();
1318
+ const mod=getM(mid);
1319
+ const tool=mod&&mod.tools.find(function(t){return t.id===tid;});
1320
+ if(!tool)return;
1321
+ _delTid=tid;_delMid=mid;
1322
+ setTxt('del-tool-name',tool.name);
1323
+ openMask('m-del-tool');
1324
+ }
1325
+ function doDelTool(){
1326
+ if(!_delTid||!_delMid)return;
1327
+ const mod=getM(_delMid);
1328
+ if(!mod)return;
1329
+ const idx=mod.tools.findIndex(function(t){return t.id===_delTid;});
1330
+ if(idx===-1)return;
1331
+ const delTool=mod.tools[idx];
1332
+ const name=delTool.name;
1333
+ mod.tools.splice(idx,1);
1334
+ // 同步清理该工具的文件数据
1335
+ if(delTool.fileId){try{iDel(ST_FILES,delTool.fileId);}catch(e){}}
1336
+ save();closeMask('m-del-tool');
1337
+ // 清除缓存页面
1338
+ const fp=$id('fp-'+_delTid);if(fp)fp.remove();
1339
+ toast('「'+name+'」已从平台移除','ok');
1340
+ renderNav();showModPage(_delMid);
1341
+ }
1342
+
1343
+ // ═══════════════════════════════════════════════════════════
1344
+ // 新增模块
1345
+ // ═══════════════════════════════════════════════════════════
1346
+ let _si=ICONS[0],_sc=COLORS[0];
1347
+ function openAddMod(){$id('nm-name').value='';$id('nm-desc').value='';_si=ICONS[0];_sc=COLORS[0];renderIcons();renderColors();openMask('m-mod');}
1348
+ function renderIcons(){$id('nm-icons').innerHTML=ICONS.map(ic=>`<div class="icon-opt${ic===_si?' sel':''}" onclick="_si='${ic}';renderIcons()">${ic}</div>`).join('');}
1349
+ function renderColors(){$id('nm-colors').innerHTML=COLORS.map(c=>`<div class="color-opt${c===_sc?' sel':''}" style="background:${c}" onclick="_sc='${c}';renderColors()"></div>`).join('');}
1350
+ function doAddMod(){
1351
+ const name=$id('nm-name').value.trim();
1352
+ if(!name){toast('请填写模块名称','err');return;}
1353
+ ST.modules.push({id:'mod'+Date.now(),name,desc:$id('nm-desc').value.trim()||name,icon:_si,color:_sc,open:false,tools:[]});
1354
+ save();closeMask('m-mod');toast('✓ 模块「'+name+'」已创建','ok');renderNav();renderHomeMods();
1355
+ }
1356
+
1357
+ // ═══════════════════════════════════════════════════════════
1358
+ // 平台更新(方案B:平台 + 内置工具统一检查)
1359
+ // ═══════════════════════════════════════════════════════════
1360
+ const PLATFORM_VER = '1.1.6';
1361
+ const NPM_PKG = 'wwt-platform'; // NPM 包名(主力分发源)
1362
+ const GITEE_TOKEN_KEY = 'wwt_gitee_token';
1363
+ const GITEE_TOKEN_DEF = 'e45883b9c89b8d9689746ce42bbc2b0b';
1364
+ const API_V5 = 'https://gitee.com/api/v5/repos/';
1365
+ const GITEE_PLATFORM_REPO = 'wwtmrhu/wwt-platform';
1366
+
1367
+ // ── 分发源:按优先级依次尝试,任一成功即用 ────────────────────────────
1368
+ // npmmirror 国内最快;npmjs 官方源兜底;Gitee 作为最终备用
1369
+ const SOURCES = [
1370
+ { id:'npmmirror', label:'NPM镜像',
1371
+ verUrl : () => `https://registry.npmmirror.com/${NPM_PKG}/latest`,
1372
+ fileUrl: (v,f) => `https://registry.npmmirror.com/${NPM_PKG}/${v}/files/${f}` },
1373
+ { id:'npmjs', label:'NPM官方',
1374
+ verUrl : () => `https://registry.npmjs.org/${NPM_PKG}/latest`,
1375
+ fileUrl: (v,f) => `https://cdn.jsdelivr.net/npm/${NPM_PKG}@${v}/${f}` },
1376
+ { id:'gitee', label:'Gitee', verUrl:null, fileUrl:null } // 走 API,单独处理
1377
+ ];
1378
+
1379
+ // 更新清单:每个条目 = 一个可更新对象(平台或工具)
1380
+ // 这是「离线兜底种子」——联网时会与远程 tools.json 合并,远程为准
1381
+ const UPD_REG_DEF = [
1382
+ {key:'platform', name:'WWT 工作平台', cur:'1.1.6', repo:GITEE_PLATFORM_REPO, verFile:'version.js', appFile:'app.js', pat:'__WWT_PLATFORM_VER', kind:'platform'},
1383
+ {key:'erp-recon', name:'ERP 对账工具', cur:'V1.0.2',repo:'wwtmrhu/erp-recon', verFile:'version.js', appFile:'app.js', pat:'__ERP_LATEST_VER'},
1384
+ {key:'erp-tool', name:'ERP 对账单生成匹配', cur:'1.0.0', repo:'wwtmrhu/erp-tool', verFile:'version.js', appFile:'app.js', pat:'__ERP_LATEST_VER'},
1385
+ {key:'voucher', name:'WWT 凭证工具', cur:'P1.2.12',repo:'wwtmrhu/certificate-generation', verFile:'dist/version.json', appFile:'', pat:'ver', appUrl:true}
1386
+ ];
1387
+
1388
+ let UPD_REG = (function(){
1389
+ try{const s=JSON.parse(localStorage.getItem('wwt_upd_reg_v2'));
1390
+ if(s&&Array.isArray(s)&&s.length)return s;}catch(e){}
1391
+ return UPD_REG_DEF.slice();
1392
+ })();
1393
+ function saveUpdReg(){try{localStorage.setItem('wwt_upd_reg_v2',JSON.stringify(UPD_REG));}catch(e){}}
1394
+
1395
+ // 平台条目的"当前版本"永远以代码常量为准,避免与 PLATFORM_VER 漂移
1396
+ // (历史上出现过:Gitee 已发 1.1.2,文件内仍写 1.1.1,导致更新后永远提示有新版本)
1397
+ function syncPlatformVer(){
1398
+ const p = UPD_REG.find(x=>x.key==='platform');
1399
+ if (p && p.cur !== PLATFORM_VER) { p.cur = PLATFORM_VER; saveUpdReg(); }
1400
+ }
1401
+ syncPlatformVer();
1402
+
1403
+ let _latestVer = null;
1404
+ let _updResults = []; // 最近一次检查结果
1405
+ let _updBadToken = false;
1406
+
1407
+ function getGiteeToken(){
1408
+ const t = localStorage.getItem(GITEE_TOKEN_KEY)||GITEE_TOKEN_DEF;
1409
+ // 已确认失效的旧令牌自动弃用,回退到当前默认令牌
1410
+ if (t === 'f7261727202f1487d953ec11a7f9ef68') return GITEE_TOKEN_DEF;
1411
+ return t;
1412
+ }
1413
+
1414
+ // 提取版本号中的数字段,兼容 "P1.2.12" / "V1.0.2" / "1.1.10" 等写法
1415
+ function verNum(s) {
1416
+ const m = String(s||'').match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
1417
+ return m ? [Number(m[1]), Number(m[2]||0), Number(m[3]||0)] : [0,0,0];
1418
+ }
1419
+ function verGt(a, b) {
1420
+ const pa = verNum(a), pb = verNum(b);
1421
+ for (let i = 0; i < 3; i++) {
1422
+ if (pa[i] > pb[i]) return true;
1423
+ if (pa[i] < pb[i]) return false;
1424
+ }
1425
+ return false;
1426
+ }
1427
+
1428
+ async function fetchGiteeFile(repo, filename, token) {
1429
+ const url = API_V5 + repo + '/contents/' + encodeURIComponent(filename).replace(/%2F/g,'/') + '?access_token=' + (token||getGiteeToken());
1430
+ const resp = await fetch(url, {cache:'no-store'});
1431
+ if (resp.status === 401) { _updBadToken = true; throw new Error('token 失效(401)'); }
1432
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
1433
+ const json = await resp.json();
1434
+ return atob((json.content||'').replace(/\n/g, ''));
1435
+ }
1436
+
1437
+ // ═══════════════════════════════════════════════════════════
1438
+ // 多源读取层:NPM镜像 → NPM官方 → Gitee,任一成功即用
1439
+ // ═══════════════════════════════════════════════════════════
1440
+ async function fetchText(url, timeoutMs) {
1441
+ const ctl = new AbortController();
1442
+ const timer = setTimeout(()=>ctl.abort(), timeoutMs || 12000);
1443
+ try {
1444
+ const r = await fetch(url, { cache:'no-store', signal: ctl.signal });
1445
+ if (!r.ok) throw new Error('HTTP ' + r.status);
1446
+ return await r.text();
1447
+ } finally { clearTimeout(timer); }
1448
+ }
1449
+
1450
+ // 某个源上的平台版本号
1451
+ async function sourceVersion(s) {
1452
+ if (s.id === 'gitee') {
1453
+ const vc = await fetchGiteeFile(GITEE_PLATFORM_REPO, 'version.js');
1454
+ return parseVer(vc, '__WWT_PLATFORM_VER');
1455
+ }
1456
+ const j = await fetchText(s.verUrl());
1457
+ return (JSON.parse(j) || {}).version || null;
1458
+ }
1459
+
1460
+ // 某个源上的文件内容
1461
+ async function sourceFile(s, ver, name) {
1462
+ if (s.id === 'gitee') return await fetchGiteeFile(GITEE_PLATFORM_REPO, name);
1463
+ return await fetchText(s.fileUrl(ver, name));
1464
+ }
1465
+
1466
+ // 依次尝试所有源
1467
+ async function trySources(fn) {
1468
+ const errs = [];
1469
+ for (const s of SOURCES) {
1470
+ try {
1471
+ const val = await fn(s);
1472
+ if (val === null || val === undefined || val === '') throw new Error('空结果');
1473
+ return { src:s.id, label:s.label, val };
1474
+ } catch(e) { errs.push(s.label + ':' + e.message); }
1475
+ }
1476
+ throw new Error(errs.join(' | '));
1477
+ }
1478
+
1479
+ // ═══════════════════════════════════════════════════════════
1480
+ // 远程工具清单:新增工具无需重新分发平台
1481
+ // ═══════════════════════════════════════════════════════════
1482
+ let _manifest = null; // 最近一次拉到的远程清单
1483
+ let _manifestSrc = null; // 来源标签
1484
+ let _newTools = []; // 清单里有、本平台还没装的工具
1485
+
1486
+ async function fetchManifest() {
1487
+ const r = await trySources(async (s) => {
1488
+ const ver = await sourceVersion(s);
1489
+ const txt = await sourceFile(s, ver, 'tools.json');
1490
+ const m = JSON.parse(txt);
1491
+ if (!m || !Array.isArray(m.tools)) throw new Error('清单格式错误');
1492
+ m.__ver = ver;
1493
+ return m;
1494
+ });
1495
+ _manifest = r.val;
1496
+ _manifestSrc = r.label;
1497
+ mergeManifest(_manifest);
1498
+ return _manifest;
1499
+ }
1500
+
1501
+ // 把远程清单合并进本地注册表(远程为准,本地保留已安装状态)
1502
+ function mergeManifest(m) {
1503
+ if (!m || !Array.isArray(m.tools)) return;
1504
+ const seen = {};
1505
+ m.tools.forEach(t => {
1506
+ if (!t.key) return;
1507
+ seen[t.key] = true;
1508
+ let it = UPD_REG.find(x => x.key === t.key);
1509
+ const seeded = !it;
1510
+ if (!it) { it = { key: t.key }; UPD_REG.push(it); }
1511
+ it.name = t.name || it.name;
1512
+ it.module = t.module || it.module || 'fin';
1513
+ it.desc = t.desc || it.desc;
1514
+ it.path = t.path || it.path;
1515
+ it.tag = t.tag || it.tag || 'HTML';
1516
+ it.manifestVer = t.ver || null; // 清单声明的版本
1517
+ it.noSrc = !t.src; // 该工具没有远程更新源
1518
+ it.removed = false;
1519
+ if (t.src) {
1520
+ it.repo = t.src.repo;
1521
+ it.verFile = t.src.verFile;
1522
+ it.appFile = t.src.appFile;
1523
+ it.pat = t.src.pat;
1524
+ it.appUrl = !!t.src.appUrl;
1525
+ it.npmPkg = t.src.pkg || null; // 将来支持 NPM 托管的工具
1526
+ it.npmFile = t.src.file || null;
1527
+ }
1528
+ // 首次从清单出现:以清单版本作为本地基准,避免一上来就报"有新版"
1529
+ if (seeded) it.cur = t.ver || '0.0.0';
1530
+ });
1531
+ // 清单里已移除的工具:只做标记,不自动删除本地文件
1532
+ UPD_REG.forEach(it => {
1533
+ if (it.key !== 'platform' && !seen[it.key]) it.removed = true;
1534
+ });
1535
+ saveUpdReg();
1536
+ refreshNewTools();
1537
+ }
1538
+
1539
+ // 找出「清单里有、平台界面里还没装」的工具
1540
+ function refreshNewTools() {
1541
+ _newTools = UPD_REG.filter(it => {
1542
+ if (it.key === 'platform' || it.removed) return false;
1543
+ const m = (ST.modules || []).find(x => x.id === (it.module || 'fin'));
1544
+ if (!m) return false;
1545
+ return !(m.tools || []).some(t => t.mkey === it.key);
1546
+ }).map(it => ({ key: it.key, name: it.name, desc: it.desc, module: it.module || 'fin', ver: it.cur }));
1547
+ return _newTools;
1548
+ }
1549
+
1550
+ // 下载某工具的内容(HTML 文本)
1551
+ async function downloadToolContent(it) {
1552
+ const src = SOURCES.find(s => s.id === 'npmmirror');
1553
+ let b64;
1554
+ if (it.npmPkg && it.npmFile) {
1555
+ // NPM 托管的工具:直接取文件
1556
+ const ver = await sourceVersion(src);
1557
+ return await fetchText(`https://registry.npmmirror.com/${it.npmPkg}/${ver}/files/${it.npmFile}`);
1558
+ }
1559
+ if (it.appUrl) {
1560
+ const vc = await fetchGiteeFile(it.repo, it.verFile);
1561
+ const json = JSON.parse(vc);
1562
+ if (!json.url) throw new Error('版本文件中没有 url 字段');
1563
+ const sep = json.url.indexOf('?') >= 0 ? '&' : '?';
1564
+ const resp = await fetch(json.url + sep + 'access_token=' + encodeURIComponent(getGiteeToken()), {cache:'no-store'});
1565
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
1566
+ return atob(b64FromApiJson(await resp.json()));
1567
+ }
1568
+ const appJs = await fetchGiteeFile(it.repo, it.appFile);
1569
+ b64 = b64FromApiJson(appJs);
1570
+ return atob(b64);
1571
+ }
1572
+
1573
+ // 安装一个新工具:下载内容 → 存入平台数据库 → 挂到对应模块
1574
+ async function installTool(key) {
1575
+ const it = UPD_REG.find(x => x.key === key);
1576
+ if (!it) { toast('找不到该工具', 'err'); return; }
1577
+ toast('正在下载「' + it.name + '」…', 'info');
1578
+ try {
1579
+ const html = await downloadToolContent(it);
1580
+ const fid = 'mf_' + it.key;
1581
+ await iPut(ST_FILES, fid, { name: it.name + '.html', mime:'text/html', text: html });
1582
+ const mod = (ST.modules || []).find(x => x.id === (it.module || 'fin')) || ST.modules[0];
1583
+ let stTool = (mod.tools || []).find(t => t.mkey === it.key);
1584
+ if (!stTool) {
1585
+ stTool = { id:'t_'+it.key, name: it.name, desc: it.desc || it.name, tag:'HTML', fileId: fid, local:false, mkey: it.key };
1586
+ mod.tools.push(stTool);
1587
+ } else {
1588
+ stTool.fileId = fid; stTool.local = false; stTool.name = it.name; stTool.desc = it.desc || stTool.desc;
1589
+ }
1590
+ save();
1591
+ refreshNewTools();
1592
+ renderNav();
1593
+ toast('✓ 「' + it.name + '」已安装到「' + mod.name + '」模块', 'ok');
1594
+ return true;
1595
+ } catch(e) {
1596
+ toast('安装失败:' + e.message, 'err');
1597
+ return false;
1598
+ }
1599
+ }
1600
+
1601
+ // 把已有界面工具与注册表关联(按名称匹配)
1602
+ function linkExistingTools() {
1603
+ let changed = false;
1604
+ UPD_REG.forEach(it => {
1605
+ if (it.key === 'platform') return;
1606
+ const n = String(it.name || '').replace(/\s/g,'');
1607
+ if (!n) return;
1608
+ (ST.modules || []).forEach(m => {
1609
+ (m.tools || []).forEach(t => {
1610
+ if (t.mkey) return;
1611
+ const tn = String(t.name || '').replace(/\s/g,'');
1612
+ if (tn && (tn === n || tn.indexOf(n) >= 0 || n.indexOf(tn) >= 0)) { t.mkey = it.key; changed = true; }
1613
+ });
1614
+ });
1615
+ });
1616
+ if (changed) save();
1617
+ }
1618
+
1619
+ // 从版本文件内容中解析版本号
1620
+ function parseVer(content, pat) {
1621
+ if (!content || !pat) return null;
1622
+ const p = pat.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
1623
+ // JSON 格式:"pat" : "x.x.x"(如 {"ver":"1.0.0"} 或 {"version":"1.0.0"})
1624
+ let m = content.match(new RegExp('["\']' + p + '["\']\\s*:\\s*["\']([^"\']+)["\']'));
1625
+ if (m) return m[1];
1626
+ // JS 格式:var pat = "x.x.x"(如 window.__ERP_LATEST_VER="1.0.2")
1627
+ m = content.match(new RegExp(p + '\\s*=\\s*["\']([^"\']+)["\']'));
1628
+ return m ? m[1] : null;
1629
+ }
1630
+
1631
+ async function checkOneUpd(it, token) {
1632
+ const r = {key:it.key, name:it.name, cur:it.cur, latest:null, hasNew:false, error:null,
1633
+ noSrc:!!it.noSrc, removed:!!it.removed, module:it.module};
1634
+
1635
+ // 平台自身:走多源(NPM镜像 → NPM官方 → Gitee)
1636
+ if (it.key === 'platform') {
1637
+ try {
1638
+ const res = await trySources(async (s) => {
1639
+ const v = await sourceVersion(s);
1640
+ if (!v) throw new Error('未取到版本号');
1641
+ return v;
1642
+ });
1643
+ r.latest = res.val; r.srcLabel = res.label;
1644
+ r.hasNew = verGt(r.latest, r.cur);
1645
+ } catch(e) { r.error = e.message; }
1646
+ return r;
1647
+ }
1648
+
1649
+ // 没有远程更新源的工具:不做版本比对
1650
+ if (it.noSrc) { r.latest = it.cur; r.note = '本地工具(无远程更新源)'; return r; }
1651
+
1652
+ try {
1653
+ const vc = await fetchGiteeFile(it.repo, it.verFile, token);
1654
+ r.latest = parseVer(vc, it.pat);
1655
+ if (!r.latest) { r.error = '版本文件中未找到版本号'; return r; }
1656
+ r.hasNew = verGt(r.latest, r.cur);
1657
+ } catch(e) { r.error = e.message; }
1658
+ return r;
1659
+ }
1660
+
1661
+ function updSetDot() {
1662
+ const n = _updResults.filter(r=>r.hasNew).length;
1663
+ const dot = $id('upd-dot');
1664
+ if (dot) dot.style.display = n>0 ? 'block' : 'none';
1665
+ const btn = $id('upd-btn');
1666
+ if (btn && n>0) btn.title = n + ' 个对象有新版';
1667
+ }
1668
+
1669
+ let _manifestErr = null;
1670
+
1671
+ async function checkUpdates(silent) {
1672
+ const token = getGiteeToken();
1673
+ _updBadToken = false;
1674
+ _manifestErr = null;
1675
+ linkExistingTools();
1676
+
1677
+ // ① 先拉远程工具清单(新增工具靠它下发)。失败不阻塞版本检查。
1678
+ try {
1679
+ await fetchManifest();
1680
+ } catch(e) {
1681
+ _manifestErr = e.message;
1682
+ refreshNewTools();
1683
+ }
1684
+
1685
+ // ② 再逐项检查版本
1686
+ _updResults = await Promise.all(UPD_REG.map(it=>checkOneUpd(it, token)));
1687
+ updSetDot();
1688
+ if (!silent) renderUpdList();
1689
+ return _updResults;
1690
+ }
1691
+
1692
+ async function openUpdateModal() {
1693
+ openMask('m-update');
1694
+ const body = $id('upd-body');
1695
+ body.innerHTML = '<div style="text-align:center;padding:30px;color:var(--text-3)">⏳ 正在检查平台和工具更新…</div>';
1696
+ try {
1697
+ await checkUpdates(false);
1698
+ } catch(e) {
1699
+ body.innerHTML = '<div style="text-align:center;padding:24px;color:var(--red)">⚠️ 检查失败:'+esc(e.message)+'</div>';
1700
+ }
1701
+ }
1702
+
1703
+ function renderUpdList() {
1704
+ const body = $id('upd-body');
1705
+
1706
+ // ── 🆕 新工具安装区 ──
1707
+ let newSec = '';
1708
+ if (_newTools && _newTools.length) {
1709
+ newSec = '<div style="margin-bottom:14px;padding:12px 14px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px">'
1710
+ + '<div style="font-weight:700;font-size:14px;color:#1d4ed8;margin-bottom:8px">🆕 发现 ' + _newTools.length + ' 个新工具(来自远程清单)</div>'
1711
+ + _newTools.map(t =>
1712
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-top:1px solid #dbeafe">'
1713
+ + '<div><div style="font-weight:600;font-size:13.5px">' + esc(t.name) + '</div>'
1714
+ + '<div style="font-size:12px;color:var(--text-3)">' + esc(t.desc || '') + ' · v' + esc(t.ver || '') + '</div></div>'
1715
+ + '<button class="btn sm prim" onclick="installTool(\'' + t.key + '\')">⬇ 安装</button></div>').join('')
1716
+ + '</div>';
1717
+ }
1718
+
1719
+ const rows = _updResults.filter(r => !r.removed).map(r => {
1720
+ let st;
1721
+ if (r.removed) st = '<span style="color:var(--text-3)">已下架</span>';
1722
+ else if (r.noSrc) st = '<span style="color:var(--text-3)">本地工具</span>';
1723
+ else if (r.hasNew) st = '<span style="color:#b45000;font-weight:700">● 有新版</span>';
1724
+ else if (r.latest) st = '<span style="color:var(--green)">✓ 最新</span>';
1725
+ else st = '<span style="color:var(--text-3)">⚠ 检查失败' + (r.error ? '(' + esc(r.error) + ')' : '') + '</span>';
1726
+
1727
+ const sub = r.noSrc
1728
+ ? '本地工具,无远程更新源'
1729
+ : '当前 ' + esc(r.cur) + ' → 最新 ' + esc(r.latest || '—') + (r.srcLabel ? ' <span style="color:var(--green)">来源:' + esc(r.srcLabel) + '</span>' : '');
1730
+
1731
+ return `<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border:1px solid var(--border);border-radius:10px;margin-bottom:8px">
1732
+ <div>
1733
+ <div style="font-weight:600;font-size:14px">${esc(r.name)}</div>
1734
+ <div style="font-size:12px;color:var(--text-3);margin-top:2px">${sub}</div>
1735
+ </div>
1736
+ <div style="display:flex;align-items:center;gap:8px">
1737
+ ${st}
1738
+ ${r.hasNew ? '<button class="btn sm prim" onclick="doUpdateOne(\'' + r.key + '\')">⬇ 更新</button>' : ''}
1739
+ ${r.hasNew ? '<button class="btn sm" title="已经手动替换过文件,点这里同步版本号" onclick="markUpdated(\'' + r.key + '\',' + JSON.stringify(r.latest || '').replace(/"/g, '&quot;') + ')">✓ 已手动更新</button>' : ''}
1740
+ </div>
1741
+ </div>`;
1742
+ }).join('');
1743
+
1744
+ body.innerHTML = newSec + rows;
1745
+
1746
+ // 清单拉取失败提示
1747
+ if (_manifestErr) {
1748
+ body.innerHTML += `<div style="margin-top:10px;padding:9px 13px;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;font-size:12.5px;color:#92400e">
1749
+ ⚠ 未能拉取远程工具清单(${esc(_manifestErr)}),本次仅显示本地已知工具。</div>`;
1750
+ }
1751
+ // Gitee 令牌失效提示
1752
+ if (_updBadToken) {
1753
+ body.innerHTML += `<div style="margin-top:10px;padding:10px 14px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;font-size:13px;color:var(--red)">
1754
+ ⚠ Gitee 访问令牌已失效,工具更新会受影响。请先在下方更新令牌。<div style="margin-top:8px">
1755
+ <input id="upd-token" placeholder="粘贴新的 Gitee personal access token" style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:13px">
1756
+ <button class="btn sm" onclick="saveGiteeToken()">保存</button></div></div>`;
1757
+ }
1758
+ }
1759
+
1760
+ // 手动更新后同步版本号,停止继续提示"有新版"
1761
+ function markUpdated(key, latestVer) {
1762
+ const it = UPD_REG.find(x=>x.key===key);
1763
+ if (!it) return;
1764
+ const r = _updResults.find(x=>x.key===key);
1765
+ it.cur = latestVer || (r && r.latest) || it.cur;
1766
+ saveUpdReg();
1767
+ if (r) { r.cur = it.cur; r.hasNew = false; }
1768
+ updSetDot();
1769
+ renderUpdList();
1770
+ toast('✓ 已把「'+it.name+'」标记为 '+it.cur+',不再提示更新', 'ok');
1771
+ }
1772
+
1773
+ // 一键把所有「有新版」的工具标记为已更新
1774
+ // 平台自身条目除外——它的版本号由文件内容自动识别,标记了也会在下次打开时被校正
1775
+ function markAllUpdated() {
1776
+ const pend = _updResults.filter(r => r.hasNew && r.key !== 'platform');
1777
+ if (!pend.length) { toast('没有需要标记的项', 'info'); return; }
1778
+ pend.forEach(r => {
1779
+ const it = UPD_REG.find(x => x.key === r.key);
1780
+ if (it) { it.cur = r.latest || it.cur; r.cur = it.cur; r.hasNew = false; }
1781
+ });
1782
+ saveUpdReg(); updSetDot(); renderUpdList();
1783
+ toast('✓ 已标记 ' + pend.length + ' 个工具为最新,不再提示', 'ok');
1784
+ }
1785
+
1786
+ // 工具主动上报版本号时的处理:平台据此自动识别工具真实版本
1787
+ // 工具侧只需一行:parent.postMessage({__wwt_tool_ver:true,ver:'P1.2.36',name:'WWT凭证工具'},'*')
1788
+ function handleToolVerReport(rep) {
1789
+ const ver = rep.ver ? String(rep.ver).trim() : '';
1790
+ if (!ver) return;
1791
+ let it = null;
1792
+ // 优先按工具名匹配
1793
+ if (rep.name) {
1794
+ const n = String(rep.name).replace(/\s/g,'');
1795
+ it = UPD_REG.find(x => x.key !== 'platform' &&
1796
+ (n.indexOf(x.name.replace(/\s/g,'')) >= 0 || x.name.replace(/\s/g,'').indexOf(n) >= 0));
1797
+ }
1798
+ // 其次按版本号匹配:上报的版本正好等于某条目的最新版本 → 说明该工具已是最新
1799
+ if (!it) {
1800
+ const cands = _updResults.filter(r => r.latest &&
1801
+ verNum(r.latest).join('.') === verNum(ver).join('.') && r.key !== 'platform');
1802
+ if (cands.length === 1) it = UPD_REG.find(x => x.key === cands[0].key);
1803
+ }
1804
+ if (!it) return;
1805
+ if (verNum(ver).join('.') === verNum(it.cur).join('.')) return; // 版本无变化
1806
+ it.cur = ver;
1807
+ saveUpdReg();
1808
+ const r = _updResults.find(x => x.key === it.key);
1809
+ if (r) { r.cur = ver; r.hasNew = verGt(r.latest || '', ver); }
1810
+ updSetDot();
1811
+ if ($id('upd-body') && $id('m-update') && $id('m-update').classList.contains('on')) renderUpdList();
1812
+ toast('✓ 自动识别到「' + it.name + '」实际版本 ' + ver + ',已同步', 'ok');
1813
+ }
1814
+
1815
+ // 下载成功后自动同步版本号(静默,不重绘列表)
1816
+ function markUpdatedSilent(key, ver) {
1817
+ const it = UPD_REG.find(x => x.key === key);
1818
+ if (!it) return;
1819
+ it.cur = ver || it.cur;
1820
+ saveUpdReg();
1821
+ const r = _updResults.find(x => x.key === key);
1822
+ if (r) { r.cur = it.cur; r.hasNew = false; }
1823
+ updSetDot();
1824
+ }
1825
+
1826
+ function saveGiteeToken() {
1827
+ const t = ($id('upd-token').value||'').trim();
1828
+ if (!t) { toast('令牌不能为空','err'); return; }
1829
+ localStorage.setItem(GITEE_TOKEN_KEY, t);
1830
+ toast('✓ 令牌已保存,正在重新检查…','ok');
1831
+ openUpdateModal();
1832
+ }
1833
+
1834
+ // 更新清单管理(可在界面增删条目,支持后续动态新增工具仓库)
1835
+ function openUpdRegModal() {
1836
+ openMask('m-updreg');
1837
+ renderUpdRegList();
1838
+ }
1839
+ function renderUpdRegList() {
1840
+ $id('updreg-list').innerHTML = UPD_REG.map(it=>`
1841
+ <div style="display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;font-size:13px">
1842
+ <span style="flex:1"><b>${esc(it.name)}</b><span style="color:var(--text-3);margin-left:8px">${esc(it.repo)} · 当前 ${esc(it.cur)}</span></span>
1843
+ <button class="btn sm" onclick="editUpdReg('${it.key}')">编辑</button>
1844
+ <button class="btn sm" style="color:var(--red)" onclick="delUpdReg('${it.key}')">删除</button>
1845
+ </div>`).join('')
1846
+ + '<button class="btn sm" style="width:100%;border-style:dashed" onclick="editUpdReg()">+ 新增更新条目</button>';
1847
+ }
1848
+ function editUpdReg(key) {
1849
+ const it = key ? UPD_REG.find(x=>x.key===key) : {key:'',name:'',cur:'',repo:'',verFile:'version.js',appFile:'app.js',pat:'__WWT_PLATFORM_VER'};
1850
+ $id('ur-key').value = it.key||'';
1851
+ $id('ur-name').value = it.name||'';
1852
+ $id('ur-cur').value = it.cur||'';
1853
+ $id('ur-repo').value = it.repo||'';
1854
+ $id('ur-ver').value = it.verFile||'version.js';
1855
+ $id('ur-app').value = it.appFile||'app.js';
1856
+ $id('ur-pat').value = it.pat||'';
1857
+ $id('ur-title').textContent = key ? '编辑更新条目' : '新增更新条目';
1858
+ openMask('m-updreg-item');
1859
+ }
1860
+ function saveUpdRegItem() {
1861
+ const key = $id('ur-key').value.trim();
1862
+ const name = $id('ur-name').value.trim();
1863
+ const repo = $id('ur-repo').value.trim();
1864
+ if (!key||!name||!repo) { toast('标识、名称、仓库不能为空','err'); return; }
1865
+ const it = {key, name, cur:$id('ur-cur').value.trim()||'0.0.0', repo,
1866
+ verFile:$id('ur-ver').value.trim()||'version.js', appFile:$id('ur-app').value.trim()||'app.js',
1867
+ pat:$id('ur-pat').value.trim()||'version'};
1868
+ const idx = UPD_REG.findIndex(x=>x.key===key);
1869
+ if (idx>=0) UPD_REG[idx] = it; else UPD_REG.push(it);
1870
+ saveUpdReg(); closeMask('m-updreg-item'); openUpdRegModal();
1871
+ toast('✓ 更新清单已保存','ok');
1872
+ }
1873
+ function delUpdReg(key) {
1874
+ const i = UPD_REG.findIndex(x=>x.key===key);
1875
+ if (i>=0) { UPD_REG.splice(i,1); saveUpdReg(); renderUpdRegList(); }
1876
+ }
1877
+
1878
+ // 取 Gitee 文件 API 返回 JSON 中的 base64 内容
1879
+ function b64FromApiJson(json) {
1880
+ if (json && typeof json.content === 'string') return json.content.replace(/\n/g,'');
1881
+ const m = String(json).match(new RegExp('["\']([A-Za-z0-9+/=\n]{200,})["\']'));
1882
+ if (m) return m[1].replace(/\n/g,'');
1883
+ throw new Error('更新包格式错误(未找到 base64 内容)');
1884
+ }
1885
+ function b64ToBytes(b64) {
1886
+ const binStr = atob(b64);
1887
+ const bytes = new Uint8Array(binStr.length);
1888
+ for (let i = 0; i < binStr.length; i++) bytes[i] = binStr.charCodeAt(i);
1889
+ return bytes;
1890
+ }
1891
+
1892
+ async function doUpdateOne(key) {
1893
+ const it = UPD_REG.find(x=>x.key===key);
1894
+ if (!it) return;
1895
+ const body = $id('upd-body');
1896
+ const prog = '<div id="upd-prog" style="margin-top:10px;padding:10px 14px;background:var(--bg);border-radius:8px;font-size:13px;color:var(--text-2)">⏳ 正在下载「'+esc(it.name)+'」最新版…</div>';
1897
+ // 已有进度条就更新,没有就追加
1898
+ if ($id('upd-prog')) $id('upd-prog').outerHTML = prog;
1899
+ else body.insertAdjacentHTML('beforeend', prog);
1900
+ try {
1901
+ let bytes, srcLabel = '';
1902
+ if (it.key === 'platform') {
1903
+ // 平台本体:NPM 上直接是 platform.html(明文),Gitee 上是 base64 的 app.js
1904
+ const res = await trySources(async (s) => {
1905
+ if (s.id === 'gitee') {
1906
+ const appJs = await fetchGiteeFile(GITEE_PLATFORM_REPO, 'app.js');
1907
+ return { bytes: b64ToBytes(b64FromApiJson(appJs)) };
1908
+ }
1909
+ const ver = await sourceVersion(s);
1910
+ const html = await sourceFile(s, ver, 'platform.html');
1911
+ return { bytes: new TextEncoder().encode(html) };
1912
+ });
1913
+ bytes = res.val.bytes;
1914
+ srcLabel = res.label;
1915
+ } else if (it.appUrl) {
1916
+ // version.json 里直接带最新版文件 API 地址,按 url 字段下载
1917
+ const vc = await fetchGiteeFile(it.repo, it.verFile);
1918
+ let json;
1919
+ try { json = JSON.parse(vc); } catch(e) { throw new Error('版本文件不是有效 JSON'); }
1920
+ const dlUrl = json.url;
1921
+ if (!dlUrl) throw new Error('版本文件中没有 url 字段');
1922
+ const sep = dlUrl.indexOf('?') >= 0 ? '&' : '?';
1923
+ const resp = await fetch(dlUrl + sep + 'access_token=' + encodeURIComponent(getGiteeToken()), {cache:'no-store'});
1924
+ if (resp.status === 401) { _updBadToken = true; throw new Error('token 失效(401)'); }
1925
+ if (!resp.ok) throw new Error('下载 HTTP ' + resp.status);
1926
+ bytes = b64ToBytes(b64FromApiJson(await resp.json()));
1927
+ } else {
1928
+ const appJs = await fetchGiteeFile(it.repo, it.appFile);
1929
+ bytes = b64ToBytes(b64FromApiJson(appJs));
1930
+ }
1931
+ // 清单中所有更新对象均为 HTML 应用(平台或单文件工具)
1932
+ const blob = new Blob([bytes], {type:'text/html;charset=utf-8'});
1933
+ const url = URL.createObjectURL(blob);
1934
+ const a = document.createElement('a');
1935
+ a.href = url;
1936
+ const latest = (_updResults.find(r=>r.key===key)||{}).latest || 'new';
1937
+ a.download = it.name.replace(/\s+/g,'_') + '_v' + latest + '.html';
1938
+ a.click();
1939
+ setTimeout(()=>URL.revokeObjectURL(url), 5000);
1940
+ // 先更新状态并重绘列表,再写提示——顺序反了会把提示冲掉
1941
+ markUpdatedSilent(key, latest);
1942
+ renderUpdList();
1943
+ const msg = '<div id="upd-prog" style="margin-top:10px;padding:10px 14px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;font-size:13px;color:var(--green)">'
1944
+ + '✅ 「'+esc(it.name)+'」v'+esc(latest)+' 已下载到你的下载文件夹。'
1945
+ + (srcLabel ? '<span style="color:var(--text-3);font-size:12px">(来源:'+esc(srcLabel)+')</span>' : '') + '<br>'
1946
+ + (key==='platform' ? '用新文件替换平台文件后重新打开即可,数据不受影响。'
1947
+ : '用新文件替换原工具文件后重新打开,替换完就不会再提示更新。')
1948
+ + '<br><span style="color:var(--text-3);font-size:12px">已自动标记为最新版本。若这次没有替换文件,可点「↻ 重新检查」恢复提示。</span></div>';
1949
+ const b2 = $id('upd-body');
1950
+ if ($id('upd-prog')) $id('upd-prog').outerHTML = msg;
1951
+ else b2.insertAdjacentHTML('beforeend', msg);
1952
+ $id('upd-prog').style.color = 'var(--green)';
1953
+ } catch(e) {
1954
+ $id('upd-prog').innerHTML = '❌ 下载失败:'+esc(e.message);
1955
+ $id('upd-prog').style.color = 'var(--red)';
1956
+ }
1957
+ }
1958
+
1959
+ // 启动后 3 秒静默检查(不弹窗,只亮红点)
1960
+ setTimeout(function(){ checkUpdates(true); }, 3000);
1961
+
1962
+ // ═══════════════════════════════════════════════════════════
1963
+ // 初始化
1964
+ // ═══════════════════════════════════════════════════════════
1965
+ function _init(){
1966
+ try{nav('home');}
1967
+ catch(e){
1968
+ // 兜底:强制显示首页
1969
+ document.querySelectorAll('.page,.fpage').forEach(function(el){el.classList.remove('on');});
1970
+ var ph=document.getElementById('pg-home');
1971
+ if(ph)ph.classList.add('on');
1972
+ try{renderNav();}catch(e2){}
1973
+ try{renderHomeMods();}catch(e3){
1974
+ var g=document.getElementById('home-mods');
1975
+ if(g)g.innerHTML='<div style="color:red;padding:20px">初始化错误:'+e3.message+'</div>';
1976
+ }
1977
+ }
1978
+ }
1979
+ if(document.readyState==='loading'){
1980
+ document.addEventListener('DOMContentLoaded',_init);
1981
+ }else{
1982
+ _init();
1983
+ }
1984
+ </script>
1985
+ </body>
1986
+ </html>