Forráskód Böngészése

Index page update

ryp 3 hete
szülő
commit
9496eefa10

BIN
app/controllers/.user_msg.php.swn


+ 0 - 1
app/controllers/certs_manage.php

@@ -50,7 +50,6 @@
 	$bg_form->handle = function () {
 		global $myUser;
 		$url = DB::escape($_POST['bgurl']);
-		error_log($url);
 		DB::update("update user_info set bgurl = '$url' where username = '{$myUser['username']}'");
 	};
 

+ 4 - 1
app/controllers/contest_inside.php

@@ -122,6 +122,9 @@
 				$rating_vols = array();
 				if (!isset($contest['extra_config']['unrated'])) {
 					calcRatingGlicko2($standings, $ratings, $rating_rds, $rating_vols);
+					error_log("RATING");
+					error_log(print_r($standings, true));
+					error_log(print_r($ratings, true));
 				} else {
 					for ($i = 0; $i < count($standings); $i++) {
 						$ratings[$i] = $standings[$i][2][1];
@@ -148,7 +151,7 @@ EOD;
 EOD;
 					}
 					sendSystemMsg($user['username'], 'Rating变化通知', $content);
-					DB::query("update user_info set rating = {$ratings[$i]}, rating_rd = {$rating_rd[$i]}, rating_vol = {$rating_vols[$i]} where username = '{$standings[$i][2][0]}'");
+					DB::update("update user_info set rating = {$ratings[$i]}, rating_rd = {$rating_rds[$i]}, rating_vol = {$rating_vols[$i]} where username = '{$standings[$i][2][0]}'");
 					DB::query("update contests_registrants set rank = {$standings[$i][3]} where contest_id = {$contest['id']} and username = '{$standings[$i][2][0]}'");
 				}
 				DB::query("update contests set status = 'finished' where id = {$contest['id']}");

+ 1 - 0
app/controllers/group_manage.php

@@ -84,6 +84,7 @@
 			if (strpos($text, '#') != false || strpos($text, ',') != false)
 				return '不能含有井号或逗号';
 			if (mb_strlen($text) > 8) return '称号过长' . $text;
+			if (mb_strlen($text) == 0) return '不能为空';
 			return '';
 		}, null
 	);

+ 892 - 573
app/controllers/index.php

