cacode commited on
Commit
7db57c0
·
verified ·
1 Parent(s): 26d04d9

Upload 68 files

Browse files
app/__pycache__/config.cpython-313.pyc CHANGED
Binary files a/app/__pycache__/config.cpython-313.pyc and b/app/__pycache__/config.cpython-313.pyc differ
 
app/services/__pycache__/bootstrap.cpython-313.pyc CHANGED
Binary files a/app/services/__pycache__/bootstrap.cpython-313.pyc and b/app/services/__pycache__/bootstrap.cpython-313.pyc differ
 
app/services/bootstrap.py CHANGED
@@ -73,6 +73,35 @@ def ensure_submission_constraints() -> None:
73
  pass
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  def migrate_task_media_to_files() -> None:
77
  column_details = task_column_details()
78
  if not column_details:
@@ -193,6 +222,7 @@ def upgrade_schema() -> None:
193
  )
194
 
195
  ensure_submission_constraints()
 
196
  migrate_task_media_to_files()
197
 
198
 
@@ -233,3 +263,4 @@ def seed_super_admin(db: Session) -> None:
233
 
234
 
235
 
 
 
73
  pass
74
 
75
 
76
+ def relax_legacy_task_media_columns() -> None:
77
+ if engine.dialect.name != "mysql":
78
+ return
79
+
80
+ column_details = task_column_details()
81
+ if not column_details:
82
+ return
83
+
84
+ legacy_columns = ["image_data", "clue_image_data"]
85
+ statements: list[str] = []
86
+ for column_name in legacy_columns:
87
+ detail = column_details.get(column_name)
88
+ if not detail or detail.get("nullable", True):
89
+ continue
90
+ column_type = detail.get("type")
91
+ if column_type is None:
92
+ continue
93
+ compiled_type = column_type.compile(dialect=engine.dialect)
94
+ statements.append(
95
+ f"ALTER TABLE tasks MODIFY COLUMN {column_name} {compiled_type} NULL"
96
+ )
97
+
98
+ if not statements:
99
+ return
100
+
101
+ with engine.begin() as connection:
102
+ for statement in statements:
103
+ connection.execute(text(statement))
104
+
105
  def migrate_task_media_to_files() -> None:
106
  column_details = task_column_details()
107
  if not column_details:
 
222
  )
223
 
224
  ensure_submission_constraints()
225
+ relax_legacy_task_media_columns()
226
  migrate_task_media_to_files()
227
 
228
 
 
263
 
264
 
265
 
