@steedos-labs/plugin-workflow 3.0.54 → 3.0.56
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/designer/dist/amis-renderer/amis-renderer.css +1 -1
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-DXnimQAi.js → index-B9naSD1C.js} +173 -173
- package/designer/dist/assets/index-DEEcIiu0.css +1 -0
- package/designer/dist/index.html +2 -2
- package/main/default/manager/push_manager.js +93 -113
- package/main/default/manager/uuflow_manager.js +151 -47
- package/main/default/objects/categories/buttons/badge_recalc.button.yml +44 -0
- package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
- package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -2
- package/main/default/objects/instances/buttons/instance_export.button.js +10 -0
- package/main/default/objects/instances/buttons/instance_export.button.yml +83 -0
- package/main/default/objects/instances/listviews/completed.listview.yml +1 -2
- package/main/default/objects/instances/listviews/draft.listview.yml +1 -2
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/objects/instances/listviews/pending.listview.yml +1 -2
- package/main/default/pages/page_instance_print.page.amis.json +6 -2
- package/main/default/routes/api_workflow_ai_form_design.router.js +9 -3
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +9 -3
- package/main/default/routes/api_workflow_export.router.js +97 -152
- package/main/default/routes/api_workflow_instance_forward.router.js +15 -1
- package/main/default/routes/api_workflow_instance_permissions.router.js +2 -2
- package/main/default/routes/api_workflow_nav.router.js +7 -1
- package/main/default/routes/api_workflow_next_step.router.js +1 -1
- package/main/default/services/instance.service.js +1 -1
- package/main/default/utils/business_hours.js +210 -0
- package/main/default/utils/business_timeout.js +211 -0
- package/package.json +1 -1
- package/package.service.js +9 -1
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +7 -2
- package/src/rests/badgeRecalcConsole.js +593 -0
- package/src/rests/badgeRecalcExecute.js +308 -0
- package/src/rests/index.js +2 -0
- package/src/timeout_auto_submit.js +81 -0
- package/designer/dist/assets/index-xR8ApdWL.css +0 -1
|
@@ -250,9 +250,9 @@ tbody .color-priority-muted *{
|
|
|
250
250
|
color: #40a9ff
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
.steedos-instance-print-wrapper .liquid-amis-container{
|
|
253
|
+
/* .steedos-instance-print-wrapper .liquid-amis-container{
|
|
254
254
|
width: 190mm;
|
|
255
|
-
}
|
|
255
|
+
} */
|
|
256
256
|
|
|
257
257
|
@media (max-width: 768px){
|
|
258
258
|
/* 审批单详细页手机端附件预览按钮样式 */
|
|
@@ -583,4 +583,9 @@ tbody .color-priority-muted *{
|
|
|
583
583
|
|
|
584
584
|
.ant-tree-select-dropdown{
|
|
585
585
|
z-index: 9999 !important;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
.workflow-form-v2{
|
|
590
|
+
text-align: unset !important;
|
|
586
591
|
}
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
// Badge Recalculation Console - HTML admin page
|
|
2
|
+
// Calls /api/workflow/badge-recalc-execute in serial batches
|
|
3
|
+
module.exports = {
|
|
4
|
+
rest: {
|
|
5
|
+
method: 'GET',
|
|
6
|
+
fullPath: '/api/workflow/badge-recalc-console'
|
|
7
|
+
},
|
|
8
|
+
async handler(ctx) {
|
|
9
|
+
const userSession = ctx.meta.user;
|
|
10
|
+
|
|
11
|
+
// Check if user is admin
|
|
12
|
+
if (!userSession || !userSession.is_space_admin) {
|
|
13
|
+
ctx.meta.$statusCode = 403;
|
|
14
|
+
ctx.meta.$responseType = 'text/html';
|
|
15
|
+
return 'Forbidden: Admin access required';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Detect environment
|
|
19
|
+
const env = process.env.NODE_ENV || 'development';
|
|
20
|
+
const isProduction = env === 'production';
|
|
21
|
+
|
|
22
|
+
const html = `
|
|
23
|
+
<!DOCTYPE html>
|
|
24
|
+
<html lang="zh-CN">
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="UTF-8">
|
|
27
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
28
|
+
<title>Badge 重算控制台</title>
|
|
29
|
+
<script src="/unpkg/jquery@3.7.1/dist/jquery.min.js"></script>
|
|
30
|
+
<style>
|
|
31
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
32
|
+
body {
|
|
33
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
34
|
+
background: #f5f5f5;
|
|
35
|
+
padding: 20px;
|
|
36
|
+
}
|
|
37
|
+
.container {
|
|
38
|
+
max-width: 1200px;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
background: white;
|
|
41
|
+
border-radius: 8px;
|
|
42
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
header {
|
|
46
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
47
|
+
color: white;
|
|
48
|
+
padding: 30px;
|
|
49
|
+
text-align: center;
|
|
50
|
+
}
|
|
51
|
+
header h1 { font-size: 28px; margin-bottom: 8px; }
|
|
52
|
+
header .env-badge {
|
|
53
|
+
display: inline-block;
|
|
54
|
+
padding: 4px 12px;
|
|
55
|
+
border-radius: 12px;
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
font-weight: bold;
|
|
58
|
+
margin-top: 8px;
|
|
59
|
+
}
|
|
60
|
+
header .env-prod { background: #e53e3e; }
|
|
61
|
+
header .env-test { background: #48bb78; }
|
|
62
|
+
|
|
63
|
+
.content { padding: 30px; }
|
|
64
|
+
|
|
65
|
+
.usage-guide {
|
|
66
|
+
background: #ebf8ff;
|
|
67
|
+
border: 1px solid #90cdf4;
|
|
68
|
+
border-radius: 6px;
|
|
69
|
+
padding: 20px 24px;
|
|
70
|
+
margin-bottom: 24px;
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
color: #2a4365;
|
|
73
|
+
line-height: 1.7;
|
|
74
|
+
}
|
|
75
|
+
.usage-guide h4 {
|
|
76
|
+
margin-bottom: 10px;
|
|
77
|
+
font-size: 16px;
|
|
78
|
+
color: #2b6cb0;
|
|
79
|
+
}
|
|
80
|
+
.usage-guide ol, .usage-guide ul {
|
|
81
|
+
margin-left: 20px;
|
|
82
|
+
margin-bottom: 8px;
|
|
83
|
+
}
|
|
84
|
+
.usage-guide li { margin-bottom: 4px; }
|
|
85
|
+
.usage-guide .warn {
|
|
86
|
+
color: #c05621;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.operation-panel {
|
|
91
|
+
background: #f7fafc;
|
|
92
|
+
padding: 24px;
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
margin-bottom: 20px;
|
|
95
|
+
}
|
|
96
|
+
.operation-panel h3 {
|
|
97
|
+
color: #2d3748;
|
|
98
|
+
margin-bottom: 16px;
|
|
99
|
+
font-size: 18px;
|
|
100
|
+
}
|
|
101
|
+
.form-group {
|
|
102
|
+
margin-bottom: 16px;
|
|
103
|
+
}
|
|
104
|
+
.form-group label {
|
|
105
|
+
display: block;
|
|
106
|
+
margin-bottom: 6px;
|
|
107
|
+
color: #4a5568;
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
}
|
|
110
|
+
.form-group input[type="text"],
|
|
111
|
+
.form-group input[type="number"] {
|
|
112
|
+
width: 100%;
|
|
113
|
+
padding: 10px;
|
|
114
|
+
border: 1px solid #cbd5e0;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
}
|
|
118
|
+
.form-group input[type="checkbox"] {
|
|
119
|
+
margin-right: 8px;
|
|
120
|
+
}
|
|
121
|
+
.form-group input[type="radio"] {
|
|
122
|
+
margin-right: 6px;
|
|
123
|
+
}
|
|
124
|
+
.radio-group label {
|
|
125
|
+
display: inline-block;
|
|
126
|
+
margin-right: 20px;
|
|
127
|
+
font-weight: normal;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.btn {
|
|
131
|
+
padding: 12px 24px;
|
|
132
|
+
border: none;
|
|
133
|
+
border-radius: 6px;
|
|
134
|
+
font-size: 15px;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
transition: all 0.2s;
|
|
138
|
+
margin-right: 12px;
|
|
139
|
+
}
|
|
140
|
+
.btn-primary {
|
|
141
|
+
background: #667eea;
|
|
142
|
+
color: white;
|
|
143
|
+
}
|
|
144
|
+
.btn-primary:hover { background: #5a67d8; }
|
|
145
|
+
.btn-danger {
|
|
146
|
+
background: #f56565;
|
|
147
|
+
color: white;
|
|
148
|
+
}
|
|
149
|
+
.btn-danger:hover { background: #e53e3e; }
|
|
150
|
+
.btn:disabled {
|
|
151
|
+
opacity: 0.5;
|
|
152
|
+
cursor: not-allowed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.result-panel {
|
|
156
|
+
margin-top: 24px;
|
|
157
|
+
padding: 20px;
|
|
158
|
+
border-radius: 6px;
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
.result-panel.success {
|
|
162
|
+
background: #c6f6d5;
|
|
163
|
+
border: 1px solid #9ae6b4;
|
|
164
|
+
color: #22543d;
|
|
165
|
+
}
|
|
166
|
+
.result-panel.error {
|
|
167
|
+
background: #fed7d7;
|
|
168
|
+
border: 1px solid #fc8181;
|
|
169
|
+
color: #742a2a;
|
|
170
|
+
}
|
|
171
|
+
.result-panel.info {
|
|
172
|
+
background: #bee3f8;
|
|
173
|
+
border: 1px solid #90cdf4;
|
|
174
|
+
color: #2a4365;
|
|
175
|
+
}
|
|
176
|
+
.result-panel h4 {
|
|
177
|
+
margin-bottom: 12px;
|
|
178
|
+
font-size: 16px;
|
|
179
|
+
}
|
|
180
|
+
.result-panel pre {
|
|
181
|
+
background: rgba(0,0,0,0.05);
|
|
182
|
+
padding: 12px;
|
|
183
|
+
border-radius: 4px;
|
|
184
|
+
overflow-x: auto;
|
|
185
|
+
font-size: 13px;
|
|
186
|
+
max-height: 300px;
|
|
187
|
+
overflow-y: auto;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.progress-bar-container {
|
|
191
|
+
width: 100%;
|
|
192
|
+
height: 24px;
|
|
193
|
+
background: #e2e8f0;
|
|
194
|
+
border-radius: 12px;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
margin-top: 12px;
|
|
197
|
+
margin-bottom: 8px;
|
|
198
|
+
}
|
|
199
|
+
.progress-bar-fill {
|
|
200
|
+
height: 100%;
|
|
201
|
+
background: linear-gradient(90deg, #667eea, #764ba2);
|
|
202
|
+
border-radius: 12px;
|
|
203
|
+
transition: width 0.3s ease;
|
|
204
|
+
width: 0%;
|
|
205
|
+
}
|
|
206
|
+
.progress-text {
|
|
207
|
+
font-size: 14px;
|
|
208
|
+
color: #4a5568;
|
|
209
|
+
margin-bottom: 4px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.diff-record {
|
|
213
|
+
background: #fffff0;
|
|
214
|
+
border-left: 4px solid #d69e2e;
|
|
215
|
+
padding: 8px 12px;
|
|
216
|
+
margin: 6px 0;
|
|
217
|
+
border-radius: 0 4px 4px 0;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
}
|
|
220
|
+
.modified-record {
|
|
221
|
+
background: #f0fff4;
|
|
222
|
+
border-left: 4px solid #38a169;
|
|
223
|
+
padding: 8px 12px;
|
|
224
|
+
margin: 6px 0;
|
|
225
|
+
border-radius: 0 4px 4px 0;
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.loading {
|
|
230
|
+
display: inline-block;
|
|
231
|
+
width: 16px;
|
|
232
|
+
height: 16px;
|
|
233
|
+
border: 3px solid rgba(255,255,255,.3);
|
|
234
|
+
border-radius: 50%;
|
|
235
|
+
border-top-color: #fff;
|
|
236
|
+
animation: spin 1s ease-in-out infinite;
|
|
237
|
+
margin-left: 8px;
|
|
238
|
+
}
|
|
239
|
+
@keyframes spin {
|
|
240
|
+
to { transform: rotate(360deg); }
|
|
241
|
+
}
|
|
242
|
+
.pre-wrapper {
|
|
243
|
+
position: relative;
|
|
244
|
+
}
|
|
245
|
+
.pre-wrapper pre {
|
|
246
|
+
margin: 0;
|
|
247
|
+
}
|
|
248
|
+
.btn-copy {
|
|
249
|
+
position: absolute;
|
|
250
|
+
top: 4px;
|
|
251
|
+
right: 4px;
|
|
252
|
+
padding: 2px 8px;
|
|
253
|
+
font-size: 12px;
|
|
254
|
+
background: #edf2f7;
|
|
255
|
+
border: 1px solid #cbd5e0;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
color: #4a5568;
|
|
259
|
+
z-index: 1;
|
|
260
|
+
}
|
|
261
|
+
.btn-copy:hover {
|
|
262
|
+
background: #e2e8f0;
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
265
|
+
</head>
|
|
266
|
+
<body>
|
|
267
|
+
<div class="container">
|
|
268
|
+
<header>
|
|
269
|
+
<h1>Badge 重算控制台</h1>
|
|
270
|
+
<p>Badge Recalculation Console</p>
|
|
271
|
+
<span class="env-badge ${isProduction ? 'env-prod' : 'env-test'}">
|
|
272
|
+
环境: ${isProduction ? 'PRODUCTION' : 'TEST/DEV'}
|
|
273
|
+
</span>
|
|
274
|
+
</header>
|
|
275
|
+
|
|
276
|
+
<div class="content">
|
|
277
|
+
<div class="usage-guide">
|
|
278
|
+
<h4>📖 使用说明</h4>
|
|
279
|
+
<p><strong>用途:</strong>重算用户的审批流角标(badge)数据,修复因并发、异常、流程分类解绑等原因导致的角标数值不准确。</p>
|
|
280
|
+
<p><strong>背景:</strong>角标数值在日常使用中可能因并发、异常等原因产生偏差。系统在每次新审批单提交时会自动重算相关用户的角标,大部分偏差会自然修正。本工具用于主动检查和批量修正那些长期未被自动修正的记录(如低频用户、已转岗用户等)。建议定期 Dry Run 检查,按需执行实际修正。</p>
|
|
281
|
+
<p><strong>建议操作步骤:</strong></p>
|
|
282
|
+
<ol>
|
|
283
|
+
<li>保持 <strong>Dry Run</strong> 勾选,先执行检查,查看哪些用户的角标数据存在差异;</li>
|
|
284
|
+
<li>确认差异记录合理(对比 old / new 值);</li>
|
|
285
|
+
<li>取消勾选 Dry Run,再次执行,实际写入修正后的角标数据。</li>
|
|
286
|
+
</ol>
|
|
287
|
+
<p><strong>注意事项:</strong></p>
|
|
288
|
+
<ul>
|
|
289
|
+
<li>Dry Run 默认开启,<strong>不会修改任何数据</strong>,可放心执行;</li>
|
|
290
|
+
<li>取消 Dry Run 后才会实际写入 <code>steedos_keyvalues</code> 表;</li>
|
|
291
|
+
<li class="warn">批量重算会调用 <code>count_instance_tasks</code> 逐用户计算,对数据库有一定查询压力,建议在业务低峰期执行。</li>
|
|
292
|
+
</ul>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<!-- Panel 1: Per-user recalculation -->
|
|
296
|
+
<div class="operation-panel">
|
|
297
|
+
<h3>👤 按用户重算</h3>
|
|
298
|
+
<div class="form-group">
|
|
299
|
+
<label>User ID:</label>
|
|
300
|
+
<input type="text" id="user-id" placeholder="输入用户 ID">
|
|
301
|
+
</div>
|
|
302
|
+
<div class="form-group">
|
|
303
|
+
<label>
|
|
304
|
+
<input type="checkbox" id="user-dryrun" checked>
|
|
305
|
+
Dry Run(仅检查差异,不实际修改数据)
|
|
306
|
+
</label>
|
|
307
|
+
</div>
|
|
308
|
+
<button class="btn btn-primary" id="user-btn" onclick="executeUserRecalc()">
|
|
309
|
+
▶ 执行重算
|
|
310
|
+
</button>
|
|
311
|
+
<div class="result-panel" id="user-result"></div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- Panel 2: Batch recalculation -->
|
|
315
|
+
<div class="operation-panel">
|
|
316
|
+
<h3>📦 批量重算(全量)</h3>
|
|
317
|
+
<div class="form-group" style="display:none;">
|
|
318
|
+
<label>范围选择:</label>
|
|
319
|
+
<input type="hidden" name="batch-scope" value="all">
|
|
320
|
+
</div>
|
|
321
|
+
<div class="form-group">
|
|
322
|
+
<label>每批用户数:</label>
|
|
323
|
+
<input type="number" id="batch-pagesize" value="50" min="1" max="200">
|
|
324
|
+
</div>
|
|
325
|
+
<div class="form-group">
|
|
326
|
+
<label>
|
|
327
|
+
<input type="checkbox" id="batch-dryrun" checked>
|
|
328
|
+
Dry Run(仅检查差异,不实际修改数据)
|
|
329
|
+
</label>
|
|
330
|
+
</div>
|
|
331
|
+
<button class="btn btn-primary" id="batch-start-btn" onclick="startBatch()">
|
|
332
|
+
▶ 开始批量重算
|
|
333
|
+
</button>
|
|
334
|
+
<button class="btn btn-danger" id="batch-stop-btn" onclick="stopBatch()" disabled>
|
|
335
|
+
⏹ 停止
|
|
336
|
+
</button>
|
|
337
|
+
<div id="batch-progress" style="display:none; margin-top: 16px;">
|
|
338
|
+
<div class="progress-text" id="batch-progress-text"></div>
|
|
339
|
+
<div class="progress-bar-container">
|
|
340
|
+
<div class="progress-bar-fill" id="batch-progress-bar"></div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="result-panel" id="batch-result"></div>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<script>
|
|
349
|
+
function copyPreContent(btn) {
|
|
350
|
+
var pre = btn.parentElement.querySelector('pre');
|
|
351
|
+
if (!pre) return;
|
|
352
|
+
navigator.clipboard.writeText(pre.textContent).then(function() {
|
|
353
|
+
btn.textContent = '✅ 已复制';
|
|
354
|
+
setTimeout(function() { btn.textContent = '📋 复制'; }, 1500);
|
|
355
|
+
}).catch(function() {
|
|
356
|
+
btn.textContent = '❌ 复制失败';
|
|
357
|
+
setTimeout(function() { btn.textContent = '📋 复制'; }, 1500);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// --- Per-user recalculation ---
|
|
361
|
+
function executeUserRecalc() {
|
|
362
|
+
var userId = $('#user-id').val().trim();
|
|
363
|
+
if (!userId) { alert('请输入 User ID'); return; }
|
|
364
|
+
|
|
365
|
+
var dryRun = $('#user-dryrun').is(':checked');
|
|
366
|
+
var $btn = $('#user-btn');
|
|
367
|
+
var $result = $('#user-result');
|
|
368
|
+
|
|
369
|
+
$btn.prop('disabled', true).text('⏳ 执行中...');
|
|
370
|
+
$result.removeClass('success error info').hide();
|
|
371
|
+
|
|
372
|
+
$.ajax({
|
|
373
|
+
url: '/api/workflow/badge-recalc-execute',
|
|
374
|
+
type: 'GET',
|
|
375
|
+
data: { scope: 'user', userId: userId, dryRun: dryRun },
|
|
376
|
+
success: function(data) {
|
|
377
|
+
$btn.prop('disabled', false).text('▶ 执行重算');
|
|
378
|
+
$result.addClass('success').show();
|
|
379
|
+
var html = '<h4>✅ 执行完成</h4>';
|
|
380
|
+
html += renderSinglePageResult(data, dryRun);
|
|
381
|
+
$result.html(html);
|
|
382
|
+
},
|
|
383
|
+
error: function(xhr) {
|
|
384
|
+
$btn.prop('disabled', false).text('▶ 执行重算');
|
|
385
|
+
$result.removeClass('success').addClass('error').show();
|
|
386
|
+
var errorMsg = xhr.responseJSON || xhr.responseText || '未知错误';
|
|
387
|
+
$result.html('<h4>❌ 操作失败</h4><div class="pre-wrapper"><button class="btn-copy" onclick="copyPreContent(this)">📋 复制</button><pre style="background:#f5f5f5;padding:10px;border-radius:4px">' + JSON.stringify(errorMsg, null, 2) + '</pre></div>');
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// --- Batch recalculation ---
|
|
393
|
+
var batchStopped = false;
|
|
394
|
+
|
|
395
|
+
function startBatch() {
|
|
396
|
+
var scope = 'all';
|
|
397
|
+
var pageSize = parseInt($('#batch-pagesize').val()) || 50;
|
|
398
|
+
var dryRun = $('#batch-dryrun').is(':checked');
|
|
399
|
+
|
|
400
|
+
batchStopped = false;
|
|
401
|
+
$('#batch-start-btn').prop('disabled', true).text('⏳ 执行中...');
|
|
402
|
+
$('#batch-stop-btn').prop('disabled', false);
|
|
403
|
+
$('#batch-progress').show();
|
|
404
|
+
$('#batch-progress-text').text('正在启动...');
|
|
405
|
+
$('#batch-progress-bar').css('width', '0%');
|
|
406
|
+
$('#batch-result').removeClass('success error info').hide().empty();
|
|
407
|
+
|
|
408
|
+
runBatch(scope, pageSize, dryRun);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function stopBatch() {
|
|
412
|
+
batchStopped = true;
|
|
413
|
+
$('#batch-stop-btn').prop('disabled', true).text('正在停止...');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function runBatch(scope, pageSize, dryRun) {
|
|
417
|
+
var page = 1;
|
|
418
|
+
var totalPages = 1;
|
|
419
|
+
var totalUsers = 0;
|
|
420
|
+
var processedUsers = 0;
|
|
421
|
+
var diffUserIds = {}; // track unique userIds with hasDiff
|
|
422
|
+
var diffResults = []; // only keep diff records (not all results)
|
|
423
|
+
var startTime = Date.now();
|
|
424
|
+
var $result = $('#batch-result');
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
while (page <= totalPages && !batchStopped) {
|
|
428
|
+
var params = { scope: scope, page: page, pageSize: pageSize, dryRun: dryRun };
|
|
429
|
+
|
|
430
|
+
var data = await $.ajax({
|
|
431
|
+
url: '/api/workflow/badge-recalc-execute',
|
|
432
|
+
type: 'GET',
|
|
433
|
+
data: params
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
totalPages = data.totalPages || 1;
|
|
437
|
+
totalUsers = data.totalUsers || 0;
|
|
438
|
+
processedUsers += (data.processedUsers || 0);
|
|
439
|
+
|
|
440
|
+
if (data.results && data.results.length) {
|
|
441
|
+
data.results.forEach(function(r) {
|
|
442
|
+
if (r.hasDiff) {
|
|
443
|
+
diffResults.push(r);
|
|
444
|
+
if (r.userId) diffUserIds[r.userId] = true;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Update progress
|
|
450
|
+
var elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
|
451
|
+
var elapsedMin = Math.floor(elapsedSec / 60);
|
|
452
|
+
var elapsedRemSec = elapsedSec % 60;
|
|
453
|
+
var pct = totalPages > 0 ? Math.min(Math.round(page / totalPages * 100), 100) : 0;
|
|
454
|
+
$('#batch-progress-bar').css('width', pct + '%');
|
|
455
|
+
var diffUserCount = Object.keys(diffUserIds).length;
|
|
456
|
+
var diffRecordCount = diffResults.length;
|
|
457
|
+
$('#batch-progress-text').text(
|
|
458
|
+
'已处理 ' + Math.min(processedUsers, totalUsers) + ' / ' + totalUsers + ' 用户' +
|
|
459
|
+
'(第 ' + page + ' 页 / 共 ' + totalPages + ' 页)' +
|
|
460
|
+
'耗时 ' + elapsedMin + ' 分 ' + elapsedRemSec + ' 秒' +
|
|
461
|
+
' | 找到差异用户: ' + diffUserCount + '(差异记录 ' + diffRecordCount + ' 条)'
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
page++;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Finished
|
|
468
|
+
$('#batch-start-btn').prop('disabled', false).text('▶ 开始批量重算');
|
|
469
|
+
$('#batch-stop-btn').prop('disabled', true).text('⏹ 停止');
|
|
470
|
+
|
|
471
|
+
var summary = { processedUsers: processedUsers, totalUsers: totalUsers, diffUserCount: Object.keys(diffUserIds).length, diffResults: diffResults, dryRun: dryRun, startTime: startTime };
|
|
472
|
+
_lastDiffResults = diffResults;
|
|
473
|
+
|
|
474
|
+
if (batchStopped) {
|
|
475
|
+
$result.addClass('info').show();
|
|
476
|
+
$result.html('<h4>⏹ 已手动停止</h4>' + renderBatchSummary(summary));
|
|
477
|
+
} else {
|
|
478
|
+
$('#batch-progress-bar').css('width', '100%');
|
|
479
|
+
$result.addClass('success').show();
|
|
480
|
+
$result.html('<h4>✅ 批量重算完成</h4>' + renderBatchSummary(summary));
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
$('#batch-start-btn').prop('disabled', false).text('▶ 开始批量重算');
|
|
484
|
+
$('#batch-stop-btn').prop('disabled', true).text('⏹ 停止');
|
|
485
|
+
|
|
486
|
+
var errorMsg = (err && err.responseJSON) || (err && err.responseText) || '未知错误';
|
|
487
|
+
var summary = { processedUsers: processedUsers, totalUsers: totalUsers, diffUserCount: Object.keys(diffUserIds).length, diffResults: diffResults, dryRun: dryRun, startTime: startTime };
|
|
488
|
+
_lastDiffResults = diffResults;
|
|
489
|
+
$result.addClass('error').show();
|
|
490
|
+
$result.html('<h4>❌ 批量重算失败(第 ' + (page) + ' 页时出错)</h4>' +
|
|
491
|
+
'<div class="pre-wrapper"><button class="btn-copy" onclick="copyPreContent(this)">📋 复制</button><pre style="background:#f5f5f5;padding:10px;border-radius:4px">' + JSON.stringify(errorMsg, null, 2) + '</pre></div>' +
|
|
492
|
+
renderBatchSummary(summary));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// --- Result rendering helpers ---
|
|
497
|
+
function renderSinglePageResult(data, dryRun) {
|
|
498
|
+
var html = '';
|
|
499
|
+
if (data.results && data.results.length) {
|
|
500
|
+
var diffCount = 0;
|
|
501
|
+
data.results.forEach(function(r) {
|
|
502
|
+
if (r.hasDiff) diffCount++;
|
|
503
|
+
});
|
|
504
|
+
html += '<p>处理用户数: <strong>' + (data.processedUsers || 1) + '</strong>';
|
|
505
|
+
if (dryRun) html += ' | 有差异: <strong style="color:#d69e2e;">' + diffCount + '</strong>';
|
|
506
|
+
else html += ' | 已修改: <strong style="color:#38a169;">' + diffCount + '</strong>';
|
|
507
|
+
html += '</p>';
|
|
508
|
+
html += renderResultRecords(data.results, dryRun);
|
|
509
|
+
} else {
|
|
510
|
+
html += '<p>无处理结果</p>';
|
|
511
|
+
}
|
|
512
|
+
html += '<details style="margin-top:15px"><summary>▼ 展开完整 JSON</summary><div class="pre-wrapper"><button class="btn-copy" onclick="copyPreContent(this)">📋 复制</button><pre style="max-height:300px;overflow:auto;background:#f5f5f5;padding:10px;border-radius:4px">' + JSON.stringify(data, null, 2) + '</pre></div></details>';
|
|
513
|
+
return html;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function renderBatchSummary(summary) {
|
|
517
|
+
var elapsed = Math.floor((Date.now() - summary.startTime) / 1000);
|
|
518
|
+
var mins = Math.floor(elapsed / 60);
|
|
519
|
+
var secs = elapsed % 60;
|
|
520
|
+
var dryRun = summary.dryRun;
|
|
521
|
+
var diffResults = summary.diffResults || [];
|
|
522
|
+
var diffUserCount = summary.diffUserCount || 0;
|
|
523
|
+
var MAX_DISPLAY = 100;
|
|
524
|
+
|
|
525
|
+
var html = '<hr style="margin:10px 0;">';
|
|
526
|
+
html += '<p>处理用户数: <strong>' + summary.processedUsers + '</strong> / ' + summary.totalUsers + ' | 耗时: <strong>' + mins + '分' + secs + '秒</strong></p>';
|
|
527
|
+
|
|
528
|
+
if (diffUserCount > 0) {
|
|
529
|
+
html += '<p>' + (dryRun ? '有差异用户' : '已修改用户') + ': <strong style="color:' + (dryRun ? '#d69e2e' : '#38a169') + ';">' + diffUserCount + '</strong>';
|
|
530
|
+
html += '(差异记录 ' + diffResults.length + ' 条)</p>';
|
|
531
|
+
var displayRecords = diffResults.slice(0, MAX_DISPLAY);
|
|
532
|
+
html += renderResultRecords(displayRecords, dryRun);
|
|
533
|
+
if (diffResults.length > MAX_DISPLAY) {
|
|
534
|
+
html += '<p style="color:#718096;font-size:13px;margin-top:8px;">⚠️ 仅显示前 ' + MAX_DISPLAY + ' 条差异记录(共 ' + diffResults.length + ' 条),请使用下方导出按钮查看完整数据。</p>';
|
|
535
|
+
}
|
|
536
|
+
html += '<div style="margin-top:12px;"><button class="btn btn-primary" onclick="exportDiffJson()" style="font-size:13px;padding:8px 16px;">📥 导出差异记录 JSON</button></div>';
|
|
537
|
+
} else {
|
|
538
|
+
html += '<p>所有用户均无差异 ✅</p>';
|
|
539
|
+
}
|
|
540
|
+
return html;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Store diff results for export
|
|
544
|
+
var _lastDiffResults = [];
|
|
545
|
+
|
|
546
|
+
function exportDiffJson() {
|
|
547
|
+
if (!_lastDiffResults.length) { alert('无差异记录可导出'); return; }
|
|
548
|
+
var json = JSON.stringify(_lastDiffResults, null, 2);
|
|
549
|
+
var blob = new Blob([json], { type: 'application/json' });
|
|
550
|
+
var url = URL.createObjectURL(blob);
|
|
551
|
+
var a = document.createElement('a');
|
|
552
|
+
a.href = url;
|
|
553
|
+
a.download = 'badge-diff-' + new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-') + '.json';
|
|
554
|
+
document.body.appendChild(a);
|
|
555
|
+
a.click();
|
|
556
|
+
document.body.removeChild(a);
|
|
557
|
+
URL.revokeObjectURL(url);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function escapeHtml(str) {
|
|
561
|
+
if (!str) return '';
|
|
562
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderResultRecords(records, dryRun) {
|
|
566
|
+
var html = '';
|
|
567
|
+
records.forEach(function(r) {
|
|
568
|
+
if (r.hasDiff) {
|
|
569
|
+
var cls = dryRun ? 'diff-record' : 'modified-record';
|
|
570
|
+
var icon = dryRun ? '⚠️' : '✅';
|
|
571
|
+
var hint = dryRun ? '(Dry Run 未写入)' : '(已写入)';
|
|
572
|
+
var userLabel = r.userName
|
|
573
|
+
? escapeHtml(r.userName) + '(' + escapeHtml((r.userId || '').substring(0, 8)) + ')'
|
|
574
|
+
: escapeHtml(r.userId || '-');
|
|
575
|
+
var spaceLabel = r.spaceName
|
|
576
|
+
? escapeHtml(r.spaceName) + '(' + escapeHtml((r.space || '').substring(0, 8)) + ')'
|
|
577
|
+
: escapeHtml(r.space || '-');
|
|
578
|
+
html += '<div class="' + cls + '">' + icon + ' 用户: ' + userLabel + ' 工作区: ' + spaceLabel + ' ' + hint +
|
|
579
|
+
'<details><summary>详情</summary><div class="pre-wrapper"><button class="btn-copy" onclick="copyPreContent(this)">📋 复制</button><pre>' + escapeHtml(JSON.stringify(r, null, 2)) + '</pre></div></details></div>';
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
return html;
|
|
583
|
+
}
|
|
584
|
+
</script>
|
|
585
|
+
</body>
|
|
586
|
+
</html>
|
|
587
|
+
`;
|
|
588
|
+
|
|
589
|
+
// Set response type to HTML
|
|
590
|
+
ctx.meta.$responseType = 'text/html';
|
|
591
|
+
return html;
|
|
592
|
+
}
|
|
593
|
+
};
|