@@ -1,573 +1,892 @@
-<?php
-$blogs = DB::selectAll("select blogs.id, title, poster, post_time from important_blogs, blogs where is_hidden = 0 and important_blogs.blog_id = blogs.id order by level desc, important_blogs.blog_id desc limit 8");
-$activateusers = DB::selectAll("select submitter, count(*) as cnt from submissions where datediff(now(), submit_time) <= 30 group by submitter order by cnt desc limit 12;");
-$topusers = DB::selectAll("select u.username, u.rating, u.rating_rd, u.rating_vol, u.motto, u.qq, count(*) cnt from user_info u inner join contests_registrants c on u.username = c.username inner join contests s on s.id = c.contest_id where datediff(now(), s.start_time) <= 180 group by c.username having cnt >= 3 order by rating desc, username asc limit 12;");
-$hotusers = DB::selectAll("select username, rating, rating_rd, rating_vol, qq, motto from user_info order by rating_vol desc, username asc limit 12;");
-$topblogs = DB::selectAll("select id from blogs where is_hidden = false and zan >= 10 order by zan desc limit 10;");
-$recentblogs = DB::selectAll("select id from blogs where is_hidden = false and zan < 10 order by post_time desc limit 10;");
-$songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
-
-// 最近比赛:未结束的按开始时间升序取,不足 5 场时补最近结束的
-$recent_contests = array();
-$result = DB::selectAll("select * from contests where status != 'finished' order by start_time asc, id asc limit 5");
-foreach ($result as $contest) {
-	genMoreContestInfo($contest);
-	$recent_contests[] = $contest;
-}
-if (count($recent_contests) < 5) {
-	$need = 5 - count($recent_contests);
-	$result = DB::selectAll("select * from contests where status = 'finished' order by start_time desc, id desc limit $need");
-	foreach ($result as $contest) {
-		genMoreContestInfo($contest);
-		$recent_contests[] = $contest;
-	}
-}
-?>
-<?php echoUOJPageHeader(UOJConfig::$data['profile']['oj-name-short']) ?>
-<div id="songs-data" data-songs='<?php echo $songs_data; ?>'></div>
-
-<div class="row my-4 wyoj-topcard">
-	<div class="card-body">
-		<div class="row">
-			<div class="col-sm-12 col-md-9">
-				<table class="table table-sm">
-				  	<tbody>
-					<?php $now_cnt = 0; ?>
-					<?php foreach ($blogs as $blog): ?>
-						<?php
-							$now_cnt++;
-							$new_tag = '';
-							if ((time() - strtotime($blog['post_time'])) / 3600 / 24 <= 7) {
-								$new_tag = '<sup style="color:red">&nbsp;new</sup>';
-							}
-						?>
-						<tr>
-							<td><a href="/blogs/<?= $blog['id'] ?>"><?= $blog['title'] ?></a><?= $new_tag ?></td>
-							<td>by <?= getUserLink($blog['poster']) ?></td>
-							<td><small><?= $blog['post_time'] ?></small></td>
-						</tr>
-					<?php endforeach ?>
-					<?php for ($i = $now_cnt + 1; $i <= 8; $i++): ?>
-						<tr><td colspan="233">&nbsp;</td></tr>
-					<?php endfor ?>
-						<tr><td class="text-right" colspan="233">
-							<span>本歌曲由 </span><span id="recommender"></span><span> 推荐</span>
-						</td></tr>
-						<tr><td class="text-right" colspan="233">
-							<a href="https://git.ryp.org.cn/wyoj/" class="text-info font-weight-bold">WyOJ 以及所有相关项目均为自由软件。</a>
-							<a href="/announcements"><?= UOJLocale::get('all the announcements') ?></a>
-						</td></tr>
-						<tr>
-							<td class="text-right" colspan="233">
-								<span class="text-danger font-weight-bold">声明:本网站属个人非经营性网站,仅用于学习交流,无任何盈利目的,与任何组织无关</span>
-								<a href="/certs-manage/songs">我要推歌</a>
-							</td>
-						</tr>
-					</tbody>
-				</table>
-			</div>
-			<div class="col-xs-6 col-sm-4 col-md-3">
-	<div class="card" style="max-width:300px; width:100%; border-radius:0; ">
-        <div class="image-frame" style="aspect-ratio:1/1; position:relative;">
-            <img id="photo" src="" alt="" style="width:100%; height:100%; object-fit:cover; display:block; cursor:pointer; -webkit-tap-highlight-color:transparent;">
-        </div>
-        <div class="intro" style="padding:1.6rem 1.8rem 1.2rem; text-align:center; background:#ffffff;">
-            <p id="name" style="font-size:1.0rem; font-weight:800; letter-spacing:1px; margin-bottom:0.5rem; line-height:1.2; margin-block:0;"></p>
-            <p id="amb" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; border-top:1px dashed #cdd9e3; padding-top:0.8rem; white-space:pre-wrap; margin-block:0;"></p>
-            <p id="writer" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; white-space:pre-wrap; margin-block:0;"></p>
-        </div>
-        <div class="button-wrapper" style="padding:0.2rem 1.8rem 2rem 1.8rem; background:#ffffff;">
-            <style>
-                #randomBtn{
-                    display: inline-flex;
-                    align-items: center;
-                    justify-content: center;
-                    gap: 0.6rem;
-                    width: 100%;
-                    background: #1f2f3b;
-                    color: white;
-                    padding: 0.7rem 0;
-                    cursor: pointer;
-                    transition: all 0.2s ease;
-                    letter-spacing: 0.5px;
-                    border: 1px solid rgba(255, 255, 255, 0.2);
-                    backdrop-filter: blur(4px);
-                    font-family: inherit;
-                    line-height: 1;
-                }
-            </style>
-            <button id="randomBtn">换一首</button>
-        </div>
-    </div>
-    <script>
-        (function() {
-            let dataDiv = document.getElementById('songs-data');
-	    let library = JSON.parse(dataDiv.dataset.songs);
-
-            const photoImg = document.getElementById('photo');
-            const nameEl = document.getElementById('name');
-            const ambEl = document.getElementById('amb');
-            const writerEl = document.getElementById('writer');
-            const randomBtn = document.getElementById('randomBtn');
-            const recommenderEl = document.getElementById('recommender');
-
-            let lastIndex = -1;
-            let currentMusicLink = library[0].musiclink; 
-
-            photoImg.addEventListener('mousedown', function(e) {
-                this.style.opacity = '0.8';
-            });
-            photoImg.addEventListener('mouseup', function(e) {
-                this.style.opacity = '';
-            });
-            photoImg.addEventListener('mouseleave', function(e) {
-                this.style.opacity = '';
-            });
-            photoImg.addEventListener('touchstart', function(e) {
-                this.style.opacity = '0.8';
-            });
-            photoImg.addEventListener('touchend', function(e) {
-                this.style.opacity = '';
-            });
-            photoImg.addEventListener('touchcancel', function(e) {
-                this.style.opacity = '';
-            });
-            function setBtnActive() {
-                randomBtn.style.background = '#17242e';
-                randomBtn.style.transform = 'scale(0.98)';
-                randomBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
-            }
-            function setBtnNormal() {
-                randomBtn.style.background = '#1f2f3b';
-                randomBtn.style.transform = 'scale(1)';
-                randomBtn.style.boxShadow = 'none';
-            }
-            randomBtn.addEventListener('mousedown', setBtnActive);
-            randomBtn.addEventListener('mouseup', setBtnNormal);
-            randomBtn.addEventListener('mouseleave', setBtnNormal);
-            randomBtn.addEventListener('touchstart', setBtnActive);
-            randomBtn.addEventListener('touchend', setBtnNormal);
-            randomBtn.addEventListener('touchcancel', setBtnNormal);
-
-            photoImg.addEventListener('click', function() {
-                if (currentMusicLink) {
-                    window.open(currentMusicLink, '_blank');
-                }
-            });
-	    function randmusic() {
-	        let randid = lastIndex;
-	        while (randid === lastIndex) {
-	            randid = Math.floor(Math.random() * library.length);
-		}
-
-		if (lastIndex == -1) randid = 3;
-
-		lastIndex = randid;
-                const ys = library[randid];
-                photoImg.src = ys.cover;
-                photoImg.alt = ys.name;
-                nameEl.textContent = ys.name;
-                ambEl.textContent = ys.album;
-		writerEl.textContent = ys.writer;
-		recommenderEl.textContent = ys.user;
-                currentMusicLink = ys.click;
-            }
-
-            randmusic();
-
-            randomBtn.addEventListener('click', function() {
-                randmusic();
-            });
-
-        })();
-    </script>
-</div>
-		</div>
-	</div>
-</div>
-<?php
-// ---- 日期 ----
-$todayTs   = time();
-$dayNum    = (int)date('j', $todayTs);
-$monthNum  = (int)date('n', $todayTs);
-$wday      = (int)date('w', $todayTs);
-$monthCN   = ['一','二','三','四','五','六','七','八','九','十','十一','十二'];
-$weekCN    = ['日','一','二','三','四','五','六'];
-$monthText = $monthCN[$monthNum - 1] . '月';
-$weekText  = '星期' . $weekCN[$wday];
-
-// ---- NOIP 倒计时 ----
-$noipTs        = strtotime('2026-11-28');
-$todayMidnight = strtotime(date('Y-m-d', $todayTs));
-$daysLeft      = (int)floor(($noipTs - $todayMidnight) / 86400);
-?>
-
-<?php
-$todayTs   = time();
-$dayNum    = (int)date('j', $todayTs);
-$monthNum  = (int)date('n', $todayTs);
-$wday      = (int)date('w', $todayTs);
-$monthCN   = ['一','二','三','四','五','六','七','八','九','十','十一','十二'];
-$weekCN    = ['日','一','二','三','四','五','六'];
-
-$bigMonths = [1,3,5,7,8,10,12];
-if ($monthNum == 2) {
-	$sizeChar = '平';           // 闰年可改成 '闰'
-} elseif (in_array($monthNum, $bigMonths)) {
-	$sizeChar = '大';
-} else {
-	$sizeChar = '小';
-}
-$monthText = $monthCN[$monthNum - 1] . '月' . $sizeChar;
-$weekText  = '星期' . $weekCN[$wday];
-
-$todayMidnight = strtotime(date('Y-m-d', $todayTs));
-$noipTs        = strtotime('2026-11-28');
-$daysLeft      = max(0, (int)floor(($noipTs - $todayMidnight) / 86400));
-$csj1Ts        = strtotime('2026-09-19');
-$csp1daysLeft  = max(0, (int)floor(($csp1Ts - $todayMidnight) / 86400));
-$csp2Ts        = strtotime('2026-10-31');
-$csp2daysLeft  = max(0, (int)floor(($csp2Ts - $todayMidnight) / 86400));
-$gk27Ts        = strtotime('2027-06-07');
-$gk27daysLeft  = max(0, (int)floor(($gk27Ts - $todayMidnight) / 86400));
-$gk28Ts        = strtotime('2028-06-07');
-$gk28daysLeft  = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
-?>
-
-<div class="row my-4">
-    <!-- ============ 左列:日程 + 博客 ============ -->
-    <div class="col-12 col-lg-9">
-        <!-- 黑色日期卡片 -->
-        <div class="mb-4 wyoj-date-card" id="wyoj-date-card"
-             style="position:relative;overflow:hidden;background:#000;color:#fff;padding:2rem 1.5rem;border-radius:0;text-shadow:0 1px 3px rgba(0,0,0,0.75);">
-
-            <!-- 随机方格背景:canvas -->
-            <canvas id="wyoj-date-canvas"
-                    style="position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none;"></canvas>
-
-            <!-- 内容层 -->
-            <div style="position:relative;z-index:1;">
-                <div class="d-flex justify-content-center align-items-center">
-                    <div style="writing-mode:vertical-rl;font-size:1.5rem;font-weight:bold;letter-spacing:0.3em;line-height:1;">
-                        <?= $monthText ?>
-                    </div>
-                    <div class="mx-4" style="font-size:5rem;font-weight:600;line-height:1;">
-                        <?= $dayNum ?>
-                    </div>
-                    <div style="writing-mode:vertical-rl;font-size:1.5rem;font-weight:bold;letter-spacing:0.3em;line-height:1;">
-                        <?= $weekText ?>
-                    </div>
-                </div>
-                    <div class="text-center mt-4" style="font-size:1rem;letter-spacing:0.05em;">
-                        <?php if ($daysLeft > 0): ?>
-                            距 <strong>NOIP2026</strong> 还有 <strong><?= $daysLeft ?></strong> 天
-                        <?php elseif ($daysLeft === 0): ?>
-                            <strong>NOIP2026</strong> 就是今天
-                        <?php else: ?>
-                            <strong>NOIP2026</strong> 已结束
-                        <?php endif; ?>
-                        <br/>
-                        <?php if ($csp1daysLeft > 0): ?>
-                            距 <strong>CSP-J/S 2026 第一轮</strong> 还有 <strong><?= $csp1daysLeft ?></strong> 天
-                       <?php elseif ($csp1daysLeft === 0): ?>
-                            <strong>CSP-J/S 2026 第一轮</strong> 就是今天
-                        <?php else: ?>
-                            <strong>CSP-J/S 2026 第一轮</strong> 已结束
-                        <?php endif; ?>
-                        <br/>
-                        <?php if ($csp2daysLeft > 0): ?>
-                            距 <strong>CSP-J/S 2026 第二轮</strong> 还有 <strong><?= $csp2daysLeft ?></strong> 天
-                        <?php elseif ($csp2daysLeft === 0): ?>
-                            <strong>CSP-J/S 2026 第二轮</strong> 就是今天!
-                        <?php else: ?>
-                            <strong>CSP-J/S 2026 第二轮</strong> 已结束
-                        <?php endif; ?>
-                        <br/>
-                        <?php if ($gk27daysLeft > 0): ?>
-                            距 <strong>2027 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk27daysLeft ?></strong> 天
-                        <?php elseif ($gk27daysLeft === 0): ?>
-                            <strong>2027 年普通高等学校招生全国统一考试</strong> 就是今天!
-                        <?php else: ?>
-                            <strong>2027 年普通高等学校招生全国统一考试</strong> 已结束
-                        <?php endif; ?>
-                        <br/>
-                        <?php if ($gk28daysLeft > 0): ?>
-                            距 <strong>2028 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk28daysLeft ?></strong> 天
-                        <?php elseif ($gk28daysLeft === 0): ?>
-                            <strong>2028 年普通高等学校招生全国统一考试</strong> 就是今天!
-                        <?php else: ?>
-                            <strong>2028 年普通高等学校招生全国统一考试</strong> 已结束
-                        <?php endif; ?>
-                    </div>
-                </div>
-        </div>
-        <script>
-        (function () {
-            var card   = document.getElementById('wyoj-date-card');
-            var canvas = document.getElementById('wyoj-date-canvas');
-            if (!card || !canvas) return;
-
-            var ctx = canvas.getContext('2d');
-
-            // ===== 可调参数 =====
-            var COLORS   = ['#000000', '#0c0c0c', '#181818', '#242424', '#303030'];
-            var CELL     = 26;     // 每格边长(px)
-            var P        = 0.3;    // 每格每轮变色概率(期望值)
-            var PERIOD   = 1500;   // 刷新间隔(ms)
-            var Z        = 1.96;   // 95% 置信区间
-            var USE_DPR  = false;  // 是否按设备像素比放大 canvas(false 更省 CPU)
-            // ====================
-
-            var dpr = USE_DPR ? (window.devicePixelRatio || 1) : 1;
-            var cols = 0, rows = 0, nCells = 0;
-            var lastW = 0, lastH = 0;
-            var gridColors = [];
-            var idx = null;
-            var xs = null, ys = null;
-
-            var rafId = null;
-            var running = false;
-            var lastTick = 0;
-
-            function randColor() {
-                return COLORS[(Math.random() * COLORS.length) | 0];
-            }
-
-            function computeBounds() {
-                var w = card.clientWidth;
-                var h = card.clientHeight;
-                xs = new Int32Array(cols + 1);
-                ys = new Int32Array(rows + 1);
-                for (var i = 0; i <= cols; i++) xs[i] = Math.round(i * w / cols);
-                for (var j = 0; j <= rows; j++) ys[j] = Math.round(j * h / rows);
-            }
-
-            function build() {
-                var w = card.clientWidth;
-                var h = card.clientHeight;
-                if (w === 0 || h === 0) return;
-                if (w === lastW && h === lastH) return;   // 尺寸没变就不重建
-                lastW = w; lastH = h;
-
-                cols = Math.max(1, Math.ceil(w / CELL));
-                rows = Math.max(1, Math.ceil(h / CELL));
-                nCells = cols * rows;
-
-                canvas.width  = Math.round(w * dpr);
-                canvas.height = Math.round(h * dpr);
-                canvas.style.width  = w + 'px';
-                canvas.style.height = h + 'px';
-                ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
-                ctx.imageSmoothingEnabled = false;
-
-                computeBounds();
-
-                gridColors = [];
-                for (var y = 0; y < rows; y++) {
-                    var row = [];
-                    for (var x = 0; x < cols; x++) row.push(randColor());
-                    gridColors.push(row);
-                }
-
-                idx = new Int32Array(nCells);
-                for (var i = 0; i < nCells; i++) idx[i] = i;
-
-                redrawAll();
-            }
-
-            function redrawAll() {
-                for (var y = 0; y < rows; y++) {
-                    var y0 = ys[y], y1 = ys[y + 1];
-                    for (var x = 0; x < cols; x++) {
-                        ctx.fillStyle = gridColors[y][x];
-                        ctx.fillRect(xs[x], y0, xs[x + 1] - xs[x], y1 - y0);
-                    }
-                }
-            }
-
-            function tick() {
-                if (nCells === 0) return;
-
-                var mu    = nCells * P;
-                var sigma = Math.sqrt(nCells * P * (1 - P));
-                var lo    = Math.max(0, Math.floor(mu - Z * sigma));
-                var hi    = Math.min(nCells, Math.ceil(mu + Z * sigma));
-                var m     = lo + Math.floor(Math.random() * (hi - lo + 1));
-
-                for (var i = 0; i < m; i++) {
-                    var j = i + Math.floor(Math.random() * (nCells - i));
-                    var t = idx[i]; idx[i] = idx[j]; idx[j] = t;
-                }
-
-                for (var k = 0; k < m; k++) {
-                    var ci = idx[k];
-                    var x  = ci % cols;
-                    var y  = (ci / cols) | 0;
-                    var c  = randColor();
-                    gridColors[y][x] = c;
-                    ctx.fillStyle = c;
-                    ctx.fillRect(xs[x], ys[y], xs[x + 1] - xs[x], ys[y + 1] - ys[y]);
-                }
-            }
-
-            function loop(now) {
-                rafId = null;
-                if (now - lastTick >= PERIOD) {
-                    lastTick = now;
-                    tick();
-                }
-                if (running) rafId = requestAnimationFrame(loop);
-            }
-
-            function start() {
-                if (running) return;
-                running = true;
-                lastTick = performance.now();
-                rafId = requestAnimationFrame(loop);
-            }
-
-            function stop() {
-                running = false;
-                if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
-            }
-
-            build();
-
-            if (window.ResizeObserver) {
-                new ResizeObserver(function () { build(); }).observe(card);
-            } else {
-                window.addEventListener('resize', build);
-            }
-
-            if (window.IntersectionObserver) {
-                new IntersectionObserver(function (entries) {
-                    if (entries[0].isIntersecting) start(); else stop();
-                }, { threshold: 0 }).observe(card);
-            } else {
-                start();
-            }
-
-            document.addEventListener('visibilitychange', function () {
-                if (document.hidden) stop(); else start();
-            });
-        })();
-        </script>
-
-	<!-- 博客列表 -->
-        <div class="row">
-            <?php foreach ($recentblogs as $blog): ?>
-                <?= echoBlogCard(queryBlog($blog['id'])) ?>
-            <?php endforeach ?>
-            <?php foreach ($topblogs as $blog): ?>
-                <?= echoBlogCard(queryBlog($blog['id'])) ?>
-            <?php endforeach ?>
-
-        </div>
-
-    </div>
-
-    <!-- ============ 右列:排行榜 ============ -->
-    <div class="col-12 col-lg-3">
-
-        <!-- 活跃 -->
-        <div class="card shadow-sm border-0 mb-3">
-            <div class="card-body p-3">
-                <h6 class="font-weight-bold mb-3">活跃排行榜</h6>
-                <?php foreach ($activateusers as $username): ?>
-                    <?php $user = queryUser($username['submitter']); ?>
-                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
-                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
-                             style="width:36px;height:36px;object-fit:cover;"
-                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
-                        <div class="flex-grow-1" style="min-width:0;">
-                            <div class="text-truncate font-weight-bold" style="font-size:0.9rem;">
-                                <?= getUserLink($user['username']) ?>
-                            </div>
-                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
-                            </div>
-                        </div>
-                    </div>
-                <?php endforeach ?>
-            </div>
-        </div>
-
-        <!-- 比赛 -->
-        <div class="card shadow-sm border-0 mb-3">
-            <div class="card-body p-3">
-                <h6 class="font-weight-bold mb-1">比赛排行榜</h6>
-                <div class="mb-3" style="font-size:0.75rem;">
-                    <span class="text-muted">180 天内三场及以上</span>
-                    <a href="/ranklist" class="ml-1">全部排名</a>
-                </div>
-                <?php foreach ($topusers as $user): ?>
-                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
-                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
-                             style="width:36px;height:36px;object-fit:cover;"
-                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
-                        <div class="flex-grow-1" style="min-width:0;">
-                            <div class="d-flex justify-content-between align-items-baseline">
-                                <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
-                                    <?= getUserLink($user['username']) ?>
-                                </span>
-                                <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
-                                    <?= $user['rating'] ?> <small>(±<?= round($user['rating_rd']) * 2 ?>)</small>
-                                </strong>
-                            </div>
-                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
-                            </div>
-                        </div>
-                    </div>
-                <?php endforeach ?>
-            </div>
-        </div>
-
-        <!-- 黑马 -->
-        <div class="card shadow-sm border-0 mb-3">
-            <div class="card-body p-3">
-                <h6 class="font-weight-bold mb-3">黑马 / 爆冷榜</h6>
-                <?php foreach ($hotusers as $user): ?>
-                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
-                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
-                             style="width:36px;height:36px;object-fit:cover;"
-                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
-                        <div class="flex-grow-1" style="min-width:0;">
-                            <div class="d-flex justify-content-between align-items-baseline">
-                                <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
-                                    <?= getUserLink($user['username']) ?>
-                                </span>
-                                <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
-                                    Vol <?= round(1000 * $user['rating_vol']) / 1000 ?>
-                                </strong>
-                            </div>
-                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
-                                <?= $user['motto'] ?>
-                            </div>
-                        </div>
-                    </div>
-                <?php endforeach ?>
-            </div>
-        </div>
-
-    </div>
-</div>
-
-<!-- 博客卡片悬停效果 -->
-<style>
-.wyoj-blog-card {
-    transition: box-shadow .2s ease;
-}
-.wyoj-blog-card:hover {
-    box-shadow: 0 4px 14px rgba(0,0,0,.10) !important;
-}
-.wyoj-blog-card .wyoj-blog-title {
-    font-size: 1.05rem;
-    line-height: 1.5;
-}
-</style>
-
-<?php echoUOJPageFooter() ?>
-
-<div class="row my-4">
-
+<?php
+$blogs = DB::selectAll("select blogs.id, title, poster, post_time from important_blogs, blogs where is_hidden = 0 and important_blogs.blog_id = blogs.id order by level desc, important_blogs.blog_id desc limit 8");
+$activateusers = DB::selectAll("select submitter, count(*) as cnt from submissions where datediff(now(), submit_time) <= 30 group by submitter order by cnt desc limit 12;");
+$topusers = DB::selectAll("select u.username, u.rating, u.rating_rd, u.rating_vol, u.motto, u.qq, count(*) cnt from user_info u inner join contests_registrants c on u.username = c.username inner join contests s on s.id = c.contest_id where datediff(now(), s.start_time) <= 180 group by c.username having cnt >= 3 order by rating desc, username asc limit 12;");
+$hotusers = DB::selectAll("select username, rating, rating_rd, rating_vol, qq, motto from user_info order by rating_vol desc, username asc limit 12;");
+$topblogs = DB::selectAll("select id from blogs where is_hidden = false and zan >= 10 order by zan desc limit 10;");
+$recentblogs = DB::selectAll("select id from blogs where is_hidden = false and zan < 10 order by post_time desc limit 10;");
+$songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
+
+// 最近比赛:未结束的按开始时间升序取,不足 5 场时补最近结束的
+$recent_contests = array();
+$result = DB::selectAll("select * from contests where status != 'finished' order by start_time asc, id asc limit 5");
+foreach ($result as $contest) {
+        genMoreContestInfo($contest);
+        $recent_contests[] = $contest;
+}
+if (count($recent_contests) < 5) {
+        $need = 5 - count($recent_contests);
+        $result = DB::selectAll("select * from contests where status = 'finished' order by start_time desc, id desc limit $need");
+        foreach ($result as $contest) {
+                genMoreContestInfo($contest);
+                $recent_contests[] = $contest;
+        }
+}
+
+// 随机用户背景图展示用
+$rand_users = [];
+$rand_users_raw = DB::selectAll("select username, bgurl, motto from user_info where bgurl is not null and bgurl != ''");
+
+foreach ($rand_users_raw as $u) {
+    if (!preg_match('#^https?://#i', $u['bgurl'])) continue;
+    $u_obj = queryUser($u['username']);
+    if (!$u_obj) continue;
+    $rand_users[] = [
+	'username' => $u['username'],
+        'href'     => '/user/profile/' . urlencode($u['username']),
+        'bgurl'    => $u['bgurl'],
+        'motto'    => $u['motto'],
+        'userlink' => getUserLink($u['username']),
+        'avatar'   => HTML::avatar_addr($u_obj, 128),
+    ];
+}
+
+$rand_users_json = htmlspecialchars(json_encode($rand_users), ENT_QUOTES, 'UTF-8');
+?>
+<?php echoUOJPageHeader(UOJConfig::$data['profile']['oj-name-short']) ?>
+<div id="songs-data" data-songs='<?php echo $songs_data; ?>'></div>
+
+<div class="row my-4 wyoj-topcard">
+        <div class="card-body">
+                <div class="row">
+                        <div class="col-sm-12 col-md-9">
+                                <table class="table table-sm">
+                                        <tbody>
+                                        <?php $now_cnt = 0; ?>
+                                        <?php foreach ($blogs as $blog): ?>
+                                                <?php
+                                                        $now_cnt++;
+                                                        $new_tag = '';
+                                                        if ((time() - strtotime($blog['post_time'])) / 3600 / 24 <= 7) {
+                                                                $new_tag = '<sup style="color:red">&nbsp;new</sup>';
+                                                        }
+                                                ?>
+                                                <tr>
+                                                        <td><a href="/blogs/<?= $blog['id'] ?>"><?= $blog['title'] ?></a><?= $new_tag ?></td>
+                                                        <td>by <?= getUserLink($blog['poster']) ?></td>
+                                                        <td><small><?= $blog['post_time'] ?></small></td>
+                                                </tr>
+                                        <?php endforeach ?>
+                                        <?php for ($i = $now_cnt + 1; $i <= 8; $i++): ?>
+                                                <tr><td colspan="233">&nbsp;</td></tr>
+                                        <?php endfor ?>
+                                                <tr><td class="text-right" colspan="233">
+                                                        <span>本歌曲由 </span><span id="recommender"></span><span> 推荐</span>
+                                                </td></tr>
+                                                <tr><td class="text-right" colspan="233">
+                                                        <a href="https://git.ryp.org.cn/wyoj/" class="text-info font-weight-bold">WyOJ 以及所有相关项目均为自由软件。</a>
+                                                        <a href="/announcements"><?= UOJLocale::get('all the announcements') ?></a>
+                                                </td></tr>
+                                                <tr>
+                                                        <td class="text-right" colspan="233">
+                                                                <span class="text-danger font-weight-bold">声明:本网站属个人非经营性网站,仅用于学习交流,无任何盈利目的,与任何组织无关</span>
+                                                                <a href="/certs-manage/songs">我要推歌</a>
+                                                        </td>
+                                                </tr>
+                                        </tbody>
+                                </table>
+                        </div>
+                        <div class="col-xs-6 col-sm-4 col-md-3">
+        <div class="card" style="max-width:300px; width:100%; border-radius:0; ">
+        <div class="image-frame" style="aspect-ratio:1/1; position:relative;">
+            <img id="photo" src="" alt="" style="width:100%; height:100%; object-fit:cover; display:block; cursor:pointer; -webkit-tap-highlight-color:transparent;">
+        </div>
+        <div class="intro" style="padding:1.6rem 1.8rem 1.2rem; text-align:center; background:#ffffff;">
+            <p id="name" style="font-size:1.0rem; font-weight:800; letter-spacing:1px; margin-bottom:0.5rem; line-height:1.2; margin-block:0;"></p>
+            <p id="amb" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; border-top:1px dashed #cdd9e3; padding-top:0.8rem; white-space:pre-wrap; margin-block:0;"></p>
+            <p id="writer" style="font-size:0.7rem; font-weight:400; color:#4f5d69; margin-bottom:0.25rem; line-height:1.5; white-space:pre-wrap; margin-block:0;"></p>
+        </div>
+        <div class="button-wrapper" style="padding:0.2rem 1.8rem 2rem 1.8rem; background:#ffffff;">
+            <style>
+                #randomBtn{
+                    display: inline-flex;
+                    align-items: center;
+                    justify-content: center;
+                    gap: 0.6rem;
+                    width: 100%;
+                    background: #1f2f3b;
+                    color: white;
+                    padding: 0.7rem 0;
+                    cursor: pointer;
+                    transition: all 0.2s ease;
+                    letter-spacing: 0.5px;
+                    border: 1px solid rgba(255, 255, 255, 0.2);
+                    backdrop-filter: blur(4px);
+                    font-family: inherit;
+                    line-height: 1;
+                }
+            </style>
+            <button id="randomBtn">换一首</button>
+        </div>
+    </div>
+    <script>
+        (function() {
+            let dataDiv = document.getElementById('songs-data');
+            let library = JSON.parse(dataDiv.dataset.songs);
+
+            const photoImg = document.getElementById('photo');
+            const nameEl = document.getElementById('name');
+            const ambEl = document.getElementById('amb');
+            const writerEl = document.getElementById('writer');
+            const randomBtn = document.getElementById('randomBtn');
+            const recommenderEl = document.getElementById('recommender');
+
+            let lastIndex = -1;
+            let currentMusicLink = library[0].musiclink;
+
+            photoImg.addEventListener('mousedown', function(e) {
+                this.style.opacity = '0.8';
+            });
+            photoImg.addEventListener('mouseup', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('mouseleave', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('touchstart', function(e) {
+                this.style.opacity = '0.8';
+            });
+            photoImg.addEventListener('touchend', function(e) {
+                this.style.opacity = '';
+            });
+            photoImg.addEventListener('touchcancel', function(e) {
+                this.style.opacity = '';
+            });
+            function setBtnActive() {
+                randomBtn.style.background = '#17242e';
+                randomBtn.style.transform = 'scale(0.98)';
+                randomBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
+            }
+            function setBtnNormal() {
+                randomBtn.style.background = '#1f2f3b';
+                randomBtn.style.transform = 'scale(1)';
+                randomBtn.style.boxShadow = 'none';
+            }
+            randomBtn.addEventListener('mousedown', setBtnActive);
+            randomBtn.addEventListener('mouseup', setBtnNormal);
+            randomBtn.addEventListener('mouseleave', setBtnNormal);
+            randomBtn.addEventListener('touchstart', setBtnActive);
+            randomBtn.addEventListener('touchend', setBtnNormal);
+            randomBtn.addEventListener('touchcancel', setBtnNormal);
+
+            photoImg.addEventListener('click', function() {
+                if (currentMusicLink) {
+                    window.open(currentMusicLink, '_blank');
+                }
+            });
+            function randmusic() {
+                let randid = lastIndex;
+                while (randid === lastIndex) {
+                    randid = Math.floor(Math.random() * library.length);
+                }
+
+                if (lastIndex == -1) randid = 3;
+
+                lastIndex = randid;
+                const ys = library[randid];
+                photoImg.src = ys.cover;
+                photoImg.alt = ys.name;
+                nameEl.textContent = ys.name;
+                ambEl.textContent = ys.album;
+                writerEl.textContent = ys.writer;
+                recommenderEl.textContent = ys.user;
+                currentMusicLink = ys.click;
+            }
+
+            randmusic();
+
+            randomBtn.addEventListener('click', function() {
+                randmusic();
+            });
+
+        })();
+    </script>
+</div>
+                </div>
+        </div>
+</div>
+
+<?php
+// ---- 日期 ----
+$todayTs   = time();
+$dayNum    = (int)date('j', $todayTs);
+$monthNum  = (int)date('n', $todayTs);
+$wday      = (int)date('w', $todayTs);
+$monthCN   = ['一','二','三','四','五','六','七','八','九','十','十一','十二'];
+$weekCN    = ['日','一','二','三','四','五','六'];
+
+$bigMonths = [1,3,5,7,8,10,12];
+if ($monthNum == 2) {
+        $sizeChar = '平';           // 闰年可改成 '闰'
+} elseif (in_array($monthNum, $bigMonths)) {
+        $sizeChar = '大';
+} else {
+        $sizeChar = '小';
+}
+$monthText = $monthCN[$monthNum - 1] . '月' . $sizeChar;
+$weekText  = '星期' . $weekCN[$wday];
+
+$todayMidnight = strtotime(date('Y-m-d', $todayTs));
+$noipTs        = strtotime('2026-11-28');
+$daysLeft      = max(0, (int)floor(($noipTs - $todayMidnight) / 86400));
+$csp1Ts        = strtotime('2026-09-19');
+$csp1daysLeft  = max(0, (int)floor(($csp1Ts - $todayMidnight) / 86400));
+$csp2Ts        = strtotime('2026-10-31');
+$csp2daysLeft  = max(0, (int)floor(($csp2Ts - $todayMidnight) / 86400));
+$gk27Ts        = strtotime('2027-06-07');
+$gk27daysLeft  = max(0, (int)floor(($gk27Ts - $todayMidnight) / 86400));
+$gk28Ts        = strtotime('2028-06-07');
+$gk28daysLeft  = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
+?>
+
+<div class="row my-4">
+    <!-- ============ 左列:日程 + 博客 ============ -->
+    <div class="col-12 col-lg-9">
+        <!-- 黑色日期卡片 -->
+        <div class="mb-4 wyoj-date-card" id="wyoj-date-card"
+             style="position:relative;overflow:hidden;background:#000;color:#fff;padding:2rem 1.5rem;border-radius:0;text-shadow:0 1px 3px rgba(0,0,0,0.75);">
+
+            <!-- 随机方格背景:canvas -->
+            <canvas id="wyoj-date-canvas"
+                    style="position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none;"></canvas>
+
+            <!-- 内容层 -->
+            <div style="position:relative;z-index:1;">
+                <div class="d-flex justify-content-center align-items-center">
+                    <div style="writing-mode:vertical-rl;font-size:1.5rem;font-weight:bold;letter-spacing:0.3em;line-height:1;">
+                        <?= $monthText ?>
+                    </div>
+                    <div class="mx-4" style="font-size:5rem;font-weight:600;line-height:1;">
+                        <?= $dayNum ?>
+                    </div>
+                    <div style="writing-mode:vertical-rl;font-size:1.5rem;font-weight:bold;letter-spacing:0.3em;line-height:1;">
+                        <?= $weekText ?>
+                    </div>
+                </div>
+                    <div class="text-center mt-4" style="font-size:1rem;letter-spacing:0.05em;">
+                        <?php if ($daysLeft > 0): ?>
+                            距 <strong>NOIP2026</strong> 还有 <strong><?= $daysLeft ?></strong> 天
+                        <?php elseif ($daysLeft === 0): ?>
+                            <strong>NOIP2026</strong> 就是今天
+                        <?php else: ?>
+                            <strong>NOIP2026</strong> 已结束
+                        <?php endif; ?>
+                        <br/>
+                        <?php if ($csp1daysLeft > 0): ?>
+                            距 <strong>CSP-J/S 2026 第一轮</strong> 还有 <strong><?= $csp1daysLeft ?></strong> 天
+                       <?php elseif ($csp1daysLeft === 0): ?>
+                            <strong>CSP-J/S 2026 第一轮</strong> 就是今天
+                        <?php else: ?>
+                            <strong>CSP-J/S 2026 第一轮</strong> 已结束
+                        <?php endif; ?>
+                        <br/>
+                        <?php if ($csp2daysLeft > 0): ?>
+                            距 <strong>CSP-J/S 2026 第二轮</strong> 还有 <strong><?= $csp2daysLeft ?></strong> 天
+                        <?php elseif ($csp2daysLeft === 0): ?>
+                            <strong>CSP-J/S 2026 第二轮</strong> 就是今天!
+                        <?php else: ?>
+                            <strong>CSP-J/S 2026 第二轮</strong> 已结束
+                        <?php endif; ?>
+                        <br/>
+                        <?php if ($gk27daysLeft > 0): ?>
+                            距 <strong>2027 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk27daysLeft ?></strong> 天
+                        <?php elseif ($gk27daysLeft === 0): ?>
+                            <strong>2027 年普通高等学校招生全国统一考试</strong> 就是今天!
+                        <?php else: ?>
+                            <strong>2027 年普通高等学校招生全国统一考试</strong> 已结束
+                        <?php endif; ?>
+                        <br/>
+                        <?php if ($gk28daysLeft > 0): ?>
+                            距 <strong>2028 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk28daysLeft ?></strong> 天
+                        <?php elseif ($gk28daysLeft === 0): ?>
+                            <strong>2028 年普通高等学校招生全国统一考试</strong> 就是今天!
+                        <?php else: ?>
+                            <strong>2028 年普通高等学校招生全国统一考试</strong> 已结束
+                        <?php endif; ?>
+                    </div>
+                </div>
+        </div>
+
+        <script>
+        (function () {
+            var card   = document.getElementById('wyoj-date-card');
+            var canvas = document.getElementById('wyoj-date-canvas');
+            if (!card || !canvas) return;
+
+            var ctx = canvas.getContext('2d');
+
+            // ===== 可调参数 =====
+            var COLORS   = ['#000000', '#0c0c0c', '#181818', '#242424', '#303030'];
+            var CELL     = 26;     // 每格边长(px)
+            var P        = 0.3;    // 每格每轮变色概率(期望值)
+            var PERIOD   = 3000;   // 刷新间隔(ms)
+            var Z        = 1.96;   // 95% 置信区间
+            var USE_DPR  = false;  // 是否按设备像素比放大 canvas(false 更省 CPU)
+            // ====================
+
+            var dpr = USE_DPR ? (window.devicePixelRatio || 1) : 1;
+            var cols = 0, rows = 0, nCells = 0;
+            var lastW = 0, lastH = 0;
+            var gridColors = [];
+            var idx = null;
+            var xs = null, ys = null;
+
+            var rafId = null;
+            var running = false;
+            var lastTick = 0;
+
+            function randColor() {
+                return COLORS[(Math.random() * COLORS.length) | 0];
+            }
+
+            function computeBounds() {
+                var w = card.clientWidth;
+                var h = card.clientHeight;
+                xs = new Int32Array(cols + 1);
+                ys = new Int32Array(rows + 1);
+                for (var i = 0; i <= cols; i++) xs[i] = Math.round(i * w / cols);
+                for (var j = 0; j <= rows; j++) ys[j] = Math.round(j * h / rows);
+            }
+
+            function build() {
+                var w = card.clientWidth;
+                var h = card.clientHeight;
+                if (w === 0 || h === 0) return;
+                if (w === lastW && h === lastH) return;   // 尺寸没变就不重建
+                lastW = w; lastH = h;
+
+                cols = Math.max(1, Math.ceil(w / CELL));
+                rows = Math.max(1, Math.ceil(h / CELL));
+                nCells = cols * rows;
+
+                canvas.width  = Math.round(w * dpr);
+                canvas.height = Math.round(h * dpr);
+                canvas.style.width  = w + 'px';
+                canvas.style.height = h + 'px';
+                ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+                ctx.imageSmoothingEnabled = false;
+
+                computeBounds();
+
+                gridColors = [];
+                for (var y = 0; y < rows; y++) {
+                    var row = [];
+                    for (var x = 0; x < cols; x++) row.push(randColor());
+                    gridColors.push(row);
+                }
+
+                idx = new Int32Array(nCells);
+                for (var i = 0; i < nCells; i++) idx[i] = i;
+
+                redrawAll();
+            }
+
+            function redrawAll() {
+                for (var y = 0; y < rows; y++) {
+                    var y0 = ys[y], y1 = ys[y + 1];
+                    for (var x = 0; x < cols; x++) {
+                        ctx.fillStyle = gridColors[y][x];
+                        ctx.fillRect(xs[x], y0, xs[x + 1] - xs[x], y1 - y0);
+                    }
+                }
+            }
+
+            function tick() {
+                if (nCells === 0) return;
+
+                var mu    = nCells * P;
+                var sigma = Math.sqrt(nCells * P * (1 - P));
+                var lo    = Math.max(0, Math.floor(mu - Z * sigma));
+                var hi    = Math.min(nCells, Math.ceil(mu + Z * sigma));
+                var m     = lo + Math.floor(Math.random() * (hi - lo + 1));
+
+                for (var i = 0; i < m; i++) {
+                    var j = i + Math.floor(Math.random() * (nCells - i));
+                    var t = idx[i]; idx[i] = idx[j]; idx[j] = t;
+                }
+
+                for (var k = 0; k < m; k++) {
+                    var ci = idx[k];
+                    var x  = ci % cols;
+                    var y  = (ci / cols) | 0;
+                    var c  = randColor();
+                    gridColors[y][x] = c;
+                    ctx.fillStyle = c;
+                    ctx.fillRect(xs[x], ys[y], xs[x + 1] - xs[x], ys[y + 1] - ys[y]);
+                }
+            }
+
+            function loop(now) {
+                rafId = null;
+                if (now - lastTick >= PERIOD) {
+                    lastTick = now;
+                    tick();
+                }
+                if (running) rafId = requestAnimationFrame(loop);
+            }
+
+            function start() {
+                if (running) return;
+                running = true;
+                lastTick = performance.now();
+                rafId = requestAnimationFrame(loop);
+            }
+
+            function stop() {
+                running = false;
+                if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
+            }
+
+            build();
+
+            if (window.ResizeObserver) {
+                new ResizeObserver(function () { build(); }).observe(card);
+            } else {
+                window.addEventListener('resize', build);
+            }
+
+            if (window.IntersectionObserver) {
+                new IntersectionObserver(function (entries) {
+                    if (entries[0].isIntersecting) start(); else stop();
+                }, { threshold: 0 }).observe(card);
+            } else {
+                start();
+            }
+
+            document.addEventListener('visibilitychange', function () {
+                if (document.hidden) stop(); else start();
+            });
+        })();
+        </script>
+
+	<?php if (!empty($rand_users)): ?>
+	<div class="mb-4" id="wyoj-random-user-wrap">
+	    <div class="wyoj-random-user" id="wyoj-random-user-link">
+	        <div class="wyoj-random-img-wrap">
+	            <img class="wyoj-rand-img" id="wyoj-rand-img-a" src="" alt="">
+	            <img class="wyoj-rand-img" id="wyoj-rand-img-b" src="" alt="">
+	            <button type="button" class="wyoj-rand-arrow wyoj-rand-arrow-left"
+	                    id="wyoj-rand-prev" aria-label="上一个">‹</button>
+	            <button type="button" class="wyoj-rand-arrow wyoj-rand-arrow-right"
+	                    id="wyoj-rand-next" aria-label="下一个">›</button>
+	            <button type="button" class="wyoj-rand-pin pinned"
+	                    id="wyoj-rand-pin" aria-label="固定" title="已固定,点击恢复自动切换">📌</button>
+	        </div>
+	        <div class="wyoj-random-info">
+	            <span class="wyoj-rand-motto" id="wyoj-rand-motto"></span>
+	            <span class="wyoj-rand-userlink" id="wyoj-rand-userlink"></span>
+	            <img class="wyoj-rand-avatar" id="wyoj-rand-avatar" src="" alt="">
+	        </div>
+	    </div>
+	    <div id="wyoj-random-user-data" data-users='<?= $rand_users_json ?>' style="display:none;"></div>
+	</div>
+
+	<script>
+	(function () {
+	    var wrap = document.getElementById('wyoj-random-user-wrap');
+	    if (!wrap) return;
+	    var dataEl = document.getElementById('wyoj-random-user-data');
+	    if (!dataEl) return;
+
+	    var users;
+	    try { users = JSON.parse(dataEl.dataset.users); } catch (e) { wrap.style.display = 'none'; return; }
+	    if (!users || users.length === 0) { wrap.style.display = 'none'; return; }
+
+	    var link     = document.getElementById('wyoj-random-user-link');
+	    var imgA     = document.getElementById('wyoj-rand-img-a');
+	    var imgB     = document.getElementById('wyoj-rand-img-b');
+	    var mottoEl  = document.getElementById('wyoj-rand-motto');
+	    var userEl   = document.getElementById('wyoj-rand-userlink');
+	    var avatarEl = document.getElementById('wyoj-rand-avatar');
+	    var prevBtn  = document.getElementById('wyoj-rand-prev');
+	    var nextBtn  = document.getElementById('wyoj-rand-next');
+	    var pinBtn   = document.getElementById('wyoj-rand-pin');
+
+	    var frontImg = imgA;
+	    var backImg  = imgB;
+	    var currentIdx = -1;
+	    var currentUrl = '';
+	    var timer = null;
+	    var pinned = true;
+	    var SWAP_MS = 5000;
+
+	    function preload(url) {
+	        return new Promise(function (resolve, reject) {
+	            var im = new Image();
+	            im.onload = function () { resolve(); };
+	            im.onerror = function () { reject(); };
+	            im.src = url;
+	        });
+	    }
+
+	    function applyInfo(u) {
+	        mottoEl.textContent = u.motto || '';
+	        userEl.innerHTML = u.userlink;
+	        avatarEl.src = u.avatar;
+	        currentUrl = u.href;
+	    }
+
+	    function showImage(u) {
+	        backImg.src = u.bgurl;
+	        requestAnimationFrame(function () {
+	            backImg.classList.add('active');
+	            frontImg.classList.remove('active');
+	            var t = frontImg; frontImg = backImg; backImg = t;
+	        });
+	    }
+
+	    // 展示指定索引;如果图挂了就往同方向继续找
+	    function goTo(idx, dir, tries) {
+	        dir = dir || 1;
+	        tries = tries || 0;
+	        if (tries >= users.length) return;
+	        var realIdx = ((idx % users.length) + users.length) % users.length;
+	        var u = users[realIdx];
+	        preload(u.bgurl).then(function () {
+	            currentIdx = realIdx;
+	            applyInfo(u);
+	            showImage(u);
+	        }, function () {
+	            goTo(realIdx + dir, dir, tries + 1);
+	        });
+	    }
+
+	    function schedule() {
+	        clearTimeout(timer);
+	        timer = null;
+	        if (pinned) return;
+	        if (users.length <= 1) return;
+	        timer = setTimeout(function () {
+	            goTo(currentIdx + 1, 1, 0);
+	            schedule();
+	        }, SWAP_MS);
+	    }
+
+	    function pickRandom() {
+	        if (users.length === 1) return 0;
+	        var i = currentIdx;
+	        while (i === currentIdx) i = Math.floor(Math.random() * users.length);
+	        return i;
+	    }
+
+	    // 初始化
+	    (function init() {
+	        var tries = 0;
+		var idx = -1;
+		for (var i = 0; i < users.length; i++)
+			if (users[i].username == 'ryp') { idx = i; break; }
+		// 夹带私货了
+		if (idx < 0) idx = Math.floor(Math.random() * users.length);
+	        function attempt() {
+	            if (tries >= users.length) { wrap.style.display = 'none'; return; }
+	            tries++;
+	            var u = users[idx];
+	            preload(u.bgurl).then(function () {
+	                currentIdx = idx;
+	                frontImg.src = u.bgurl;
+	                frontImg.classList.add('active');
+	                applyInfo(u);
+	                schedule();
+	            }, function () {
+	                idx = (idx + 1) % users.length;
+	                attempt();
+	            });
+	        }
+	        attempt();
+	    })();
+
+	    // 点击卡片主体(非按钮、非内部链接)跳转
+	    link.addEventListener('click', function (e) {
+	        if (e.target.closest('button')) return;
+	        if (e.target.closest('a')) return;
+	        if (currentUrl) window.location.href = currentUrl;
+	    });
+	
+	    // 上一个
+	    prevBtn.addEventListener('click', function (e) {
+	        e.stopPropagation();
+	        e.preventDefault();
+	        goTo(currentIdx - 1, -1, 0);
+	        schedule();
+	    });
+
+	    // 下一个
+	    nextBtn.addEventListener('click', function (e) {
+	        e.stopPropagation();
+	        e.preventDefault();
+	        goTo(currentIdx + 1, 1, 0);
+	        schedule();
+	    });
+
+	    // 固定 / 取消固定
+	    pinBtn.addEventListener('click', function (e) {
+	        e.stopPropagation();
+	        e.preventDefault();
+	        pinned = !pinned;
+	        pinBtn.classList.toggle('pinned', pinned);
+	        pinBtn.title = pinned ? '已固定,点击恢复自动切换' : '固定当前';
+	        if (pinned) {
+	            clearTimeout(timer);
+	            timer = null;
+	        } else {
+	            schedule();
+	        }
+	    });
+
+	    document.addEventListener('visibilitychange', function () {
+	        if (document.hidden) {
+	            clearTimeout(timer);
+	            timer = null;
+	        } else {
+	            schedule();
+	        }
+	    });
+	})();
+	</script>
+	<?php endif ?>
+
+        
+        <!-- 博客列表 -->
+        <div class="row">
+            <?php foreach ($recentblogs as $blog): ?>
+                <?= echoBlogCard(queryBlog($blog['id'])) ?>
+            <?php endforeach ?>
+            <?php foreach ($topblogs as $blog): ?>
+                <?= echoBlogCard(queryBlog($blog['id'])) ?>
+            <?php endforeach ?>
+        </div>
+    </div>
+
+    <!-- ============ 右列:排行榜 ============ -->
+    <div class="col-12 col-lg-3">
+
+        <!-- 活跃 -->
+        <div class="card shadow-sm border-0 mb-3">
+            <div class="card-body p-3">
+                <h6 class="font-weight-bold mb-3">活跃排行榜</h6>
+                <?php foreach ($activateusers as $username): ?>
+                    <?php $user = queryUser($username['submitter']); ?>
+                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
+                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
+                             style="width:36px;height:36px;object-fit:cover;"
+                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
+                        <div class="flex-grow-1" style="min-width:0;">
+                            <div class="text-truncate font-weight-bold" style="font-size:0.9rem;">
+                                <?= getUserLink($user['username']) ?>
+                            </div>
+                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
+                                <?= $user['motto'] ?>
+                            </div>
+                        </div>
+                    </div>
+                <?php endforeach ?>
+            </div>
+        </div>
+
+        <!-- 比赛 -->
+        <div class="card shadow-sm border-0 mb-3">
+            <div class="card-body p-3">
+                <h6 class="font-weight-bold mb-1">比赛排行榜</h6>
+                <div class="mb-3" style="font-size:0.75rem;">
+                    <span class="text-muted">180 天内三场及以上</span>
+                    <a href="/ranklist" class="ml-1">全部排名</a>
+                </div>
+                <?php foreach ($topusers as $user): ?>
+                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
+                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
+                             style="width:36px;height:36px;object-fit:cover;"
+                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
+                        <div class="flex-grow-1" style="min-width:0;">
+                            <div class="d-flex justify-content-between align-items-baseline">
+                                <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
+                                    <?= getUserLink($user['username']) ?>
+                                </span>
+                                <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
+                                    <?= $user['rating'] ?> <small>(±<?= round($user['rating_rd']) * 2 ?>)</small>
+                                </strong>
+                            </div>
+                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
+                                <?= $user['motto'] ?>
+                            </div>
+                        </div>
+                    </div>
+                <?php endforeach ?>
+            </div>
+        </div>
+
+        <!-- 黑马 -->
+        <div class="card shadow-sm border-0 mb-3">
+            <div class="card-body p-3">
+                <h6 class="font-weight-bold mb-3">黑马 / 爆冷榜</h6>
+                <?php foreach ($hotusers as $user): ?>
+                    <div class="d-flex align-items-start border-bottom pb-2 mb-2">
+                        <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
+                             style="width:36px;height:36px;object-fit:cover;"
+                             src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
+                        <div class="flex-grow-1" style="min-width:0;">
+                            <div class="d-flex justify-content-between align-items-baseline">
+                                <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
+                                    <?= getUserLink($user['username']) ?>
+                                </span>
+                                <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
+                                    Vol <?= round(1000 * $user['rating_vol']) / 1000 ?>
+                                </strong>
+                            </div>
+                            <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
+                                <?= $user['motto'] ?>
+                            </div>
+                        </div>
+                    </div>
+                <?php endforeach ?>
+            </div>
+        </div>
+
+    </div>
+</div>
+
+<!-- 博客卡片悬停效果 -->
+<style>
+.wyoj-blog-card {
+    transition: box-shadow .2s ease;
+}
+.wyoj-blog-card:hover {
+    box-shadow: 0 4px 14px rgba(0,0,0,.10) !important;
+}
+.wyoj-blog-card .wyoj-blog-title {
+    font-size: 1.05rem;
+    line-height: 1.5;
+}
+
+
+/* ---------- 随机用户背景图卡片 ---------- */
+.wyoj-random-user {
+    display: block;
+    background: #fff;
+    border: 0;
+    border-radius: 0;
+    overflow: hidden;
+    color: inherit;
+    box-shadow: 0 .125rem .25rem rgba(0,0,0,.075);
+    transition: box-shadow 0.2s ease;
+    cursor: pointer;
+}
+.wyoj-random-user:hover {
+    box-shadow: 0 .25rem .75rem rgba(0,0,0,.10);
+}
+.wyoj-random-img-wrap {
+    position: relative;
+    aspect-ratio: 16 / 7;
+    background: #f8f9fa;
+    overflow: hidden;
+}
+.wyoj-rand-img {
+    position: absolute;
+    inset: 0;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    opacity: 0;
+    transition: opacity 0.6s ease;
+}
+.wyoj-rand-img.active { opacity: 1; }
+
+/* 左右箭头 */
+.wyoj-rand-arrow {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 32px;
+    height: 32px;
+    border: 0;
+    border-radius: 50%;
+    background: rgba(255,255,255,0.85);
+    color: #333;
+    font-size: 20px;
+    line-height: 1;
+    padding: 0 0 2px 0;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 3;
+    opacity: 0.4;
+    transition: opacity 0.2s ease, background 0.2s ease, transform 0.1s ease;
+    -webkit-tap-highlight-color: transparent;
+}
+.wyoj-random-user:hover .wyoj-rand-arrow { opacity: 1; }
+.wyoj-rand-arrow:hover {
+    background: #fff;
+    transform: translateY(-50%) scale(1.08);
+}
+.wyoj-rand-arrow:active { transform: translateY(-50%) scale(0.96); }
+.wyoj-rand-arrow-left  { left: 10px; }
+.wyoj-rand-arrow-right { right: 10px; }
+
+/* 固定按钮 */
+.wyoj-rand-pin {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    width: 28px;
+    height: 28px;
+    border: 0;
+    border-radius: 50%;
+    background: rgba(255,255,255,0.85);
+    font-size: 13px;
+    line-height: 1;
+    padding: 0;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 3;
+    opacity: 0.4;
+    transition: opacity 0.2s ease, background 0.2s ease, transform 0.1s ease;
+    -webkit-tap-highlight-color: transparent;
+}
+.wyoj-random-user:hover .wyoj-rand-pin { opacity: 1; }
+.wyoj-rand-pin:hover { background: #fff; }
+.wyoj-rand-pin:active { transform: scale(0.94); }
+.wyoj-rand-pin.pinned {
+    opacity: 1;
+    background: #ffc107;
+}
+.wyoj-rand-pin.pinned:hover { background: #ffb300; }
+
+/* 信息行 */
+.wyoj-random-info {
+    padding: 12px 16px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    min-width: 0;
+}
+.wyoj-rand-motto {
+    color: #6c757d;
+    font-size: 0.85rem;
+    line-height: 1.4;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    flex-shrink: 1;
+    min-width: 0;
+}
+.wyoj-rand-userlink {
+    font-size: 0.9rem;
+    line-height: 1.3;
+    flex-shrink: 0;
+    white-space: nowrap;
+}
+.wyoj-rand-avatar {
+    width: 24px;
+    height: 24px;
+    object-fit: cover;
+    border-radius: 50%;
+    border: 1px solid #dee2e6;
+    flex-shrink: 0;
+    margin-left: auto;
+}
+</style>
+
+<?php echoUOJPageFooter() ?>
+

+ 4 - 0
app/controllers/super_manage.php

@@ -36,6 +36,10 @@
 			calcStandings($contest, $contest_data, $score, $standings, true);
 			calcRatingGlicko2($standings, $ratings, $rating_rds, $rating_vols);
 
+			error_log("SHABI RATING");
+			error_log(print_r($standings, true));
+			error_log(print_r($ratings, true));
+
 			for ($i = 0; $i < count($standings); ++$i) {
 				DB::update("update user_info set rating = {$ratings[$i]}, rating_rd = {$rating_rds[$i]}, rating_vol = {$rating_vols[$i]} where username = '{$standings[$i][2][0]}';");
 			}

+ 29 - 1
app/controllers/user_info.php

@@ -24,7 +24,8 @@
         $bg_style = $has_bg ? 'background-image: url(\'' . HTML::escape($bgurl) . '\');' : '';
 
         // rating 排名:rating 严格小于他的人数 + 1
-        $rating_rank = DB::selectCount("select count(*) from user_info where rating > {$user['rating']}") + 1;
+	$rating_rank = DB::selectCount("select count(*) from user_info where rating > {$user['rating']}") + 1;
+	$blog_url = HTML::blog_url($user['username'], '/');
     ?>
 
     <div class="wyoj-profile">
@@ -86,6 +87,11 @@
                     <div class="wyoj-info-item">
                         <div class="wyoj-info-label">注册时间</div>
                         <div class="wyoj-info-value"><?= $user['register_time'] ?></div>
+		    </div>
+		    <div class="wyoj-info-item wyoj-info-action">
+                        <a href="<?= $blog_url ?>" class="wyoj-blog-btn">
+			    <span class="glyphicon glyphicon-arrow-right"></span>访问 TA 的博客
+			</a>
                     </div>
                 </div>
             </div>
@@ -202,6 +208,28 @@
         word-break: break-all;
     }
 
+    .wyoj-info-action {
+        padding: 16px 0;
+    }
+    .wyoj-blog-btn {
+        display: block;
+        width: 100%;
+        text-align: center;
+        padding: 9px 12px;
+        background: #fff;
+        border: 1px solid #dc3545;
+        color: #dc3545;
+        border-radius: 6px;
+        font-size: 0.9rem;
+        text-decoration: none;
+        transition: all 0.15s ease;
+    }
+    .wyoj-blog-btn:hover {
+        background: #dc3545;
+        color: #fff;
+        text-decoration: none;
+    }
+
     /* ---------- rating 图表 ---------- */
     .wyoj-rating-row { margin-top: 24px; }
     .wyoj-chart-card {

+ 1 - 1
app/libs/uoj-contest-lib.php

@@ -102,7 +102,7 @@ function calcRatingGlicko2($standings, &$new_rating, &$new_rd, &$new_vol) {
 		$new_rd[$i] = sqrt($rd[$i] * $rd[$i] + $new_vol[$i] * $new_vol[$i]);
 		$new_rd[$i] = 1 / sqrt(1 / $v + 1 / $new_rd[$i] / $new_rd[$i]);
 		$new_rating[$i] = $rating[$i] + $delta / $v * $new_rd[$i] * $new_rd[$i];
-		$new_rating[$i] = $new_rating[$i] * 173.7178 + 1500;
+		$new_rating[$i] = round($new_rating[$i] * 173.7178 + 1500);
 		$new_rd[$i] *= 173.7178;
 	}
 }

+ 0 - 5
app/libs/uoj-html-lib.php

@@ -1223,11 +1223,6 @@ function echoBlogCard($blog) {
 	$zanblock = getClickZanBlock('B', $blog['id'], $blog['zan']);
 	$avatar = "https://q.qlogo.cn/g?b=qq&nk=" . $poster['qq'] . "&s=100";
 
-	error_log($blog);
-	error_log($poster);
-	error_log($poster['qq']);
-	error_log("FACK");
-
 	return <<<EOF
         <div class="col-12 mb-3">
                 <div class="card wyoj-blog-card border-0 shadow-sm">