266
+
app/static/style.css CHANGED
@@ -1,4 +1,4 @@
1
- :root {
2
  --bg: #eef8ef;
3
  --bg-deep: #d7edd7;
4
  --surface: rgba(255, 255, 255, 0.75);
@@ -1350,4 +1350,371 @@ button {
1350
  .login-panel {
1351
  padding: 24px;
1352
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
  }
 
1
+ :root {
2
  --bg: #eef8ef;
3
  --bg-deep: #d7edd7;
4
  --surface: rgba(255, 255, 255, 0.75);
 
1350
  .login-panel {
1351
  padding: 24px;
1352
  }
1353
+ }
1354
+ /* Mobile polish */
1355
+ input[type="checkbox"],
1356
+ input[type="radio"] {
1357
+ width: 18px;
1358
+ height: 18px;
1359
+ padding: 0;
1360
+ margin: 0;
1361
+ border-radius: 6px;
1362
+ accent-color: var(--primary);
1363
+ box-shadow: none;
1364
+ transform: none;
1365
+ flex: 0 0 auto;
1366
+ }
1367
+
1368
+ input[type="checkbox"]:focus,
1369
+ input[type="radio"]:focus {
1370
+ outline: 2px solid rgba(78, 148, 97, 0.24);
1371
+ outline-offset: 2px;
1372
+ box-shadow: none;
1373
+ }
1374
+
1375
+ input[type="file"] {
1376
+ padding: 10px 12px;
1377
+ border-style: dashed;
1378
+ background: rgba(255, 255, 255, 0.8);
1379
+ cursor: pointer;
1380
+ }
1381
+
1382
+ input[type="file"]::file-selector-button {
1383
+ margin-right: 12px;
1384
+ padding: 9px 14px;
1385
+ border: 0;
1386
+ border-radius: 999px;
1387
+ background: rgba(78, 148, 97, 0.12);
1388
+ color: var(--primary-deep);
1389
+ cursor: pointer;
1390
+ }
1391
+
1392
+ .table-checkbox {
1393
+ justify-self: center;
1394
+ }
1395
+
1396
+ .table-inline-form {
1397
+ min-width: 220px;
1398
+ }
1399
+
1400
+ .carousel-controls-bottom {
1401
+ display: none;
1402
+ }
1403
+
1404
+ .topnav a {
1405
+ flex: 0 0 auto;
1406
+ white-space: nowrap;
1407
+ }
1408
+
1409
+ @media (max-width: 980px) {
1410
+ .topbar {
1411
+ gap: 14px;
1412
+ }
1413
+
1414
+ .topnav {
1415
+ width: 100%;
1416
+ flex-wrap: nowrap;
1417
+ overflow-x: auto;
1418
+ overflow-y: hidden;
1419
+ justify-content: flex-start;
1420
+ padding: 0 4px 4px;
1421
+ margin: 0 -4px -4px;
1422
+ -webkit-overflow-scrolling: touch;
1423
+ scrollbar-width: none;
1424
+ }
1425
+
1426
+ .topnav::-webkit-scrollbar {
1427
+ display: none;
1428
+ }
1429
+
1430
+ .published-activity-hero {
1431
+ grid-template-columns: 1fr;
1432
+ }
1433
+
1434
+ .published-activity-badges {
1435
+ justify-content: flex-start;
1436
+ }
1437
+ }
1438
+
1439
+ @media (max-width: 720px) {
1440
+ body {
1441
+ overflow-x: hidden;
1442
+ }
1443
+
1444
+ .shell {
1445
+ width: min(100% - 16px, 1200px);
1446
+ padding-top: 14px;
1447
+ }
1448
+
1449
+ .topbar {
1450
+ position: static;
1451
+ padding: 14px 16px;
1452
+ margin-bottom: 18px;
1453
+ border-radius: 24px;
1454
+ }
1455
+
1456
+ .brand-mark {
1457
+ width: 44px;
1458
+ height: 44px;
1459
+ font-size: 1.2rem;
1460
+ }
1461
+
1462
+ .brand-title {
1463
+ font-size: 1.08rem;
1464
+ }
1465
+
1466
+ .topnav {
1467
+ gap: 8px;
1468
+ }
1469
+
1470
+ .topnav a,
1471
+ .ghost-link {
1472
+ padding: 9px 12px;
1473
+ font-size: 0.92rem;
1474
+ }
1475
+
1476
+ .hero-card,
1477
+ .glass-card,
1478
+ .login-copy,
1479
+ .login-panel {
1480
+ padding: 18px;
1481
+ border-radius: 22px;
1482
+ }
1483
+
1484
+ .hero-card h2,
1485
+ .login-copy h1,
1486
+ .login-panel h2 {
1487
+ font-size: 1.62rem;
1488
+ line-height: 1.18;
1489
+ }
1490
+
1491
+ .lead {
1492
+ font-size: 0.96rem;
1493
+ line-height: 1.65;
1494
+ }
1495
+
1496
+ .card-topline .status-badge,
1497
+ .card-topline .eyebrow,
1498
+ .card-footer .mini-note,
1499
+ .section-head .mini-note,
1500
+ .task-status-row .status-badge,
1501
+ .task-status-row .mini-note,
1502
+ .hero-badges,
1503
+ .chip-row {
1504
+ align-self: flex-start;
1505
+ }
1506
+
1507
+ .bulk-toolbar,
1508
+ .inline-form,
1509
+ .table-inline-form,
1510
+ .upload-form,
1511
+ .task-carousel-head {
1512
+ grid-template-columns: 1fr;
1513
+ flex-direction: column;
1514
+ align-items: stretch;
1515
+ }
1516
+
1517
+ .carousel-controls {
1518
+ width: 100%;
1519
+ justify-content: space-between;
1520
+ gap: 8px;
1521
+ flex-wrap: nowrap;
1522
+ }
1523
+
1524
+ .carousel-controls .btn {
1525
+ flex: 1 1 0;
1526
+ min-width: 0;
1527
+ }
1528
+
1529
+ .carousel-controls .mini-note {
1530
+ flex: 0 0 auto;
1531
+ align-self: center;
1532
+ text-align: center;
1533
+ }
1534
+
1535
+ .carousel-controls-bottom {
1536
+ display: flex;
1537
+ }
1538
+
1539
+ .table-shell,
1540
+ .rank-table-wrap {
1541
+ margin-inline: -18px;
1542
+ padding: 0 18px 4px;
1543
+ }
1544
+
1545
+ .data-table,
1546
+ .rank-table {
1547
+ min-width: 560px;
1548
+ font-size: 0.92rem;
1549
+ }
1550
+
1551
+ .rank-table th,
1552
+ .rank-table td,
1553
+ .data-table th,
1554
+ .data-table td {
1555
+ padding: 12px 10px;
1556
+ white-space: nowrap;
1557
+ }
1558
+
1559
+ .activity-grid {
1560
+ grid-template-columns: 1fr;
1561
+ gap: 16px;
1562
+ }
1563
+
1564
+ .activity-card > .muted {
1565
+ display: -webkit-box;
1566
+ overflow: hidden;
1567
+ -webkit-line-clamp: 2;
1568
+ -webkit-box-orient: vertical;
1569
+ }
1570
+
1571
+ .activity-card .meta-grid {
1572
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1573
+ margin: 14px 0;
1574
+ }
1575
+
1576
+ .member-row {
1577
+ flex-direction: column;
1578
+ align-items: flex-start;
1579
+ }
1580
+
1581
+ .published-activity-card {
1582
+ padding: 20px 18px;
1583
+ border-radius: 24px;
1584
+ }
1585
+
1586
+ .published-activity-card::before {
1587
+ left: 18px;
1588
+ width: 84px;
1589
+ }
1590
+
1591
+ .published-activity-title {
1592
+ font-size: 1.45rem;
1593
+ }
1594
+
1595
+ .published-activity-metrics {
1596
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1597
+ gap: 12px;
1598
+ }
1599
+
1600
+ .activity-metric-card {
1601
+ padding: 14px;
1602
+ border-radius: 18px;
1603
+ }
1604
+
1605
+ .task-carousel-shell {
1606
+ gap: 14px;
1607
+ overflow: hidden;
1608
+ }
1609
+
1610
+ .task-flip-book {
1611
+ min-height: 0;
1612
+ height: auto;
1613
+ overflow: hidden;
1614
+ transition: height 0.28s ease;
1615
+ }
1616
+
1617
+ .task-page {
1618
+ width: 100%;
1619
+ transform: perspective(1000px) rotateY(0deg) translateX(14px) scale(0.992);
1620
+ }
1621
+
1622
+ .task-page.is-left {
1623
+ transform: perspective(1000px) rotateY(0deg) translateX(-14px) scale(0.988);
1624
+ }
1625
+
1626
+ .task-page-grid {
1627
+ grid-template-columns: 1fr;
1628
+ gap: 16px;
1629
+ }
1630
+
1631
+ .task-page-content,
1632
+ .task-page-inner {
1633
+ gap: 12px;
1634
+ }
1635
+
1636
+ .task-description {
1637
+ min-height: 0;
1638
+ }
1639
+
1640
+ .task-media,
1641
+ .submission-preview img,
1642
+ .review-image {
1643
+ aspect-ratio: auto;
1644
+ max-height: 48vh;
1645
+ }
1646
+
1647
+ .clue-toggle {
1648
+ width: 42px;
1649
+ height: 42px;
1650
+ border-radius: 16px;
1651
+ }
1652
+
1653
+ .clue-modal {
1654
+ padding: 16px;
1655
+ }
1656
+
1657
+ .clue-gradient-panel {
1658
+ padding: 12px;
1659
+ border-radius: 22px;
1660
+ }
1661
+
1662
+ .clue-gradient-panel img {
1663
+ max-height: 70dvh;
1664
+ border-radius: 18px;
1665
+ }
1666
+
1667
+ .review-card .action-grid {
1668
+ flex-direction: column;
1669
+ align-items: stretch;
1670
+ }
1671
+
1672
+ .login-body {
1673
+ min-height: 100dvh;
1674
+ }
1675
+
1676
+ .login-page {
1677
+ width: min(100% - 16px, 1200px);
1678
+ min-height: 100dvh;
1679
+ padding: 16px 0 24px;
1680
+ gap: 16px;
1681
+ align-content: start;
1682
+ }
1683
+
1684
+ .login-panel {
1685
+ order: 1;
1686
+ gap: 14px;
1687
+ }
1688
+
1689
+ .login-copy {
1690
+ order: 2;
1691
+ gap: 14px;
1692
+ min-height: auto;
1693
+ align-content: start;
1694
+ }
1695
+
1696
+ .spring-copy,
1697
+ .admin-copy,
1698
+ .admin-panel {
1699
+ min-height: auto;
1700
+ align-content: start;
1701
+ }
1702
+ }
1703
+
1704
+ @media (max-width: 560px) {
1705
+ .published-activity-metrics {
1706
+ grid-template-columns: 1fr;
1707
+ }
1708
+
1709
+ .hero-side-rank {
1710
+ padding: 16px;
1711
+ }
1712
+
1713
+ .task-page {
1714
+ transform: translateX(10px) scale(0.995);
1715
+ }
1716
+
1717
+ .task-page.is-left {
1718
+ transform: translateX(-10px) scale(0.99);
1719
+ }
1720
  }
app/templates/activity_detail.html CHANGED
@@ -74,9 +74,9 @@
74
  <span class="chip" id="rejected-progress-chip">被驳回 {{ group_progress.rejected_count }}</span>
75
  </div>
76
  <div class="carousel-controls">
77
- <button class="btn btn-secondary small-btn" type="button" id="prev-task-btn">上一张</button>
78
- <span class="mini-note" id="task-counter">1 / {{ activity.tasks|length }}</span>
79
- <button class="btn btn-secondary small-btn" type="button" id="next-task-btn">下一张</button>
80
  </div>
81
  </div>
82
 
@@ -158,6 +158,12 @@
158
  </article>
159
  {% endfor %}
160
  </div>
 
 
 
 
 
 
161
  </section>
162
 
163
  <div class="clue-modal" id="clue-modal" aria-hidden="true">
@@ -175,9 +181,10 @@
175
  const pages = Array.from(document.querySelectorAll('[data-task-page]'));
176
  if (!pages.length) return;
177
 
178
- const counter = document.getElementById('task-counter');
179
- const prevBtn = document.getElementById('prev-task-btn');
180
- const nextBtn = document.getElementById('next-task-btn');
 
181
  const clueModal = document.getElementById('clue-modal');
182
  const clueModalImage = document.getElementById('clue-modal-image');
183
  const clueCloseBtn = document.getElementById('clue-close-btn');
@@ -186,6 +193,8 @@
186
  const pendingProgressChip = document.getElementById('pending-progress-chip');
187
  const rejectedProgressChip = document.getElementById('rejected-progress-chip');
188
  let currentIndex = 0;
 
 
189
  const seenReleased = new Set(
190
  pages.filter((page) => page.dataset.clueReleased === 'true').map((page) => page.dataset.taskId)
191
  );
@@ -200,25 +209,81 @@
200
  return parts.join(' · ');
201
  };
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  const setActivePage = (index) => {
204
  currentIndex = (index + pages.length) % pages.length;
205
  pages.forEach((page, pageIndex) => {
206
  page.classList.toggle('is-active', pageIndex === currentIndex);
207
  page.classList.toggle('is-left', pageIndex < currentIndex);
208
  });
209
- if (counter) counter.textContent = `${currentIndex + 1} / ${pages.length}`;
 
 
 
210
  };
211
 
212
- prevBtn?.addEventListener('click', () => setActivePage(currentIndex - 1));
213
- nextBtn?.addEventListener('click', () => setActivePage(currentIndex + 1));
 
 
 
 
214
  setActivePage(0);
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  const closeClueModal = () => {
217
  clueModal?.classList.remove('is-open');
218
  if (clueModal) clueModal.setAttribute('aria-hidden', 'true');
219
  };
220
 
221
  clueCloseBtn?.addEventListener('click', closeClueModal);
 
 
 
222
  clueModal?.addEventListener('click', (event) => {
223
  if (event.target === clueModal || event.target.classList.contains('clue-modal-backdrop')) {
224
  closeClueModal();
@@ -332,6 +397,7 @@
332
  }
333
  });
334
  refreshLeaderboard(payload.leaderboard || []);
 
335
  } catch (error) {
336
  console.debug('task status refresh skipped', error);
337
  }
