PageSourceSearch

https://sjtuguoxue.space/

html sjtuguoxue.space collected 2026-09-25 21:06:25 UTC 124,638 bytes, 3,198 lines download raw bytes

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>南洋吟游-首页</title>
7    <link href="/static/favicon.ico" rel="shortcut icon">
8    <style>
9        /* --- 1. 基础、字体和全局样式 --- */
10        html {
11            box-sizing: border-box;
12            font-size: clamp(14px, 1.6vw, 18px);
13            overflow-x: hidden;
14        }
15        *, *:before, *:after {
16            box-sizing: inherit;
17        }
18        /* Hide all scrollbars globally */
19        *::-webkit-scrollbar { display: none; }
20        * { scrollbar-width: none; }
21
22        body {
23            background: #333333;
24            font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
25            margin: 0;
26            padding: 0;
27            color: #DDDDDD;
28            line-height: 1.7;
29            position: relative;
30            min-height: 100vh;
31            overflow-x: hidden;
32        }
33        a {
34            text-decoration: none;
35            color: #D1C4E9; /* 柔和的链接颜色 */
36        }
37        a:hover {
38            text-decoration: underline;
39        }
40
41        /* --- 2. 动态辉光背景 (from author.html) --- */
42        .glow {
43            position: fixed;
44            border-radius: 50%;
45            opacity: 0.3;
46            filter: blur(5em);
47            z-index: -1;
48            pointer-events: none;
49        }
50        .glow-1 {
51            width: 15em;
52            height: 15em;
53            background: #995577;
54            top: 10%;
55            left: 15%;
56            animation: float 20s infinite alternate;
57        }
58        .glow-2 {
59            width: 20em;
60            height: 20em;
61            background: #559977;
62            top: 40%;
63            right: 10%;
64            animation: float 25s infinite alternate -5s;
65        }
66        @keyframes float {
67            0% { transform: translate(0, 0); }
68            50% { transform: translate(10em, 20em) scale(1.5); }
69            100% { transform: translate(-10em, -10em); }
70        }
71
72        /* --- 3. 新增: “中枢” 布局 (from index.html) --- */
73        .hub-container {
74            display: flex;
75            min-height: 100vh;
76            max-width: 100%;
77            overflow-x: hidden;
78        }
79
80        /* --- 4. 新增: 左侧边栏 (from index.html) --- */
81        .hub-sidebar {
82            width: 240px;
83            background: #222222;
84            position: fixed;
85            top: 0;
86            left: 0;
87            bottom: 0;
88            padding: 1.5em;
89            display: flex; /* 默认 display:flex,用于 .desktop-only */
90            flex-direction: column;
91            justify-content: space-between;
92            z-index: 1000;
93            overflow-y: auto;
94        }
95        
96        .sidebar-header .site-title {
97            font-size: 1.8em;
98            font-weight: bold;
99            color: #FFFFFF;
100            margin: 0.0em -0.2em 0.5em -0.2em;
101        }
102        .sidebar-header .site-title a:hover {
103            text-decoration: underline;
104        }
105
106        /* 新增:标题图片样式 */
107        .site-title-image {
108            width: 100%;
109            max-width: 190px; /* 桌面侧边栏的最大宽度 */
110            height: auto;
111            display: block; /* 允许居中等 */
112        }
113
114        /* 新增:标题图片样式 */
115        .site-title-image-2 {
116            background-color: #ccc;
117            border-radius: 10px;
118            width: 100%;
119            max-width: 220px; /* 桌面侧边栏的最大宽度 */
120            height: auto;
121            display: block; /* 允许居中等 */
122        }
123
124        .main-nav ul {
125            list-style: none;
126            padding: 0;
127            margin: 0;
128        }
129        .main-nav li a {
130            display: block;
131            /* 缩小行距:减小 padding 和 margin */
132            padding: 0.3em 0.5em;
133            color: #BBBBBB;
134            border-radius: 8px;
135            margin-bottom: 0.3em; /* 缩小行距 */
136            font-size: 0.8em;
137            transition: background-color 0.2s, color 0.2s, transform 0.2s;
138        }
139        /* 新增:现代化的悬停微交互 */
140        .main-nav li a:hover,
141        .main-nav li a.active {
142            background: rgba(255, 255, 255, 0.05); /* 5% 白色透明层 */
143            color: #FFFFFF;
144            transform: translateX(3px); /* 轻微右移 */
145        }
146
147        .sidebar-footer {
148            font-size: 0.7em;
149            color: #888888;
150        }
151        .sidebar-footer p {
152            margin: 0.25em 0;
153        }
154        .sidebar-footer a {
155            color: #AAAAAA;
156            text-decoration: underline;
157        }
158        .sidebar-footer span {
159            margin-right: 0.5em;
160            display: inline-block;
161        }
162
163        /* --- 5. 新增: 右侧边栏 --- */
164        .hub-right-sidebar {
165            font-size: 0.6em;
166            width: 240px;
167            background: #222222;
168            position: fixed;
169            top: 0;
170            right: 0;
171            bottom: 0;
172            padding: 1.5em;
173            display: flex;
174            flex-direction: column;
175            gap: 2em; /* 模块间距 */
176            z-index: 1000;
177            overflow-y: auto;
178            color: #BBBBBB;
179        }
180        .sidebar-section-title {
181            font-size: 1.2em;
182            font-weight: bold;
183            color: #FFFFFF;
184            margin: 0 0 0.8em 0;
185            padding-bottom: 0.3em;
186            border-bottom: 1px solid #444;
187        }
188        .sidebar-text {
189            font-size: 1.0em;
190            line-height: 1.6;
191            color: #BBBBBB;
192        }
193        .sidebar-text p {
194            margin: 0 0 1em 0;
195        }
196
197        /* --- 6. 修改: 主内容区 --- */
198        .hub-main-content {
199            /* 桌面端: 左右留白 */
200            margin: 0 240px 0 240px;
201            padding: 2em;
202            width: auto;
203            flex-grow: 1;
204        }
205        
206        /* --- 7. 原有卡片样式 (无修改) --- */
207        .main-container {
208            width: 100%;
209            max-width: 48em;
210            margin: 0 auto;
211            overflow-x: hidden;
212        }
213        .intro-widget {
214            background: rgba(255, 255, 255, 0.9);
215            color: #1c1917;
216            position: relative;
217            z-index: 10;
218            @supports (backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px)) {
219                backdrop-filter: blur(5px);
220                -webkit-backdrop-filter: blur(5px);
221            }
222            border-radius: 1.5em;
223            padding: 1.5em;
224            margin-bottom: 1.5em;
225            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
226            transition: all 0.2s;
227        }
228        .intro-widget:hover {
229            transform: translateY(-4px);
230            box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
231        }
232        .image-widget {
233            padding: 0;
234            overflow: hidden;
235            transition: all 0.2s;
236        }
237        .image-widget:hover {
238            transform: translateY(-4px);
239            box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
240        }
241        .intro-image {
242            width: 100%;
243            height: auto;
244            display: block;
245        }
246        /* --- 标签页系统 (无修改) --- */
247        .tab-bar {
248            display: flex;
249            justify-content: center;
250            gap: 1em;
251            margin-bottom: 1.5em;
252            padding-bottom: 0.5em;
253            border-bottom: 1px solid rgba(0, 0, 0, 0.1);
254            flex-wrap: wrap;
255        }
256        .tab-btn {
257            font-family: 'Noto Sans SC', sans-serif;
258            font-size: 1em;
259            font-weight: 500;
260            padding: 0.5em 0.2em;
261            border: none;
262            background: none;
263            cursor: pointer;
264            color: #57534e;
265            border-bottom: 3px solid transparent;
266            transition: color 0.2s, border-color 0.2s;
267        }
268        .tab-btn.active {
269            font-weight: 700;
270            color: #1c1917;
271            border-bottom-color: #995577;
272        }
273        .tab-btn:hover:not(.active) {
274            color: #1c1917;
275        }
276        .tab-content-container { padding-top: 0em; }
277        .tab-content { display: none; text-align: justify; }
278        .tab-content.active { display: block; }
279        .tab-content p { margin: 0 0 1em 0; }
280        .tab-content h3 {
281            font-size: 1.2em;
282            color: #995577;
283            margin: 0.5em 0 0.5em 0;
284            padding-bottom: 0.25em;
285            border-bottom: 1px solid rgba(0, 0, 0, 0.05);
286        }
287        /* --- 预留空间卡片 (无修改) --- */
288        .placeholder-widget h3 {
289            font-size: 1.3em;
290            color: #1c1917;
291            margin: 0 0 1em 0;
292        }
293        .placeholder-widget .placeholder-text {
294            color: #57534e;
295            font-style: italic;
296        }
297        .placeholder-widget hr {
298            border: 0;
299            height: 1px;
300            background: rgba(0, 0, 0, 0.1);
301            margin: 1.5em 0;
302        }
303
304        /* --- 8. 新增: 响应式布局 (from index.html) --- */
305        .mobile-only {
306            display: none;
307        }
308        .medium-footer { /* 隐藏中屏页脚 */
309            display: none;
310        }
311        .desktop-only {
312            display: flex; /* .hub-sidebar 默认 */
313            flex-direction: column;
314        }
315        
316        .mobile-header {
317            padding: 0;
318        }
319        .mobile-header-bar {
320            display: flex;
321            align-items: center;
322            gap: 12px;
323            position: fixed;
324            top: 0;
325            left: 0;
326            right: 0;
327            z-index: 999;
328            background: rgba(30,30,30,0.7);
329            backdrop-filter: blur(12px);
330            -webkit-backdrop-filter: blur(12px);
331            padding: 12px 12px;
332        }
333        .mobile-menu-btn {
334            display: flex; align-items: center; justify-content: center;
335            width: 40px; height: 40px;
336            border-radius: 10px;
337            background: rgba(255,255,255,0.08);
338            color: #aaa;
339            border: none;
340            cursor: pointer;
341            flex-shrink: 0;
342        }
343        .mobile-menu-btn:active { background: rgba(255,255,255,0.15); color: #fff; }
344        .mobile-menu-btn-sm {
345            width: 32px; height: 32px;
346            background: none;
347        }
348        .mobile-title-img {
349            max-height: 40px;
350            width: auto;
351            display: block;
352        }
353        .drawer-overlay {
354            position: fixed; inset: 0;
355            background: rgba(0,0,0,0.45);
356            z-index: 997;
357            opacity: 0;
358            pointer-events: none;
359            transition: opacity 0.2s;
360        }
361        .drawer-overlay.open {
362            opacity: 1;
363            pointer-events: auto;
364        }
365        .mobile-drawer {
366            position: fixed;
367            top: 64px; bottom: 0;
368            z-index: 998;
369            overflow-y: auto;
370            display: flex;
371            flex-direction: column;
372            transition: transform 0.25s ease-out;
373            background: rgba(30,30,30,0.8);
374            backdrop-filter: blur(16px);
375            -webkit-backdrop-filter: blur(16px);
376        }
377        .mobile-drawer-left {
378            left: 0;
379            border-radius: 0 12px 12px 0;
380            transform: translateX(-100%);
381            width: 35%;
382            max-width: 280px;
383        }
384        .mobile-drawer-right {
385            right: 0;
386            border-radius: 12px 0 0 12px;
387            transform: translateX(100%);
388            width: 65%;
389            max-width: 360px;
390        }
391        .mobile-drawer.open {
392            transform: translateX(0);
393        }
394        .drawer-head {
395            display: flex;
396            align-items: center;
397            justify-content: space-between;
398            padding: 14px 14px 10px;
399            border-bottom: 1px solid rgba(255,255,255,0.08);
400            flex-shrink: 0;
401            font-size: 13px;
402            font-weight: 600;
403            color: #ccc;
404        }
405        .drawer-close {
406            background: none; border: none; color: #888; cursor: pointer;
407            display: flex; align-items: center; justify-content: center;
408            width: 28px; height: 28px; border-radius: 6px;
409        }
410        .drawer-close:active { background: rgba(255,255,255,0.1); }
411        .drawer-nav {
412            display: flex;
413            flex-direction: column;
414            gap: 2px;
415            padding: 8px 10px;
416            flex: 1;
417        }
418        .drawer-nav a {
419            display: block;
420            padding: 10px 12px;
421            color: #bbb;
422            text-decoration: none;
423            border-radius: 8px;
424            font-size: 14px;
425            transition: background 0.15s;
426        }
427        .drawer-nav a:active { background: rgba(255,255,255,0.1); color: #fff; }
428        .drawer-user {
429            display: flex; align-items: center; gap: 10px;
430            padding: 12px 14px;
431            border-top: 1px solid rgba(255,255,255,0.08);
432            font-size: 13px;
433            flex-shrink: 0;
434        }
435        .drawer-user a { color: #aaa; text-decoration: none; }
436        .drawer-info {
437            padding: 12px 14px;
438            font-size: 13px;
439            color: #999;
440            line-height: 1.8;
441        }
442        .drawer-info a { color: #aaa; text-decoration: none; }
443        .drawer-info h4 {
444            color: #ccc;
445            font-size: 12px;
446            font-weight: 600;
447            margin: 14px 0 4px;
448            text-transform: uppercase;
449            letter-spacing: 0.5px;
450        }
451        .drawer-info h4:first-child { margin-top: 0; }
452        .drawer-info p { margin: 2px 0; }
453        .mobile-user-dropdown {
454            position: fixed;
455            top: 64px;
456            right: 56px;
457            background: rgba(30,30,30,0.9);
458            backdrop-filter: blur(16px);
459            -webkit-backdrop-filter: blur(16px);
460            border-radius: 10px;
461            padding: 6px 0;
462            z-index: 1000;
463            min-width: 140px;
464            box-shadow: 0 8px 24px rgba(0,0,0,0.3);
465            opacity: 0;
466            transform: translateY(-8px);
467            pointer-events: none;
468            transition: opacity 0.15s, transform 0.15s;
469        }
470        .mobile-user-dropdown.open {
471            opacity: 1;
472            transform: translateY(0);
473            pointer-events: auto;
474        }
475        .mobile-user-dropdown-name {
476            padding: 8px 14px 6px;
477            font-size: 13px;
478            font-weight: 600;
479            color: #ddd;
480            border-bottom: 1px solid rgba(255,255,255,0.08);
481            margin-bottom: 4px;
482        }
483        .mobile-user-dropdown a {
484            display: block;
485            padding: 8px 14px;
486            color: #aaa;
487            text-decoration: none;
488            font-size: 13px;
489        }
490        .mobile-user-dropdown a:active {
491            background: rgba(255,255,255,0.08);
492        }
493        
494        /* 移动端页脚 (无修改) */
495        .mobile-footer {
496            width: 100%;
497            padding: 1.5em 1em;
498        }
499        .mobile-footer .main-nav ul {
500            display: grid;
501            grid-template-columns: 1fr 1fr 1fr;
502            gap: 0.5em;
503        }
504        .mobile-footer .main-nav li {
505            text-align: center;
506        }
507        .mobile-footer .main-nav li a {
508            padding: 0.6em 0.25em;
509            color: #BBBBBB;
510            border-radius: 8px;
511            background: rgba(255, 255, 255, 0.05);
512            margin: 0;
513        }
514        .mobile-footer .main-nav li a:hover {
515            background: rgba(255, 255, 255, 0.1);
516            color: #FFFFFF;
517        }
518        .mobile-user-block {
519            border-top: 1px solid rgba(255,255,255,0.08);
520            margin-top: 0.8em;
521            padding: 0.8em 0.5em 0;
522            font-size: 0.85em;
523            text-align: center;
524        }
525        .mobile-user-row {
526            display: flex;
527            align-items: center;
528            justify-content: center;
529            gap: 6px;
530            margin-bottom: 4px;
531        }
532        .mobile-user-name {
533            color: #ddd;
534            text-decoration: none;
535            font-weight: 500;
536        }
537        .mobile-user-badge {
538            font-size: 10px;
539            background: rgba(153,85,119,0.3);
540            color: #cc99bb;
541            padding: 1px 5px;
542            border-radius: 2px;
543        }
544        .mobile-user-links a {
545            color: #888;
546            text-decoration: none;
547            font-size: 0.9em;
548        }
549        .mobile-user-links a:hover { color: #ccc; }
550        .mobile-user-sep {
551            color: #555;
552            margin: 0 4px;
553        }
554        .mobile-footer .sidebar-footer {
555            margin-top: 1.5em;
556            text-align: center;
557        }
558        .mobile-footer-section {
559            margin: 0 auto;
560            font-size: 0.8em;
561            color: #888888;
562            align-content: center;
563            text-align: center;
564        }
565
566        /* 【新增】中屏页脚样式 */
567        .medium-footer {
568            background: none;
569            padding: 2em; /* 顶部留白多一点 */
570            margin-top: 2em; /* 与内容卡片隔开 */
571            color: #BBBBBB;
572            width: 100%;
573        }
574        /* (复用 .sidebar-module 样式) */
575        .medium-footer .sidebar-module {
576            max-width: 48em; /* 限制内容宽度,同 .main-container */
577            margin: 0 auto 2em auto; /* 居中并添加模块间距 */
578        }
579        .medium-footer .sidebar-section-title {
580            color: #FFFFFF; /* 确保标题在深色背景上是白色 */
581        }
582
583        @media (max-width: 1280px) {
584            .hub-right-sidebar { /* 隐藏右侧边栏(容器) */
585                display: none;
586            }
587            .hub-main-content { /* 主内容区-只留左侧margin */
588                margin-left: 240px;
589                margin-right: 0;
590            }
591            .medium-footer { /* 显示中屏页脚 (含联系方式) */
592                display: block;
593            }
594        }
595        
596        /* C. 移动设备 (<= 960px) */
597        /* (此处的规则将覆盖 B 中的规则) */
598        @media (max-width: 960px) {
599            .desktop-only { /* 隐藏所有桌面元素 (包括左侧栏) */
600                display: none;
601            }
602            .mobile-only { /* 显示移动端页眉/页脚 */
603                display: block;
604            }
605            .hub-main-content {
606                margin: 0;
607                padding: 64px 3vw 0 3vw;
608                overflow-x: hidden;
609            }
610            .main-container {
611                width: 100%;
612                max-width: none;
613                margin-top: 1em;
614            }
615            .medium-footer { /* 隐藏中屏页脚 */
616                display: none;
617            }
618            /* 确保 mobile-footer nav 里的 a 也是 block */
619            .mobile-footer .main-nav li a {
620                display: block;
621            }
622            /* 确保 .intro-widget.placeholder-widget.mobile-only 生效 */
623            .intro-widget.placeholder-widget.mobile-only {
624                display: block !important;
625            }
626        }
627        @media (max-width: 350px) {
628            .mobile-header-bar {
629                gap: 7px;
630                padding-left: 7px;
631                padding-right: 7px;
632            }
633            .mobile-title-img {
634                max-width: 55px;
635                max-height: 32px;
636            }
637        }
638        /* --- Action button hover/active transitions --- */
639        button, .btn-sm, .btn-xs, .btn-create, .btn-approve, .btn-reject,
640        .btn-revision, .ep-btn-save, .ep-btn-cancel,
641        [onclick][style*="cursor:pointer"], [onclick][style*="cursor: pointer"] {
642            transition: filter 0.15s, transform 0.12s, box-shadow 0.15s;
643        }
644        button:hover, .btn-sm:hover, .btn-xs:hover, .btn-create:hover,
645        .btn-approve:hover, .btn-reject:hover, .btn-revision:hover,
646        .ep-btn-save:hover, .ep-btn-cancel:hover {
647            filter: brightness(1.2);
648            transform: translateY(-1px);
649            box-shadow: 0 2px 6px rgba(0,0,0,0.25);
650        }
651        button:active, .btn-sm:active, .btn-xs:active, .btn-create:active,
652        .btn-approve:active, .btn-reject:active, .btn-revision:active,
653        .ep-btn-save:active, .ep-btn-cancel:active {
654            filter: brightness(0.9);
655            transform: translateY(0) scale(0.97);
656            box-shadow: none;
657        }
658
659    </style>
660    
661<style>
662    /* --- 5. 小部件 (Widget) 基础样式 --- */
663    /* (注意: base.html 有 .intro-widget, 这是首页专用的 .widget) */
664    .widget {
665        background: rgba(255, 255, 255, 0.9);
666        color: #1c1917;
667        font-size: 0.8em;
668        
669        @supports (backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px)) {
670            backdrop-filter: blur(5px);
671            -webkit-backdrop-filter: blur(5px);
672        }
673        z-index: 1;
674        border-radius: 1.5em;
675        padding: 1.5em;
676        margin-bottom: 0;
677        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
678        transition: transform 0.2s, background-color 0.2s, box-shadow 0.2s;
679    }
680
681    .widget:hover {
682        transform: translateY(-4px);
683        box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
684        background-color: #EEEEEE;
685    }
686
687    /* --- 搜索小部件 --- */
688    .search-widget {
689        position: relative;
690        line-height: 1.5;
691        z-index: 10;
692        margin-bottom: 1.5em;
693    }
694    .search-form-container {
695        display: flex;
696        align-items: center;
697        background: rgba(0, 0, 0, 0.05);
698        border-radius: 1em;
699        padding: 0.5em 1em;
700        position: relative;
701        z-index: 101; 
702        border: 1px solid transparent;
703        transition: border-color 0.2s, background-color 0.2s;
704    }
705    .search-form-container:focus-within {
706        background: rgba(0, 0, 0, 0.02);
707        /* (原 border-color: #F6F6E0; ) */
708        border-color: rgba(0, 0, 0, 0.1);
709    }
710    
711    .search-input {
712        flex-grow: 1;
713        border: none;
714        outline: none;
715        background: transparent;
716        font-size: 1em;
717        color: #1c1917;
718        min-width: 0;
719    }
720    .search-button {
721        background: transparent;
722        border: none;
723        cursor: pointer;
724        padding: 0;
725        flex-shrink: 0;
726    }
727    .search-button img { width: 1.8em; vertical-align: middle; opacity: 0.7; }
728    .search-button:hover img { opacity: 1; }
729    .search-button:active { transform: scale(0.95); }
730
731    .checkbox-container-switch {
732        font-family: 'Noto Sans SC', sans-serif;
733        display: flex;
734        flex-wrap: wrap;
735        gap: 1em 1.5em;
736        justify-content: center;
737        padding: 1em 0 0.5em 0;
738        color: #333;
739        font-size: 0.9em;
740    }
741    .checkbox-container-switch label {
742        display: flex;
743        align-items: center;
744        cursor: pointer;
745    }
746    .checkbox-container-switch input[type="checkbox"] {
747        opacity: 0;
748        width: 0;
749        height: 0;
750        position: absolute;
751    }
752    .switch-track {
753        position: relative;
754        display: inline-block;
755        width: 44px;
756        height: 24px;
757        background-color: #ccc;
758        border-radius: 12px;
759        transition: background-color 0.3s;
760        margin-right: 8px;
761    }
762    .switch-track::after {
763        content: '';
764        position: absolute;
765        width: 18px;
766        height: 18px;
767        background-color: white;
768        border-radius: 50%;
769        top: 3px;
770        left: 3px;
771        transition: transform 0.3s;
772        box-shadow: 0 1px 3px rgba(0,0,0,0.1);
773    }
774    input:checked + .switch-track {
775        background-color: #559977;
776    }
777    input:checked + .switch-track::after {
778        transform: translateX(20px);
779    }
780    
781    #autocomplete-suggestions {
782        display: none;
783        position: absolute; 
784        top: 8.25em; 
785        left: 1.5em;
786        right: 1.5em;
787        background-color: #FFFFFF;
788        border: 1px solid #FFFFFF;
789        border-radius: 0 0 12px 12px;
790        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
791        z-index: 102; 
792        max-height: 15em;
793        overflow-y: auto;
794    }
795    .suggestion-item {
796        padding: 0.8em 1.2em;
797        cursor: pointer;
798        color: #1c1917;
799        font-size: 0.9em;
800        border-top: 1px solid #dddddd;
801        z-index: 102;
802        position: relative;
803    }
804    .suggestion-item:hover,
805    .suggestion-item.active {
806        background-color: rgba(0, 0, 0, 0.05); /* (原 #F6F6E0) */
807    }
808
809    /* --- 统计小部件 (甜甜圈图表) --- */
810    .stats-widget-grid {
811        display: flex;
812        align-items: center;
813        gap: 1.5em;
814        z-index: 100;
815    }
816    
817    .chart-container {
818        flex-shrink: 0;
819        width: 150px;
820        height: 150px;
821        position: relative;
822    }
823    .donut-chart {
824        width: 100%;
825        height: 100%;
826        border-radius: 50%;
827        background: conic-gradient(#DDD4ED 0% 33.3%, #A8D8B9 33.3% 66.6%, #F3B9B0 66.6% 100%);
828        transition: transform 0.3s ease-in-out, box-shadow 0.3s;
829    }
830    .donut-chart:hover {
831        transform: scale(1.1);
832        box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
833    }
834    .donut-chart::before {
835        content: '';
836        position: absolute;
837        left: 50%;
838        top: 50%;
839        transform: translate(-50%, -50%);
840        width: 65%;
841        height: 65%;
842        background: #F6F6F6; /* (用一个稍亮的背景色) */
843        border-radius: 50%;
844    }
845    .chart-legend {
846        flex-grow: 1;
847        list-style: none;
848        padding: 0;
849        margin: 0;
850    }
851    .legend-item {
852        display: flex;
853        align-items: center;
854        font-size: 1.1em;
855        margin-bottom: 0.8em;
856    }
857    .legend-color-box {
858        width: 20px;
859        height: 20px;
860        border-radius: 4px;
861        margin-right: 0.8em;
862    }
863    .legend-text {
864        flex-grow: 1;
865        color: #333;
866        text-underline-offset: 0.2em;
867    }
868    .legend-percent {
869        font-weight: bold;
870        font-size: 1.1em;
871        color: #1c1917;
872    }
873    
874    /* --- 5.5. Masonry Grid Layout --- */
875    .masonry-grid {
876        display: flex;
877        gap: 0.8em;
878        align-items: flex-start;
879    }
880    .masonry-col {
881        flex: 1;
882        min-width: 0;
883        display: flex;
884        flex-direction: column;
885        gap: 0.8em;
886    }
887    .masonry-grid > .widget {
888        break-inside: avoid;
889        margin-bottom: 1.5em;
890    }
891    .new-poem-widget > *:not(canvas) {
892        position: relative;
893        z-index: 1;
894    }
895    /* legacy compat */
896    .widget-row {
897        display: flex;
898        gap: 1.5em;
899        align-items: flex-start;
900        margin-bottom: 1.5em;
901    }
902    .widget-row > .widget,
903    .widget-row > div.widget {
904        flex: 1;
905        min-width: 0; 
906        margin-bottom: 0;
907        display: flex;
908        flex-direction: column;
909    }
910    .banner-widget {
911        padding: 0;
912        overflow: hidden;
913        display: none;
914        margin-bottom: 1.5em;
915    }
916    .banner-slider-container {
917        position: relative;
918        width: 100%;
919        overflow: hidden;
920    }
921    .banner-slides {
922        display: flex;
923        width: 100%;
924        transition: transform 0.5s ease-in-out;
925    }
926    .banner-slide {
927        display: block;
928        flex-shrink: 0;
929        width: 100%;
930        position: relative;
931        text-decoration: none;
932        color: white;
933        aspect-ratio: 21 / 9;
934        overflow: hidden;
935        background: #111;
936    }
937    .banner-image {
938        width: 100%;
939        height: 100%;
940        object-fit: cover;
941        display: block;
942    }
943    .banner-caption {
944        position: absolute;
945        bottom: 0;
946        left: 0;
947        right: 0;
948        padding: 1.5em 1.2em 1em;
949        background: linear-gradient(0deg, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 100%);
950        font-size: 1.1em;
951        font-weight: 600;
952        white-space: nowrap;
953        overflow: hidden;
954        text-overflow: ellipsis;
955    }
956    .banner-nav {
957        position: absolute;
958        top: 50%;
959        transform: translateY(-50%);
960        background: rgba(0, 0, 0, 0.3);
961        color: white;
962        border: none;
963        border-radius: 50%;
964        cursor: pointer;
965        width: 2.5em;
966        height: 2.5em;
967        font-size: 1.2em;
968        display: flex;
969        align-items: center;
970        justify-content: center;
971        opacity: 0;
972        transition: opacity 0.3s;
973    }
974    .banner-slider-container:hover .banner-nav {
975        opacity: 0.8;
976    }
977    .banner-nav:hover {
978        opacity: 1;
979        background: rgba(0, 0, 0, 0.5);
980    }
981    #banner-prev { left: 0.8em; }
982    #banner-next { right: 0.8em; }
983    .banner-dots {
984        position: absolute;
985        bottom: 0.8em;
986        left: 50%;
987        transform: translateX(-50%);
988        display: flex;
989        gap: 0.5em;
990    }
991    .banner-dot {
992        width: 8px;
993        height: 8px !important;
994        padding: 0;
995        border: 1px solid #1c1917;
996        border-radius: 50%;
997        background: rgba(255, 255, 255, 0.5);
998        cursor: pointer;
999        transition: background-color 0.3s;
1000    }
1001    .banner-dot.active {
1002        background: rgba(255, 255, 255, 1);
1003    }
1004
1005    /* --- 6. 每日一诗小部件 --- */
1006    .random-poem-widget {
1007        border-left-width: 5px;
1008        border-left-style: solid;
1009        border-left-color: #cfd0ff;
1010        padding-left: 1.5em; 
1011        line-height: 1.8;
1012        position: relative;
1013        transition: transform 0.2s, background-color 0.2s, box-shadow 0.2s, border 0.5s;
1014    }
1015    .random-poem-widget:hover {
1016      border-left-width: 10px;
1017      border-right-width: 10px;
1018      border-right-style: solid;
1019      border-right-color: #cfd0ff;
1020    }
1021    .poem-header {
1022        display: flex;
1023        justify-content: space-between;
1024        align-items: center;
1025        margin-bottom: 1em;
1026        padding-bottom: 0.5em;
1027        border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1028    }
1029    .poem-header-title {
1030        font-size: 1.1em;
1031        font-weight: bold;
1032        color: #555;
1033    }
1034    .poem-ai-button {
1035        background: none;
1036        border: none;
1037        color: #995577; 
1038        font-weight: bold;
1039        text-decoration: underline;
1040        text-underline-offset: 0.2em;
1041        cursor: pointer;
1042        font-size: 0.9em;
1043        padding: 0;
1044        transition: opacity 0.2s;
1045    }
1046    .poem-ai-button:hover { opacity: 0.7; }
1047    .poem-content {
1048        font-size: 1.25em;
1049        white-space: pre-wrap;
1050        margin-bottom: 1.5em;
1051        line-height: 1.9;
1052        color: #1c1917;
1053    }
1054    .poem-footer {
1055        text-align: right;
1056        font-size: 1em;
1057        color: #444;
1058    }
1059    .poem-footer a {
1060        color: #1c1917;
1061        text-decoration: underline;
1062        text-underline-offset: 0.2em;
1063    }
1064    #interpretResult {
1065        display: none; 
1066        font-size: 0.9em; 
1067        padding: 1em; 
1068        text-align: justify; 
1069        line-height: 1.6; 
1070        margin-top: 1.5em;
1071        background: rgba(0, 0, 0, 0.03);
1072        border-radius: 8px;
1073        border-top: 1px solid rgba(0, 0, 0, 0.05);
1074    }
1075
1076    /* --- 7. 游戏模块专属样式 --- */
1077    .game-widget h3 {
1078        margin-top: 0;
1079        font-size: 1.5em;
1080        text-align: center;
1081    }
1082    .game-content { min-height: 150px; transition: opacity 0.3s ease; }
1083    .game-prompt { font-size: 1.2em; font-weight: bold; color: #333; margin-bottom: 0.5em; }
1084    .game-demo {
1085        font-family: 'lxgw', 'Noto Serif SC', serif;
1086        font-size: 1.1em;
1087        font-style: italic;
1088        background: rgba(0, 0, 0, 0.03);
1089        border-radius: 8px;
1090        padding: 1em;
1091        margin-bottom: 1.5em;
1092        white-space: pre-wrap;
1093        line-height: 1.7;
1094    }
1095    .game-area { display: grid; grid-template-columns: 1fr; gap: 0.8em; }
1096    .choice-button {
1097        display: block;
1098        width: 100%;
1099        padding: 0.5em;
1100        font-family: 'Noto Sans SC', sans-serif;
1101        font-size: 1em;
1102        background: rgba(0, 0, 0, 0.05);
1103        border: 2px solid transparent;
1104        border-radius: 8px;
1105        cursor: pointer;
1106        transition: all 0.2s ease;
1107        color: #1c1917;
1108    }
1109    .choice-button:hover:not(:disabled) {
1110        background: rgba(0, 0, 0, 0.08);
1111        border-color: rgba(0, 0, 0, 0.1);
1112    }
1113    .choice-button:disabled { cursor: not-allowed; opacity: 0.8; }
1114    .sortable-list { list-style: none; padding: 0; margin: 0; font-family: 'Noto Serif SC', serif; }
1115    .sortable-item {
1116        background: rgba(0, 0, 0, 0.05);
1117        padding: 1em;
1118        margin-bottom: 0.5em;
1119        border-radius: 8px;
1120        border: 2px solid #ccc;
1121        cursor: grab;
1122        transition: background-color 0.2s;
1123        user-select: none;
1124    }
1125    .sortable-item:hover { background: rgba(0, 0, 0, 0.08); }
1126    .sortable-item.sortable-ghost { opacity: 0.5; background: rgba(0, 0, 0, 0.1); }
1127    .sortable-item.sortable-chosen { cursor: grabbing; }
1128    
1129    /* 游戏反馈区 */
1130    .game-footer {
1131        display: flex;
1132        justify-content: flex-end;
1133        align-items: center;
1134        margin-top: 1.5em;
1135        flex-wrap: wrap; 
1136        gap: 0.8em;
1137    }
1138    .game-feedback {
1139        font-size: 1.2em;
1140        font-weight: bold;
1141        width: 100%; 
1142        margin-right: 0;
1143    }
1144    .game-feedback.correct { color: #559977; }
1145    .game-feedback.incorrect { color: #995577; }
1146    .game-button {
1147        font-family: 'Noto Sans SC', sans-serif;
1148        font-size: 0.9em;
1149        padding: 0.5em 1.5em;
1150        border-radius: 8px;
1151        border: 2px solid #995577;
1152        cursor: pointer;
1153        background: #995577;
1154        color: white;
1155        transition: background-color 0.2s;
1156        display: none;
1157        text-align: center;
1158        white-space: nowrap;
1159        flex-grow: 1; 
1160    }
1161    .game-button:hover { background: #aA6688; }
1162    .game-button.secondary { background: none; border: 2px solid #995577; color: #995577; display: none; }
1163    .game-button.secondary:hover { background: rgba(153, 85, 119, 0.1); }
1164    .game-button.skip { background: none; border: 2px solid #888; color: #555; display: none; }
1165    .game-button.skip:hover { background: rgba(0, 0, 0, 0.1); }
1166    .game-loading { text-align: center; padding: 2em; font-size: 1.2em; color: #555; }
1167    .poem-ai-button.loading::after {
1168        content: '.';
1169        animation: dots 1s steps(5, end) infinite;
1170        display: inline-block;
1171        width: 2em;
1172        text-align: left;
1173    }
1174    @keyframes dots {
1175        0% { content: '.'; } 20% { content: '..'; } 40% { content: '...'; }
1176        60% { content: '....'; } 80% { content: '.....'; } 100% { content: '......'; }
1177    }
1178
1179    /* --- 8. 榜单列表小部件 (List Widget) --- */
1180    .widget-title {
1181        font-size: 1.5em;
1182        font-weight: 600;
1183        text-align: center;
1184        margin: 0 0 1em 0;
1185        color: #333;
1186    }
1187    .list-widget .poem-list-container {
1188        max-height: 30em; 
1189        overflow-y: auto;
1190        margin: 0 -0.5em;
1191        padding: 0 0.5em;
1192        scrollbar-width: thin;
1193        scrollbar-color: #AAAAAA rgba(0, 0, 0, 0.05);
1194    }
1195    .list-widget .poem-list-container::-webkit-scrollbar { width: 8px; }
1196    .list-widget .poem-list-container::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); border-radius: 10px; }
1197    .list-widget .poem-list-container::-webkit-scrollbar-thumb { background-color: #AAAAAA; border-radius: 10px; border: 2px solid rgba(0, 0, 0, 0); background-clip: padding-box; }
1198    .list-widget .poem-list-container::-webkit-scrollbar-thumb:hover { background-color: #888888; }
1199    .list-widget .poem-list-item {
1200        display: flex;
1201        justify-content: space-between;
1202        align-items: center;
1203        text-decoration: none;
1204        color: #333;
1205        padding: 0.8em 1em;
1206        margin: 0.5em 0;
1207        border-radius: 8px;
1208        border-left-width: 6px;
1209        border-left-style: solid;
1210        background: rgba(0, 0, 0, 0.03);
1211        transition: all 0.2s;
1212    }
1213    .list-widget .poem-list-item:hover {
1214        background: rgba(0, 0, 0, 0.07);
1215        transform: translateX(3px);
1216        color: #1c1917;
1217        box-shadow: 0 4px 6px rgba(0,0,0,0.05);
1218    }
1219    .list-widget .poem-list-item .title {
1220        font-weight: 600;
1221        display: block;
1222        overflow: hidden;
1223        text-overflow: ellipsis;
1224        white-space: nowrap;
1225    }
1226    .list-widget .poem-list-item .meta {
1227        font-size: 0.8em;
1228        color: #888;
1229        flex-shrink: 0;
1230        white-space: nowrap;
1231        margin-left: 0.5em;
1232        overflow: hidden;
1233        text-overflow: ellipsis;
1234        white-space: nowrap;
1235    }
1236    .list-item-metric {
1237        font-size: 1.1em;
1238        font-weight: bold;
1239        color: #555;
1240        flex-shrink: 0;
1241        margin-left: 1em;
1242        font-feature-settings: "tnum"; /* 等宽数字 */
1243    }
1244    .empty-list-msg {
1245        text-align: center;
1246        color: #888;
1247        font-style: italic;
1248        padding: 2em 0;
1249    }
1250
1251    /* --- 9. 响应式设计 (匹配 base.html 断点) --- */
1252    
1253    /* 内容特定的小断点 */
1254    @media (max-width: 600px) {
1255        .game-area {
1256            grid-template-columns: 1fr;
1257        }
1258    }
1259    
1260    /* 切换到移动端 (与 base.html 的 960px 断点同步) */
1261    @media (max-width: 960px) {
1262        .widget {
1263            font-size: 1em;
1264        }
1265        #autocomplete-suggestions {
1266            top: 13em; 
1267        }
1268        .stats-widget-grid {
1269            flex-direction: column;
1270        }
1271        .chart-container {
1272            width: 120px;
1273            height: 120px;
1274        }
1275        .chart-legend {
1276            width: 100%;
1277        }
1278        /* 【关键】双栏布局堆叠 */
1279        .masonry-grid {
1280            display: block;
1281        }
1282        .masonry-col {
1283            display: block;
1284        }
1285        .masonry-col > .widget {
1286            margin-bottom: 1.5em;
1287        }
1288        .widget-row {
1289            display: block;
1290            margin-bottom: 0;
1291        }
1292        .widget-row > .widget,
1293        .widget-row > div.widget {
1294            margin-bottom: 1.5em;
1295        }
1296        
1297        .checkbox-container-switch {
1298            flex-direction: column;
1299            align-items: flex-start;
1300            gap: 0.8em; 
1301        }
1302        .checkbox-container-switch label .switch-track {
1303            width: 36px;
1304            height: 20px;
1305            border-radius: 10px;
1306        }
1307        .checkbox-container-switch label .switch-track::after {
1308            width: 16px;
1309            height: 16px;
1310            top: 2px;
1311            left: 2px;
1312        }
1313        .checkbox-container-switch label input:checked + .switch-track::after {
1314            transform: translateX(16px);
1315        }
1316        
1317        .list-widget .poem-list-container {
1318             max-height: 300px; /* 移动端高度小一点 */
1319        }
1320    }
1321
1322    /* ---- 双盲对诗 ---- */
1323    .duishi-side-btn {
1324        flex: 1;
1325        padding: 6px 0;
1326        border-radius: 8px;
1327        border: 1px solid #d8cce6;
1328        background: rgba(255, 255, 255, 0.7);
1329        color: #775599;
1330        font-family: 'lxgw', sans-serif;
1331        font-size: 0.9em;
1332        cursor: pointer;
1333        transition: all 0.15s ease;
1334    }
1335    .duishi-side-btn:hover {
1336        border-color: #775599;
1337    }
1338    .duishi-side-btn.active {
1339        background: #775599;
1340        border-color: #775599;
1341        color: #fff;
1342    }
1343    .duishi-input {
1344        flex: 1;
1345        padding: 8px 12px;
1346        border: 1px solid #ddd;
1347        border-radius: 8px;
1348        font-size: 1em;
1349        font-family: 'lxgw', sans-serif;
1350        color: #333;
1351        background: rgba(255, 255, 255, 0.8);
1352        box-sizing: border-box;
1353        min-width: 0;
1354    }
1355    .duishi-submit {
1356        padding: 8px 16px;
1357        background: #775599;
1358        color: #fff;
1359        border: none;
1360        border-radius: 8px;
1361        cursor: pointer;
1362        font-family: 'lxgw', sans-serif;
1363        font-size: 0.9em;
1364        white-space: nowrap;
1365    }
1366    .duishi-submit:disabled {
1367        opacity: 0.6;
1368        cursor: default;
1369    }
1370    .duishi-again {
1371        display: block;
1372        width: 100%;
1373        margin-top: 10px;
1374        padding: 7px 0;
1375        background: none;
1376        border: 1px solid #775599;
1377        border-radius: 8px;
1378        color: #775599;
1379        font-family: 'lxgw', sans-serif;
1380        font-size: 0.9em;
1381        cursor: pointer;
1382    }
1383    .duishi-again:hover {
1384        background: rgba(119, 85, 153, 0.08);
1385    }
1386    .duishi-line {
1387        line-height: 1.9;
1388        font-size: 1.05em;
1389        color: #444;
1390    }
1391    .duishi-line .duishi-line-author {
1392        color: #999;
1393        font-size: 0.8em;
1394        margin-left: 8px;
1395        white-space: nowrap;
1396    }
1397    .duishi-refresh {
1398        background: none;
1399        border: 1px solid #ccc;
1400        color: #888;
1401        font-size: 0.8em;
1402        padding: 2px 10px;
1403        border-radius: 12px;
1404        cursor: pointer;
1405        font-family: 'lxgw', sans-serif;
1406    }
1407
1408    /* ---- 配对结果:左右对话气泡 ---- */
1409    .duishi-pair {
1410        display: flex;
1411        flex-direction: column;
1412        gap: 10px;
1413    }
1414    .duishi-side {
1415        display: flex;
1416        flex-direction: column;
1417        gap: 4px;
1418        max-width: 88%;
1419    }
1420    .duishi-side.up {
1421        align-items: flex-start;
1422        margin-right: auto;
1423    }
1424    .duishi-side.down {
1425        align-items: flex-end;
1426        margin-left: auto;
1427    }
1428    .duishi-badge {
1429        display: inline-block;
1430        padding: 0;
1431        background: none;
1432        border: none;
1433        border-radius: 0;
1434        color: #888;
1435        font-size: 0.75em;
1436        line-height: 1.6;
1437        white-space: nowrap;
1438    }
1439    /* 作者有代表色时,只在名字左侧加一条竖线 */
1440    .duishi-badge.has-color {
1441        border-left: 2px solid transparent;
1442        padding-left: 6px;
1443    }
1444    .duishi-bubble {
1445        padding: 7px 13px;
1446        border-radius: 12px;
1447        background: rgba(119, 85, 153, 0.10);
1448        color: #333;
1449        font-size: 1.1em;
1450        line-height: 1.75;
1451        word-break: break-word;
1452    }
1453</style>
1454
1455</head>
1456<body>
1457    <div class="glow glow-1"></div>
1458    <div class="glow glow-2"></div>
1459
1460    <div class="hub-container">
1461
1462        <aside class="hub-sidebar desktop-only">
1463    <div class="sidebar-header">
1464        <h1 class="site-title"><a href="/"><img src="/static/pics/title.webp" alt="南洋吟游" class="site-title-image"></a></h1>
1465        <nav class="main-nav">
1466            <ul>
1467                <li><a href="/">首页</a></li>
1468                <li><a href="/submit/">上传作品</a></li>
1469                <li><a href="/random/">随机页面</a></li>
1470                <li><a href="/timeline/2026">时间线</a></li>
1471                <li><a href="/stats/">数据统计</a></li>
1472                <li><a href="/gamestats/">游戏统计</a></li>
1473                <li><a href="/intro/">社团介绍</a></li>
1474                <li><a href="http://cang.sjtuguoxue.space/">数字藏品</a></li>
1475                <li><a href="/nanyang">数字南洋集</a></li>
1476                <li><a href="/checker">格律校验</a></li>
1477                <li><a href="https://music.163.com/m/playlist?id=14332525936" style="color: #993399; font-weight: bold;">🎶 和诗以歌</a></li>
1478                <li><a href="https://mp.weixin.qq.com/s/RoCImUNKl97QmY8PwFSB0A" style="font-weight: bold;">年度报告</a></li>
1479            </ul>
1480        </nav>
1481        <div class="sidebar-user" style="margin-top:12px; padding:10px 0; border-top:1px solid rgba(255,255,255,0.08); font-size:13px;">
1482            
1483            <a href="/login/" style="color:#888; text-decoration:none;">登录</a>
1484            <span style="color:#555; margin:0 4px;">·</span>
1485            <a href="https://onboarding.sjtuguoxue.space/join" style="color:#888; text-decoration:none;">注册</a>
1486            
1487        </div>
1488    </div>
1489    <footer class="sidebar-footer">
1490        <p>Copyright 2022-2026</p>
1491        <p><a href="https://lyy0323.space/writing/">一只椰椰</a></p>
1492        <p>访问量: 3607250</p>
1493        <p>沪ICP备2025137436号</p>
1494    </footer>
1495</aside>
1496        <aside class="hub-right-sidebar desktop-only">
1497    <section class="sidebar-module">
1498    <h3 class="sidebar-section-title">联系方式</h3>
1499    <div class="sidebar-text">
1500        <p><a href="https://onboarding.sjtuguoxue.space/" style="color:#cc99bb;font-weight:bold;">南洋吟游·加入我们</a></p>
1501        <p>邮箱:<a href="mailto:[email protected]">[email protected]</a></p>
1502        <p>QQ:709881123</p>
1503        <p>地址:上海市闵行区东川路800号</p>
1504    </div>
1505    <h1 class="site-title"><img src="/static/pics/name.webp" alt="名片" class="site-title-image-2"></h1>
1506</section>
1507
1508<section class="sidebar-module">
1509    <h3 class="sidebar-section-title">常用链接</h3>
1510    <div class="sidebar-text">
1511        <p><a href="http://sjtuguoxue.space/">sjtuguoxue.space</a></p>
1512        <p><a href="https://guoxue-sjtu.feishu.cn">飞书文档</a></p>
1513        <p><img src="/static/pics/fangcun_logo.svg" style="height:1em;vertical-align:middle;margin-right:4px"><a href="https://write.sjtuguoxue.space">方寸·诗词创作</a></p>
1514        <p style="padding-left:1.2em"><a href="https://write.sjtuguoxue.space/download/android">Android 下载</a></p>
1515        <p><a href="https://yuan.sjtuguoxue.space">文渊札记</a></p>
1516        <p><a href="http://yuefu4anniv.sjtuguoxue.space/">弦歌四载·乐府风采</a></p>
1517    </div>
1518</section>
1519
1520<section class="sidebar-module">
1521    <h3 class="sidebar-section-title">新媒体矩阵</h3>
1522    <div class="sidebar-text">
1523        <p>微信公众号:上海交大国学社</p>
1524        <p>小红书:<a href="https://xhslink.com/m/2zltiBaeek6">交大国学社</a></p>
1525        <p>Bilibili:<a href="https://space.bilibili.com/698232390">上海交通大学戏曲部</a></p>
1526    </div>
1527</section>
1528
1529<section class="sidebar-module">
1530    <h3 class="sidebar-section-title">友情链接</h3>
1531    <div class="sidebar-text">
1532        <p><a href="https://club.jluzhouhang.cn/index.html">吉林大学周行诗社</a></p>
1533    </div>
1534</section>
1535</aside>
1536
1537        <main class="hub-main-content">
1538
1539            <div class="mobile-header mobile-only">
1540    <div class="mobile-header-bar">
1541        <button class="mobile-menu-btn" onclick="toggleDrawer('left')" aria-label="菜单">
1542            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
1543        </button>
1544        <button class="mobile-menu-btn mobile-menu-btn-sm" onclick="history.go(-1)" aria-label="返回">
1545            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
1546        </button>
1547        <a href="/" class="mobile-menu-btn mobile-menu-btn-sm" aria-label="主页">
1548            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
1549        </a>
1550        <a href="/"><img src="/static/pics/title.webp" alt="南洋吟游" class="mobile-title-img"></a>
1551        <div style="flex:1;"></div>
1552        
1553        <a href="/login/" class="mobile-menu-btn" aria-label="登录" style="text-decoration:none;">
1554            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
1555        </a>
1556        
1557        <button class="mobile-menu-btn" onclick="toggleDrawer('right')" aria-label="更多">
1558            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="5" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="1.5" fill="currentColor" stroke="none"/></svg>
1559        </button>
1560    </div>
1561
1562    <!-- overlay -->
1563    <div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawers()"></div>
1564
1565    <!-- left drawer: nav -->
1566    <div class="mobile-drawer mobile-drawer-left" id="drawer-left">
1567        <nav class="drawer-nav">
1568            <a href="/" onclick="closeDrawers()">首页</a>
1569            <a href="/submit/" onclick="closeDrawers()">上传作品</a>
1570            <a href="/random/" onclick="closeDrawers()">随机页面</a>
1571            <a href="/timeline/2026" onclick="closeDrawers()">时间线</a>
1572            <a href="/stats/" onclick="closeDrawers()">数据统计</a>
1573            <a href="/gamestats/" onclick="closeDrawers()">游戏统计</a>
1574            <a href="/intro/" onclick="closeDrawers()">社团介绍</a>
1575            <a href="/nanyang" onclick="closeDrawers()">数字南洋集</a>
1576            <a href="/checker" onclick="closeDrawers()">格律校验</a>
1577            <a href="http://cang.sjtuguoxue.space/">数字藏品</a>
1578            <a href="https://music.163.com/m/playlist?id=14332525936" style="color:#993399;">和诗以歌</a>
1579            <a href="https://mp.weixin.qq.com/s/RoCImUNKl97QmY8PwFSB0A">年度报告</a>
1580        </nav>
1581    </div>
1582
1583    <!-- right drawer: info -->
1584    <div class="mobile-drawer mobile-drawer-right" id="drawer-right">
1585        <div class="drawer-info">
1586            <h4>联系方式</h4>
1587            <p><a href="https://onboarding.sjtuguoxue.space/" style="color:#cc99bb;font-weight:bold;">南洋吟游·加入我们</a></p>
1588            <p>邮箱:<a href="mailto:[email protected]">[email protected]</a></p>
1589            <p>QQ:709881123</p>
1590            <p>地址:上海市闵行区东川路800号</p>
1591
1592            <h4>常用链接</h4>
1593            <p><a href="http://sjtuguoxue.space/">sjtuguoxue.space</a></p>
1594            <p><a href="https://guoxue-sjtu.feishu.cn">飞书文档</a></p>
1595            <p><img src="/static/pics/fangcun_logo.svg" style="height:1em;vertical-align:middle;margin-right:4px"><a href="https://write.sjtuguoxue.space">方寸·诗词创作</a></p>
1596            <p style="padding-left:1.2em"><a href="https://write.sjtuguoxue.space/download/android">Android 下载</a></p>
1597            <p><a href="https://yuan.sjtuguoxue.space">文渊札记</a></p>
1598
1599            <h4>新媒体矩阵</h4>
1600            <p>微信公众号:<a href="http://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MjM5NDgxODE2Ng==&scene=110#wechat_redirect">上海交大国学社</a></p>
1601            <p>小红书:<a href="https://xhslink.com/m/2zltiBaeek6">交大国学社</a></p>
1602            <p>Bilibili:<a href="https://space.bilibili.com/698232390">上海交通大学戏曲部</a></p>
1603
1604            <h4>友情链接</h4>
1605            <p><a href="https://club.jluzhouhang.cn/index.html">吉林大学周行诗社</a></p>
1606
1607            <div style="margin-top:16px; padding-top:12px; border-top:1px solid rgba(255,255,255,0.08); font-size:11px; color:#666;">
1608                <p>Copyright 2022-2026 <a href="https://lyy0323.space/writing/" style="color:#888;">一只椰椰</a></p>
1609                <p>沪ICP备2025137436号</p>
1610            </div>
1611        </div>
1612    </div>
1613</div>
1614<script>
1615function toggleDrawer(side){
1616    closeUserDropdown();
1617    var left=document.getElementById('drawer-left');
1618    var right=document.getElementById('drawer-right');
1619    var ov=document.getElementById('drawer-overlay');
1620    if(side==='left'){
1621        var opening=!left.classList.contains('open');
1622        closeDrawers();
1623        if(opening){left.classList.add('open');ov.classList.add('open');}
1624    } else {
1625        var opening=!right.classList.contains('open');
1626        closeDrawers();
1627        if(opening){right.classList.add('open');ov.classList.add('open');}
1628    }
1629}
1630function closeDrawers(){
1631    document.getElementById('drawer-left').classList.remove('open');
1632    document.getElementById('drawer-right').classList.remove('open');
1633    document.getElementById('drawer-overlay').classList.remove('open');
1634    closeUserDropdown();
1635}
1636function toggleUserDropdown(e){
1637    e.stopPropagation();
1638    var dd=document.getElementById('mobile-user-dropdown');
1639    var wasOpen = dd && dd.classList.contains('open');
1640    closeDrawers();
1641    if(dd && !wasOpen) dd.classList.add('open');
1642}
1643function closeUserDropdown(){
1644    var dd=document.getElementById('mobile-user-dropdown');
1645    if(dd) dd.classList.remove('open');
1646}
1647document.addEventListener('click',function(e){
1648    var dd=document.getElementById('mobile-user-dropdown');
1649    var btn=document.getElementById('mobile-user-btn');
1650    if(dd && !dd.contains(e.target) && btn && !btn.contains(e.target)) closeUserDropdown();
1651});
1652</script>
1652
1653
1654            <div class="main-container">
1655                
1656<!-- <section class="intro-widget image-widget">
1657    <img src="/static/pics/activities/25111501.png" alt="上海交大国学社介绍" class="intro-image" 
1658         onerror="this.style.display='none'">
1659</section> -->
1660<section class="widget banner-widget" id="top-banner-widget">
1661    <div class="banner-slider-container">
1662        <div class="banner-slides" id="banner-slides-wrapper">
1663            <!-- Slides 将由 JS 动态插入 -->
1664        </div>
1665        <button class="banner-nav" id="banner-prev" aria-label="上一张">❮</button>
1666        <button class="banner-nav" id="banner-next" aria-label="下一张">❯</button>
1667        <div class="banner-dots" id="banner-dots-container">
1668            <!-- Dots 将由 JS 动态插入 -->
1669        </div>
1670    </div>
1671</section>
1672<section class="widget search-widget">
1673    <form action="/search/" method="POST" id="main-search-form">
1674        <div class="search-form-container">
1675            <input type="text" name="searchKey" class="search-input" id="search-input-field" autocomplete="off" value="" placeholder="搜索诗词、作者、标签(#)...">
1676            <button class="search-button" type="submit" style="font-size: 1.2em;">
1677                🔍
1678            </button>
1679        </div>
1680        <div id="autocomplete-suggestions"></div>
1681        <div class="checkbox-container-switch">
1682            <label><input type="checkbox" name="include_lyrics" ><span class="switch-track"></span> 包含现代韵文</label>
1683            <label><input type="checkbox" name="vector_search" ><span class="switch-track"></span> 包含语义检索</label>
1684            <label><input type="checkbox" name="unviewed_first" ><span class="switch-track"></span> 小众作品优先</label>
1685        </div>
1686    </form>
1687</section>
1688
1689
1690
1691
1692
1693
1694
1695
1696<!-- Tracking (must be before masonry scripts) -->
1697<script>
1698function trackEvent(type, target) {
1699    fetch('/api/track', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({type:type, target:target||''})}).catch(function(){});
1700}
1701trackEvent('page_view', '/');
1702
1703function drawBlob(canvas, color, seedOffset) {
1704    var ctx = canvas.getContext('2d');
1705    var w = canvas.width, h = canvas.height;
1706    var bg = [255, 255, 255];
1707    var cr = parseInt(color.slice(1,3),16), cg = parseInt(color.slice(3,5),16), cb = parseInt(color.slice(5,7),16);
1708    var seeds = [1.3, 2.1, 0.7, 1.9, 3.1, 0.4, 2.7, 1.5, 0.9];
1709    var cx = w * 0.65, cy = h * 0.35, maxR = Math.min(w, h) * 0.7, layers = 7;
1710    ctx.clearRect(0, 0, w, h);
1711    for (var i = layers; i >= 0; i--) {
1712        var t = i / layers;
1713        var r = maxR * (0.45 + t * 0.55);
1714        var mix = t * 0.85;
1715        var fr = Math.round(cr * (1-mix) + bg[0] * mix);
1716        var fg = Math.round(cg * (1-mix) + bg[1] * mix);
1717        var fb = Math.round(cb * (1-mix) + bg[2] * mix);
1718        var pts = 8, angles = [], radii = [];
1719        for (var p = 0; p < pts; p++) {
1720            angles.push((p / pts) * Math.PI * 2);
1721            radii.push(r * (0.85 + 0.3 * Math.sin(seeds[p % 9] * (i+1) * 1.7 + p * 0.8 + (seedOffset||0))));
1722        }
1723        ctx.beginPath();
1724        for (var p = 0; p <= pts; p++) {
1725            var ai = p % pts;
1726            var ax = cx + Math.cos(angles[ai]) * radii[ai];
1727            var ay = cy + Math.sin(angles[ai]) * radii[ai];
1728            if (p === 0) { ctx.moveTo(ax, ay); continue; }
1729            var pi2 = (p-1) % pts;
1730            var a1 = angles[pi2] + 0.5/pts * Math.PI*2;
1731            var a2 = angles[ai] - 0.5/pts * Math.PI*2;
1732            ctx.bezierCurveTo(
1733                cx + Math.cos(a1)*radii[pi2]*1.05, cy + Math.sin(a1)*radii[pi2]*1.05,
1734                cx + Math.cos(a2)*radii[ai]*1.05, cy + Math.sin(a2)*radii[ai]*1.05,
1735                ax, ay
1736            );
1737        }
1738        ctx.closePath();
1739        ctx.fillStyle = 'rgb('+fr+','+fg+','+fb+')';
1740        ctx.fill();
1741    }
1742}
1743</script>
1743
1744
1745<div class="masonry-grid" id="masonry-grid">
1746<div class="masonry-col" id="masonry-left"></div>
1747<div class="masonry-col" id="masonry-right"></div>
1748</div>
1749<div id="masonry-source" style="display:none;">
1750
1751<!-- 0.5 双盲对诗:提交入口 -->
1752<section class="widget new-poem-widget" id="duishi-entry-card" style="border-left: 5px solid #775599; position:relative; overflow:hidden;">
1753    <canvas class="blob-bg" id="blob-duishi" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1754    <div style="position:relative; z-index:1;">
1755        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
1756            <span style="font-weight:bold; font-size:1.1em; color:#775599;">双盲对诗</span>
1757        </div>
1758
1759        <div id="duishi-form">
1760            <div style="display:flex; gap:6px; margin-bottom:8px;">
1761                <button type="button" class="duishi-side-btn active" data-side="up" onclick="duishiPickSide('up')">写上句</button>
1762                <button type="button" class="duishi-side-btn" data-side="down" onclick="duishiPickSide('down')">写下句</button>
1763            </div>
1764            <div style="display:flex; gap:6px;">
1765                <input type="text" id="duishi-input" class="duishi-input" maxlength="24" autocomplete="off" placeholder="写下你的上句(2-10 个汉字)">
1766                <button type="button" id="duishi-submit-btn" class="duishi-submit" onclick="duishiSubmit()">提交</button>
1767            </div>
1768            <div id="duishi-msg" style="color:#cc5555; font-size:0.85em; margin-top:4px; min-height:1.2em;"></div>
1769        </div>
1770
1771        <div id="duishi-result" style="display:none;">
1772            <div id="duishi-waiting" style="display:none;">
1773                <div style="color:#999; font-size:0.85em;" id="duishi-waiting-side">你的上句</div>
1774                <div id="duishi-waiting-text" style="color:#444; font-size:1.15em; line-height:1.9; margin-bottom:8px;"></div>
1775                <div style="color:#775599; font-size:0.95em;">等待心有灵犀中…</div>
1776            </div>
1777            <div id="duishi-matched" style="display:none;">
1778                <div style="color:#775599; font-size:0.9em; margin-bottom:8px;">心有灵犀,配到啦</div>
1779                <div class="duishi-pair">
1780                    <div class="duishi-side up">
1781                        <span class="duishi-badge" id="duishi-up-badge"></span>
1782                        <div class="duishi-bubble" id="duishi-up-text"></div>
1783                    </div>
1784                    <div class="duishi-side down">
1785                        <span class="duishi-badge" id="duishi-down-badge"></span>
1786                        <div class="duishi-bubble" id="duishi-down-text"></div>
1787                    </div>
1788                </div>
1789            </div>
1790            <button type="button" class="duishi-again" id="duishi-again" onclick="duishiAgain()">再写一个</button>
1791        </div>
1792
1793        <div id="duishi-guest" style="display:none;">
1794            <a href="/login/?next=/" onclick="trackEvent('duishi_login_prompt','duishi');" style="display:inline-block; padding:6px 18px; background:#775599; color:white; border-radius:6px; text-decoration:none; font-size:0.9em; font-family:'lxgw',sans-serif;">登录参与</a>
1795        </div>
1796    </div>
1797</section>
1798
1799<!-- 0.6 双盲对诗:最近对诗 -->
1800<section class="widget new-poem-widget" id="duishi-recent-card" style="border-left: 5px solid #775599; cursor:pointer; position:relative; overflow:hidden;">
1801    <canvas class="blob-bg" id="blob-duishi-recent" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1802    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; position:relative; z-index:1;">
1803        <span style="font-weight:bold; font-size:1.1em; color:#775599;">最近对诗</span>
1804        <button id="duishi-recent-refresh" class="duishi-refresh" onclick="event.stopPropagation(); trackEvent('card_refresh','duishi_recent'); refreshDuishiRecent(true);">换一换</button>
1805    </div>
1806    <div style="position:relative; z-index:1;">
1807        <div id="duishi-recent-up" style="color:#444; line-height:1.8; font-size:1.25em;">加载中…</div>
1808        <div id="duishi-recent-detail" style="display:none; margin-top:10px; padding-top:10px; border-top:1px solid #eee;">
1809            <div class="duishi-pair">
1810                <div class="duishi-side up">
1811                    <span class="duishi-badge" id="duishi-recent-up-badge"></span>
1812                    <div class="duishi-bubble" id="duishi-recent-up-text"></div>
1813                </div>
1814                <div class="duishi-side down">
1815                    <span class="duishi-badge" id="duishi-recent-down-badge"></span>
1816                    <div class="duishi-bubble" id="duishi-recent-down-text"></div>
1817                </div>
1818            </div>
1819        </div>
1820    </div>
1821</section>
1822
1823<!-- 0. 南洋集征稿(限时) -->
1824<section class="widget new-poem-widget" id="nanyang-call-card" style="border-left: 5px solid #ccaa44; position:relative; overflow:hidden; display:none;">
1825    <canvas class="blob-bg" id="blob-nanyang-call" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1826    <div style="position:relative; z-index:1;">
1827        <div style="font-weight:bold; font-size:1.1em; color:#ccaa44; margin-bottom:6px;">南洋集 十一 · 征稿</div>
1828        <div style="color:#666; font-size:0.9em; line-height:1.6; margin-bottom:10px;">2026年《南洋集》征稿即日起至5月31日限时开放,诚挚期待您的作品!</div>
1829        <a href="/nanyang-submit/" style="display:inline-block; padding:6px 18px; background:#ccaa44; color:white; border-radius:6px; text-decoration:none; font-size:0.9em; font-family:'lxgw',sans-serif;">立即提交</a>
1830    </div>
1831</section>
1832<script>
1833(function(){
1834    var deadline = new Date('2026-05-31T23:59:59+08:00');
1835    if (new Date() <= deadline) {
1836        var card = document.getElementById('nanyang-call-card');
1837        if(card) card.style.display = '';
1838    }
1839})();
1840</script>
1840
1841
1842<!-- 1. 新作推荐 -->
1843
1844<section class="widget new-poem-widget" id="new-poem-card" style="border-left: 5px solid #bbffee; cursor:pointer; position:relative; overflow:hidden;">
1845    <canvas class="blob-bg" id="blob-new" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1846    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
1847        <span style="font-weight:bold; font-size:1.1em; color:#995577;">新作推荐</span>
1848        <button id="new-poem-refresh" onclick="event.stopPropagation(); trackEvent('card_refresh','new_poem'); refreshNewPoem();" style="background:none; border:1px solid #ccc; color:#888; font-size:0.8em; padding:2px 10px; border-radius:12px; cursor:pointer; font-family:'lxgw',sans-serif;">换一换</button>
1849    </div>
1850    <div id="new-poem-sentence" style="color:#444; line-height:1.8; font-size:1.25em;">君未见,梦未醒,我未眠。</div>
1851    <div id="new-poem-detail" style="display:none; margin-top:8px; padding-top:8px; border-top:1px solid #eee;">
1852        <div style="margin-bottom:6px;">
1853            <a id="new-poem-title-link" href="/poem/c1baed80" onclick="trackEvent('card_link',this.href)" style="color:#995577; font-weight:bold; text-decoration:none; font-size:1em;">水调歌头·用东坡韵作中秋月其望矣</a>
1854            <span style="color:#999; font-size:0.9em;"> · 广寒居士 · 2026.9.23</span>
1855        </div>
1856        <div id="new-poem-content" style="color:#666; font-size:1em; line-height:1.8; white-space:pre-line;">沉默他乡地,昏乏共秋天。几时明月,一望孤独便穷年。游过光萍灯藻,飘向车池人海,疲久不知寒。无数愁鱼影,泊在水云间。
1857君未见,梦未醒,我未眠。镜中相看,渐次晴暖渐成圆。似是名声酒色,似有嗔情痴意,于此舍求全。如果团圆了,何必更婵娟。</div>
1858    </div>
1859</section>
1860
1861
1862<!-- 2. 古典推荐 -->
1863<section class="widget new-poem-widget" id="classic-rec-card" style="border-left: 5px solid #557799; cursor:pointer; position:relative; overflow:hidden;">
1864    <canvas class="blob-bg" id="blob-classic" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1865    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
1866        <span style="font-weight:bold; font-size:1.1em; color:#557799;">古典推荐</span>
1867        <button id="classic-rec-refresh" onclick="event.stopPropagation(); trackEvent('card_refresh','classic_rec'); refreshClassicRec();" style="background:none; border:1px solid #ccc; color:#888; font-size:0.8em; padding:2px 10px; border-radius:12px; cursor:pointer; font-family:'lxgw',sans-serif;">换一换</button>
1868    </div>
1869    <div id="classic-rec-sentence" style="color:#444; line-height:1.8; font-size:1.25em;">加载中...</div>
1870    <div id="classic-rec-detail" style="display:none; margin-top:8px; padding-top:8px; border-top:1px solid #eee;">
1871        <div style="margin-bottom:6px;">
1872            <span id="classic-rec-title" style="color:#557799; font-weight:bold; font-size:1em;"></span>
1873            <span id="classic-rec-meta" style="color:#999; font-size:0.9em;"></span>
1874        </div>
1875        <div id="classic-rec-content" style="color:#666; font-size:1em; line-height:1.8; white-space:pre-line;"></div>
1876    </div>
1877    <div id="classic-rec-reason" style="display:none; margin-top:8px; padding-top:8px; border-top:1px solid #eee;">
1878        <span style="color:#999; font-size:0.85em;" id="classic-rec-reason-text"></span>
1879    </div>
1880</section>
1881
1882<!-- 2.5 飞花令 -->
1883<section class="widget new-poem-widget" id="feihua-card" style="border-left: 5px solid #ccaa44; position:relative; overflow:hidden;">
1884    <canvas class="blob-bg" id="blob-feihua" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1885    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; position:relative; z-index:1;">
1886        <span style="font-weight:bold; font-size:1.1em; color:#ccaa44;">飞花令 · 今日:<span id="fh-char-display"></span></span>
1887    </div>
1888    <div id="fh-history" style="position:relative; z-index:1;"></div>
1889    <div id="fh-current" style="position:relative; z-index:1;">
1890        <div id="fh-pos-label" style="color:#888; font-size:0.9em; margin-bottom:6px;"></div>
1891        <div style="display:flex; gap:6px;">
1892            <input type="text" id="fh-input" placeholder="" style="flex:1; padding:8px 12px; border:1px solid #ddd; border-radius:8px; font-size:1em; font-family:'lxgw',sans-serif; color:#333; background:rgba(255,255,255,0.8);">
1893            <button id="fh-submit" onclick="fhSubmit()" style="padding:8px 16px; background:#ccaa44; color:white; border:none; border-radius:8px; cursor:pointer; font-family:'lxgw',sans-serif; font-size:0.9em;">提交</button>
1894        </div>
1895        <div id="fh-msg" style="color:#cc5555; font-size:0.85em; margin-top:4px; min-height:1.2em;"></div>
1896    </div>
1897    <div id="fh-done" style="display:none; text-align:center; padding:12px 0; color:#559977; font-weight:bold; font-size:1.1em; position:relative; z-index:1;">
1898        通关!
1899    </div>
1900</section>
1901
1902<!-- 3. 今日历史 -->
1903
1904<section class="widget new-poem-widget" id="today-history-card" style="border-left: 5px solid #559977; cursor:pointer; position:relative; overflow:hidden;">
1905    <canvas class="blob-bg" id="blob-history" width="200" height="200" style="position:absolute; top:-30px; right:-30px; width:200px; height:200px; pointer-events:none; opacity:0.5; z-index:0;"></canvas>
1906    <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
1907        <span style="font-weight:bold; font-size:1.1em; color:#559977;">今日历史</span>
1908        
1909        <button id="history-refresh" onclick="event.stopPropagation(); trackEvent('card_refresh','today_history'); refreshHistory();" style="background:none; border:1px solid #ccc; color:#888; font-size:0.8em; padding:2px 10px; border-radius:12px; cursor:pointer; font-family:'lxgw',sans-serif;">换一换</button>
1910        
1911    </div>
1912    <div id="history-sentence" style="color:#444; line-height:1.8; font-size:1.25em;">胡洪釐兮长保,灵不来兮永受祝于大荒。</div>
1913    <div id="history-detail" style="display:none; margin-top:8px; padding-top:8px; border-top:1px solid #eee;">
1914        <div style="margin-bottom:6px;">
1915            <a id="history-title-link" href="/poem/abe19687" onclick="trackEvent('card_link',this.href)" style="color:#559977; font-weight:bold; text-decoration:none; font-size:1em;">东君</a>
1916            <span id="history-meta" style="color:#999; font-size:0.9em;"> · 承青 · 2025年</span>
1917        </div>
1918        <div id="history-content" style="color:#666; font-size:1em; line-height:1.8; white-space:pre-line;">日出兮西极,日没兮扶桑。
1919金乌晃兮无迹,六龙奔走兮茫茫。
1920回风兮将喑,精灵苦兮常羊。
1921暮昏昏兮袤延,瞻拜兮东皇。
1922天阍频开关,响云门兮浩昌。
1923琼楼比玉槛,文陛列高骧。
1924翠玕合兮环佩,献诗兮琳琅。
1925素娥兮青女,袖联翩兮进瑶浆。
1926善醇醲兮既醉,放帝辇于八极。
1927霞溶溶兮荡波,云冉冉兮开张。
1928淫霖兮有霁,交碧桃兮青棠。
1929鸠呼兮蛙鼓,天烂烂兮迎日光。
1930游兴兮有尽,仙袂重举兮杂芳。
1931缓辔兮回御,星轨断兮下垂霜。
1932天久暗而复祀,耳目娱兮上皇。
1933胡洪釐兮长保,灵不来兮永受祝于大荒。</div>
1934    </div>
1935</section>
1936
1937
1938<!-- 4. 每日一诗 -->
1939<section class="widget random-poem-widget" style="border-left-color: #cfd0ff;">
1940    <div class="poem-header">
1941        <span class="poem-header-title">每日一诗 2026-09-26</span>
1942        <button id="showInterpret" data-poem-id="706d58dd" class="poem-ai-button" onclick="trackEvent('interpret_click','706d58dd')">
1943            查看 AI 赏析
1944        </button>
1945    </div>
1946    <div class="poem-content">春樱三月水阑干。</div>
1947    <div class="poem-footer">
1948        ——林雨夜 《<a href="/poem/706d58dd" onclick="trackEvent('card_link',this.href)">水灯浅梦三首次义山碧城韵(其一)</a>》 2025.3.19
1949    </div>
1950    <div id="interpretResult" style="display: none;"></div>
1951</section>
1952
1953<!-- 5. 小游戏 -->
1954<section class="widget game-widget">
1955    <h3>猜一猜</h3>
1956    <div id="game-container">
1957        <div class="game-loading">正在加载题目...</div>
1958        <div class="game-content" style="display: none;">
1959            <p class="game-prompt"></p>
1960            <div class="game-demo" style="display: none;"></div>
1961            <div class="game-area"></div>
1962            <div class="game-footer">
1963                <div class="game-feedback"></div>
1964                <a id="view-poem-btn" class="game-button secondary" target="_blank" href="#" onclick="trackEvent('game_action','view_poem')">查看作品</a>
1965                <button id="skip-game-btn" class="game-button skip" onclick="trackEvent('game_action','skip')">跳过</button>
1966                <button id="next-game-btn" class="game-button" onclick="trackEvent('game_action','next')">下一题</button>
1967                <button id="submit-sort-btn" class="game-button" onclick="trackEvent('game_action','submit')">提交答案</button>
1968            </div>
1969        </div>
1970    </div>
1971</section>
1972
1973<!-- 6. 统计面板 -->
1974<section class="widget stats-widget">
1975    <div class="stats-widget-grid">
1976        <div class="chart-container">
1977            <div class="donut-chart" style="background: conic-gradient(#995577 0% 57.11139896373057%, #559977 57.11139896373057% 97.91450777202073%, #557799 97.91450777202073% 100%);"></div>
1978        </div>
1979        <ul class="chart-legend">
1980            <li class="legend-item">
1981                <span class="legend-color-box" style="background-color: #995577;"></span>
1982                <span class="legend-text"><a href="/class/1" onclick="trackEvent('stats_click','class/1')" style="color: #333; text-decoration: underline;">诗</a> (4409)</span>
1983                <span class="legend-percent">57.1%</span>
1984            </li>
1985            <li class="legend-item">
1986                <span class="legend-color-box" style="background-color: #559977;"></span>
1987                <span class="legend-text"><a href="/class/2" onclick="trackEvent('stats_click','class/2')" style="color: #333; text-decoration: underline;">词</a> (3150)</span>
1988                <span class="legend-percent">40.8%</span>
1989            </li>
1990            <li class="legend-item">
1991                <span class="legend-color-box" style="background-color: #557799;"></span>
1992                <span class="legend-text"><a href="/class/3" onclick="trackEvent('stats_click','class/3')" style="color: #333; text-decoration: underline;">现代韵文</a> (161)</span>
1993                <span class="legend-percent">2.1%</span>
1994            </li>
1995        </ul>
1996    </div>
1997</section>
1998
1999<!-- 7. 飙升榜 -->
2000<section class="widget list-widget">
2001    <h3 class="widget-title">作品飙升榜 (7日)</h3>
2002    <div class="poem-list-container">
2003        
2004            <a href="/poem/5619f72f" class="poem-list-item" style="border-left-color: #e2e6d5;" onclick="trackEvent('trending_click','5619f72f')">
2005                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2006                    <span class="title">仿乐府子夜春歌·春雨(其一)</span>
2007                    <span class="meta">/ 尘旅</span>
2008                </div>
2009                <span class="list-item-metric">87+</span>
2010            </a>
2011        
2012            <a href="/poem/f909384f" class="poem-list-item" style="border-left-color: #ccddff;" onclick="trackEvent('trending_click','f909384f')">
2013                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2014                    <span class="title">莺啼序·怀秋</span>
2015                    <span class="meta">/ 尤共</span>
2016                </div>
2017                <span class="list-item-metric">86+</span>
2018            </a>
2019        
2020            <a href="/poem/4c731e2c" class="poem-list-item" style="border-left-color: #00ffff;" onclick="trackEvent('trending_click','4c731e2c')">
2021                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2022                    <span class="title">夔州</span>
2023                    <span class="meta">/ 寒花</span>
2024                </div>
2025                <span class="list-item-metric">78+</span>
2026            </a>
2027        
2028            <a href="/poem/47aebd33" class="poem-list-item" style="border-left-color: #397467;" onclick="trackEvent('trending_click','47aebd33')">
2029                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2030                    <span class="title">兰陵王·再临春</span>
2031                    <span class="meta">/ 南厢友人</span>
2032                </div>
2033                <span class="list-item-metric">73+</span>
2034            </a>
2035        
2036            <a href="/poem/c3b8f234" class="poem-list-item" style="border-left-color: #a31eeb;" onclick="trackEvent('trending_click','c3b8f234')">
2037                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2038                    <span class="title">善哉行</span>
2039                    <span class="meta">/ 徐渐</span>
2040                </div>
2041                <span class="list-item-metric">62+</span>
2042            </a>
2043        
2044            <a href="/poem/5b3f086e" class="poem-list-item" style="border-left-color: #00ffff;" onclick="trackEvent('trending_click','5b3f086e')">
2045                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2046                    <span class="title">æµ·</span>
2047                    <span class="meta">/ 寒花</span>
2048                </div>
2049                <span class="list-item-metric">60+</span>
2050            </a>
2051        
2052            <a href="/poem/15ff4b66" class="poem-list-item" style="border-left-color: #a31eeb;" onclick="trackEvent('trending_click','15ff4b66')">
2053                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2054                    <span class="title">還鄉道中暮至楓涇</span>
2055                    <span class="meta">/ 徐渐</span>
2056                </div>
2057                <span class="list-item-metric">53+</span>
2058            </a>
2059        
2060            <a href="/poem/ac68dc16" class="poem-list-item" style="border-left-color: #a31eeb;" onclick="trackEvent('trending_click','ac68dc16')">
2061                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2062                    <span class="title">覽古</span>
2063                    <span class="meta">/ 徐渐</span>
2064                </div>
2065                <span class="list-item-metric">52+</span>
2066            </a>
2067        
2068            <a href="/poem/1d5763b1" class="poem-list-item" style="border-left-color: #a31eeb;" onclick="trackEvent('trending_click','1d5763b1')">
2069                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2070                    <span class="title">從軍行</span>
2071                    <span class="meta">/ 徐渐</span>
2072                </div>
2073                <span class="list-item-metric">49+</span>
2074            </a>
2075        
2076            <a href="/poem/0f1c52d2" class="poem-list-item" style="border-left-color: #000000;" onclick="trackEvent('trending_click','0f1c52d2')">
2077                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2078                    <span class="title">笛家 北隍城</span>
2079                    <span class="meta">/ 玄尚白</span>
2080                </div>
2081                <span class="list-item-metric">49+</span>
2082            </a>
2083        
2084    </div>
2085</section>
2086
2087<!-- 8. 热门榜 -->
2088<section class="widget list-widget">
2089    <h3 class="widget-title">热门作品榜 (总)</h3>
2090    <div class="poem-list-container">
2091        
2092            <a href="/poem/4cab65dc" class="poem-list-item" style="border-left-color: #bb99dd;" onclick="trackEvent('popular_click','4cab65dc')">
2093                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2094                    <span class="title">次韵柳柳望江十咏其三</span>
2095                    <span class="meta">/ 祁水</span>
2096                </div>
2097                <span class="list-item-metric">2255</span>
2098            </a>
2099        
2100            <a href="/poem/91cf6e33" class="poem-list-item" style="border-left-color: #1c5f4f;" onclick="trackEvent('popular_click','91cf6e33')">
2101                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2102                    <span class="title">杂诗九十九</span>
2103                    <span class="meta">/ 承青</span>
2104                </div>
2105                <span class="list-item-metric">2151</span>
2106            </a>
2107        
2108            <a href="/poem/8a8cdf3a" class="poem-list-item" style="border-left-color: #8ccd91;" onclick="trackEvent('popular_click','8a8cdf3a')">
2109                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2110                    <span class="title">夜半归寝</span>
2111                    <span class="meta">/ 明棂晓月</span>
2112                </div>
2113                <span class="list-item-metric">2102</span>
2114            </a>
2115        
2116            <a href="/poem/2ca75a70" class="poem-list-item" style="border-left-color: #1c5f3e;" onclick="trackEvent('popular_click','2ca75a70')">
2117                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2118                    <span class="title">无题</span>
2119                    <span class="meta">/ 希之</span>
2120                </div>
2121                <span class="list-item-metric">2079</span>
2122            </a>
2123        
2124            <a href="/poem/6f8ec919" class="poem-list-item" style="border-left-color: #ecd5e6;" onclick="trackEvent('popular_click','6f8ec919')">
2125                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2126                    <span class="title">鹧鸪天·杂咏</span>
2127                    <span class="meta">/ 令君</span>
2128                </div>
2129                <span class="list-item-metric">2073</span>
2130            </a>
2131        
2132            <a href="/poem/a49b7763" class="poem-list-item" style="border-left-color: #1c5f4f;" onclick="trackEvent('popular_click','a49b7763')">
2133                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2134                    <span class="title">夜作</span>
2135                    <span class="meta">/ 承青</span>
2136                </div>
2137                <span class="list-item-metric">2064</span>
2138            </a>
2139        
2140            <a href="/poem/2c3d280e" class="poem-list-item" style="border-left-color: #1c5f4f;" onclick="trackEvent('popular_click','2c3d280e')">
2141                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2142                    <span class="title">鹧鸪天·步舟远韵兼赠</span>
2143                    <span class="meta">/ 承青</span>
2144                </div>
2145                <span class="list-item-metric">2040</span>
2146            </a>
2147        
2148            <a href="/poem/48f5dea4" class="poem-list-item" style="border-left-color: #e2e2d1;" onclick="trackEvent('popular_click','48f5dea4')">
2149                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2150                    <span class="title">浣溪沙</span>
2151                    <span class="meta">/ 溪烟</span>
2152                </div>
2153                <span class="list-item-metric">2003</span>
2154            </a>
2155        
2156            <a href="/poem/0d46efd2" class="poem-list-item" style="border-left-color: #bb99dd;" onclick="trackEvent('popular_click','0d46efd2')">
2157                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2158                    <span class="title">七律</span>
2159                    <span class="meta">/ 祁水</span>
2160                </div>
2161                <span class="list-item-metric">1965</span>
2162            </a>
2163        
2164            <a href="/poem/2b0d96a8" class="poem-list-item" style="border-left-color: #bb99dd;" onclick="trackEvent('popular_click','2b0d96a8')">
2165                <div style="min-width: 0; margin-right: 0.5em; overflow: hidden;">
2166                    <span class="title">摊破浣溪沙 中夜独行,感秋风起兮</span>
2167                    <span class="meta">/ 祁水</span>
2168                </div>
2169                <span class="list-item-metric">1944</span>
2170            </a>
2171        
2172    </div>
2173</section>
2174
2175</div><!-- end masonry-source -->
2176
2177<!-- Masonry distribution (widgets only, no scripts) -->
2178<script>
2179(function(){
2180    var src = document.getElementById('masonry-source');
2181    var grid = document.getElementById('masonry-grid');
2182    var left = document.getElementById('masonry-left');
2183    var right = document.getElementById('masonry-right');
2184    var widgets = Array.from(src.querySelectorAll(':scope > .widget'));
2185    var isMobile = window.innerWidth <= 960;
2186    if (isMobile) {
2187        widgets.forEach(function(el){ grid.appendChild(el); });
2188        left.remove();
2189        right.remove();
2190    } else {
2191        var leftH = 0, rightH = 0;
2192        widgets.forEach(function(el) {
2193            var col = leftH <= rightH ? left : right;
2194            col.appendChild(el);
2195            leftH = left.offsetHeight;
2196            rightH = right.offsetHeight;
2197        });
2198    }
2199    src.remove();
2200})();
2201</script>
2201
2202
2203<!-- Card init scripts (run after masonry distribution) -->
2204<script>
2205// Draw blobs on recommendation cards
2206
2207var bc1 = document.getElementById('blob-new');
2208if(bc1) drawBlob(bc1, '#995577', 0);
2209
2210var bc2 = document.getElementById('blob-classic');
2211if(bc2) drawBlob(bc2, '#557799', 2.1);
2212var bc4 = document.getElementById('blob-feihua');
2213if(bc4) drawBlob(bc4, '#ccaa44', 6.7);
2214var bc5 = document.getElementById('blob-nanyang-call');
2215if(bc5) drawBlob(bc5, '#ccaa44', 8.2);
2216var bc6 = document.getElementById('blob-duishi');
2217if(bc6) drawBlob(bc6, '#775599', 5.4);
2218var bc7 = document.getElementById('blob-duishi-recent');
2219if(bc7) drawBlob(bc7, '#775599', 9.1);
2220
2221var bc3 = document.getElementById('blob-history');
2222if(bc3) drawBlob(bc3, '#559977', 4.3);
2223
2224
2225(function(){
2226    var card = document.getElementById('new-poem-card');
2227    var detail = document.getElementById('new-poem-detail');
2228    if (!card) return;
2229    fetch('/api/record_view/', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({poem_id: "c1baed80"})});
2230    card.addEventListener('click', function(e){
2231        if (!e.target.closest('a') && !e.target.closest('button')) {
2232            var opening = detail.style.display === 'none';
2233            detail.style.display = opening ? '' : 'none';
2234            if(opening) trackEvent('card_expand','new_poem');
2235        }
2236    });
2237})();
2238
2239
2240function refreshNewPoem(){
2241    fetch('/api/new_poem').then(function(r){return r.json()}).then(function(d){
2242        if(!d.id) return;
2243        document.getElementById('new-poem-sentence').textContent = d.sentence;
2244        document.getElementById('new-poem-title-link').textContent = d.title;
2245        document.getElementById('new-poem-title-link').href = '/poem/' + d.id;
2246        document.getElementById('new-poem-title-link').nextElementSibling.textContent = ' · ' + d.author + ' · ' + d.date;
2247        document.getElementById('new-poem-content').textContent = d.content;
2248        document.getElementById('new-poem-detail').style.display = 'none';
2249        document.getElementById('new-poem-card').style.borderLeftColor = d.color;
2250        fetch('/api/record_view/', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({poem_id: d.id})});
2251    });
2252}
2253
2254(function(){
2255    var card = document.getElementById('classic-rec-card');
2256    var detail = document.getElementById('classic-rec-detail');
2257    var reason = document.getElementById('classic-rec-reason');
2258    var expanded = false;
2259    card.addEventListener('click', function(e){
2260        if(e.target.closest('button') || e.target.closest('a')) return;
2261        expanded = !expanded;
2262        detail.style.display = expanded ? '' : 'none';
2263        reason.style.display = expanded ? '' : 'none';
2264        if(expanded) trackEvent('card_expand','classic_rec');
2265    });
2266    var cached = localStorage.getItem('classicRec');
2267    if(cached){ try { renderClassicRec(JSON.parse(cached)); } catch(e){ loadClassicRec(); } }
2268    else { loadClassicRec(); }
2269})();
2270function loadClassicRec(){
2271    fetch('/api/classic_recommend').then(function(r){return r.json()}).then(function(d){
2272        if(!d.ok) return;
2273        localStorage.setItem('classicRec', JSON.stringify(d));
2274        renderClassicRec(d);
2275    }).catch(function(){ document.getElementById('classic-rec-sentence').textContent = '暂无推荐'; });
2276}
2277function renderClassicRec(d){
2278    document.getElementById('classic-rec-sentence').textContent = d.sentence;
2279    document.getElementById('classic-rec-title').textContent = '《' + d.title + '》';
2280    document.getElementById('classic-rec-meta').textContent = ' · ' + (d.dynasty||'') + ' · ' + d.author;
2281    document.getElementById('classic-rec-content').textContent = d.content;
2282    document.getElementById('classic-rec-reason-text').innerHTML =
2283        '和' + d.source_author + '《<a href="/poem/' + d.source_id + '" onclick="trackEvent(\'card_link\',this.href)" style="color:#995577; text-decoration:none;">' + d.source_title + '</a>》相似度 ' + d.score;
2284    document.getElementById('classic-rec-detail').style.display = 'none';
2285    document.getElementById('classic-rec-reason').style.display = 'none';
2286}
2287function refreshClassicRec(){
2288    localStorage.removeItem('classicRec');
2289    document.getElementById('classic-rec-sentence').textContent = '加载中...';
2290    document.getElementById('classic-rec-detail').style.display = 'none';
2291    document.getElementById('classic-rec-reason').style.display = 'none';
2292    loadClassicRec();
2293}
2294
2295
2296(function(){
2297    var card = document.getElementById('today-history-card');
2298    var detail = document.getElementById('history-detail');
2299    if (!card) return;
2300    card.addEventListener('click', function(e){
2301        if(e.target.closest('button') || e.target.closest('a')) return;
2302        var opening = detail.style.display === 'none';
2303        detail.style.display = opening ? '' : 'none';
2304        if(opening) trackEvent('card_expand','today_history');
2305    });
2306})();
2307
2308function refreshHistory(){
2309    fetch('/api/today_history').then(function(r){return r.json()}).then(function(d){
2310        if(!d.ok && !d.id) return;
2311        document.getElementById('history-sentence').textContent = d.sentence;
2312        document.getElementById('history-title-link').textContent = d.title;
2313        document.getElementById('history-title-link').href = '/poem/' + d.id;
2314        document.getElementById('history-meta').textContent = ' · ' + d.author + ' · ' + d.year + '年';
2315        document.getElementById('history-content').textContent = d.content;
2316        document.getElementById('history-detail').style.display = 'none';
2317        document.getElementById('today-history-card').style.borderLeftColor = d.color;
2318    });
2319}
2320
2321// --- 飞花令 ---
2322(function(){
2323    var chars = ['月','花','风','雪','春','秋','山','水','云','雨','酒','梦','心','夜','天','红','青','白','长','归'];
2324    var today = new Date(Date.now() + 8*3600000);
2325    var dayKey = today.toISOString().slice(0,10);
2326    var todayChar = chars[parseInt(dayKey.replace(/-/g,'')) % chars.length];
2327    var storeKey = 'feihua_' + dayKey;
2328
2329    document.getElementById('fh-char-display').textContent = '“' + todayChar + '”';
2330    document.getElementById('fh-input').placeholder = '请输入含“' + todayChar + '”的七言诗句...';
2331
2332    var state = JSON.parse(localStorage.getItem(storeKey) || 'null');
2333    if (!state || state.char !== todayChar) {
2334        state = { char: todayChar, currentPos: 1, history: [] };
2335    }
2336    saveState();
2337    render();
2338
2339    function saveState() { localStorage.setItem(storeKey, JSON.stringify(state)); }
2340
2341    function render() {
2342        var histEl = document.getElementById('fh-history');
2343        histEl.innerHTML = '';
2344        state.history.forEach(function(h) {
2345            histEl.innerHTML += '<div style="margin-bottom:6px; padding:4px 0; border-bottom:1px solid #f0f0f0;">'
2346                + '<div style="color:#333; font-size:0.95em;">第' + h.pos + '位 · <span style="color:#995577;">你:</span>' + h.userText + '</div>'
2347                + '<div style="color:#666; font-size:0.9em; margin-top:2px; display:flex; align-items:center; gap:6px;"><img src="/static/pics/tianquan_avatar.webp" style="width:20px;height:20px;border-radius:50%;flex-shrink:0;"><span><span style="color:#557799;">天权:</span>' + h.sysText + ' —— ' + h.sysAuthor + '《' + h.sysTitle + '》</span></div>'
2348                + '</div>';
2349        });
2350        if (state.currentPos > 7) {
2351            document.getElementById('fh-current').style.display = 'none';
2352            document.getElementById('fh-done').style.display = '';
2353        } else {
2354            document.getElementById('fh-current').style.display = '';
2355            document.getElementById('fh-done').style.display = 'none';
2356            document.getElementById('fh-pos-label').textContent = '第 ' + state.currentPos + ' 位';
2357        }
2358    }
2359
2360    window.fhSubmit = function() {
2361        var input = document.getElementById('fh-input').value.trim();
2362        var msg = document.getElementById('fh-msg');
2363        msg.textContent = '';
2364        if (!input) { msg.textContent = '请输入诗句'; return; }
2365
2366        var clean = input.replace(/[,。!?、;:""''()\s]/g, '');
2367        if (clean.length !== 5 && clean.length !== 7) {
2368            msg.textContent = '请输入五言或七言诗句(' + clean.length + '字)';
2369            return;
2370        }
2371        var pos = state.currentPos;
2372        if (clean.charAt(pos - 1) !== todayChar) {
2373            msg.textContent = '“' + todayChar + '”应在第' + pos + '位,当前第' + pos + '位是“' + clean.charAt(pos-1) + '”';
2374            return;
2375        }
2376
2377        msg.textContent = '验证中...';
2378        msg.style.color = '#888';
2379        document.getElementById('fh-submit').disabled = true;
2380
2381        fetch('https://shi.sjtuguoxue.space/api/search/text?q=' + encodeURIComponent(clean) + '&limit=1')
2382            .then(function(r){ return r.json(); })
2383            .then(function(d) {
2384                if (!d.total || d.total === 0) {
2385                    msg.style.color = '#cc5555';
2386                    msg.textContent = '未找到此诗句,请检查或换一句';
2387                    document.getElementById('fh-submit').disabled = false;
2388                    return;
2389                }
2390                msg.textContent = '正确!天权接招...';
2391                msg.style.color = '#559977';
2392                trackEvent('card_expand', 'feihua');
2393
2394                fetch('https://shi.sjtuguoxue.space/api/feihua?char=' + encodeURIComponent(todayChar) + '&position=' + pos + '&length=7&limit=10')
2395                    .then(function(r){ return r.json(); })
2396                    .then(function(fd) {
2397                        var sysResult = fd.results && fd.results.length > 0
2398                            ? fd.results[Math.floor(Math.random() * fd.results.length)]
2399                            : { text: '(天权无句可出)', author: '', title: '' };
2400
2401                        state.history.push({
2402                            pos: pos,
2403                            userText: input,
2404                            sysText: sysResult.text,
2405                            sysAuthor: sysResult.author || '佚名',
2406                            sysTitle: sysResult.title || ''
2407                        });
2408                        state.currentPos = pos + 1;
2409                        saveState();
2410                        render();
2411                        document.getElementById('fh-input').value = '';
2412                        document.getElementById('fh-submit').disabled = false;
2413                        msg.textContent = '';
2414                    });
2415            })
2416            .catch(function() {
2417                msg.style.color = '#cc5555';
2418                msg.textContent = '网络错误,请重试';
2419                document.getElementById('fh-submit').disabled = false;
2420            });
2421    };
2422
2423    document.getElementById('fh-input').addEventListener('keydown', function(e) {
2424        if (e.key === 'Enter') fhSubmit();
2425    });
2426})();
2427</script>
2427
2428
2429
2430            </div>
2431
2432            <footer class="mobile-footer mobile-only">
2433    <div class="sidebar-footer">
2434        <p>Copyright 2022-2026 <a href="https://lyy0323.space/writing/">一只椰椰</a> | 访问量: 3607250</p>
2435        <p>沪ICP备2025137436号</p>
2436    </div>
2437</footer>
2438
2439            <footer class="medium-footer">
2440                <section class="sidebar-module">
2441    <h3 class="sidebar-section-title">联系方式</h3>
2442    <div class="sidebar-text">
2443        <p><a href="https://onboarding.sjtuguoxue.space/" style="color:#cc99bb;font-weight:bold;">南洋吟游·加入我们</a></p>
2444        <p>邮箱:<a href="mailto:[email protected]">[email protected]</a></p>
2445        <p>QQ:709881123</p>
2446        <p>地址:上海市闵行区东川路800号</p>
2447    </div>
2448    <h1 class="site-title"><img src="/static/pics/name.webp" alt="名片" class="site-title-image-2"></h1>
2449</section>
2450
2451<section class="sidebar-module">
2452    <h3 class="sidebar-section-title">常用链接</h3>
2453    <div class="sidebar-text">
2454        <p><a href="http://sjtuguoxue.space/">sjtuguoxue.space</a></p>
2455        <p><a href="https://guoxue-sjtu.feishu.cn">飞书文档</a></p>
2456        <p><img src="/static/pics/fangcun_logo.svg" style="height:1em;vertical-align:middle;margin-right:4px"><a href="https://write.sjtuguoxue.space">方寸·诗词创作</a></p>
2457        <p style="padding-left:1.2em"><a href="https://write.sjtuguoxue.space/download/android">Android 下载</a></p>
2458        <p><a href="https://yuan.sjtuguoxue.space">文渊札记</a></p>
2459        <p><a href="http://yuefu4anniv.sjtuguoxue.space/">弦歌四载·乐府风采</a></p>
2460    </div>
2461</section>
2462
2463<section class="sidebar-module">
2464    <h3 class="sidebar-section-title">新媒体矩阵</h3>
2465    <div class="sidebar-text">
2466        <p>微信公众号:上海交大国学社</p>
2467        <p>小红书:<a href="https://xhslink.com/m/2zltiBaeek6">交大国学社</a></p>
2468        <p>Bilibili:<a href="https://space.bilibili.com/698232390">上海交通大学戏曲部</a></p>
2469    </div>
2470</section>
2471
2472<section class="sidebar-module">
2473    <h3 class="sidebar-section-title">友情链接</h3>
2474    <div class="sidebar-text">
2475        <p><a href="https://club.jluzhouhang.cn/index.html">吉林大学周行诗社</a></p>
2476    </div>
2477</section>
2478            </footer>
2479
2480        </main>
2481
2482    </div> 
2483    
2484    
2485
2486<script>
2487  function streamText(element, text, speed = 15) {
2488    element.innerHTML = '';
2489    element.style.display = 'block';
2490    let currentText = '';
2491    let i = 0;
2492    const formattedText = text.replace(/\\t/g, '\t').replace(/\\n/g, '\n').replace(/\\"/g, '"');
2493    const intervalId = setInterval(() => {
2494      if (i < formattedText.length) {
2495        currentText += formattedText[i];
2496        element.innerHTML = currentText
2497                              .replace(/(?:\r\n|\r|\n)/g, '<br>')
2498                              .replace(/\t/g, '&emsp;&emsp;');
2499        i++;
2500      } else {
2501        clearInterval(intervalId);
2502      }
2503    }, speed);
2504  }
2505  document.body.addEventListener('click', function(e) {
2506    if (e.target && e.target.id == 'showInterpret') {
2507      const button = e.target;
2508      const poemId = button.getAttribute('data-poem-id');
2509      const resultEl = document.getElementById('interpretResult');
2510      if (button.disabled || !resultEl) return;
2511      button.disabled = true;
2512      button.textContent = 'AI 赏析生成中';
2513      button.classList.add('loading');
2514      const originalText = '查看 AI 赏析';
2515      setTimeout(() => {
2516        button.textContent = '加载中';
2517        fetch('/api/interpret/' + poemId)
2518          .then(response => {
2519            if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
2520            return response.json();
2521          })
2522          .then(data => {
2523            if (data.ok && data.result) {
2524              button.style.display = 'none';
2525              streamText(resultEl, data.result, 15);
2526            } else {
2527              throw new Error(data.error || '暂无赏析');
2528            }
2529          })
2530          .catch(err => {
2531            console.error('Error fetching or processing interpretation:', err);
2532            if(resultEl) {
2533                resultEl.innerHTML = '赏析加载失败,请稍后再试。可能是今天的赏析尚未生成。';
2534                resultEl.style.display = 'block';
2535            }
2536            button.disabled = false;
2537            button.textContent = originalText;
2538            button.classList.remove('loading');
2539          });
2540      }, 3000);
2541    }
2542  });
2543</script>
2543
2544
2545<script>
2546document.addEventListener('DOMContentLoaded', function() {
2547    const bannerWidget = document.getElementById('top-banner-widget');
2548    const slidesWrapper = document.getElementById('banner-slides-wrapper');
2549    const dotsContainer = document.getElementById('banner-dots-container');
2550    const prevButton = document.getElementById('banner-prev');
2551    const nextButton = document.getElementById('banner-next');
2552    
2553    let slidesData = [];
2554    let currentIndex = 0;
2555    let slideInterval;
2556    const AUTO_PLAY_DELAY = 5000; // 5 秒
2557
2558    async function fetchBannerData() {
2559        try {
2560            // 假设 JSON 文件放在 static 目录下
2561            // 注意: 请确保 /static/banner_data.json 路径是正确的
2562            const response = await fetch("/static/banner_data.json?v=1790307595");
2563            if (!response.ok) {
2564                throw new Error(`HTTP error! status: ${response.status}`);
2565            }
2566            const data = await response.json();
2567
2568            // --- 修改开始: 过滤过期的活动 ---
2569            // 获取 YYYY-MM-DD 格式的今天日期
2570            const today = new Date().toISOString().split('T')[0];
2571            
2572            const validData = data.filter(item => {
2573                // 如果没有 'activityDate' 字段 (老数据),则始终显示
2574                if (!item.activityDate) {
2575                    return true;
2576                }
2577                // 如果有日期,则比较日期
2578                return item.activityDate >= today;
2579            });
2580            // --- 修改结束 ---
2581            
2582            // 校验数据 (使用过滤后的 validData)
2583            if (Array.isArray(validData) && validData.length >= 2) {
2584                slidesData = validData; // 使用过滤后的 validData
2585                initBanner();
2586            } else {
2587                console.warn('Banner data is invalid or has less than 2 items (after filtering). Banner will be hidden.');
2588                bannerWidget.style.display = 'none';
2589            }
2590        } catch (error) {
2591            console.error('Failed to fetch banner data:', error);
2592            bannerWidget.style.display = 'none'; // 加载失败时隐藏
2593        }
2594    }
2595
2596    function initBanner() {
2597        // 动态创建 slides 和 dots
2598        slidesWrapper.innerHTML = '';
2599        dotsContainer.innerHTML = '';
2600
2601        slidesData.forEach((item, index) => {
2602            // 创建 Slide
2603            const slide = document.createElement('a');
2604            slide.href = item.targetUrl;
2605            slide.className = 'banner-slide';
2606            slide.target = '_blank';
2607            slide.rel = 'noopener noreferrer';
2608            slide.onclick = function(){ if(typeof trackEvent==='function') trackEvent('banner_click', item.caption); };
2609
2610            slide.innerHTML = `
2611                <img src="${item.imageUrl}" class="banner-image" alt="${item.caption}" onerror="this.parentElement.style.display='none'">
2612                <div class="banner-caption">${item.caption}</div>
2613            `;
2614            slidesWrapper.appendChild(slide);
2615
2616            // 创建 Dot
2617            const dot = document.createElement('button');
2618            dot.className = 'banner-dot';
2619            dot.setAttribute('aria-label', `Go to slide ${index + 1}`);
2620            dot.addEventListener('click', () => {
2621                goToSlide(index);
2622                resetInterval();
2623            });
2624            dotsContainer.appendChild(dot);
2625        });
2626
2627        // 添加事件监听
2628        prevButton.addEventListener('click', () => {
2629            goToSlide(currentIndex - 1);
2630            resetInterval();
2631        });
2632        nextButton.addEventListener('click', () => {
2633            goToSlide(currentIndex + 1);
2634            resetInterval();
2635        });
2636        
2637        // 触摸滑动支持 (简化版)
2638        let touchStartX = 0;
2639        bannerWidget.addEventListener('touchstart', (e) => touchStartX = e.touches[0].clientX);
2640        bannerWidget.addEventListener('touchend', (e) => {
2641            let touchEndX = e.changedTouches[0].clientX;
2642            if (touchStartX - touchEndX > 50) { // 向左滑
2643                nextButton.click();
2644            } else if (touchEndX - touchStartX > 50) { // 向右滑
2645                prevButton.click();
2646            }
2647        });
2648
2649
2650        // 初始化
2651        bannerWidget.style.display = 'block'; // 显示 Banner
2652        goToSlide(0); // 显示第一张
2653        startInterval(); // 开始自动播放
2654    }
2655
2656    function goToSlide(index) {
2657        const totalSlides = slidesData.length;
2658        // 循环播放
2659        if (index < 0) {
2660            currentIndex = totalSlides - 1;
2661        } else if (index >= totalSlides) {
2662            currentIndex = 0;
2663        } else {
2664            currentIndex = index;
2665        }
2666
2667        // 移动 slides
2668        slidesWrapper.style.transform = `translateX(-${currentIndex * 100}%)`;
2669
2670        // 更新 dots
2671        const dots = dotsContainer.querySelectorAll('.banner-dot');
2672        dots.forEach((dot, i) => {
2673            dot.classList.toggle('active', i === currentIndex);
2674        });
2675    }
2676    
2677    function startInterval() {
2678        clearInterval(slideInterval); // 清除旧的
2679        slideInterval = setInterval(() => {
2680            goToSlide(currentIndex + 1);
2681        }, AUTO_PLAY_DELAY);
2682    }
2683    
2684    function resetInterval() {
2685        startInterval(); // 重置计时器
2686    }
2687
2688    // 开始加载数据
2689    fetchBannerData();
2690});
2691</script>
2691
2692
2693<script>
2694document.addEventListener('DOMContentLoaded', function() {
2695    const searchInput = document.getElementById('search-input-field');
2696    const suggestionsBox = document.getElementById('autocomplete-suggestions');
2697    const searchForm = document.getElementById('main-search-form');
2698    let debounceTimer;
2699    searchInput.addEventListener('input', function() {
2700        const prefix = this.value;
2701        clearTimeout(debounceTimer);
2702        if (prefix.length === 0) {
2703            hideSuggestions();
2704            return;
2705        }
2706        debounceTimer = setTimeout(() => {
2707            fetchSuggestions(prefix);
2708        }, 300);
2709    });
2710    async function fetchSuggestions(prefix) {
2711        try {
2712            const response = await fetch(`/api/autocomplete?q=${encodeURIComponent(prefix)}`);
2713            if (!response.ok) throw new Error('Network error');
2714            const suggestions = await response.json();
2715            showSuggestions(suggestions);
2716        } catch (error) {
2717            console.error('Error fetching autocomplete:', error);
2718            hideSuggestions();
2719        }
2720    }
2721    function showSuggestions(suggestions) {
2722        if (suggestions.length === 0) {
2723            hideSuggestions();
2724            return;
2725        }
2726        suggestionsBox.innerHTML = '';
2727        suggestions.forEach(text => {
2728            const item = document.createElement('div');
2729            item.className = 'suggestion-item';
2730            item.textContent = text;
2731            item.addEventListener('click', function() {
2732                searchInput.value = text;
2733                hideSuggestions();
2734                searchForm.submit();
2735            });
2736            suggestionsBox.appendChild(item);
2737        });
2738        suggestionsBox.style.display = 'block';
2739    }
2740    function hideSuggestions() {
2741        suggestionsBox.style.display = 'none';
2742    }
2743    document.addEventListener('click', function(e) {
2744        if (!e.target.closest('.search-widget')) {
2745            hideSuggestions();
2746        }
2747    });
2748    searchInput.addEventListener('keydown', function(e) {
2749        if (e.key === 'Enter' && suggestionsBox.style.display === 'block') {
2750            if (suggestionsBox.children.length > 0) {
2751                e.preventDefault();
2752            }
2753        }
2754    });
2755});
2756</script>
2756
2757
2758<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
2758
2759
2760<script>
2761document.addEventListener('DOMContentLoaded', function() {
2762    const gameContainer = document.getElementById('game-container');
2763    if (!gameContainer) return;
2764    const loadingEl = gameContainer.querySelector('.game-loading');
2765    const contentEl = gameContainer.querySelector('.game-content');
2766    const promptEl = gameContainer.querySelector('.game-prompt');
2767    const demoEl = gameContainer.querySelector('.game-demo');
2768    const gameArea = gameContainer.querySelector('.game-area');
2769    const feedbackEl = gameContainer.querySelector('.game-feedback');
2770    const nextBtn = document.getElementById('next-game-btn');
2771    const submitSortBtn = document.getElementById('submit-sort-btn');
2772    const viewPoemBtn = document.getElementById('view-poem-btn');
2773    const skipBtn = document.getElementById('skip-game-btn');
2774    let currentGameData = null;
2775    let isAnswered = false;
2776
2777    function logGameAnswer(logData) {
2778        fetch('/api/log_game_answer', {
2779            method: 'POST',
2780            headers: {'Content-Type': 'application/json'},
2781            body: JSON.stringify(logData)
2782        })
2783        .then(response => {
2784            if (!response.ok) console.error('游戏日志记录失败 (API 响应非 200)。');
2785        })
2786        .catch(error => console.error('游戏日志记录失败 (网络错误):', error));
2787    }
2788
2789    async function fetchGame() {
2790        isAnswered = false;
2791        contentEl.style.display = 'none';
2792        loadingEl.style.display = 'block';
2793        feedbackEl.textContent = '';
2794        nextBtn.style.display = 'none';
2795        submitSortBtn.style.display = 'none';
2796        viewPoemBtn.style.display = 'none';
2797        skipBtn.style.display = 'none';
2798        
2799        const storedGame = localStorage.getItem('unansweredGame');
2800        if (storedGame) {
2801            console.log("从 localStorage 加载未答题目。");
2802            currentGameData = JSON.parse(storedGame);
2803            renderGame(currentGameData);
2804            return;
2805        }
2806
2807        console.log("localStorage 为空,从 API 获取新题目。");
2808        try {
2809            const response = await fetch('/api/game_question/');
2810            if (!response.ok) throw new Error('Network error');
2811            const data = await response.json();
2812            if (data.ok && data.content) {
2813                localStorage.setItem('unansweredGame', JSON.stringify(data.content));
2814                currentGameData = data.content;
2815                renderGame(currentGameData);
2816            } else {
2817                throw new Error('API 响应格式不正确');
2818            }
2819        } catch (error) {
2820            console.error('加载题目失败:', error);
2821            loadingEl.textContent = '加载题目失败,请刷新。';
2822        }
2823    }
2824
2825    function renderGame(data) {
2826        promptEl.textContent = data.prompt;
2827        if (data.demo) {
2828            demoEl.textContent = data.demo;
2829            demoEl.style.display = 'block';
2830        } else {
2831            demoEl.style.display = 'none';
2832        }
2833        if (data.type === 'multiple_choice') {
2834            renderMultipleChoice(data.choices);
2835        } else if (data.type === 'sorting') {
2836            renderSortingGame(data.choices);
2837        }
2838        loadingEl.style.display = 'none';
2839        contentEl.style.display = 'block';
2840    }
2841
2842    function renderMultipleChoice(choices) {
2843        gameArea.innerHTML = '';
2844        gameArea.style.gridColumn = '';
2845        choices.forEach(text => {
2846            const btn = document.createElement('button');
2847            btn.className = 'choice-button';
2848            btn.textContent = text;
2849            btn.addEventListener('click', () => {
2850                handleChoiceClick(btn, text, currentGameData.goal);
2851            });
2852            gameArea.appendChild(btn);
2853        });
2854        skipBtn.style.display = 'none';
2855        submitSortBtn.style.display = 'none';
2856    }
2857    
2858    function handleChoiceClick(clickedButton, selectedChoice, correctChoice) {
2859        if (isAnswered) return;
2860        isAnswered = true;
2861        localStorage.removeItem('unansweredGame');
2862        const isCorrect = (selectedChoice === correctChoice);
2863        if (isCorrect) {
2864            feedbackEl.textContent = '回答正确!';
2865            feedbackEl.className = 'game-feedback correct';
2866        } else {
2867            feedbackEl.textContent = `回答错误。正确答案: ${correctChoice}`;
2868            feedbackEl.className = 'game-feedback incorrect';
2869        }
2870        logGameAnswer({
2871            content: currentGameData,
2872            is_correct: isCorrect ? 1 : 0,
2873            attempt_count: 1
2874        });
2875        gameArea.querySelectorAll('.choice-button').forEach(btn => {
2876            btn.disabled = true;
2877            if (btn.textContent === correctChoice) {
2878                btn.style.borderColor = '#559977';
2879                btn.style.backgroundColor = 'rgba(85, 153, 119, 0.2)';
2880            } else if (btn === clickedButton) {
2881                btn.style.borderColor = '#995577';
2882                btn.style.backgroundColor = 'rgba(153, 85, 119, 0.2)';
2883            }
2884        });
2885        if (currentGameData.poem_id) {
2886            viewPoemBtn.href = `/poem/${currentGameData.poem_id}`;
2887            viewPoemBtn.style.display = 'inline-block';
2888        }
2889        nextBtn.style.display = 'inline-block';
2890    }
2891
2892    function renderSortingGame(choices) {
2893        gameArea.innerHTML = '';
2894        const list = document.createElement('ul');
2895        list.className = 'sortable-list';
2896        list.id = 'sortable-list';
2897        choices.forEach(text => {
2898            const item = document.createElement('li');
2899            item.className = 'sortable-item';
2900            item.textContent = text;
2901            list.appendChild(item);
2902        });
2903        gameArea.appendChild(list);
2904        skipBtn.style.display = 'inline-block';
2905        submitSortBtn.style.display = 'inline-block'; 
2906        new Sortable(list, {
2907            animation: 150,
2908            forceFallback: true,
2909            onStart: function () {
2910                if (window.innerWidth <= 768) document.body.style.overflow = 'hidden';
2911            },
2912            onEnd: function () {
2913                if (window.innerWidth <= 768) document.body.style.overflow = 'auto';
2914            }
2915        });
2916    }
2917    
2918    function handleSubmitSort() {
2919        if (isAnswered) return;
2920        isAnswered = true;
2921        skipBtn.style.display = 'none'
2922        localStorage.removeItem('unansweredGame');
2923        const items = gameArea.querySelectorAll('.sortable-item');
2924        const currentOrder = Array.from(items).map(item => item.textContent);
2925        const correctOrder = currentGameData.goal;
2926        let isCorrect = true;
2927        for (let i = 0; i < currentOrder.length; i++) {
2928            if (currentOrder[i] !== correctOrder[i]) {
2929                isCorrect = false;
2930                break;
2931            }
2932        }
2933        if (isCorrect) {
2934            feedbackEl.textContent = '排序正确!';
2935            feedbackEl.className = 'game-feedback correct';
2936        } else {
2937            feedbackEl.textContent = '排序错误。';
2938            feedbackEl.className = 'game-feedback incorrect';
2939        }
2940        logGameAnswer({
2941            content: currentGameData,
2942            is_correct: isCorrect ? 1 : 0,
2943            attempt_count: 1
2944        });
2945        const sortableList = document.getElementById('sortable-list');
2946        const sortableInstance = Sortable.get(sortableList);
2947        if (sortableInstance) sortableInstance.destroy();
2948        submitSortBtn.style.display = 'none';
2949        if (currentGameData.poem_id) {
2950            viewPoemBtn.href = `/poem/${currentGameData.poem_id}`;
2951            viewPoemBtn.style.display = 'inline-block';
2952        }
2953        nextBtn.style.display = 'inline-block';
2954    }
2955
2956    function handleSkipGame() {
2957        if (isAnswered) return;
2958        isAnswered = true;
2959        logGameAnswer({
2960            content: currentGameData,
2961            is_correct: -1,
2962            attempt_count: 0
2963        });
2964        localStorage.removeItem('unansweredGame');
2965        fetchGame();
2966    }
2967    
2968    nextBtn.addEventListener('click', fetchGame);
2969    submitSortBtn.addEventListener('click', handleSubmitSort);
2970    skipBtn.addEventListener('click', handleSkipGame);
2971
2972    fetchGame();
2973});
2974</script>
2974
2975
2976<script>
2977// ---- 双盲对诗 ----
2978// 配对结果统一渲染:上句在左、下句在右,作者名 badge 贴在各自气泡上方;
2979// badge 默认无边框,仅当作者有代表色时在名字左侧加一条竖线。
2980window.duishiFillBadge = function(el, penName, color) {
2981    if (!el) { return; }
2982    el.textContent = penName || '佚名';
2983    if (color) {
2984        el.className = 'duishi-badge has-color';
2985        el.style.borderLeftColor = color;
2986    } else {
2987        el.className = 'duishi-badge';
2988        el.style.borderLeftColor = '';
2989    }
2990};
2991
2992window.duishiFillPair = function(prefix, pair) {
2993    window.duishiFillBadge(document.getElementById(prefix + '-up-badge'), pair.upPenName, pair.upColor);
2994    window.duishiFillBadge(document.getElementById(prefix + '-down-badge'), pair.downPenName, pair.downColor);
2995    var upText = document.getElementById(prefix + '-up-text');
2996    var downText = document.getElementById(prefix + '-down-text');
2997    if (upText) { upText.textContent = pair.upText; }
2998    if (downText) { downText.textContent = pair.downText; }
2999};
3000
3001(function(){
3002    var entry = document.getElementById('duishi-entry-card');
3003    if (!entry) return;
3004
3005    var LOGGED_IN = false;
3006    var STATE_KEY = 'duishiState';
3007
3008    var formEl = document.getElementById('duishi-form');
3009    var resultEl = document.getElementById('duishi-result');
3010    var guestEl = document.getElementById('duishi-guest');
3011    var waitingEl = document.getElementById('duishi-waiting');
3012    var matchedEl = document.getElementById('duishi-matched');
3013    var msgEl = document.getElementById('duishi-msg');
3014    var inputEl = document.getElementById('duishi-input');
3015    var submitBtn = document.getElementById('duishi-submit-btn');
3016
3017    var side = 'up';
3018
3019    function readState() {
3020        try { return JSON.parse(localStorage.getItem(STATE_KEY) || 'null'); }
3021        catch (e) { return null; }
3022    }
3023    function writeState(state) {
3024        try { localStorage.setItem(STATE_KEY, JSON.stringify(state)); } catch (e) {}
3025    }
3026    function clearState() {
3027        try { localStorage.removeItem(STATE_KEY); } catch (e) {}
3028    }
3029    function sideLabel(value) { return value === 'down' ? '下句' : '上句'; }
3030
3031    function showForm() {
3032        formEl.style.display = '';
3033        resultEl.style.display = 'none';
3034    }
3035
3036    function renderWaiting(pair) {
3037        formEl.style.display = 'none';
3038        resultEl.style.display = '';
3039        waitingEl.style.display = '';
3040        matchedEl.style.display = 'none';
3041        var mine = pair.mySide === 'down' ? 'down' : 'up';
3042        document.getElementById('duishi-waiting-side').textContent = '你的' + sideLabel(mine);
3043        document.getElementById('duishi-waiting-text').textContent =
3044            mine === 'down' ? pair.downText : pair.upText;
3045    }
3046
3047    function renderMatched(pair) {
3048        formEl.style.display = 'none';
3049        resultEl.style.display = '';
3050        waitingEl.style.display = 'none';
3051        matchedEl.style.display = '';
3052        window.duishiFillPair('duishi', pair);
3053    }
3054
3055    function applyState(state) {
3056        if (!state || !state.pair) { showForm(); return; }
3057        if (state.pair.matched) { renderMatched(state.pair); } else { renderWaiting(state.pair); }
3058    }
3059
3060    window.duishiPickSide = function(next) {
3061        side = next === 'down' ? 'down' : 'up';
3062        var btns = entry.querySelectorAll('.duishi-side-btn');
3063        for (var i = 0; i < btns.length; i++) {
3064            var isActive = btns[i].getAttribute('data-side') === side;
3065            btns[i].className = 'duishi-side-btn' + (isActive ? ' active' : '');
3066        }
3067        inputEl.placeholder = '写下你的' + sideLabel(side) + '(2-10 个汉字)';
3068        msgEl.textContent = '';
3069    };
3070
3071    window.duishiSubmit = function() {
3072        var text = (inputEl.value || '').trim();
3073        msgEl.textContent = '';
3074        if (!text) { msgEl.textContent = '请先写一句诗'; return; }
3075
3076        submitBtn.disabled = true;
3077        fetch('/api/duishi/submit', {
3078            method: 'POST',
3079            headers: {'Content-Type': 'application/json'},
3080            body: JSON.stringify({side: side, text: text})
3081        }).then(function(r){
3082            return r.json().then(function(body){ return {status: r.status, body: body}; })
3083                .catch(function(){ return {status: r.status, body: null}; });
3084        }).then(function(res){
3085            submitBtn.disabled = false;
3086            var body = res.body;
3087            if (!body || !body.ok) {
3088                if (res.status === 401) { showGuest(); return; }
3089                msgEl.textContent = (body && body.message) || '提交失败,请重试';
3090                return;
3091            }
3092            trackEvent('duishi_submit', side);
3093            var state = {side: side, pair: body.pair};
3094            writeState(state);
3095            inputEl.value = '';
3096            applyState(state);
3097        }).catch(function(){
3098            submitBtn.disabled = false;
3099            msgEl.textContent = '网络错误,请重试';
3100        });
3101    };
3102
3103    window.duishiAgain = function() {
3104        trackEvent('duishi_again', 'duishi');
3105        clearState();
3106        inputEl.value = '';
3107        msgEl.textContent = '';
3108        showForm();
3109    };
3110
3111    function showGuest() {
3112        guestEl.style.display = '';
3113        formEl.style.display = 'none';
3114        resultEl.style.display = 'none';
3115    }
3116
3117    if (!LOGGED_IN) {
3118        showGuest();
3119    } else {
3120        var state = readState();
3121        showForm();
3122        if (state && state.pair && state.pair.id) {
3123            fetch('/api/duishi/me/latest?id=' + encodeURIComponent(state.pair.id))
3124                .then(function(r){ return r.json(); })
3125                .then(function(data){
3126                    if (!data || !data.ok) { return; }
3127                    var current = readState();
3128                    if (!current || !current.pair || current.pair.id !== state.pair.id) return;
3129                    if (data.pair) {
3130                        var next = {side: state.side, pair: data.pair};
3131                        writeState(next);
3132                        applyState(next);
3133                    } else {
3134                        // 缓存的记录已隐藏,或不属于当前登录用户。
3135                        clearState();
3136                        showForm();
3137                    }
3138                }).catch(function(){});
3139        }
3140        inputEl.addEventListener('keydown', function(e) {
3141            if (e.key === 'Enter') { window.duishiSubmit(); }
3142        });
3143    }
3144})();
3145
3146// ---- 最近对诗 ----
3147(function(){
3148    var card = document.getElementById('duishi-recent-card');
3149    if (!card) return;
3150
3151    var upEl = document.getElementById('duishi-recent-up');
3152    var detailEl = document.getElementById('duishi-recent-detail');
3153    var currentId = null;
3154    var expanded = false;
3155
3156    function render(pair) {
3157        if (!pair) {
3158            currentId = null;
3159            upEl.textContent = '还没有人对上,来做第一个吧';
3160            detailEl.style.display = 'none';
3161            return;
3162        }
3163        currentId = pair.id;
3164        expanded = false;
3165        upEl.textContent = pair.upText + '……';
3166        detailEl.style.display = 'none';
3167        window.duishiFillPair('duishi-recent', pair);
3168    }
3169
3170    window.refreshDuishiRecent = function() {
3171        var url = '/api/duishi/recent' + (currentId ? '?exclude=' + encodeURIComponent(currentId) : '');
3172        fetch(url).then(function(r){ return r.json(); }).then(function(data){
3173            if (data && data.ok) { render(data.pair); }
3174        }).catch(function(){});
3175    };
3176
3177    card.addEventListener('click', function(e){
3178        if (e.target.closest('button') || e.target.closest('a')) return;
3179        if (!currentId) return;
3180        expanded = !expanded;
3181        detailEl.style.display = expanded ? '' : 'none';
3182        if (expanded) {
3183            trackEvent('card_expand', 'duishi_recent');
3184            fetch('/api/duishi/record_view', {
3185                method: 'POST',
3186                headers: {'Content-Type': 'application/json'},
3187                body: JSON.stringify({id: currentId})
3188            }).catch(function(){});
3189        }
3190    });
3191
3192    refreshDuishiRecent();
3193})();
3194</script>
3194
3195
3196
3197</body>
3198</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.