ErisPulse-Raffle 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,921 @@
1
+ var _rfState = {
2
+ platforms: [],
3
+ activities: [],
4
+ currentId: null,
5
+ currentActivity: null,
6
+ confirmed: [],
7
+ pending: [],
8
+ allParticipants: [],
9
+ activeGroup: 'all',
10
+ editMode: false,
11
+ editId: null,
12
+ tempGroups: [],
13
+ blacklist: [],
14
+ whitelist: [],
15
+ notifyTargets: [],
16
+ notifyActivityId: null,
17
+ notifyHistory: [],
18
+ };
19
+
20
+ function loadRaffleView() {
21
+ rfLoadPlatforms();
22
+ rfLoadActivities();
23
+ rfLoadSettings();
24
+ }
25
+
26
+ function _rfTk() {
27
+ return localStorage.getItem('__ep_tk__');
28
+ }
29
+
30
+ function _rfHeaders() {
31
+ return { 'Authorization': 'Bearer ' + _rfTk(), 'Content-Type': 'application/json' };
32
+ }
33
+
34
+ function rfLoadPlatforms() {
35
+ fetch('/Raffle/api/platforms', { headers: _rfHeaders() })
36
+ .then(function(r) { return r.json(); })
37
+ .then(function(d) {
38
+ _rfState.platforms = d.platforms || [];
39
+ var sel = document.getElementById('rf-f-platform');
40
+ var sel2 = document.getElementById('rf-nf-platform');
41
+ if (sel) {
42
+ sel.innerHTML = '<option value="">选择平台</option>';
43
+ _rfState.platforms.forEach(function(p) {
44
+ var opt = document.createElement('option');
45
+ opt.value = p;
46
+ opt.textContent = p;
47
+ sel.appendChild(opt);
48
+ });
49
+ }
50
+ if (sel2) {
51
+ sel2.innerHTML = '<option value="">平台</option>';
52
+ _rfState.platforms.forEach(function(p) {
53
+ var opt = document.createElement('option');
54
+ opt.value = p;
55
+ opt.textContent = p;
56
+ sel2.appendChild(opt);
57
+ });
58
+ }
59
+ })
60
+ .catch(function() {});
61
+ }
62
+
63
+ function rfLoadActivities() {
64
+ fetch('/Raffle/api/activities', { headers: _rfHeaders() })
65
+ .then(function(r) { return r.json(); })
66
+ .then(function(d) {
67
+ _rfState.activities = d.activities || [];
68
+ rfRenderActivityList();
69
+ })
70
+ .catch(function() {});
71
+ }
72
+
73
+ function rfLoadSettings() {
74
+ fetch('/Raffle/api/settings', { headers: _rfHeaders() })
75
+ .then(function(r) { return r.json(); })
76
+ .then(function(d) {
77
+ var settings = d.settings || {};
78
+ _rfState.currentId = settings.current_activity || null;
79
+ var tpl = settings.reply_templates || {};
80
+ var elS = document.getElementById('rf-tpl-success');
81
+ var elJ = document.getElementById('rf-tpl-joined');
82
+ var elH = document.getElementById('rf-tpl-hint');
83
+ var elP = document.getElementById('rf-tpl-pending');
84
+ var elNA = document.getElementById('rf-tpl-noactivity');
85
+ var elBL = document.getElementById('rf-tpl-blacklisted');
86
+ var elNWL = document.getElementById('rf-tpl-notwhitelist');
87
+ if (elS) elS.value = tpl.success || '';
88
+ if (elJ) elJ.value = tpl.already_joined || '';
89
+ if (elH) elH.value = tpl.hint || '';
90
+ if (elP) elP.value = tpl.pending || '';
91
+ if (elNA) elNA.value = tpl.no_activity || '';
92
+ if (elBL) elBL.value = tpl.blacklisted || '';
93
+ if (elNWL) elNWL.value = tpl.not_in_whitelist || '';
94
+ var elN = document.getElementById('rf-tpl-notify');
95
+ if (elN) elN.value = tpl.notify || '';
96
+ var elBC = document.getElementById('rf-tpl-broadcast');
97
+ if (elBC) elBC.value = tpl.broadcast || '';
98
+
99
+ if (_rfState.currentId) {
100
+ rfSelectActivity(_rfState.currentId);
101
+ }
102
+ })
103
+ .catch(function() {});
104
+ }
105
+
106
+ function rfRenderActivityList() {
107
+ var el = document.getElementById('rf-activity-list');
108
+ if (!el) return;
109
+
110
+ if (_rfState.activities.length === 0) {
111
+ el.innerHTML = '<div style="text-align:center;padding:16px;color:var(--tx-t)">暂无活动,点击右上角创建</div>';
112
+ rfUpdateStats(null);
113
+ return;
114
+ }
115
+
116
+ var html = '';
117
+ _rfState.activities.forEach(function(act) {
118
+ var statusText = act.status === 'open' ? '报名中' : (act.status === 'drawn' ? '已开奖' : '已关闭');
119
+ var statusClass = act.status === 'open' ? 'chip-ok' : (act.status === 'drawn' ? 'chip-sc' : 'chip-wr');
120
+ var isCurrent = act.id === _rfState.currentId;
121
+ html += '<div class="rf-activity-item' + (isCurrent ? ' rf-current' : '') + '" onclick="rfSelectActivity(\'' + rfEsc(act.id) + '\')">';
122
+ html += '<div style="flex:1;cursor:pointer">';
123
+ html += '<div style="font-weight:600;margin-bottom:2px">' + rfEsc(act.name) + '</div>';
124
+ html += '<div style="font-size:12px;color:var(--tx-t)">' + rfEsc(act.id) + ' · 开奖 ' + act.draw_count + ' 人';
125
+ if (act.whitelist_mode) html += ' · <span style="color:var(--wr-c)">白名单</span>';
126
+ html += '</div></div>';
127
+ html += '<span class="chip ' + statusClass + '">' + statusText + '</span>';
128
+ html += '<button class="btn btn-icon btn-sm" title="编辑" onclick="event.stopPropagation();rfEditActivity(\'' + rfEsc(act.id) + '\')" style="margin-left:4px">';
129
+ html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
130
+ html += '</button>';
131
+ html += '<button class="btn btn-icon btn-sm" title="发送通知" onclick="event.stopPropagation();rfShowNotify(\'' + rfEsc(act.id) + '\')" style="margin-left:2px">';
132
+ html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>';
133
+ html += '</button>';
134
+ html += '<button class="btn btn-icon btn-sm" title="删除" onclick="event.stopPropagation();rfDeleteActivity(\'' + rfEsc(act.id) + '\')" style="margin-left:2px">';
135
+ html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
136
+ html += '</button>';
137
+ html += '</div>';
138
+ });
139
+ el.innerHTML = html;
140
+ }
141
+
142
+ function rfSelectActivity(id) {
143
+ _rfState.currentId = id;
144
+ rfRenderActivityList();
145
+ rfLoadActivityDetail(id);
146
+ rfLoadParticipants(id);
147
+ rfLoadBlacklist(id);
148
+ rfLoadWhitelist(id);
149
+ }
150
+
151
+ function rfLoadActivityDetail(id) {
152
+ fetch('/Raffle/api/activities/' + encodeURIComponent(id), { headers: _rfHeaders() })
153
+ .then(function(r) { return r.json(); })
154
+ .then(function(d) {
155
+ var act = d.activity;
156
+ if (!act) return;
157
+ _rfState.currentActivity = act;
158
+ rfUpdateStats(act);
159
+
160
+ if (act.status === 'drawn') {
161
+ document.getElementById('rf-draw-btn').style.display = 'none';
162
+ document.getElementById('rf-draw-result-btn').style.display = 'inline-flex';
163
+ var revertBtn = document.getElementById('rf-draw-revert-btn');
164
+ if (revertBtn) revertBtn.style.display = 'inline-flex';
165
+ } else if (act.status === 'open') {
166
+ document.getElementById('rf-draw-btn').style.display = 'inline-flex';
167
+ document.getElementById('rf-draw-btn').disabled = false;
168
+ document.getElementById('rf-draw-result-btn').style.display = 'none';
169
+ var revertBtn = document.getElementById('rf-draw-revert-btn');
170
+ if (revertBtn) revertBtn.style.display = 'none';
171
+ } else {
172
+ document.getElementById('rf-draw-btn').style.display = 'none';
173
+ document.getElementById('rf-draw-result-btn').style.display = 'none';
174
+ var revertBtn = document.getElementById('rf-draw-revert-btn');
175
+ if (revertBtn) revertBtn.style.display = 'none';
176
+ }
177
+ })
178
+ .catch(function() {});
179
+ }
180
+
181
+ function rfUpdateStats(act) {
182
+ var elName = document.getElementById('rf-stat-current');
183
+ var elConfirmed = document.getElementById('rf-stat-confirmed');
184
+ var elDraw = document.getElementById('rf-stat-draw');
185
+ var elStatus = document.getElementById('rf-stat-status');
186
+
187
+ if (!act) {
188
+ if (elName) elName.textContent = '未选择';
189
+ if (elConfirmed) elConfirmed.textContent = '0';
190
+ if (elDraw) elDraw.textContent = '0';
191
+ if (elStatus) elStatus.textContent = '--';
192
+ return;
193
+ }
194
+ if (elName) elName.textContent = act.name || act.id;
195
+ if (elConfirmed) elConfirmed.textContent = act.participant_count || 0;
196
+ if (elDraw) elDraw.textContent = act.draw_count || 0;
197
+ var statusMap = { open: '报名中', closed: '已关闭', drawn: '已开奖' };
198
+ if (elStatus) elStatus.textContent = statusMap[act.status] || act.status;
199
+ }
200
+
201
+ function rfLoadParticipants(activityId) {
202
+ fetch('/Raffle/api/activities/' + encodeURIComponent(activityId) + '/participants', { headers: _rfHeaders() })
203
+ .then(function(r) { return r.json(); })
204
+ .then(function(d) {
205
+ _rfState.confirmed = d.confirmed || [];
206
+ _rfState.pending = d.pending || [];
207
+ _rfState.allParticipants = _rfState.confirmed.concat(_rfState.pending);
208
+ rfRenderParticipants();
209
+ })
210
+ .catch(function() {});
211
+ }
212
+
213
+ function rfSwitchGroup(group) {
214
+ _rfState.activeGroup = group;
215
+ var tabs = document.querySelectorAll('#rf-group-tabs .rf-tab');
216
+ tabs.forEach(function(t) {
217
+ if (t.getAttribute('data-group') === group) {
218
+ t.classList.add('rf-tab-active');
219
+ } else {
220
+ t.classList.remove('rf-tab-active');
221
+ }
222
+ });
223
+ rfRenderParticipants();
224
+ }
225
+
226
+ function rfGetDisplayList() {
227
+ var list;
228
+ if (_rfState.activeGroup === 'confirmed') {
229
+ list = _rfState.confirmed;
230
+ } else if (_rfState.activeGroup === 'pending') {
231
+ list = _rfState.pending;
232
+ } else {
233
+ list = _rfState.allParticipants;
234
+ }
235
+ return list;
236
+ }
237
+
238
+ function rfRenderParticipants(filter) {
239
+ var empty = document.getElementById('rf-participants-empty');
240
+ var table = document.getElementById('rf-participants-table');
241
+ var body = document.getElementById('rf-participants-body');
242
+ var countEl = document.getElementById('rf-participant-count');
243
+ if (!body) return;
244
+
245
+ var list = rfGetDisplayList();
246
+ if (filter) {
247
+ var fl = filter.toLowerCase();
248
+ list = list.filter(function(p) {
249
+ return (p.user_name || '').toLowerCase().indexOf(fl) >= 0 ||
250
+ (p.user_id || '').toLowerCase().indexOf(fl) >= 0;
251
+ });
252
+ }
253
+
254
+ var total = _rfState.confirmed.length + _rfState.pending.length;
255
+ if (countEl) countEl.textContent = '(' + _rfState.confirmed.length + ' 已确认 / ' + _rfState.pending.length + ' 待录入 / 共 ' + total + ' 人)';
256
+
257
+ if (list.length === 0) {
258
+ if (empty) empty.style.display = 'block';
259
+ if (table) table.style.display = 'none';
260
+ return;
261
+ }
262
+ if (empty) empty.style.display = 'none';
263
+ if (table) table.style.display = 'table';
264
+
265
+ var html = '';
266
+ list.forEach(function(p, i) {
267
+ var date = p.joined_at ? new Date(p.joined_at * 1000).toLocaleString() : '--';
268
+ var groupLabel = p.group === 'confirmed' ? '<span class="rf-group-tag rf-group-confirmed">已确认</span>' : '<span class="rf-group-tag rf-group-pending">待录入</span>';
269
+ html += '<tr>';
270
+ html += '<td>' + (i + 1) + '</td>';
271
+ html += '<td>' + rfEsc(p.user_name || '--') + '</td>';
272
+ html += '<td style="font-size:12px;font-family:monospace">' + rfEsc(p.user_id) + '</td>';
273
+ html += '<td><span class="chip chip-pr">' + rfEsc(p.platform || '--') + '</span></td>';
274
+ html += '<td>' + groupLabel + '</td>';
275
+ html += '<td style="font-size:12px;color:var(--tx-s)">' + date + '</td>';
276
+ html += '<td><div style="display:flex;gap:4px">';
277
+ if (p.group === 'pending') {
278
+ html += '<button class="btn btn-primary btn-xs" onclick="rfConfirmParticipant(\'' + rfEsc(p.user_id) + '\')">确认</button>';
279
+ } else {
280
+ html += '<button class="btn btn-secondary btn-xs" onclick="rfRevokeParticipant(\'' + rfEsc(p.user_id) + '\')">撤回</button>';
281
+ }
282
+ html += '<button class="btn btn-danger btn-xs" onclick="rfRemoveParticipant(\'' + rfEsc(p.user_id) + '\')">移除</button>';
283
+ html += '</div></td>';
284
+ html += '</tr>';
285
+ });
286
+ body.innerHTML = html;
287
+ }
288
+
289
+ function rfFilterParticipants() {
290
+ var input = document.getElementById('rf-search');
291
+ rfRenderParticipants(input ? input.value : '');
292
+ }
293
+
294
+ function rfConfirmParticipant(userId) {
295
+ if (!_rfState.currentId) return;
296
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/participants/' + encodeURIComponent(userId), {
297
+ method: 'PUT', headers: _rfHeaders(),
298
+ body: JSON.stringify({ action: 'confirm' })
299
+ })
300
+ .then(function(r) { return r.json(); })
301
+ .then(function() { rfLoadParticipants(_rfState.currentId); rfLoadActivityDetail(_rfState.currentId); })
302
+ .catch(function() {});
303
+ }
304
+
305
+ function rfRevokeParticipant(userId) {
306
+ if (!_rfState.currentId) return;
307
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/participants/' + encodeURIComponent(userId), {
308
+ method: 'PUT', headers: _rfHeaders(),
309
+ body: JSON.stringify({ action: 'revoke' })
310
+ })
311
+ .then(function(r) { return r.json(); })
312
+ .then(function() { rfLoadParticipants(_rfState.currentId); rfLoadActivityDetail(_rfState.currentId); })
313
+ .catch(function() {});
314
+ }
315
+
316
+ function rfRemoveParticipant(userId) {
317
+ if (!_rfState.currentId) return;
318
+ if (!confirm('确定移除该参与者?')) return;
319
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/participants/' + encodeURIComponent(userId), {
320
+ method: 'DELETE', headers: _rfHeaders()
321
+ })
322
+ .then(function(r) { return r.json(); })
323
+ .then(function() { rfLoadParticipants(_rfState.currentId); rfLoadActivityDetail(_rfState.currentId); })
324
+ .catch(function() {});
325
+ }
326
+
327
+ function rfShowCreate() {
328
+ _rfState.editMode = false;
329
+ _rfState.editId = null;
330
+ _rfState.tempGroups = [];
331
+ document.getElementById('rf-create-title').textContent = '创建新活动';
332
+ document.getElementById('rf-f-id').value = '';
333
+ document.getElementById('rf-f-id').disabled = false;
334
+ document.getElementById('rf-f-name').value = '';
335
+ document.getElementById('rf-f-desc').value = '';
336
+ document.getElementById('rf-f-draw').value = '5';
337
+ document.getElementById('rf-f-keywords').value = '我想要礼物,想要礼物,参与抽奖,抽奖,周边';
338
+ document.getElementById('rf-f-autoconfirm').checked = false;
339
+ document.getElementById('rf-f-autoconfirm-label').textContent = '关闭';
340
+ document.getElementById('rf-f-whitelist').checked = false;
341
+ document.getElementById('rf-f-whitelist-label').textContent = '关闭';
342
+ rfRenderGroupsList();
343
+ document.getElementById('rf-create-panel').style.display = 'block';
344
+ }
345
+
346
+ function rfHideCreate() {
347
+ document.getElementById('rf-create-panel').style.display = 'none';
348
+ }
349
+
350
+ function rfEditActivity(id) {
351
+ var act = _rfState.activities.find(function(a) { return a.id === id; });
352
+ if (!act) return;
353
+ _rfState.editMode = true;
354
+ _rfState.editId = id;
355
+ _rfState.tempGroups = (act.allowed_groups || []).slice();
356
+ document.getElementById('rf-create-title').textContent = '编辑活动: ' + act.name;
357
+ document.getElementById('rf-f-id').value = act.id;
358
+ document.getElementById('rf-f-id').disabled = true;
359
+ document.getElementById('rf-f-name').value = act.name || '';
360
+ document.getElementById('rf-f-desc').value = act.description || '';
361
+ document.getElementById('rf-f-draw').value = act.draw_count || 1;
362
+ document.getElementById('rf-f-keywords').value = (act.keywords || []).join(',');
363
+ document.getElementById('rf-f-autoconfirm').checked = !!act.auto_confirm;
364
+ document.getElementById('rf-f-autoconfirm-label').textContent = act.auto_confirm ? '开启' : '关闭';
365
+ document.getElementById('rf-f-whitelist').checked = !!act.whitelist_mode;
366
+ document.getElementById('rf-f-whitelist-label').textContent = act.whitelist_mode ? '开启' : '关闭';
367
+ rfRenderGroupsList();
368
+ document.getElementById('rf-create-panel').style.display = 'block';
369
+ }
370
+
371
+ function rfAddGroup() {
372
+ var platform = document.getElementById('rf-f-platform').value;
373
+ var groupId = document.getElementById('rf-f-groupid').value.trim();
374
+ if (!platform || !groupId) return;
375
+ var exists = _rfState.tempGroups.some(function(g) { return g.platform === platform && g.group_id === groupId; });
376
+ if (exists) return;
377
+ _rfState.tempGroups.push({ platform: platform, group_id: groupId });
378
+ document.getElementById('rf-f-groupid').value = '';
379
+ rfRenderGroupsList();
380
+ }
381
+
382
+ function rfRemoveGroup(idx) {
383
+ _rfState.tempGroups.splice(idx, 1);
384
+ rfRenderGroupsList();
385
+ }
386
+
387
+ function rfRenderGroupsList() {
388
+ var el = document.getElementById('rf-groups-list');
389
+ if (!el) return;
390
+ if (_rfState.tempGroups.length === 0) {
391
+ el.innerHTML = '<div style="font-size:12px;color:var(--tx-t)">暂未添加群聊</div>';
392
+ return;
393
+ }
394
+ var html = '';
395
+ _rfState.tempGroups.forEach(function(g, i) {
396
+ html += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0">';
397
+ html += '<span class="chip chip-pr">' + rfEsc(g.platform) + '</span>';
398
+ html += '<span style="font-size:13px;font-family:monospace">' + rfEsc(g.group_id) + '</span>';
399
+ html += '<button class="btn btn-icon btn-xs" onclick="rfRemoveGroup(' + i + ')">';
400
+ html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
401
+ html += '</button></div>';
402
+ });
403
+ el.innerHTML = html;
404
+ }
405
+
406
+ function rfSaveActivity() {
407
+ var id = document.getElementById('rf-f-id').value.trim();
408
+ var name = document.getElementById('rf-f-name').value.trim();
409
+ var desc = document.getElementById('rf-f-desc').value.trim();
410
+ var drawCount = parseInt(document.getElementById('rf-f-draw').value) || 1;
411
+ var keywordsStr = document.getElementById('rf-f-keywords').value.trim();
412
+ var keywords = keywordsStr ? keywordsStr.split(',').map(function(k) { return k.trim(); }).filter(Boolean) : [];
413
+ var autoConfirm = document.getElementById('rf-f-autoconfirm').checked;
414
+ var whitelistMode = document.getElementById('rf-f-whitelist').checked;
415
+
416
+ if (!name) { alert('请输入活动名称'); return; }
417
+
418
+ var url, method, body;
419
+ if (_rfState.editMode) {
420
+ url = '/Raffle/api/activities/' + encodeURIComponent(_rfState.editId);
421
+ method = 'PUT';
422
+ body = JSON.stringify({
423
+ name: name, description: desc, draw_count: drawCount,
424
+ keywords: keywords, allowed_groups: _rfState.tempGroups,
425
+ auto_confirm: autoConfirm, whitelist_mode: whitelistMode,
426
+ });
427
+ } else {
428
+ url = '/Raffle/api/activities';
429
+ method = 'POST';
430
+ body = JSON.stringify({
431
+ id: id || undefined, name: name, description: desc,
432
+ draw_count: drawCount, keywords: keywords,
433
+ allowed_groups: _rfState.tempGroups,
434
+ auto_confirm: autoConfirm, whitelist_mode: whitelistMode,
435
+ });
436
+ }
437
+
438
+ fetch(url, { method: method, headers: _rfHeaders(), body: body })
439
+ .then(function(r) { return r.json(); })
440
+ .then(function(d) {
441
+ if (d.error) { alert('保存失败: ' + d.error); return; }
442
+ rfHideCreate();
443
+ rfLoadActivities();
444
+ if (d.activity && d.activity.id) {
445
+ rfSelectActivity(d.activity.id);
446
+ }
447
+ })
448
+ .catch(function(e) { alert('保存失败: ' + e); });
449
+ }
450
+
451
+ function rfDeleteActivity(id) {
452
+ if (!confirm('确定删除此活动?所有参与者数据将被清除!')) return;
453
+ fetch('/Raffle/api/activities/' + encodeURIComponent(id), { method: 'DELETE', headers: _rfHeaders() })
454
+ .then(function(r) { return r.json(); })
455
+ .then(function() {
456
+ if (_rfState.currentId === id) {
457
+ _rfState.currentId = null;
458
+ _rfState.currentActivity = null;
459
+ }
460
+ rfLoadActivities();
461
+ })
462
+ .catch(function(e) { alert('删除失败: ' + e); });
463
+ }
464
+
465
+ function rfStartDraw() {
466
+ if (!_rfState.currentId) { alert('请先选择活动'); return; }
467
+ var act = _rfState.activities.find(function(a) { return a.id === _rfState.currentId; });
468
+ if (!act) return;
469
+ if (!confirm('确定对「' + act.name + '」开奖?开奖后报名将关闭,仅从「已确认」参与者中抽取。')) return;
470
+
471
+ document.getElementById('rf-draw-idle').style.display = 'none';
472
+ document.getElementById('rf-draw-animating').style.display = 'block';
473
+ document.getElementById('rf-draw-done').style.display = 'none';
474
+
475
+ var names = _rfState.confirmed.map(function(p) { return p.user_name || p.user_id; });
476
+ if (names.length === 0) {
477
+ document.getElementById('rf-draw-animating').style.display = 'none';
478
+ document.getElementById('rf-draw-idle').style.display = 'block';
479
+ alert('没有已确认的参与者');
480
+ return;
481
+ }
482
+
483
+ var slotEl = document.getElementById('rf-draw-slot');
484
+ var drawCount = Math.min(act.draw_count || 1, names.length);
485
+ var animDuration = 3500;
486
+ var startTime = Date.now();
487
+ var interval = 80;
488
+
489
+ var animTimer = setInterval(function() {
490
+ var elapsed = Date.now() - startTime;
491
+ var progress = Math.min(elapsed / animDuration, 1);
492
+ var delay = interval + (progress * progress * 600);
493
+
494
+ var displayNames = [];
495
+ for (var i = 0; i < drawCount; i++) {
496
+ displayNames.push(names[Math.floor(Math.random() * names.length)]);
497
+ }
498
+ slotEl.textContent = displayNames.join(' / ');
499
+
500
+ if (progress >= 1) {
501
+ clearInterval(animTimer);
502
+ rfDoDraw();
503
+ } else {
504
+ setTimeout(function() {}, delay - interval);
505
+ }
506
+ }, interval);
507
+ }
508
+
509
+ function rfDoDraw() {
510
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/draw', {
511
+ method: 'POST', headers: _rfHeaders()
512
+ })
513
+ .then(function(r) { return r.json(); })
514
+ .then(function(d) {
515
+ if (d.error) {
516
+ document.getElementById('rf-draw-animating').style.display = 'none';
517
+ document.getElementById('rf-draw-idle').style.display = 'block';
518
+ alert('开奖失败: ' + d.error);
519
+ return;
520
+ }
521
+ rfShowDrawResult(d.winners, d.total_participants);
522
+ rfLoadActivities();
523
+ })
524
+ .catch(function(e) {
525
+ document.getElementById('rf-draw-animating').style.display = 'none';
526
+ document.getElementById('rf-draw-idle').style.display = 'block';
527
+ alert('开奖请求失败: ' + e);
528
+ });
529
+ }
530
+
531
+ function rfShowDrawResult(winners, total) {
532
+ document.getElementById('rf-draw-animating').style.display = 'none';
533
+ document.getElementById('rf-draw-done').style.display = 'block';
534
+
535
+ document.getElementById('rf-draw-summary').textContent =
536
+ '共 ' + total + ' 人参与,抽取 ' + winners.length + ' 位获奖者';
537
+
538
+ var html = '<div class="rf-winners-grid">';
539
+ winners.forEach(function(w, i) {
540
+ html += '<div class="rf-winner-card">';
541
+ html += '<div class="rf-winner-rank">' + (i + 1) + '</div>';
542
+ html += '<div class="rf-winner-name">' + rfEsc(w.user_name || '--') + '</div>';
543
+ html += '<div class="rf-winner-id">' + rfEsc(w.user_id) + '</div>';
544
+ html += '<div class="rf-winner-platform"><span class="chip chip-pr">' + rfEsc(w.platform || '--') + '</span></div>';
545
+ html += '</div>';
546
+ });
547
+ html += '</div>';
548
+ document.getElementById('rf-winners-list').innerHTML = html;
549
+ }
550
+
551
+ function rfShowResult() {
552
+ if (!_rfState.currentId) return;
553
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/result', { headers: _rfHeaders() })
554
+ .then(function(r) { return r.json(); })
555
+ .then(function(d) {
556
+ var result = d.draw_result;
557
+ if (!result || !result.winners) { alert('暂无开奖结果'); return; }
558
+ document.getElementById('rf-draw-idle').style.display = 'none';
559
+ document.getElementById('rf-draw-animating').style.display = 'none';
560
+ rfShowDrawResult(result.winners, result.total_participants);
561
+ })
562
+ .catch(function(e) { alert('加载失败: ' + e); });
563
+ }
564
+
565
+ function rfRevertDraw() {
566
+ if (!_rfState.currentId) return;
567
+ var act = _rfState.activities.find(function(a) { return a.id === _rfState.currentId; });
568
+ if (!act) return;
569
+ if (!confirm('确定撤回「' + act.name + '」的开奖结果?\n开奖结果将被清除,活动将恢复为报名中状态。')) return;
570
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/draw/revert', {
571
+ method: 'POST', headers: _rfHeaders()
572
+ })
573
+ .then(function(r) { return r.json(); })
574
+ .then(function(d) {
575
+ if (d.error) { alert('撤回失败: ' + d.error); return; }
576
+ document.getElementById('rf-draw-done').style.display = 'none';
577
+ document.getElementById('rf-draw-idle').style.display = 'block';
578
+ rfLoadActivities();
579
+ rfSelectActivity(_rfState.currentId);
580
+ })
581
+ .catch(function(e) { alert('撤回请求失败: ' + e); });
582
+ }
583
+
584
+ function rfLoadBlacklist(activityId) {
585
+ fetch('/Raffle/api/activities/' + encodeURIComponent(activityId) + '/blacklist', { headers: _rfHeaders() })
586
+ .then(function(r) { return r.json(); })
587
+ .then(function(d) {
588
+ _rfState.blacklist = d.blacklist || [];
589
+ rfRenderBlacklist();
590
+ })
591
+ .catch(function() {});
592
+ }
593
+
594
+ function rfLoadWhitelist(activityId) {
595
+ fetch('/Raffle/api/activities/' + encodeURIComponent(activityId) + '/whitelist', { headers: _rfHeaders() })
596
+ .then(function(r) { return r.json(); })
597
+ .then(function(d) {
598
+ _rfState.whitelist = d.whitelist || [];
599
+ rfRenderWhitelist();
600
+ })
601
+ .catch(function() {});
602
+ }
603
+
604
+ function rfAddBlacklist() {
605
+ if (!_rfState.currentId) { alert('请先选择活动'); return; }
606
+ var userId = document.getElementById('rf-bl-input').value.trim();
607
+ if (!userId) return;
608
+ var remark = document.getElementById('rf-bl-name').value.trim();
609
+ _rfState.blacklist.push({ user_id: userId, remark: remark });
610
+ document.getElementById('rf-bl-input').value = '';
611
+ document.getElementById('rf-bl-name').value = '';
612
+ rfSaveBlacklist();
613
+ }
614
+
615
+ function rfRemoveBlacklist(idx) {
616
+ _rfState.blacklist.splice(idx, 1);
617
+ rfSaveBlacklist();
618
+ }
619
+
620
+ function rfSaveBlacklist() {
621
+ if (!_rfState.currentId) return;
622
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/blacklist', {
623
+ method: 'PUT', headers: _rfHeaders(),
624
+ body: JSON.stringify({ blacklist: _rfState.blacklist })
625
+ })
626
+ .then(function() { rfRenderBlacklist(); })
627
+ .catch(function() {});
628
+ }
629
+
630
+ function rfRenderBlacklist() {
631
+ var el = document.getElementById('rf-blacklist-tags');
632
+ if (!el) return;
633
+ if (_rfState.blacklist.length === 0) {
634
+ el.innerHTML = '<span style="font-size:12px;color:var(--tx-t)">暂无黑名单</span>';
635
+ return;
636
+ }
637
+ var html = '';
638
+ _rfState.blacklist.forEach(function(b, i) {
639
+ html += '<span class="rf-user-tag rf-user-tag-bl">';
640
+ html += rfEsc(b.remark || b.user_id);
641
+ if (b.remark) html += ' <span style="opacity:0.6;font-size:11px">' + rfEsc(b.user_id) + '</span>';
642
+ html += ' <span class="rf-user-tag-x" onclick="rfRemoveBlacklist(' + i + ')">×</span>';
643
+ html += '</span>';
644
+ });
645
+ el.innerHTML = html;
646
+ }
647
+
648
+ function rfAddWhitelist() {
649
+ if (!_rfState.currentId) { alert('请先选择活动'); return; }
650
+ var userId = document.getElementById('rf-wl-input').value.trim();
651
+ if (!userId) return;
652
+ var remark = document.getElementById('rf-wl-name').value.trim();
653
+ _rfState.whitelist.push({ user_id: userId, remark: remark });
654
+ document.getElementById('rf-wl-input').value = '';
655
+ document.getElementById('rf-wl-name').value = '';
656
+ rfSaveWhitelist();
657
+ }
658
+
659
+ function rfRemoveWhitelist(idx) {
660
+ _rfState.whitelist.splice(idx, 1);
661
+ rfSaveWhitelist();
662
+ }
663
+
664
+ function rfSaveWhitelist() {
665
+ if (!_rfState.currentId) return;
666
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.currentId) + '/whitelist', {
667
+ method: 'PUT', headers: _rfHeaders(),
668
+ body: JSON.stringify({ whitelist: _rfState.whitelist })
669
+ })
670
+ .then(function() { rfRenderWhitelist(); })
671
+ .catch(function() {});
672
+ }
673
+
674
+ function rfRenderWhitelist() {
675
+ var el = document.getElementById('rf-whitelist-tags');
676
+ if (!el) return;
677
+ if (_rfState.whitelist.length === 0) {
678
+ el.innerHTML = '<span style="font-size:12px;color:var(--tx-t)">暂无白名单</span>';
679
+ return;
680
+ }
681
+ var html = '';
682
+ _rfState.whitelist.forEach(function(w, i) {
683
+ html += '<span class="rf-user-tag rf-user-tag-wl">';
684
+ html += rfEsc(w.remark || w.user_id);
685
+ if (w.remark) html += ' <span style="opacity:0.6;font-size:11px">' + rfEsc(w.user_id) + '</span>';
686
+ html += ' <span class="rf-user-tag-x" onclick="rfRemoveWhitelist(' + i + ')">×</span>';
687
+ html += '</span>';
688
+ });
689
+ el.innerHTML = html;
690
+ }
691
+
692
+ function rfSaveTemplates() {
693
+ var templates = {
694
+ success: document.getElementById('rf-tpl-success').value,
695
+ already_joined: document.getElementById('rf-tpl-joined').value,
696
+ hint: document.getElementById('rf-tpl-hint').value,
697
+ pending: document.getElementById('rf-tpl-pending').value,
698
+ no_activity: document.getElementById('rf-tpl-noactivity').value,
699
+ blacklisted: document.getElementById('rf-tpl-blacklisted').value,
700
+ not_in_whitelist: document.getElementById('rf-tpl-notwhitelist').value,
701
+ notify: document.getElementById('rf-tpl-notify').value,
702
+ broadcast: document.getElementById('rf-tpl-broadcast').value,
703
+ };
704
+ fetch('/Raffle/api/settings', {
705
+ method: 'PUT',
706
+ headers: _rfHeaders(),
707
+ body: JSON.stringify({ settings: { reply_templates: templates } })
708
+ })
709
+ .then(function(r) { return r.json(); })
710
+ .then(function() { alert('模板已保存'); })
711
+ .catch(function(e) { alert('保存失败: ' + e); });
712
+ }
713
+
714
+ function rfEsc(str) {
715
+ if (!str) return '';
716
+ var d = document.createElement('div');
717
+ d.appendChild(document.createTextNode(str));
718
+ return d.innerHTML;
719
+ }
720
+
721
+ document.addEventListener('DOMContentLoaded', function() {
722
+ var acCb = document.getElementById('rf-f-autoconfirm');
723
+ if (acCb) acCb.addEventListener('change', function() {
724
+ document.getElementById('rf-f-autoconfirm-label').textContent = this.checked ? '开启' : '关闭';
725
+ });
726
+ var wlCb = document.getElementById('rf-f-whitelist');
727
+ if (wlCb) wlCb.addEventListener('change', function() {
728
+ document.getElementById('rf-f-whitelist-label').textContent = this.checked ? '开启' : '关闭';
729
+ });
730
+ });
731
+
732
+ function rfShowNotify(activityId) {
733
+ _rfState.notifyActivityId = activityId;
734
+ _rfState.notifyTargets = [];
735
+ var act = _rfState.activities.find(function(a) { return a.id === activityId; });
736
+ if (!act) { alert('活动不存在'); return; }
737
+ var nameEl = document.getElementById('rf-notify-act-name');
738
+ var descEl = document.getElementById('rf-notify-act-desc');
739
+ if (nameEl) nameEl.textContent = act.name || act.id;
740
+ if (descEl) descEl.textContent = act.description || '';
741
+ var customEl = document.getElementById('rf-nf-custom');
742
+ if (customEl) customEl.value = '';
743
+ rfRenderNotifyTargets();
744
+ rfPreviewNotify();
745
+ rfLoadNotifyHistory(activityId);
746
+ rfNotifyTab('send');
747
+ document.getElementById('rf-notify-modal').style.display = 'flex';
748
+ }
749
+
750
+ function rfHideNotify() {
751
+ document.getElementById('rf-notify-modal').style.display = 'none';
752
+ _rfState.notifyActivityId = null;
753
+ _rfState.notifyTargets = [];
754
+ }
755
+
756
+ function rfNotifyTab(tab) {
757
+ var tabs = document.querySelectorAll('.rf-modal-tabs .rf-tab');
758
+ tabs.forEach(function(t) {
759
+ if (t.getAttribute('data-ntab') === tab) {
760
+ t.classList.add('rf-tab-active');
761
+ } else {
762
+ t.classList.remove('rf-tab-active');
763
+ }
764
+ });
765
+ document.getElementById('rf-notify-send').style.display = tab === 'send' ? 'block' : 'none';
766
+ document.getElementById('rf-notify-history').style.display = tab === 'history' ? 'block' : 'none';
767
+ }
768
+
769
+ function rfAddNotifyTarget() {
770
+ var platform = document.getElementById('rf-nf-platform').value;
771
+ var sessionType = document.getElementById('rf-nf-type').value;
772
+ var targetId = document.getElementById('rf-nf-targetid').value.trim();
773
+ var accountId = document.getElementById('rf-nf-accountid').value.trim();
774
+ if (!platform || !targetId) { alert('请选择平台并输入目标 ID'); return; }
775
+ var exists = _rfState.notifyTargets.some(function(t) {
776
+ return t.platform === platform && t.session_type === sessionType && t.target_id === targetId;
777
+ });
778
+ if (exists) { alert('该目标已添加'); return; }
779
+ _rfState.notifyTargets.push({
780
+ platform: platform,
781
+ session_type: sessionType,
782
+ target_id: targetId,
783
+ account_id: accountId
784
+ });
785
+ document.getElementById('rf-nf-targetid').value = '';
786
+ document.getElementById('rf-nf-accountid').value = '';
787
+ rfRenderNotifyTargets();
788
+ }
789
+
790
+ function rfRemoveNotifyTarget(idx) {
791
+ _rfState.notifyTargets.splice(idx, 1);
792
+ rfRenderNotifyTargets();
793
+ }
794
+
795
+ function rfRenderNotifyTargets() {
796
+ var el = document.getElementById('rf-notify-targets');
797
+ if (!el) return;
798
+ if (_rfState.notifyTargets.length === 0) {
799
+ el.innerHTML = '<div style="font-size:12px;color:var(--tx-t)">暂未添加发送目标</div>';
800
+ return;
801
+ }
802
+ var html = '';
803
+ _rfState.notifyTargets.forEach(function(t, i) {
804
+ var typeLabels = { user: '私聊', group: '群聊', channel: '频道', guild: '服务器', thread: '话题' };
805
+ html += '<div class="rf-target-item">';
806
+ html += '<span class="chip chip-pr">' + rfEsc(t.platform) + '</span>';
807
+ html += '<span style="color:var(--tx-t)">' + (typeLabels[t.session_type] || t.session_type) + '</span>';
808
+ html += '<span style="font-family:monospace;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + rfEsc(t.target_id) + '</span>';
809
+ if (t.account_id) html += '<span style="font-size:11px;color:var(--tx-t)">@' + rfEsc(t.account_id) + '</span>';
810
+ html += '<button class="btn btn-icon btn-xs" onclick="rfRemoveNotifyTarget(' + i + ')">';
811
+ html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
812
+ html += '</button></div>';
813
+ });
814
+ el.innerHTML = html;
815
+ }
816
+
817
+ function rfPreviewNotify() {
818
+ var act = _rfState.activities.find(function(a) { return a.id === _rfState.notifyActivityId; });
819
+ var el = document.getElementById('rf-notify-preview');
820
+ if (!el || !act) return;
821
+ var tplEl = document.getElementById('rf-tpl-notify');
822
+ var tpl = tplEl ? tplEl.value : '';
823
+ if (!tpl) { el.textContent = '请先在回复模板中配置活动通知模板'; return; }
824
+ var customEl = document.getElementById('rf-nf-custom');
825
+ var custom = customEl ? customEl.value : '';
826
+ var msg = tpl
827
+ .replace(/\{activity_name\}/g, act.name || '抽奖活动')
828
+ .replace(/\{description\}/g, act.description || '')
829
+ .replace(/\{draw_count\}/g, act.draw_count || 1)
830
+ .replace(/\{keywords\}/g, (act.keywords || []).join('、'));
831
+ if (custom) msg += '\n\n' + custom;
832
+ el.textContent = msg;
833
+ }
834
+
835
+ function rfSendNotify() {
836
+ if (!_rfState.notifyActivityId) return;
837
+ if (_rfState.notifyTargets.length === 0) { alert('请至少添加一个发送目标'); return; }
838
+ var btn = document.getElementById('rf-notify-send-btn');
839
+ if (btn) { btn.disabled = true; btn.textContent = '发送中...'; }
840
+ var customEl = document.getElementById('rf-nf-custom');
841
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.notifyActivityId) + '/notify', {
842
+ method: 'POST',
843
+ headers: _rfHeaders(),
844
+ body: JSON.stringify({
845
+ targets: _rfState.notifyTargets,
846
+ custom_content: customEl ? customEl.value : ''
847
+ })
848
+ })
849
+ .then(function(r) { return r.json(); })
850
+ .then(function(d) {
851
+ if (btn) { btn.disabled = false; btn.textContent = '发送'; }
852
+ if (d.error) { alert('发送失败: ' + d.error); return; }
853
+ var info = '发送完成:成功 ' + d.success_count + '/' + d.total_count;
854
+ if (d.success_count < d.total_count) {
855
+ var fails = d.record.results.filter(function(r) { return !r.success; });
856
+ info += '\n失败: ' + fails.map(function(f) { return f.platform + '/' + f.target_id + ' - ' + f.error; }).join(', ');
857
+ }
858
+ alert(info);
859
+ rfLoadNotifyHistory(_rfState.notifyActivityId);
860
+ })
861
+ .catch(function(e) {
862
+ if (btn) { btn.disabled = false; btn.textContent = '发送'; }
863
+ alert('发送请求失败: ' + e);
864
+ });
865
+ }
866
+
867
+ function rfLoadNotifyHistory(activityId) {
868
+ fetch('/Raffle/api/activities/' + encodeURIComponent(activityId) + '/notify/history', { headers: _rfHeaders() })
869
+ .then(function(r) { return r.json(); })
870
+ .then(function(d) {
871
+ _rfState.notifyHistory = d.history || [];
872
+ rfRenderNotifyHistory();
873
+ })
874
+ .catch(function() { _rfState.notifyHistory = []; rfRenderNotifyHistory(); });
875
+ }
876
+
877
+ function rfRenderNotifyHistory() {
878
+ var el = document.getElementById('rf-history-list');
879
+ if (!el) return;
880
+ if (_rfState.notifyHistory.length === 0) {
881
+ el.innerHTML = '<div style="text-align:center;padding:24px;color:var(--tx-t)">暂无发送记录</div>';
882
+ return;
883
+ }
884
+ var html = '';
885
+ _rfState.notifyHistory.forEach(function(h) {
886
+ var date = h.sent_at ? new Date(h.sent_at * 1000).toLocaleString() : '--';
887
+ var successCount = (h.results || []).filter(function(r) { return r.success; }).length;
888
+ var totalCount = (h.results || []).length;
889
+ var targetText = (h.targets || []).map(function(t) { return t.platform + '/' + t.target_id; }).join(', ');
890
+ html += '<div class="rf-history-item">';
891
+ html += '<div class="rf-history-header">';
892
+ html += '<span style="font-size:13px;font-weight:600">' + date + '</span>';
893
+ html += '<div style="display:flex;gap:6px;align-items:center">';
894
+ html += '<span class="chip ' + (successCount === totalCount ? 'chip-ok' : 'chip-wr') + '">' + successCount + '/' + totalCount + '</span>';
895
+ html += '<button class="btn btn-secondary btn-xs" onclick="rfResendNotify(\'' + rfEsc(h.id) + '\')">重发</button>';
896
+ html += '</div></div>';
897
+ html += '<div class="rf-history-targets">目标: ' + rfEsc(targetText) + '</div>';
898
+ if (h.custom_content) {
899
+ html += '<div style="font-size:11px;color:var(--tx-t);margin-bottom:4px">备注: ' + rfEsc(h.custom_content) + '</div>';
900
+ }
901
+ html += '<div class="rf-history-msg">' + rfEsc(h.message || '') + '</div>';
902
+ html += '</div>';
903
+ });
904
+ el.innerHTML = html;
905
+ }
906
+
907
+ function rfResendNotify(historyId) {
908
+ if (!_rfState.notifyActivityId) return;
909
+ if (!confirm('确定重发此通知?')) return;
910
+ fetch('/Raffle/api/activities/' + encodeURIComponent(_rfState.notifyActivityId) + '/notify/resend/' + encodeURIComponent(historyId), {
911
+ method: 'POST',
912
+ headers: _rfHeaders()
913
+ })
914
+ .then(function(r) { return r.json(); })
915
+ .then(function(d) {
916
+ if (d.error) { alert('重发失败: ' + d.error); return; }
917
+ alert('重发完成:成功 ' + d.success_count + '/' + d.total_count);
918
+ rfLoadNotifyHistory(_rfState.notifyActivityId);
919
+ })
920
+ .catch(function(e) { alert('重发请求失败: ' + e); });
921
+ }