@@ -341,8 +407,5 @@
341
  window.setInterval(refreshStatuses, 10000);
342
  })();
343
  </script>
344
- {% endblock %}
345
-
346
-
347
-
348
 
 
74
  <span class="chip" id="rejected-progress-chip">被驳回 {{ group_progress.rejected_count }}</span>
75
  </div>
76
  <div class="carousel-controls">
77
+ <button class="btn btn-secondary small-btn" type="button" data-task-prev>上一张</button>
78
+ <span class="mini-note" data-task-counter>1 / {{ activity.tasks|length }}</span>
79
+ <button class="btn btn-secondary small-btn" type="button" data-task-next>下一张</button>
80
  </div>
81
  </div>
82
 
 
158
  </article>
159
  {% endfor %}
160
  </div>
161
+
162
+ <div class="carousel-controls carousel-controls-bottom">
163
+ <button class="btn btn-secondary small-btn" type="button" data-task-prev>上一张</button>
164
+ <span class="mini-note" data-task-counter>1 / {{ activity.tasks|length }}</span>
165
+ <button class="btn btn-secondary small-btn" type="button" data-task-next>下一张</button>
166
+ </div>
167
  </section>
168
 
169
  <div class="clue-modal" id="clue-modal" aria-hidden="true">
 
181
  const pages = Array.from(document.querySelectorAll('[data-task-page]'));
