user_info.php 17 KB

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