user_info.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <?php
  2. $username = $_GET['username'];
  3. requireLib('flot');
  4. $REQUIRE_LIB['mathjax'] = '';
  5. $REQUIRE_LIB['hljs'] = '';
  6. ?>
  7. <?php if (validateUsername($username) && ($user = queryUser($username))): ?>
  8. <?php echoUOJPageHeader($user['username'] . ' - ' . UOJLocale::get('user profile')) ?>
  9. <?php
  10. $esc_motto = HTML::escape($user['motto']);
  11. $esc_qq = HTML::escape($user['qq'] != 0 ? $user['qq'] : 'Unfilled');
  12. $groups = explode(',', $user['extgroups']);
  13. $extgroups = '';
  14. foreach ($groups as $grp) {
  15. $grpname = HTML::escape(queryUser($grp)['email']);
  16. if ($grpname == '') $grpname = '无昵称';
  17. $extgroups .= "<a href=\"/user/profile/{$grp}\">$grpname (" . HTML::escape($grp) . ")</a>";
  18. $extgroups .= '<br/>';
  19. }
  20. $bgurl = isset($user['bgurl']) ? trim($user['bgurl']) : '';
  21. $has_bg = $bgurl !== '' && preg_match('#^https?://#i', $bgurl);
  22. $bg_style = $has_bg ? 'background-image: url(\'' . HTML::escape($bgurl) . '\');' : '';
  23. // rating 排名:rating 严格小于他的人数 + 1
  24. $rating_rank = DB::selectCount("select count(*) from user_info where rating > {$user['rating']}") + 1;
  25. $blog_url = HTML::blog_url($user['username'], '/');
  26. ?>
  27. <div class="wyoj-profile">
  28. <!-- Banner -->
  29. <div class="wyoj-banner" style="<?= $bg_style ?>">
  30. <div class="wyoj-banner-overlay"></div>
  31. <div class="wyoj-banner-content">
  32. <div class="wyoj-identity">
  33. <div class="wyoj-username"><?= $user['username'] ?></div>
  34. <div class="wyoj-motto"><?= $esc_motto ?></div>
  35. </div>
  36. <img class="wyoj-avatar"
  37. src="<?= HTML::avatar_addr($user, 256) ?>"
  38. alt="<?= $user['username'] ?> Avatar">
  39. </div>
  40. </div>
  41. <!-- 下方:左 aboutme,右信息卡 -->
  42. <div class="row wyoj-profile-body">
  43. <div class="col-12 col-lg-8">
  44. <div class="wyoj-about">
  45. <?php
  46. $blogid = $user['aboutme_blogid'];
  47. if ($blogid) {
  48. echoBlog(queryBlog($blogid), array(
  49. 'show_content_only' => true,
  50. 'outside_blog_domain' => true
  51. ));
  52. } else {
  53. echo '<p class="wyoj-empty">这个人很懒,什么都没有写。</p>';
  54. }
  55. ?>
  56. </div>
  57. </div>
  58. <div class="col-12 col-lg-4">
  59. <div class="wyoj-info-card">
  60. <div class="wyoj-info-item">
  61. <div class="wyoj-info-label">Rating</div>
  62. <div class="wyoj-info-value">
  63. <strong style="color:#dc3545;"><?= $user['rating'] ?></strong>
  64. <small>(±<?= round($user['rating_rd']) * 2 ?>)</small>
  65. </div>
  66. </div>
  67. <div class="wyoj-info-item">
  68. <div class="wyoj-info-label">Rating 排名</div>
  69. <div class="wyoj-info-value">
  70. <strong style="color:#dc3545;">#<?= $rating_rank ?></strong>
  71. </div>
  72. </div>
  73. <div class="wyoj-info-item">
  74. <div class="wyoj-info-label">QQ</div>
  75. <div class="wyoj-info-value"><?= $esc_qq ?></div>
  76. </div>
  77. <div class="wyoj-info-item">
  78. <div class="wyoj-info-label">用户组</div>
  79. <div class="wyoj-info-value"><?= $extgroups ?></div>
  80. </div>
  81. <div class="wyoj-info-item">
  82. <div class="wyoj-info-label">注册时间</div>
  83. <div class="wyoj-info-value"><?= $user['register_time'] ?></div>
  84. </div>
  85. <div class="wyoj-info-item wyoj-info-action">
  86. <a href="<?= $blog_url ?>" class="wyoj-blog-btn">
  87. <span class="glyphicon glyphicon-arrow-right"></span>访问 TA 的博客
  88. </a>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <!-- rating 曲线 + 分布 -->
  94. <div class="row wyoj-rating-row">
  95. <div class="col-12 col-lg-8">
  96. <div class="wyoj-chart-card">
  97. <h5 class="wyoj-chart-title"><?= UOJLocale::get('rating changes') ?></h5>
  98. <div id="rating-plot" class="wyoj-chart"></div>
  99. </div>
  100. </div>
  101. <div class="col-12 col-lg-4">
  102. <div class="wyoj-chart-card">
  103. <h5 class="wyoj-chart-title">Rating 分布</h5>
  104. <div id="rating-dist-plot" class="wyoj-chart"></div>
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. <style>
  110. .wyoj-profile { color: #212529; }
  111. /* ---------- Banner ---------- */
  112. .wyoj-banner {
  113. position: relative;
  114. height: 220px;
  115. border-radius: 8px;
  116. overflow: hidden;
  117. background-color: #f8f9fa;
  118. background-size: cover;
  119. background-position: center;
  120. background-repeat: no-repeat;
  121. background-image: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  122. border: 1px solid #dee2e6;
  123. }
  124. .wyoj-banner-overlay {
  125. position: absolute; inset: 0;
  126. background: linear-gradient(180deg,
  127. rgba(255,255,255,0.10) 0%,
  128. rgba(255,255,255,0.55) 100%);
  129. pointer-events: none;
  130. }
  131. .wyoj-banner-content {
  132. position: absolute; inset: 0;
  133. display: flex; align-items: flex-end; justify-content: flex-end;
  134. padding: 20px 28px; gap: 20px; z-index: 1;
  135. }
  136. .wyoj-identity {
  137. text-align: right;
  138. max-width: 420px;
  139. padding-bottom: 6px;
  140. }
  141. .wyoj-username {
  142. color: #212529;
  143. font-size: 1.4rem;
  144. font-weight: 700;
  145. line-height: 1.2;
  146. text-shadow: 0 1px 2px rgba(255,255,255,0.9);
  147. }
  148. .wyoj-motto {
  149. color: #212529;
  150. font-size: 1rem;
  151. line-height: 1.5;
  152. text-shadow: 0 1px 2px rgba(255,255,255,0.9);
  153. }
  154. .wyoj-avatar {
  155. width: 96px; height: 96px; object-fit: cover;
  156. border-radius: 50%;
  157. border: 3px solid #fff;
  158. box-shadow: 0 2px 10px rgba(0,0,0,0.15);
  159. flex-shrink: 0;
  160. }
  161. /* ---------- 下方内容 ---------- */
  162. .wyoj-profile-body { margin-top: 24px; }
  163. .wyoj-about { min-height: 1px; }
  164. .wyoj-about a { color: #dc3545; }
  165. .wyoj-empty {
  166. color: #6c757d;
  167. margin: 0;
  168. padding: 24px;
  169. background: #fff;
  170. border: 1px solid #dee2e6;
  171. border-radius: 8px;
  172. }
  173. .wyoj-info-card {
  174. background: #fff;
  175. border: 1px solid #dee2e6;
  176. border-radius: 8px;
  177. padding: 8px 20px;
  178. }
  179. .wyoj-info-item {
  180. padding: 14px 0;
  181. border-bottom: 1px solid #e9ecef;
  182. }
  183. .wyoj-info-item:last-child { border-bottom: none; }
  184. .wyoj-info-label {
  185. color: #6c757d;
  186. font-size: 0.78rem;
  187. letter-spacing: 0.04em;
  188. margin-bottom: 4px;
  189. }
  190. .wyoj-info-value {
  191. color: #212529;
  192. font-size: 0.95rem;
  193. word-break: break-all;
  194. }
  195. .wyoj-info-action {
  196. padding: 16px 0;
  197. }
  198. .wyoj-blog-btn {
  199. display: block;
  200. width: 100%;
  201. text-align: center;
  202. padding: 9px 12px;
  203. background: #fff;
  204. border: 1px solid #dc3545;
  205. color: #dc3545;
  206. border-radius: 6px;
  207. font-size: 0.9rem;
  208. text-decoration: none;
  209. transition: all 0.15s ease;
  210. }
  211. .wyoj-blog-btn:hover {
  212. background: #dc3545;
  213. color: #fff;
  214. text-decoration: none;
  215. }
  216. /* ---------- rating 图表 ---------- */
  217. .wyoj-rating-row { margin-top: 24px; }
  218. .wyoj-chart-card {
  219. background: #fff;
  220. border: 1px solid #dee2e6;
  221. border-radius: 8px;
  222. padding: 20px;
  223. height: 100%;
  224. }
  225. .wyoj-chart-title {
  226. color: #212529;
  227. font-size: 0.95rem;
  228. font-weight: 600;
  229. margin-bottom: 14px;
  230. }
  231. .wyoj-chart { width: 100%; height: 320px; }
  232. .wyoj-chart .flot-tick-label {
  233. color: #6c757d !important;
  234. font-size: 0.75rem;
  235. }
  236. .wyoj-chart .flot-text { color: #6c757d; }
  237. .wyoj-chart canvas { background: transparent; }
  238. #rating-tooltip {
  239. position: absolute; display: none;
  240. background: #fff !important;
  241. color: #212529 !important;
  242. border: 1px solid #dc3545 !important;
  243. border-radius: 6px;
  244. padding: 6px 10px !important;
  245. font-size: 12px !important;
  246. opacity: 0.98 !important;
  247. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  248. z-index: 9999;
  249. pointer-events: none;
  250. }
  251. #rating-tooltip a { color: #dc3545 !important; }
  252. /* ---------- 小屏适配 ---------- */
  253. @media (max-width: 991.98px) {
  254. .wyoj-banner { height: 180px; }
  255. .wyoj-banner-content { padding: 16px 18px; gap: 14px; }
  256. .wyoj-identity { max-width: 60%; }
  257. .wyoj-username { font-size: 1.15rem; }
  258. .wyoj-motto { font-size: 0.9rem; }
  259. .wyoj-avatar { width: 72px; height: 72px; }
  260. .wyoj-info-card { margin-top: 20px; }
  261. .wyoj-chart { height: 260px; }
  262. .wyoj-chart-card + .wyoj-chart-card { margin-top: 20px; }
  263. }
  264. </style>
  265. <script type="text/javascript">
  266. var rating_data = [[
  267. <?php
  268. $user_rating_min = $user_rating_max = 1500;
  269. $result = DB::query("select contest_id, `rank`, user_rating from contests_registrants join contests on id = contest_id where username = '{$user['username']}' and has_participated = 1 order by contests.start_time");
  270. $is_first_row = true;
  271. $last_rating = 1500;
  272. while ($row = DB::fetch($result)) {
  273. $contest = queryContest($row['contest_id']);
  274. $rating_delta = $row['user_rating'] - $last_rating;
  275. if (!$is_first_row) {
  276. echo "[$last_contest_time, {$row['user_rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta],";
  277. } else {
  278. $is_first_row = false;
  279. }
  280. $contest_start_time = new DateTime($contest['start_time']);
  281. $last_contest_time = ($contest_start_time->getTimestamp() + $contest_start_time->getOffset()) * 1000;
  282. $last_contest_name = json_encode($contest['name']);
  283. $last_contest_id = $contest['id'];
  284. $last_rank = $row['rank'];
  285. $last_rating = $row['user_rating'];
  286. if ($row['user_rating'] < $user_rating_min) $user_rating_min = $row['user_rating'];
  287. if ($row['user_rating'] > $user_rating_max) $user_rating_max = $row['user_rating'];
  288. }
  289. if ($is_first_row) {
  290. $time_now_stamp = (UOJTime::$time_now->getTimestamp() + UOJTime::$time_now->getOffset()) * 1000;
  291. echo "[{$time_now_stamp}, {$user['rating']}, 0]";
  292. } else {
  293. $rating_delta = $user['rating'] - $last_rating;
  294. echo "[$last_contest_time, {$user['rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta]";
  295. }
  296. if ($user['rating'] < $user_rating_min) $user_rating_min = $user['rating'];
  297. if ($user['rating'] > $user_rating_max) $user_rating_max = $user['rating'];
  298. $user_rating_min -= 400;
  299. $user_rating_max += 400;
  300. ?>
  301. ]];
  302. var rating_plot = $.plot($("#rating-plot"), [{
  303. color: "#dc3545",
  304. label: "<?= $user['username'] ?>",
  305. data: rating_data[0]
  306. }], {
  307. series: {
  308. lines: { show: true, lineWidth: 2 },
  309. points: { show: true, radius: 3, fillColor: "#fff" }
  310. },
  311. xaxis: { mode: "time", color: "#6c757d" },
  312. yaxis: {
  313. min: <?= $user_rating_min ?>,
  314. max: <?= $user_rating_max ?>,
  315. color: "#6c757d"
  316. },
  317. legend: {
  318. labelFormatter: function(username) {
  319. return getUserLink(username, <?= $user['rating'] ?>, false);
  320. }
  321. },
  322. grid: {
  323. clickable: true,
  324. hoverable: true,
  325. borderWidth: 1,
  326. borderColor: "#e9ecef",
  327. color: "#6c757d",
  328. backgroundColor: null
  329. },
  330. hooks: {
  331. drawBackground: [
  332. function(plot, ctx) {
  333. var plotOffset = plot.getPlotOffset();
  334. for (var y = 0; y < plot.height(); y++) {
  335. var rating = <?= $user_rating_max ?> - <?= $user_rating_max - $user_rating_min ?> * y / plot.height();
  336. ctx.fillStyle = getColOfRating(rating);
  337. ctx.globalAlpha = 0.10;
  338. ctx.fillRect(plotOffset.left, plotOffset.top + y, plot.width(), Math.min(5, plot.height() - y));
  339. }
  340. ctx.globalAlpha = 1;
  341. }
  342. ]
  343. }
  344. });
  345. function showTooltip(x, y, contents) {
  346. $('<div id="rating-tooltip">' + contents + '</div>').css({
  347. position: 'absolute',
  348. display: 'none',
  349. top: y - 20,
  350. left: x + 10
  351. }).appendTo("body").fadeIn(200);
  352. }
  353. var prev = -1;
  354. function onHoverRating(event, pos, item) {
  355. if (prev != item.dataIndex) {
  356. $("#rating-tooltip").remove();
  357. var params = rating_data[item.seriesIndex][item.dataIndex];
  358. var total = params[1];
  359. var contestId = params[2];
  360. if (contestId != 0) {
  361. var change = params[5] > 0 ? "+" + params[5] : params[5];
  362. var contestName = params[3];
  363. var rank = params[4];
  364. var html = "= " + total + " (" + change + "), <br/>"
  365. + "Rank: " + rank + "<br/>"
  366. + '<a href="' + '/contest/' + contestId + '">' + contestName + '</a>';
  367. } else {
  368. var html = "= " + total + "<br/>Unrated";
  369. }
  370. showTooltip(item.pageX, item.pageY, html);
  371. prev = item.dataIndex;
  372. }
  373. }
  374. $("#rating-plot").bind("plothover", function (event, pos, item) {
  375. if (item) onHoverRating(event, pos, item);
  376. });
  377. $("#rating-plot").bind("plotclick", function (event, pos, item) {
  378. if (item && prev == -1) {
  379. onHoverRating(event, pos, item);
  380. } else {
  381. $("#rating-tooltip").fadeOut(200);
  382. prev = -1;
  383. }
  384. });
  385. (() => {
  386. var E = <?php echo $user['rating']; ?>, sigma = <?php echo $user['rating_rd']; ?>;
  387. var data = [], step = 0.1;
  388. for (var x = E - 6 * sigma; x <= E + 6 * sigma; x += step) {
  389. var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
  390. var pdf = (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(e);
  391. data.push([x, pdf]);
  392. }
  393. var realratings = [
  394. <?php
  395. $result = DB::query("select contest_id, user_rating from contests_registrants where username = '{$user['username']}' and has_participated = 1");
  396. foreach ($result as $r) echo "{$r['user_rating']}, ";
  397. echo '0';
  398. ?>
  399. ];
  400. var realdata = [];
  401. realratings.forEach((x) => {
  402. if (x >= E - 6 * sigma && x <= E + 6 * sigma) {
  403. var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
  404. var pdf = (1 / sigma / Math.sqrt(2 * Math.PI)) * Math.exp(e);
  405. realdata.push([x, pdf]);
  406. }
  407. });
  408. var maxY = 0;
  409. data.forEach(p => { if (p[1] > maxY) maxY = p[1]; });
  410. $.plot('#rating-dist-plot', [{
  411. data: data,
  412. color: '#dc3545',
  413. lines: { show: true, lineWidth: 2 },
  414. points: { show: false }
  415. }, {
  416. data: realdata,
  417. color: '#6c757d',
  418. lines: { show: false },
  419. points: {
  420. show: true,
  421. radius: 3,
  422. fillColor: '#fff',
  423. symbol: 'circle'
  424. }
  425. }, {
  426. data: [[E, 0], [E, maxY]],
  427. color: '#dc3545',
  428. lines: { show: true, lineWidth: 1 },
  429. dashes: { show: true, lineWidth: 1, dashLength: 4 }
  430. }], {
  431. grid: {
  432. hoverable: true,
  433. borderWidth: 1,
  434. borderColor: "#e9ecef",
  435. color: "#6c757d",
  436. backgroundColor: null,
  437. markings: [{
  438. xaxis: { from: E, to: E },
  439. color: "rgba(220,53,69,0.10)"
  440. }]
  441. },
  442. xaxis: { color: "#6c757d" },
  443. yaxis: { color: "#6c757d", show: false },
  444. legend: { show: false }
  445. });
  446. })();
  447. </script>
  448. <?php else: ?>
  449. <?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
  450. <div class="card border-danger">
  451. <div class="card-header bg-danger">用户信息</div>
  452. <div class="card-body">
  453. <h4>不存在该用户</h4>
  454. </div>
  455. </div>
  456. <?php endif ?>
  457. <?php echoUOJPageFooter() ?>