182
  if (!pages.length) return;
183
 
184
+ const flipBook = document.getElementById('task-flip-book');
185
+ const counters = Array.from(document.querySelectorAll('[data-task-counter]'));
186
+ const prevButtons = Array.from(document.querySelectorAll('[data-task-prev]'));
187
+ const nextButtons = Array.from(document.querySelectorAll('[data-task-next]'));
188
  const clueModal = document.getElementById('clue-modal');
189
  const clueModalImage = document.getElementById('clue-modal-image');
190
  const clueCloseBtn = document.getElementById('clue-close-btn');
 
193
  const pendingProgressChip = document.getElementById('pending-progress-chip');
194
  const rejectedProgressChip = document.getElementById('rejected-progress-chip');
195
  let currentIndex = 0;
196
+ let touchStartX = 0;
197
+ let touchStartY = 0;
198
  const seenReleased = new Set(
199
  pages.filter((page) => page.dataset.clueReleased === 'true').map((page) => page.dataset.taskId)
200
  );
 
209
  return parts.join(' · ');
210
  };
211
 
212
+ const syncFlipBookHeight = () => {
213
+ if (!flipBook) return;
214
+ const activePage = pages[currentIndex];
215
+ if (!activePage) return;
216
+ const inner = activePage.querySelector('.task-page-inner');
217
+ const styles = window.getComputedStyle(activePage);
218
+ const paddingY = parseFloat(styles.paddingTop || '0') + parseFloat(styles.paddingBottom || '0');
219
+ const height = (inner ? inner.scrollHeight : activePage.scrollHeight) + paddingY;
220
+ flipBook.style.height = `${Math.ceil(height)}px`;
221
+ };
222
+
223
+ const queueFlipBookHeightSync = () => {
224
+ window.requestAnimationFrame(syncFlipBookHeight);
225
+ };
226
+
227
  const setActivePage = (index) => {
228
  currentIndex = (index + pages.length) % pages.length;
229
  pages.forEach((page, pageIndex) => {
230
  page.classList.toggle('is-active', pageIndex === currentIndex);
231
  page.classList.toggle('is-left', pageIndex < currentIndex);
232
  });
233
+ counters.forEach((counter) => {
234
+ counter.textContent = `${currentIndex + 1} / ${pages.length}`;
235
+ });
236
+ queueFlipBookHeightSync();
237
  };
