index.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. <?php
  2. $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");
  3. $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;");
  4. $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;");
  5. $hotusers = DB::selectAll("select username, rating, rating_rd, rating_vol, qq, motto from user_info order by rating_vol desc, username asc limit 12;");
  6. $topblogs = DB::selectAll("select id from blogs where is_hidden = false and zan >= 10 order by zan desc limit 10;");
  7. $recentblogs = DB::selectAll("select id from blogs where is_hidden = false and zan < 10 order by post_time desc limit 10;");
  8. $songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
  9. // 最近比赛:未结束的按开始时间升序取,不足 5 场时补最近结束的
  10. $recent_contests = array();
  11. $result = DB::selectAll("select * from contests where status != 'finished' order by start_time asc, id asc limit 5");
  12. foreach ($result as $contest) {
  13. genMoreContestInfo($contest);
  14. $recent_contests[] = $contest;
  15. }
  16. if (count($recent_contests) < 5) {
  17. $need = 5 - count($recent_contests);
  18. $result = DB::selectAll("select * from contests where status = 'finished' order by start_time desc, id desc limit $need");
  19. foreach ($result as $contest) {
  20. genMoreContestInfo($contest);
  21. $recent_contests[] = $contest;
  22. }
  23. }
  24. // 随机用户背景图展示用
  25. $rand_users = [];
  26. $rand_users_raw = DB::selectAll("select username, bgurl, motto from user_info where bgurl is not null and bgurl != ''");
  27. foreach ($rand_users_raw as $u) {
  28. if (!preg_match('#^https?://#i', $u['bgurl'])) continue;
  29. $u_obj = queryUser($u['username']);
  30. if (!$u_obj) continue;
  31. $rand_users[] = [
  32. 'username' => $u['username'],
  33. 'href' => '/user/profile/' . urlencode($u['username']),
  34. 'bgurl' => $u['bgurl'],
  35. 'motto' => $u['motto'],
  36. 'userlink' => getUserLink($u['username']),
  37. 'avatar' => HTML::avatar_addr($u_obj, 128),
  38. ];
  39. }
  40. $rand_users_json = htmlspecialchars(json_encode($rand_users), ENT_QUOTES, 'UTF-8');
  41. ?>
  42. <?php echoUOJPageHeader(UOJConfig::$data['profile']['oj-name-short']) ?>
  43. <div id="songs-data" data-songs='<?php echo $songs_data; ?>'></div>
  44. <div class="row my-4 wyoj-topcard">
  45. <div class="card-body">
  46. <div class="row">
  47. <div class="col-sm-12 col-md-9">
  48. <table class="table table-sm">
  49. <tbody>
  50. <?php $now_cnt = 0; ?>
  51. <?php foreach ($blogs as $blog): ?>
  52. <?php
  53. $now_cnt++;
  54. $new_tag = '';
  55. if ((time() - strtotime($blog['post_time'])) / 3600 / 24 <= 7) {
  56. $new_tag = '<sup style="color:red">&nbsp;new</sup>';
  57. }
  58. ?>
  59. <tr>
  60. <td><a href="/blogs/<?= $blog['id'] ?>"><?= $blog['title'] ?></a><?= $new_tag ?></td>
  61. <td>by <?= getUserLink($blog['poster']) ?></td>
  62. <td><small><?= $blog['post_time'] ?></small></td>
  63. </tr>
  64. <?php endforeach ?>
  65. <?php for ($i = $now_cnt + 1; $i <= 8; $i++): ?>
  66. <tr><td colspan="233">&nbsp;</td></tr>
  67. <?php endfor ?>
  68. <tr><td class="text-right" colspan="233">
  69. <span>本歌曲由 </span><span id="recommender"></span><span> 推荐</span>
  70. </td></tr>
  71. <tr><td class="text-right" colspan="233">
  72. <a href="https://git.ryp.org.cn/wyoj/" class="text-info font-weight-bold">WyOJ 以及所有相关项目均为自由软件。</a>
  73. <a href="/announcements"><?= UOJLocale::get('all the announcements') ?></a>
  74. </td></tr>
  75. <tr>
  76. <td class="text-right" colspan="233">
  77. <span class="text-danger font-weight-bold">声明:本网站属个人非经营性网站,仅用于学习交流,无任何盈利目的,与任何组织无关</span>
  78. <a href="/certs-manage/songs">我要推歌</a>
  79. </td>
  80. </tr>
  81. </tbody>
  82. </table>
  83. </div>
  84. <div class="col-xs-6 col-sm-4 col-md-3">
  85. <div class="card" style="max-width:300px; width:100%; border-radius:0; ">
  86. <div class="image-frame" style="aspect-ratio:1/1; position:relative;">
  87. <img id="photo" src="" alt="" style="width:100%; height:100%; object-fit:cover; display:block; cursor:pointer; -webkit-tap-highlight-color:transparent;">
  88. </div>
  89. <div class="intro" style="padding:1.6rem 1.8rem 1.2rem; text-align:center; background:#ffffff;">
  90. <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>
  91. <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>
  92. <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>
  93. </div>
  94. <div class="button-wrapper" style="padding:0.2rem 1.8rem 2rem 1.8rem; background:#ffffff;">
  95. <style>
  96. #randomBtn{
  97. display: inline-flex;
  98. align-items: center;
  99. justify-content: center;
  100. gap: 0.6rem;
  101. width: 100%;
  102. background: #1f2f3b;
  103. color: white;
  104. padding: 0.7rem 0;
  105. cursor: pointer;
  106. transition: all 0.2s ease;
  107. letter-spacing: 0.5px;
  108. border: 1px solid rgba(255, 255, 255, 0.2);
  109. backdrop-filter: blur(4px);
  110. font-family: inherit;
  111. line-height: 1;
  112. }
  113. </style>
  114. <button id="randomBtn">换一首</button>
  115. </div>
  116. </div>
  117. <script>
  118. (function() {
  119. let dataDiv = document.getElementById('songs-data');
  120. let library = JSON.parse(dataDiv.dataset.songs);
  121. const photoImg = document.getElementById('photo');
  122. const nameEl = document.getElementById('name');
  123. const ambEl = document.getElementById('amb');
  124. const writerEl = document.getElementById('writer');
  125. const randomBtn = document.getElementById('randomBtn');
  126. const recommenderEl = document.getElementById('recommender');
  127. let lastIndex = -1;
  128. let currentMusicLink = library[0].musiclink;
  129. photoImg.addEventListener('mousedown', function(e) {
  130. this.style.opacity = '0.8';
  131. });
  132. photoImg.addEventListener('mouseup', function(e) {
  133. this.style.opacity = '';
  134. });
  135. photoImg.addEventListener('mouseleave', function(e) {
  136. this.style.opacity = '';
  137. });
  138. photoImg.addEventListener('touchstart', function(e) {
  139. this.style.opacity = '0.8';
  140. });
  141. photoImg.addEventListener('touchend', function(e) {
  142. this.style.opacity = '';
  143. });
  144. photoImg.addEventListener('touchcancel', function(e) {
  145. this.style.opacity = '';
  146. });
  147. function setBtnActive() {
  148. randomBtn.style.background = '#17242e';
  149. randomBtn.style.transform = 'scale(0.98)';
  150. randomBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
  151. }
  152. function setBtnNormal() {
  153. randomBtn.style.background = '#1f2f3b';
  154. randomBtn.style.transform = 'scale(1)';
  155. randomBtn.style.boxShadow = 'none';
  156. }
  157. randomBtn.addEventListener('mousedown', setBtnActive);
  158. randomBtn.addEventListener('mouseup', setBtnNormal);
  159. randomBtn.addEventListener('mouseleave', setBtnNormal);
  160. randomBtn.addEventListener('touchstart', setBtnActive);
  161. randomBtn.addEventListener('touchend', setBtnNormal);
  162. randomBtn.addEventListener('touchcancel', setBtnNormal);
  163. photoImg.addEventListener('click', function() {
  164. if (currentMusicLink) {
  165. window.open(currentMusicLink, '_blank');
  166. }
  167. });
  168. function randmusic() {
  169. let randid = lastIndex;
  170. while (randid === lastIndex) {
  171. randid = Math.floor(Math.random() * library.length);
  172. }
  173. if (lastIndex == -1) randid = 3;
  174. lastIndex = randid;
  175. const ys = library[randid];
  176. photoImg.src = ys.cover;
  177. photoImg.alt = ys.name;
  178. nameEl.textContent = ys.name;
  179. ambEl.textContent = ys.album;
  180. writerEl.textContent = ys.writer;
  181. recommenderEl.textContent = ys.user;
  182. currentMusicLink = ys.click;
  183. }
  184. randmusic();
  185. randomBtn.addEventListener('click', function() {
  186. randmusic();
  187. });
  188. })();
  189. </script>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. <?php
  195. // ---- 日期 ----
  196. $todayTs = time();
  197. $dayNum = (int)date('j', $todayTs);
  198. $monthNum = (int)date('n', $todayTs);
  199. $wday = (int)date('w', $todayTs);
  200. $monthCN = ['一','二','三','四','五','六','七','八','九','十','十一','十二'];
  201. $weekCN = ['日','一','二','三','四','五','六'];
  202. $bigMonths = [1,3,5,7,8,10,12];
  203. if ($monthNum == 2) {
  204. $sizeChar = '平'; // 闰年可改成 '闰'
  205. } elseif (in_array($monthNum, $bigMonths)) {
  206. $sizeChar = '大';
  207. } else {
  208. $sizeChar = '小';
  209. }
  210. $monthText = $monthCN[$monthNum - 1] . '月' . $sizeChar;
  211. $weekText = '星期' . $weekCN[$wday];
  212. $todayMidnight = strtotime(date('Y-m-d', $todayTs));
  213. $noipTs = strtotime('2026-11-28');
  214. $daysLeft = max(0, (int)floor(($noipTs - $todayMidnight) / 86400));
  215. $csp1Ts = strtotime('2026-09-19');
  216. $csp1daysLeft = max(0, (int)floor(($csp1Ts - $todayMidnight) / 86400));
  217. $csp2Ts = strtotime('2026-10-31');
  218. $csp2daysLeft = max(0, (int)floor(($csp2Ts - $todayMidnight) / 86400));
  219. $gk27Ts = strtotime('2027-06-07');
  220. $gk27daysLeft = max(0, (int)floor(($gk27Ts - $todayMidnight) / 86400));
  221. $gk28Ts = strtotime('2028-06-07');
  222. $gk28daysLeft = max(0, (int)floor(($gk28Ts - $todayMidnight) / 86400));
  223. ?>
  224. <div class="row my-4">
  225. <!-- ============ 左列:日程 + 博客 ============ -->
  226. <div class="col-12 col-lg-9">
  227. <!-- 黑色日期卡片 -->
  228. <div class="mb-4 wyoj-date-card" id="wyoj-date-card"
  229. 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);">
  230. <!-- 随机方格背景:canvas -->
  231. <canvas id="wyoj-date-canvas"
  232. style="position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;pointer-events:none;"></canvas>
  233. <!-- 内容层 -->
  234. <div style="position:relative;z-index:1;">
  235. <style>
  236. /* 竖排文字:逐字竖直排列,不依赖 writing-mode:vertical-rl(Firefox 下该属性会把汉字压扁/渲染成空白) */
  237. .wyoj-vtext {
  238. display: flex;
  239. flex-direction: column;
  240. align-items: center;
  241. font-size: 1.5rem;
  242. font-weight: bold;
  243. line-height: 1;
  244. }
  245. .wyoj-vtext>span {
  246. display: block;
  247. line-height: 1.3; /* 等价于原来的 letter-spacing: 0.3em */
  248. }
  249. </style>
  250. <?php
  251. // 把一段文字拆成"每字一个 span",交给上面的 flex 纵向排列
  252. $wyoj_vtext = function ($text) {
  253. $html = '';
  254. foreach (preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY) as $ch) {
  255. $html .= '<span>' . htmlspecialchars($ch, ENT_QUOTES, 'UTF-8') . '</span>';
  256. }
  257. return $html;
  258. };
  259. ?>
  260. <div class="d-flex justify-content-center align-items-center">
  261. <div class="wyoj-vtext"><?= $wyoj_vtext($monthText) ?></div>
  262. <div class="mx-4" style="font-size:5rem;font-weight:600;line-height:1;">
  263. <?= $dayNum ?>
  264. </div>
  265. <div class="wyoj-vtext"><?= $wyoj_vtext($weekText) ?></div>
  266. </div>
  267. <div class="text-center mt-4" style="font-size:1rem;letter-spacing:0.05em;">
  268. <?php if ($daysLeft > 0): ?>
  269. 距 <strong>NOIP2026</strong> 还有 <strong><?= $daysLeft ?></strong> 天
  270. <?php elseif ($daysLeft === 0): ?>
  271. <strong>NOIP2026</strong> 就是今天
  272. <?php else: ?>
  273. <strong>NOIP2026</strong> 已结束
  274. <?php endif; ?>
  275. <br/>
  276. <?php if ($csp1daysLeft > 0): ?>
  277. 距 <strong>CSP-J/S 2026 第一轮</strong> 还有 <strong><?= $csp1daysLeft ?></strong> 天
  278. <?php elseif ($csp1daysLeft === 0): ?>
  279. <strong>CSP-J/S 2026 第一轮</strong> 就是今天
  280. <?php else: ?>
  281. <strong>CSP-J/S 2026 第一轮</strong> 已结束
  282. <?php endif; ?>
  283. <br/>
  284. <?php if ($csp2daysLeft > 0): ?>
  285. 距 <strong>CSP-J/S 2026 第二轮</strong> 还有 <strong><?= $csp2daysLeft ?></strong> 天
  286. <?php elseif ($csp2daysLeft === 0): ?>
  287. <strong>CSP-J/S 2026 第二轮</strong> 就是今天!
  288. <?php else: ?>
  289. <strong>CSP-J/S 2026 第二轮</strong> 已结束
  290. <?php endif; ?>
  291. <br/>
  292. <?php if ($gk27daysLeft > 0): ?>
  293. 距 <strong>2027 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk27daysLeft ?></strong> 天
  294. <?php elseif ($gk27daysLeft === 0): ?>
  295. <strong>2027 年普通高等学校招生全国统一考试</strong> 就是今天!
  296. <?php else: ?>
  297. <strong>2027 年普通高等学校招生全国统一考试</strong> 已结束
  298. <?php endif; ?>
  299. <br/>
  300. <?php if ($gk28daysLeft > 0): ?>
  301. 距 <strong>2028 年普通高等学校招生全国统一考试</strong> 还有 <strong><?= $gk28daysLeft ?></strong> 天
  302. <?php elseif ($gk28daysLeft === 0): ?>
  303. <strong>2028 年普通高等学校招生全国统一考试</strong> 就是今天!
  304. <?php else: ?>
  305. <strong>2028 年普通高等学校招生全国统一考试</strong> 已结束
  306. <?php endif; ?>
  307. </div>
  308. </div>
  309. </div>
  310. <script>
  311. (function () {
  312. var card = document.getElementById('wyoj-date-card');
  313. var canvas = document.getElementById('wyoj-date-canvas');
  314. if (!card || !canvas) return;
  315. var ctx = canvas.getContext('2d');
  316. // ===== 可调参数 =====
  317. var COLORS = ['#000000', '#0c0c0c', '#181818', '#242424', '#303030'];
  318. var CELL = 26; // 每格边长(px)
  319. var P = 0.3; // 每格每轮变色概率(期望值)
  320. var PERIOD = 3000; // 刷新间隔(ms)
  321. var Z = 1.96; // 95% 置信区间
  322. var USE_DPR = false; // 是否按设备像素比放大 canvas(false 更省 CPU)
  323. // ====================
  324. var dpr = USE_DPR ? (window.devicePixelRatio || 1) : 1;
  325. var cols = 0, rows = 0, nCells = 0;
  326. var lastW = 0, lastH = 0;
  327. var gridColors = [];
  328. var idx = null;
  329. var xs = null, ys = null;
  330. var rafId = null;
  331. var running = false;
  332. var lastTick = 0;
  333. function randColor() {
  334. return COLORS[(Math.random() * COLORS.length) | 0];
  335. }
  336. function computeBounds() {
  337. var w = card.clientWidth;
  338. var h = card.clientHeight;
  339. xs = new Int32Array(cols + 1);
  340. ys = new Int32Array(rows + 1);
  341. for (var i = 0; i <= cols; i++) xs[i] = Math.round(i * w / cols);
  342. for (var j = 0; j <= rows; j++) ys[j] = Math.round(j * h / rows);
  343. }
  344. function build() {
  345. var w = card.clientWidth;
  346. var h = card.clientHeight;
  347. if (w === 0 || h === 0) return;
  348. if (w === lastW && h === lastH) return; // 尺寸没变就不重建
  349. lastW = w; lastH = h;
  350. cols = Math.max(1, Math.ceil(w / CELL));
  351. rows = Math.max(1, Math.ceil(h / CELL));
  352. nCells = cols * rows;
  353. canvas.width = Math.round(w * dpr);
  354. canvas.height = Math.round(h * dpr);
  355. canvas.style.width = w + 'px';
  356. canvas.style.height = h + 'px';
  357. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  358. ctx.imageSmoothingEnabled = false;
  359. computeBounds();
  360. gridColors = [];
  361. for (var y = 0; y < rows; y++) {
  362. var row = [];
  363. for (var x = 0; x < cols; x++) row.push(randColor());
  364. gridColors.push(row);
  365. }
  366. idx = new Int32Array(nCells);
  367. for (var i = 0; i < nCells; i++) idx[i] = i;
  368. redrawAll();
  369. }
  370. function redrawAll() {
  371. for (var y = 0; y < rows; y++) {
  372. var y0 = ys[y], y1 = ys[y + 1];
  373. for (var x = 0; x < cols; x++) {
  374. ctx.fillStyle = gridColors[y][x];
  375. ctx.fillRect(xs[x], y0, xs[x + 1] - xs[x], y1 - y0);
  376. }
  377. }
  378. }
  379. function tick() {
  380. if (nCells === 0) return;
  381. var mu = nCells * P;
  382. var sigma = Math.sqrt(nCells * P * (1 - P));
  383. var lo = Math.max(0, Math.floor(mu - Z * sigma));
  384. var hi = Math.min(nCells, Math.ceil(mu + Z * sigma));
  385. var m = lo + Math.floor(Math.random() * (hi - lo + 1));
  386. for (var i = 0; i < m; i++) {
  387. var j = i + Math.floor(Math.random() * (nCells - i));
  388. var t = idx[i]; idx[i] = idx[j]; idx[j] = t;
  389. }
  390. for (var k = 0; k < m; k++) {
  391. var ci = idx[k];
  392. var x = ci % cols;
  393. var y = (ci / cols) | 0;
  394. var c = randColor();
  395. gridColors[y][x] = c;
  396. ctx.fillStyle = c;
  397. ctx.fillRect(xs[x], ys[y], xs[x + 1] - xs[x], ys[y + 1] - ys[y]);
  398. }
  399. }
  400. function loop(now) {
  401. rafId = null;
  402. if (now - lastTick >= PERIOD) {
  403. lastTick = now;
  404. tick();
  405. }
  406. if (running) rafId = requestAnimationFrame(loop);
  407. }
  408. function start() {
  409. if (running) return;
  410. running = true;
  411. lastTick = performance.now();
  412. rafId = requestAnimationFrame(loop);
  413. }
  414. function stop() {
  415. running = false;
  416. if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
  417. }
  418. build();
  419. if (window.ResizeObserver) {
  420. new ResizeObserver(function () { build(); }).observe(card);
  421. } else {
  422. window.addEventListener('resize', build);
  423. }
  424. if (window.IntersectionObserver) {
  425. new IntersectionObserver(function (entries) {
  426. if (entries[0].isIntersecting) start(); else stop();
  427. }, { threshold: 0 }).observe(card);
  428. } else {
  429. start();
  430. }
  431. document.addEventListener('visibilitychange', function () {
  432. if (document.hidden) stop(); else start();
  433. });
  434. })();
  435. </script>
  436. <?php if (!empty($rand_users)): ?>
  437. <div class="mb-4" id="wyoj-random-user-wrap">
  438. <div class="wyoj-random-user" id="wyoj-random-user-link">
  439. <div class="wyoj-random-img-wrap">
  440. <img class="wyoj-rand-img" id="wyoj-rand-img-a" src="" alt="">
  441. <img class="wyoj-rand-img" id="wyoj-rand-img-b" src="" alt="">
  442. <button type="button" class="wyoj-rand-arrow wyoj-rand-arrow-left"
  443. id="wyoj-rand-prev" aria-label="上一个">‹</button>
  444. <button type="button" class="wyoj-rand-arrow wyoj-rand-arrow-right"
  445. id="wyoj-rand-next" aria-label="下一个">›</button>
  446. <button type="button" class="wyoj-rand-pin pinned"
  447. id="wyoj-rand-pin" aria-label="固定" title="已固定,点击恢复自动切换">📌</button>
  448. </div>
  449. <div class="wyoj-random-info">
  450. <span class="wyoj-rand-motto" id="wyoj-rand-motto"></span>
  451. <span class="wyoj-rand-userlink" id="wyoj-rand-userlink"></span>
  452. <img class="wyoj-rand-avatar" id="wyoj-rand-avatar" src="" alt="">
  453. </div>
  454. </div>
  455. <div id="wyoj-random-user-data" data-users='<?= $rand_users_json ?>' style="display:none;"></div>
  456. </div>
  457. <script>
  458. (function () {
  459. var wrap = document.getElementById('wyoj-random-user-wrap');
  460. if (!wrap) return;
  461. var dataEl = document.getElementById('wyoj-random-user-data');
  462. if (!dataEl) return;
  463. var users;
  464. try { users = JSON.parse(dataEl.dataset.users); } catch (e) { wrap.style.display = 'none'; return; }
  465. if (!users || users.length === 0) { wrap.style.display = 'none'; return; }
  466. var link = document.getElementById('wyoj-random-user-link');
  467. var imgA = document.getElementById('wyoj-rand-img-a');
  468. var imgB = document.getElementById('wyoj-rand-img-b');
  469. var mottoEl = document.getElementById('wyoj-rand-motto');
  470. var userEl = document.getElementById('wyoj-rand-userlink');
  471. var avatarEl = document.getElementById('wyoj-rand-avatar');
  472. var prevBtn = document.getElementById('wyoj-rand-prev');
  473. var nextBtn = document.getElementById('wyoj-rand-next');
  474. var pinBtn = document.getElementById('wyoj-rand-pin');
  475. var frontImg = imgA;
  476. var backImg = imgB;
  477. var currentIdx = -1;
  478. var currentUrl = '';
  479. var timer = null;
  480. var pinned = true;
  481. var SWAP_MS = 5000;
  482. function preload(url) {
  483. return new Promise(function (resolve, reject) {
  484. var im = new Image();
  485. im.onload = function () { resolve(); };
  486. im.onerror = function () { reject(); };
  487. im.src = url;
  488. });
  489. }
  490. function applyInfo(u) {
  491. mottoEl.textContent = u.motto || '';
  492. userEl.innerHTML = u.userlink;
  493. avatarEl.src = u.avatar;
  494. currentUrl = u.href;
  495. }
  496. function showImage(u) {
  497. backImg.src = u.bgurl;
  498. requestAnimationFrame(function () {
  499. backImg.classList.add('active');
  500. frontImg.classList.remove('active');
  501. var t = frontImg; frontImg = backImg; backImg = t;
  502. });
  503. }
  504. // 展示指定索引;如果图挂了就往同方向继续找
  505. function goTo(idx, dir, tries) {
  506. dir = dir || 1;
  507. tries = tries || 0;
  508. if (tries >= users.length) return;
  509. var realIdx = ((idx % users.length) + users.length) % users.length;
  510. var u = users[realIdx];
  511. preload(u.bgurl).then(function () {
  512. currentIdx = realIdx;
  513. applyInfo(u);
  514. showImage(u);
  515. }, function () {
  516. goTo(realIdx + dir, dir, tries + 1);
  517. });
  518. }
  519. function schedule() {
  520. clearTimeout(timer);
  521. timer = null;
  522. if (pinned) return;
  523. if (users.length <= 1) return;
  524. timer = setTimeout(function () {
  525. goTo(currentIdx + 1, 1, 0);
  526. schedule();
  527. }, SWAP_MS);
  528. }
  529. function pickRandom() {
  530. if (users.length === 1) return 0;
  531. var i = currentIdx;
  532. while (i === currentIdx) i = Math.floor(Math.random() * users.length);
  533. return i;
  534. }
  535. // 初始化
  536. (function init() {
  537. var tries = 0;
  538. var idx = -1;
  539. for (var i = 0; i < users.length; i++)
  540. if (users[i].username == 'ryp') { idx = i; break; }
  541. // 夹带私货了
  542. if (idx < 0) idx = Math.floor(Math.random() * users.length);
  543. function attempt() {
  544. if (tries >= users.length) { wrap.style.display = 'none'; return; }
  545. tries++;
  546. var u = users[idx];
  547. preload(u.bgurl).then(function () {
  548. currentIdx = idx;
  549. frontImg.src = u.bgurl;
  550. frontImg.classList.add('active');
  551. applyInfo(u);
  552. schedule();
  553. }, function () {
  554. idx = (idx + 1) % users.length;
  555. attempt();
  556. });
  557. }
  558. attempt();
  559. })();
  560. // 点击卡片主体(非按钮、非内部链接)跳转
  561. link.addEventListener('click', function (e) {
  562. if (e.target.closest('button')) return;
  563. if (e.target.closest('a')) return;
  564. if (currentUrl) window.location.href = currentUrl;
  565. });
  566. // 上一个
  567. prevBtn.addEventListener('click', function (e) {
  568. e.stopPropagation();
  569. e.preventDefault();
  570. goTo(currentIdx - 1, -1, 0);
  571. schedule();
  572. });
  573. // 下一个
  574. nextBtn.addEventListener('click', function (e) {
  575. e.stopPropagation();
  576. e.preventDefault();
  577. goTo(currentIdx + 1, 1, 0);
  578. schedule();
  579. });
  580. // 固定 / 取消固定
  581. pinBtn.addEventListener('click', function (e) {
  582. e.stopPropagation();
  583. e.preventDefault();
  584. pinned = !pinned;
  585. pinBtn.classList.toggle('pinned', pinned);
  586. pinBtn.title = pinned ? '已固定,点击恢复自动切换' : '固定当前';
  587. if (pinned) {
  588. clearTimeout(timer);
  589. timer = null;
  590. } else {
  591. schedule();
  592. }
  593. });
  594. document.addEventListener('visibilitychange', function () {
  595. if (document.hidden) {
  596. clearTimeout(timer);
  597. timer = null;
  598. } else {
  599. schedule();
  600. }
  601. });
  602. })();
  603. </script>
  604. <?php endif ?>
  605. <!-- 博客列表 -->
  606. <div class="row">
  607. <?php foreach ($recentblogs as $blog): ?>
  608. <?= echoBlogCard(queryBlog($blog['id'])) ?>
  609. <?php endforeach ?>
  610. <?php foreach ($topblogs as $blog): ?>
  611. <?= echoBlogCard(queryBlog($blog['id'])) ?>
  612. <?php endforeach ?>
  613. </div>
  614. </div>
  615. <!-- ============ 右列:排行榜 ============ -->
  616. <div class="col-12 col-lg-3">
  617. <!-- 活跃 -->
  618. <div class="card shadow-sm border-0 mb-3">
  619. <div class="card-body p-3">
  620. <h6 class="font-weight-bold mb-3">活跃排行榜</h6>
  621. <?php foreach ($activateusers as $username): ?>
  622. <?php $user = queryUser($username['submitter']); ?>
  623. <div class="d-flex align-items-start border-bottom pb-2 mb-2">
  624. <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
  625. style="width:36px;height:36px;object-fit:cover;"
  626. src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
  627. <div class="flex-grow-1" style="min-width:0;">
  628. <div class="text-truncate font-weight-bold" style="font-size:0.9rem;">
  629. <?= getUserLink($user['username']) ?>
  630. </div>
  631. <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
  632. <?= $user['motto'] ?>
  633. </div>
  634. </div>
  635. </div>
  636. <?php endforeach ?>
  637. </div>
  638. </div>
  639. <!-- 比赛 -->
  640. <div class="card shadow-sm border-0 mb-3">
  641. <div class="card-body p-3">
  642. <h6 class="font-weight-bold mb-1">比赛排行榜</h6>
  643. <div class="mb-3" style="font-size:0.75rem;">
  644. <span class="text-muted">180 天内三场及以上</span>
  645. <a href="/ranklist" class="ml-1">全部排名</a>
  646. </div>
  647. <?php foreach ($topusers as $user): ?>
  648. <div class="d-flex align-items-start border-bottom pb-2 mb-2">
  649. <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
  650. style="width:36px;height:36px;object-fit:cover;"
  651. src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
  652. <div class="flex-grow-1" style="min-width:0;">
  653. <div class="d-flex justify-content-between align-items-baseline">
  654. <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
  655. <?= getUserLink($user['username']) ?>
  656. </span>
  657. <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
  658. <?= $user['rating'] ?> <small>(±<?= round($user['rating_rd']) * 2 ?>)</small>
  659. </strong>
  660. </div>
  661. <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
  662. <?= $user['motto'] ?>
  663. </div>
  664. </div>
  665. </div>
  666. <?php endforeach ?>
  667. </div>
  668. </div>
  669. <!-- 黑马 -->
  670. <div class="card shadow-sm border-0 mb-3">
  671. <div class="card-body p-3">
  672. <h6 class="font-weight-bold mb-3">黑马 / 爆冷榜</h6>
  673. <?php foreach ($hotusers as $user): ?>
  674. <div class="d-flex align-items-start border-bottom pb-2 mb-2">
  675. <img class="uoj-avatar rounded-circle flex-shrink-0 mr-2"
  676. style="width:36px;height:36px;object-fit:cover;"
  677. src="https://q.qlogo.cn/g?b=qq&nk=<?= $user['qq'] ?>&s=100" alt="...">
  678. <div class="flex-grow-1" style="min-width:0;">
  679. <div class="d-flex justify-content-between align-items-baseline">
  680. <span class="text-truncate font-weight-bold" style="font-size:0.9rem;">
  681. <?= getUserLink($user['username']) ?>
  682. </span>
  683. <strong class="text-danger ml-1 flex-shrink-0" style="font-size:0.75rem;">
  684. Vol <?= round(1000 * $user['rating_vol']) / 1000 ?>
  685. </strong>
  686. </div>
  687. <div class="text-truncate" style="font-size:0.8rem;font-family:'SimSun','宋体','Songti SC','STSong',serif;">
  688. <?= $user['motto'] ?>
  689. </div>
  690. </div>
  691. </div>
  692. <?php endforeach ?>
  693. </div>
  694. </div>
  695. </div>
  696. </div>
  697. <!-- 博客卡片悬停效果 -->
  698. <style>
  699. .wyoj-blog-card {
  700. transition: box-shadow .2s ease;
  701. }
  702. .wyoj-blog-card:hover {
  703. box-shadow: 0 4px 14px rgba(0,0,0,.10) !important;
  704. }
  705. .wyoj-blog-card .wyoj-blog-title {
  706. font-size: 1.05rem;
  707. line-height: 1.5;
  708. }
  709. /* ---------- 随机用户背景图卡片 ---------- */
  710. .wyoj-random-user {
  711. display: block;
  712. background: #fff;
  713. border: 0;
  714. border-radius: 0;
  715. overflow: hidden;
  716. color: inherit;
  717. box-shadow: 0 .125rem .25rem rgba(0,0,0,.075);
  718. transition: box-shadow 0.2s ease;
  719. cursor: pointer;
  720. }
  721. .wyoj-random-user:hover {
  722. box-shadow: 0 .25rem .75rem rgba(0,0,0,.10);
  723. }
  724. .wyoj-random-img-wrap {
  725. position: relative;
  726. aspect-ratio: 16 / 7;
  727. background: #f8f9fa;
  728. overflow: hidden;
  729. }
  730. .wyoj-rand-img {
  731. position: absolute;
  732. inset: 0;
  733. width: 100%;
  734. height: 100%;
  735. object-fit: cover;
  736. opacity: 0;
  737. transition: opacity 0.6s ease;
  738. }
  739. .wyoj-rand-img.active { opacity: 1; }
  740. /* 左右箭头 */
  741. .wyoj-rand-arrow {
  742. position: absolute;
  743. top: 50%;
  744. transform: translateY(-50%);
  745. width: 32px;
  746. height: 32px;
  747. border: 0;
  748. border-radius: 50%;
  749. background: rgba(255,255,255,0.85);
  750. color: #333;
  751. font-size: 20px;
  752. line-height: 1;
  753. padding: 0 0 2px 0;
  754. cursor: pointer;
  755. display: flex;
  756. align-items: center;
  757. justify-content: center;
  758. z-index: 3;
  759. opacity: 0.4;
  760. transition: opacity 0.2s ease, background 0.2s ease, transform 0.1s ease;
  761. -webkit-tap-highlight-color: transparent;
  762. }
  763. .wyoj-random-user:hover .wyoj-rand-arrow { opacity: 1; }
  764. .wyoj-rand-arrow:hover {
  765. background: #fff;
  766. transform: translateY(-50%) scale(1.08);
  767. }
  768. .wyoj-rand-arrow:active { transform: translateY(-50%) scale(0.96); }
  769. .wyoj-rand-arrow-left { left: 10px; }
  770. .wyoj-rand-arrow-right { right: 10px; }
  771. /* 固定按钮 */
  772. .wyoj-rand-pin {
  773. position: absolute;
  774. top: 10px;
  775. right: 10px;
  776. width: 28px;
  777. height: 28px;
  778. border: 0;
  779. border-radius: 50%;
  780. background: rgba(255,255,255,0.85);
  781. font-size: 13px;
  782. line-height: 1;
  783. padding: 0;
  784. cursor: pointer;
  785. display: flex;
  786. align-items: center;
  787. justify-content: center;
  788. z-index: 3;
  789. opacity: 0.4;
  790. transition: opacity 0.2s ease, background 0.2s ease, transform 0.1s ease;
  791. -webkit-tap-highlight-color: transparent;
  792. }
  793. .wyoj-random-user:hover .wyoj-rand-pin { opacity: 1; }
  794. .wyoj-rand-pin:hover { background: #fff; }
  795. .wyoj-rand-pin:active { transform: scale(0.94); }
  796. .wyoj-rand-pin.pinned {
  797. opacity: 1;
  798. background: #ffc107;
  799. }
  800. .wyoj-rand-pin.pinned:hover { background: #ffb300; }
  801. /* 信息行 */
  802. .wyoj-random-info {
  803. padding: 12px 16px;
  804. display: flex;
  805. align-items: center;
  806. gap: 10px;
  807. min-width: 0;
  808. }
  809. .wyoj-rand-motto {
  810. color: #6c757d;
  811. font-size: 0.85rem;
  812. line-height: 1.4;
  813. overflow: hidden;
  814. text-overflow: ellipsis;
  815. white-space: nowrap;
  816. flex-shrink: 1;
  817. min-width: 0;
  818. }
  819. .wyoj-rand-userlink {
  820. font-size: 0.9rem;
  821. line-height: 1.3;
  822. flex-shrink: 0;
  823. white-space: nowrap;
  824. }
  825. .wyoj-rand-avatar {
  826. width: 24px;
  827. height: 24px;
  828. object-fit: cover;
  829. border-radius: 50%;
  830. border: 1px solid #dee2e6;
  831. flex-shrink: 0;
  832. margin-left: auto;
  833. }
  834. </style>
  835. <?php echoUOJPageFooter() ?>