238
 
239
+ prevButtons.forEach((button) => {
240
+ button.addEventListener('click', () => setActivePage(currentIndex - 1));
241
+ });
242
+ nextButtons.forEach((button) => {
243
+ button.addEventListener('click', () => setActivePage(currentIndex + 1));
244
+ });
245
  setActivePage(0);
246
 
247
+ flipBook?.addEventListener('touchstart', (event) => {
248
+ if (!event.touches.length) return;
249
+ touchStartX = event.touches[0].clientX;
250
+ touchStartY = event.touches[0].clientY;
251
+ }, { passive: true });
252
+
253
+ flipBook?.addEventListener('touchend', (event) => {
254
+ if (!event.changedTouches.length) return;
255
+ const deltaX = event.changedTouches[0].clientX - touchStartX;
256
+ const deltaY = event.changedTouches[0].clientY - touchStartY;
257
+ touchStartX = 0;
258
+ touchStartY = 0;
259
+ if (Math.abs(deltaX) < 56 || Math.abs(deltaX) <= Math.abs(deltaY) * 1.15) return;
260
+ setActivePage(deltaX < 0 ? currentIndex + 1 : currentIndex - 1);
261
+ }, { passive: true });
262
+
263
+ if ('ResizeObserver' in window) {
264
+ const resizeObserver = new ResizeObserver(() => {
265
+ queueFlipBookHeightSync();
266
+ });
267
+ pages.forEach((page) => resizeObserver.observe(page));
268
+ }
269
+
270
+ pages.forEach((page) => {
271
+ page.querySelectorAll('img').forEach((image) => {
272
+ if (image.complete) return;
273
+ image.addEventListener('load', queueFlipBookHeightSync);
274
+ });
275
+ });
276
+ window.addEventListener('resize', queueFlipBookHeightSync);
277
+
278
  const closeClueModal = () => {
279
  clueModal?.classList.remove('is-open');
280
  if (clueModal) clueModal.setAttribute('aria-hidden', 'true');
281
  };
282
 
283
  clueCloseBtn?.addEventListener('click', closeClueModal);
284
+ document.addEventListener('keydown', (event) => {
285
+ if (event.key === 'Escape') closeClueModal();
286
+ });
287
  clueModal?.addEventListener('click', (event) => {
288
  if (event.target === clueModal || event.target.classList.contains('clue-modal-backdrop')) {
289
  closeClueModal();
 
397
  }
398
  });
399
  refreshLeaderboard(payload.leaderboard || []);
400
+ queueFlipBookHeightSync();
401
  } catch (error) {
402
  console.debug('task status refresh skipped', error);
403
  }
 
407
  window.setInterval(refreshStatuses, 10000);
408
  })();
409
  </script>
410
+ {% endblock %}
 
 
 
411
 
app/templates/admin_users.html CHANGED
@@ -97,7 +97,7 @@
97
  <thead>
98
  <tr>
99
  <th>
100
- <input type="checkbox" data-check-all />
101
  </th>
102
  <th>学号</th>
103
  <th>姓名</th>
@@ -109,13 +109,13 @@
109
  {% for user_item in users %}
110
  <tr>
111
  <td>
112
- <input type="checkbox" name="user_ids" value="{{ user_item.id }}" data-user-check form="bulk-users-form" />
113
  </td>
114
  <td>{{ user_item.student_id }}</td>
115
  <td>{{ user_item.full_name }}</td>
116
  <td>{{ user_item.group.name if user_item.group else '未分组' }}</td>
117
  <td>
118
- <form method="post" action="/admin/users/{{ user_item.id }}/group" class="inline-form">
119
  <select name="group_id">
120
  <option value="">未分组</option>
121
  {% for group in groups %}
 
97
  <thead>
98
  <tr>
99
  <th>
100
+ <input class="table-checkbox" type="checkbox" data-check-all />
101
  </th>
102
  <th>学号</th>
103
  <th>姓名</th>
 
109
  {% for user_item in users %}
110
  <tr>
111
  <td>
112
+ <input class="table-checkbox" type="checkbox" name="user_ids" value="{{ user_item.id }}" data-user-check form="bulk-users-form" />
113
  </td>
114
  <td>{{ user_item.student_id }}</td>
115
  <td>{{ user_item.full_name }}</td>
116
  <td>{{ user_item.group.name if user_item.group else '未分组' }}</td>
117
  <td>
118
+ <form method="post" action="/admin/users/{{ user_item.id }}/group" class="inline-form table-inline-form">
119
  <select name="group_id">
120
  <option value="">未分组</option>
121
  {% for group in groups %}
docker_data/submissions/mobile-test.png ADDED