|
|
@@ -1,327 +1,463 @@
|
|
|
-<?php
|
|
|
- $username = $_GET['username'];
|
|
|
-
|
|
|
- requireLib('flot');
|
|
|
- $REQUIRE_LIB['mathjax'] = '';
|
|
|
- $REQUIRE_LIB['hljs'] = '';
|
|
|
-
|
|
|
-?>
|
|
|
-<?php if (validateUsername($username) && ($user = queryUser($username))): ?>
|
|
|
- <?php echoUOJPageHeader($user['username'] . ' - ' . UOJLocale::get('user profile')) ?>
|
|
|
- <?php
|
|
|
- $esc_email = HTML::escape($user['email']);
|
|
|
- $esc_qq = HTML::escape($user['qq'] != 0 ? $user['qq'] : 'Unfilled');
|
|
|
- $esc_sex = HTML::escape($user['sex']);
|
|
|
- $col_sex="color:blue";
|
|
|
- if ($esc_sex == "M") {
|
|
|
- $esc_sex="♂";
|
|
|
- $col_sex="color:blue";
|
|
|
- } elseif ($esc_sex == "F") {
|
|
|
- $esc_sex="♀";
|
|
|
- $col_sex="color:red";
|
|
|
- } else {
|
|
|
- $esc_sex="";
|
|
|
- $col_sex="color:black";
|
|
|
- }
|
|
|
- $esc_motto = HTML::escape($user['motto']);
|
|
|
- $extgroups = $user['extgroups'];
|
|
|
- ?>
|
|
|
- <div class="card border-info">
|
|
|
- <h5 class="card-header bg-info"><?= UOJLocale::get('user profile') ?></h5>
|
|
|
- <div class="card-body">
|
|
|
- <div class="row mb-4">
|
|
|
- <div class="col-md-4 order-md-9">
|
|
|
- <img class="media-object img-thumbnail d-block mx-auto" alt="<?= $user['username'] ?> Avatar" height="250" width="250" src="<?= HTML::avatar_addr($user, 256) ?>" />
|
|
|
- </div>
|
|
|
- <div class="col-md-8 order-md-1">
|
|
|
- <h2><span class="uoj-honor" data-rating="<?= $user['rating'] ?>" data-certs="<?= $user['certs'] ?>"><?= $user['username'] ?></span> <span><strong style="<?= $col_sex ?>"><?= $esc_sex ?></strong></span></h2>
|
|
|
- <div class="list-group">
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('rating') ?></h4>
|
|
|
- <p class="list-group-item-text"><strong style="color:red"><?= $user['rating'] ?> (±<?= 2 * round($user['rating_rd']) ?>), vol <?= round($user['rating_vol'] * 100) / 100.0 ?></strong></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('email') ?></h4>
|
|
|
- <p class="list-group-item-text"><?= $esc_email ?></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('QQ') ?></h4>
|
|
|
- <p class="list-group-item-text"><?= $esc_qq ?></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('motto') ?></h4>
|
|
|
- <p class="list-group-item-text"><?= $esc_motto ?></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading">用户组信息</h4>
|
|
|
- <p class="list-group-item-text"><?= $extgroups ?></p>
|
|
|
- </div>
|
|
|
-
|
|
|
- <?php if (isSuperUser($myUser)): ?>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading">register time</h4>
|
|
|
- <p class="list-group-item-text"><?= $user['register_time'] ?></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading">remote_addr</h4>
|
|
|
- <p class="list-group-item-text"><?= $user['remote_addr'] ?></p>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading">http_x_forwarded_for</h4>
|
|
|
- <p class="list-group-item-text"><?= $user['http_x_forwarded_for'] ?></p>
|
|
|
- </div>
|
|
|
- <?php endif ?>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <?php if (Auth::check()): ?>
|
|
|
- <?php if (Auth::id() != $user['username']): ?>
|
|
|
- <a type="button" class="btn btn-info btn-sm" href="/user/msg?enter=<?= $user['username'] ?>"><span class="glyphicon glyphicon-envelope"></span> <?= UOJLocale::get('send private message') ?></a>
|
|
|
- <?php else: ?>
|
|
|
- <a type="button" class="btn btn-info btn-sm" href="/user/modify-profile"><span class="glyphicon glyphicon-pencil"></span> <?= UOJLocale::get('modify my profile') ?></a>
|
|
|
- <?php endif ?>
|
|
|
- <?php endif ?>
|
|
|
-
|
|
|
- <a type="button" class="btn btn-success btn-sm" href="<?= HTML::blog_url($user['username'], '/') ?>"><span class="glyphicon glyphicon-arrow-right"></span> <?= UOJLocale::get('visit his blog', $username) ?></a>
|
|
|
-
|
|
|
- <div class="top-buffer-lg"></div>
|
|
|
- <div class="list-group">
|
|
|
- <?php
|
|
|
- $blogid = $user['aboutme_blogid'];
|
|
|
- if ($blogid) {
|
|
|
- echo '<div class="list-group-item">';
|
|
|
- echoBlog(queryBlog($blogid), array('show_content_only' => true, 'outside_blog_domain' => true));
|
|
|
- echo '</div>';
|
|
|
- }
|
|
|
- ?>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('rating changes') ?></h4>
|
|
|
- <div class="list-group-item-text" id="rating-plot" style="height:500px;"></div>
|
|
|
- </div>
|
|
|
- <div class="list-group-item">
|
|
|
- <h4 class="list-group-item-heading">Rating 分布</h4>
|
|
|
- <div class="list-group-item-text" id="rating-dist-plot" style="height:500px;"></div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="list-group-item">
|
|
|
- <?php
|
|
|
- $ac_problems = DB::selectAll("select problem_id from best_ac_submissions where submitter = '{$user['username']}'");
|
|
|
- ?>
|
|
|
- <h4 class="list-group-item-heading"><?= UOJLocale::get('accepted problems').':'.UOJLocale::get('n problems in total', count($ac_problems))?> </h4>
|
|
|
- <p class="list-group-item-text">
|
|
|
- <?php
|
|
|
- foreach ($ac_problems as $problem) {
|
|
|
- echo '<a href="/problem/', $problem['problem_id'], '" style="display:inline-block; width:4em;">', $problem['problem_id'], '</a>';
|
|
|
- }
|
|
|
- if (empty($ac_problems)) {
|
|
|
- echo UOJLocale::get('none');
|
|
|
- }
|
|
|
- ?>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-<script type="text/javascript">
|
|
|
-var rating_data = [[
|
|
|
-<?php
|
|
|
- $user_rating_min = $user_rating_max = 1500;
|
|
|
- $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");
|
|
|
- $is_first_row = true;
|
|
|
- $last_rating = 1500;
|
|
|
- while ($row = DB::fetch($result)) {
|
|
|
- $contest = queryContest($row['contest_id']);
|
|
|
- $rating_delta = $row['user_rating'] - $last_rating;
|
|
|
- if (!$is_first_row) {
|
|
|
- echo "[$last_contest_time, {$row['user_rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta],";
|
|
|
- } else {
|
|
|
- $is_first_row = false;
|
|
|
- }
|
|
|
- $contest_start_time = new DateTime($contest['start_time']);
|
|
|
- $last_contest_time = ($contest_start_time->getTimestamp() + $contest_start_time->getOffset()) * 1000;
|
|
|
- $last_contest_name = json_encode($contest['name']);
|
|
|
- $last_contest_id = $contest['id'];
|
|
|
- $last_rank = $row['rank'];
|
|
|
- $last_rating = $row['user_rating'];
|
|
|
-
|
|
|
- if ($row['user_rating'] < $user_rating_min) {
|
|
|
- $user_rating_min = $row['user_rating'];
|
|
|
- }
|
|
|
- if ($row['user_rating'] > $user_rating_max) {
|
|
|
- $user_rating_max = $row['user_rating'];
|
|
|
- }
|
|
|
- }
|
|
|
- if ($is_first_row) {
|
|
|
- $time_now_stamp = (UOJTime::$time_now->getTimestamp() + UOJTime::$time_now->getOffset()) * 1000;
|
|
|
- echo "[{$time_now_stamp}, {$user['rating']}, 0]";
|
|
|
- } else {
|
|
|
- $rating_delta = $user['rating'] - $last_rating;
|
|
|
- echo "[$last_contest_time, {$user['rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta]";
|
|
|
- }
|
|
|
- if ($user['rating'] < $user_rating_min) {
|
|
|
- $user_rating_min = $user['rating'];
|
|
|
- }
|
|
|
- if ($user['rating'] > $user_rating_max) {
|
|
|
- $user_rating_max = $user['rating'];
|
|
|
- }
|
|
|
-
|
|
|
- $user_rating_min -= 400;
|
|
|
- $user_rating_max += 400;
|
|
|
-?>
|
|
|
-]];
|
|
|
-var rating_plot = $.plot($("#rating-plot"), [{
|
|
|
- color: "#3850eb",
|
|
|
- label: "<?= $user['username'] ?>",
|
|
|
- data: rating_data[0]
|
|
|
-}], {
|
|
|
- series: {
|
|
|
- lines: {
|
|
|
- show: true
|
|
|
- },
|
|
|
- points: {
|
|
|
- show: true
|
|
|
- }
|
|
|
- },
|
|
|
- xaxis: {
|
|
|
- mode: "time"
|
|
|
- },
|
|
|
- yaxis: {
|
|
|
- min: <?= $user_rating_min ?>,
|
|
|
- max: <?= $user_rating_max ?>
|
|
|
- },
|
|
|
- legend: {
|
|
|
- labelFormatter: function(username) {
|
|
|
- return getUserLink(username, <?= $user['rating'] ?>, false);
|
|
|
- }
|
|
|
- },
|
|
|
- grid: {
|
|
|
- clickable: true,
|
|
|
- hoverable: true
|
|
|
- },
|
|
|
- hooks: {
|
|
|
- drawBackground: [
|
|
|
- function(plot, ctx) {
|
|
|
- var plotOffset = plot.getPlotOffset();
|
|
|
- for (var y = 0; y < plot.height(); y++) {
|
|
|
- var rating = <?= $user_rating_max ?> - <?= $user_rating_max - $user_rating_min ?> * y / plot.height();
|
|
|
- ctx.fillStyle = getColOfRating(rating);
|
|
|
- ctx.fillRect(plotOffset.left, plotOffset.top + y, plot.width(), Math.min(5, plot.height() - y));
|
|
|
- }
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-function showTooltip(x, y, contents) {
|
|
|
- $('<div id="rating-tooltip">' + contents + '</div>').css({
|
|
|
- position: 'absolute',
|
|
|
- display: 'none',
|
|
|
- top: y - 20,
|
|
|
- left: x + 10,
|
|
|
- border: '1px solid #fdd',
|
|
|
- padding: '2px',
|
|
|
- 'font-size' : '11px',
|
|
|
- 'background-color': '#fee',
|
|
|
- opacity: 0.80
|
|
|
- }).appendTo("body").fadeIn(200);
|
|
|
-}
|
|
|
-
|
|
|
-var prev = -1;
|
|
|
-function onHoverRating(event, pos, item) {
|
|
|
- if (prev != item.dataIndex) {
|
|
|
- $("#rating-tooltip").remove();
|
|
|
- var params = rating_data[item.seriesIndex][item.dataIndex];
|
|
|
-
|
|
|
- var total = params[1];
|
|
|
- var contestId = params[2];
|
|
|
- if (contestId != 0) {
|
|
|
- var change = params[5] > 0 ? "+" + params[5] : params[5];
|
|
|
- var contestName = params[3];
|
|
|
- var rank = params[4];
|
|
|
- var html = "= " + total + " (" + change + "), <br/>"
|
|
|
- + "Rank: " + rank + "<br/>"
|
|
|
- + '<a href="' + '/contest/' + contestId + '">' + contestName + '</a>';
|
|
|
- } else {
|
|
|
- var html = "= " + total + "<br/>"
|
|
|
- + "Unrated";
|
|
|
- }
|
|
|
- showTooltip(item.pageX, item.pageY, html);
|
|
|
- prev = item.dataIndex;
|
|
|
- }
|
|
|
-}
|
|
|
-$("#rating-plot").bind("plothover", function (event, pos, item) {
|
|
|
- if (item) {
|
|
|
- onHoverRating(event, pos, item);
|
|
|
- }
|
|
|
-});
|
|
|
-$("#rating-plot").bind("plotclick", function (event, pos, item) {
|
|
|
- if (item && prev == -1) {
|
|
|
- onHoverRating(event, pos, item);
|
|
|
- } else {
|
|
|
- $("#rating-tooltip").fadeOut(200);
|
|
|
- prev = -1;
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-(() => {
|
|
|
- var E = <?php echo $user['rating']; ?>, sigma = <?php echo $user['rating_rd']; ?>;
|
|
|
- var data = [], step = 0.1;
|
|
|
- for (var x = E - 6 * sigma; x <= E + 6 * sigma; x += step) {
|
|
|
- var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
|
|
|
- var pdf = (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(e);
|
|
|
- data.push([x, pdf]);
|
|
|
- }
|
|
|
-
|
|
|
- var realratings = [
|
|
|
- <?php
|
|
|
- $result = DB::query("select contest_id, user_rating from contests_registrants where username = '{$user['username']}' and has_participated = 1");
|
|
|
- foreach ($result as $r) echo "{$r['user_rating']}, ";
|
|
|
- echo '0';
|
|
|
- ?>
|
|
|
- ];
|
|
|
-
|
|
|
- console.log(realratings);
|
|
|
-
|
|
|
- var realdata = [];
|
|
|
- realratings.forEach((x) => {
|
|
|
- if (x >= E - 6 * sigma && x <= E + 6 * sigma) {
|
|
|
- var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
|
|
|
- var pdf = (1 / sigma / Math.sqrt(2 * Math.PI)) * Math.exp(e);
|
|
|
- realdata.push([x, pdf]);
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- console.log(realdata);
|
|
|
-
|
|
|
- $.plot('#rating-dist-plot', [{
|
|
|
- data: data,
|
|
|
- color: '#FF0000',
|
|
|
- lines: { show: true, lineWidth: 2 },
|
|
|
- points: { show: false }
|
|
|
- }, {
|
|
|
- data: realdata,
|
|
|
- color: '#0000FF',
|
|
|
- lines: { show: false },
|
|
|
- points: {
|
|
|
- show: true,
|
|
|
- radius: 3,
|
|
|
- fillColor: '#FFFFFF',
|
|
|
- symbol: 'circle'
|
|
|
- }
|
|
|
- }], {
|
|
|
- grid: { hoverable: true, borderWidth: 1 },
|
|
|
- legend: { show: true, position: 'ne' }
|
|
|
- });
|
|
|
-}) ();
|
|
|
-</script>
|
|
|
-<?php else: ?>
|
|
|
- <?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
|
|
|
- <div class="card border-danger">
|
|
|
- <div class="card-header bg-danger">用户信息</div>
|
|
|
- <div class="card-body">
|
|
|
- <h4>不存在该用户</h4>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-<?php endif ?>
|
|
|
-
|
|
|
-<?php echoUOJPageFooter() ?>
|
|
|
+<?php
|
|
|
+ $username = $_GET['username'];
|
|
|
+
|
|
|
+ requireLib('flot');
|
|
|
+ $REQUIRE_LIB['mathjax'] = '';
|
|
|
+ $REQUIRE_LIB['hljs'] = '';
|
|
|
+?>
|
|
|
+<?php if (validateUsername($username) && ($user = queryUser($username))): ?>
|
|
|
+ <?php echoUOJPageHeader($user['username'] . ' - ' . UOJLocale::get('user profile')) ?>
|
|
|
+ <?php
|
|
|
+ $esc_motto = HTML::escape($user['motto']);
|
|
|
+ $esc_qq = HTML::escape($user['qq'] != 0 ? $user['qq'] : 'Unfilled');
|
|
|
+ $groups = explode(',', $user['extgroups']);
|
|
|
+ $extgroups = '';
|
|
|
+ foreach ($groups as $grp) {
|
|
|
+ $grpname = queryUser($grp)['email'];
|
|
|
+ if ($grpname == '') $grpname = '无昵称';
|
|
|
+ $extgroups .= "<a href=\"/user/profile/{$grp}\">$grpname ($grp)</a>";
|
|
|
+ $extgroups .= '<br/>';
|
|
|
+ }
|
|
|
+
|
|
|
+ $bgurl = isset($user['bgurl']) ? trim($user['bgurl']) : '';
|
|
|
+ $has_bg = $bgurl !== '' && preg_match('#^https?://#i', $bgurl);
|
|
|
+ $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;
|
|
|
+ ?>
|
|
|
+
|
|
|
+ <div class="wyoj-profile">
|
|
|
+
|
|
|
+ <!-- Banner -->
|
|
|
+ <div class="wyoj-banner" style="<?= $bg_style ?>">
|
|
|
+ <div class="wyoj-banner-overlay"></div>
|
|
|
+ <div class="wyoj-banner-content">
|
|
|
+ <div class="wyoj-identity">
|
|
|
+ <div class="wyoj-username"><?= $user['username'] ?></div>
|
|
|
+ <div class="wyoj-motto"><?= $esc_motto ?></div>
|
|
|
+ </div>
|
|
|
+ <img class="wyoj-avatar"
|
|
|
+ src="<?= HTML::avatar_addr($user, 256) ?>"
|
|
|
+ alt="<?= $user['username'] ?> Avatar">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 下方:左 aboutme,右信息卡 -->
|
|
|
+ <div class="row wyoj-profile-body">
|
|
|
+ <div class="col-12 col-lg-8">
|
|
|
+ <div class="wyoj-about">
|
|
|
+ <?php
|
|
|
+ $blogid = $user['aboutme_blogid'];
|
|
|
+ if ($blogid) {
|
|
|
+ echoBlog(queryBlog($blogid), array(
|
|
|
+ 'show_content_only' => true,
|
|
|
+ 'outside_blog_domain' => true
|
|
|
+ ));
|
|
|
+ } else {
|
|
|
+ echo '<p class="wyoj-empty">这个人很懒,什么都没有写。</p>';
|
|
|
+ }
|
|
|
+ ?>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="col-12 col-lg-4">
|
|
|
+ <div class="wyoj-info-card">
|
|
|
+ <div class="wyoj-info-item">
|
|
|
+ <div class="wyoj-info-label">Rating</div>
|
|
|
+ <div class="wyoj-info-value">
|
|
|
+ <strong style="color:#dc3545;"><?= $user['rating'] ?></strong>
|
|
|
+ <small>(±<?= round($user['rating_rd']) * 2 ?>)</small>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="wyoj-info-item">
|
|
|
+ <div class="wyoj-info-label">Rating 排名</div>
|
|
|
+ <div class="wyoj-info-value">
|
|
|
+ <strong style="color:#dc3545;">#<?= $rating_rank ?></strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="wyoj-info-item">
|
|
|
+ <div class="wyoj-info-label">QQ</div>
|
|
|
+ <div class="wyoj-info-value"><?= $esc_qq ?></div>
|
|
|
+ </div>
|
|
|
+ <div class="wyoj-info-item">
|
|
|
+ <div class="wyoj-info-label">用户组</div>
|
|
|
+ <div class="wyoj-info-value"><?= $extgroups ?></div>
|
|
|
+ </div>
|
|
|
+ <div class="wyoj-info-item">
|
|
|
+ <div class="wyoj-info-label">注册时间</div>
|
|
|
+ <div class="wyoj-info-value"><?= $user['register_time'] ?></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- rating 曲线 + 分布 -->
|
|
|
+ <div class="row wyoj-rating-row">
|
|
|
+ <div class="col-12 col-lg-8">
|
|
|
+ <div class="wyoj-chart-card">
|
|
|
+ <h5 class="wyoj-chart-title"><?= UOJLocale::get('rating changes') ?></h5>
|
|
|
+ <div id="rating-plot" class="wyoj-chart"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="col-12 col-lg-4">
|
|
|
+ <div class="wyoj-chart-card">
|
|
|
+ <h5 class="wyoj-chart-title">Rating 分布</h5>
|
|
|
+ <div id="rating-dist-plot" class="wyoj-chart"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <style>
|
|
|
+ .wyoj-profile { color: #212529; }
|
|
|
+
|
|
|
+ /* ---------- Banner ---------- */
|
|
|
+ .wyoj-banner {
|
|
|
+ position: relative;
|
|
|
+ height: 220px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ background-size: cover;
|
|
|
+ background-position: center;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ background-image: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ }
|
|
|
+ .wyoj-banner-overlay {
|
|
|
+ position: absolute; inset: 0;
|
|
|
+ background: linear-gradient(180deg,
|
|
|
+ rgba(255,255,255,0.10) 0%,
|
|
|
+ rgba(255,255,255,0.55) 100%);
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ .wyoj-banner-content {
|
|
|
+ position: absolute; inset: 0;
|
|
|
+ display: flex; align-items: flex-end; justify-content: flex-end;
|
|
|
+ padding: 20px 28px; gap: 20px; z-index: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .wyoj-identity {
|
|
|
+ text-align: right;
|
|
|
+ max-width: 420px;
|
|
|
+ padding-bottom: 6px;
|
|
|
+ }
|
|
|
+ .wyoj-username {
|
|
|
+ color: #212529;
|
|
|
+ font-size: 1.4rem;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.2;
|
|
|
+ text-shadow: 0 1px 2px rgba(255,255,255,0.9);
|
|
|
+ }
|
|
|
+ .wyoj-motto {
|
|
|
+ color: #212529;
|
|
|
+ font-size: 1rem;
|
|
|
+ line-height: 1.5;
|
|
|
+ text-shadow: 0 1px 2px rgba(255,255,255,0.9);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .wyoj-avatar {
|
|
|
+ width: 96px; height: 96px; object-fit: cover;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 3px solid #fff;
|
|
|
+ box-shadow: 0 2px 10px rgba(0,0,0,0.15);
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------- 下方内容 ---------- */
|
|
|
+ .wyoj-profile-body { margin-top: 24px; }
|
|
|
+
|
|
|
+ .wyoj-about { min-height: 1px; }
|
|
|
+ .wyoj-about a { color: #dc3545; }
|
|
|
+ .wyoj-empty {
|
|
|
+ color: #6c757d;
|
|
|
+ margin: 0;
|
|
|
+ padding: 24px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .wyoj-info-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 20px;
|
|
|
+ }
|
|
|
+ .wyoj-info-item {
|
|
|
+ padding: 14px 0;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ }
|
|
|
+ .wyoj-info-item:last-child { border-bottom: none; }
|
|
|
+ .wyoj-info-label {
|
|
|
+ color: #6c757d;
|
|
|
+ font-size: 0.78rem;
|
|
|
+ letter-spacing: 0.04em;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+ .wyoj-info-value {
|
|
|
+ color: #212529;
|
|
|
+ font-size: 0.95rem;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------- rating 图表 ---------- */
|
|
|
+ .wyoj-rating-row { margin-top: 24px; }
|
|
|
+ .wyoj-chart-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ .wyoj-chart-title {
|
|
|
+ color: #212529;
|
|
|
+ font-size: 0.95rem;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ }
|
|
|
+ .wyoj-chart { width: 100%; height: 320px; }
|
|
|
+
|
|
|
+ .wyoj-chart .flot-tick-label {
|
|
|
+ color: #6c757d !important;
|
|
|
+ font-size: 0.75rem;
|
|
|
+ }
|
|
|
+ .wyoj-chart .flot-text { color: #6c757d; }
|
|
|
+ .wyoj-chart canvas { background: transparent; }
|
|
|
+
|
|
|
+ #rating-tooltip {
|
|
|
+ position: absolute; display: none;
|
|
|
+ background: #fff !important;
|
|
|
+ color: #212529 !important;
|
|
|
+ border: 1px solid #dc3545 !important;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 6px 10px !important;
|
|
|
+ font-size: 12px !important;
|
|
|
+ opacity: 0.98 !important;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ z-index: 9999;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ #rating-tooltip a { color: #dc3545 !important; }
|
|
|
+
|
|
|
+ /* ---------- 小屏适配 ---------- */
|
|
|
+ @media (max-width: 991.98px) {
|
|
|
+ .wyoj-banner { height: 180px; }
|
|
|
+ .wyoj-banner-content { padding: 16px 18px; gap: 14px; }
|
|
|
+ .wyoj-identity { max-width: 60%; }
|
|
|
+ .wyoj-username { font-size: 1.15rem; }
|
|
|
+ .wyoj-motto { font-size: 0.9rem; }
|
|
|
+ .wyoj-avatar { width: 72px; height: 72px; }
|
|
|
+ .wyoj-info-card { margin-top: 20px; }
|
|
|
+ .wyoj-chart { height: 260px; }
|
|
|
+ .wyoj-chart-card + .wyoj-chart-card { margin-top: 20px; }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|
|
|
+ <script type="text/javascript">
|
|
|
+ var rating_data = [[
|
|
|
+ <?php
|
|
|
+ $user_rating_min = $user_rating_max = 1500;
|
|
|
+ $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");
|
|
|
+ $is_first_row = true;
|
|
|
+ $last_rating = 1500;
|
|
|
+ while ($row = DB::fetch($result)) {
|
|
|
+ $contest = queryContest($row['contest_id']);
|
|
|
+ $rating_delta = $row['user_rating'] - $last_rating;
|
|
|
+ if (!$is_first_row) {
|
|
|
+ echo "[$last_contest_time, {$row['user_rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta],";
|
|
|
+ } else {
|
|
|
+ $is_first_row = false;
|
|
|
+ }
|
|
|
+ $contest_start_time = new DateTime($contest['start_time']);
|
|
|
+ $last_contest_time = ($contest_start_time->getTimestamp() + $contest_start_time->getOffset()) * 1000;
|
|
|
+ $last_contest_name = json_encode($contest['name']);
|
|
|
+ $last_contest_id = $contest['id'];
|
|
|
+ $last_rank = $row['rank'];
|
|
|
+ $last_rating = $row['user_rating'];
|
|
|
+
|
|
|
+ if ($row['user_rating'] < $user_rating_min) $user_rating_min = $row['user_rating'];
|
|
|
+ if ($row['user_rating'] > $user_rating_max) $user_rating_max = $row['user_rating'];
|
|
|
+ }
|
|
|
+ if ($is_first_row) {
|
|
|
+ $time_now_stamp = (UOJTime::$time_now->getTimestamp() + UOJTime::$time_now->getOffset()) * 1000;
|
|
|
+ echo "[{$time_now_stamp}, {$user['rating']}, 0]";
|
|
|
+ } else {
|
|
|
+ $rating_delta = $user['rating'] - $last_rating;
|
|
|
+ echo "[$last_contest_time, {$user['rating']}, $last_contest_id, $last_contest_name, $last_rank, $rating_delta]";
|
|
|
+ }
|
|
|
+ if ($user['rating'] < $user_rating_min) $user_rating_min = $user['rating'];
|
|
|
+ if ($user['rating'] > $user_rating_max) $user_rating_max = $user['rating'];
|
|
|
+
|
|
|
+ $user_rating_min -= 400;
|
|
|
+ $user_rating_max += 400;
|
|
|
+ ?>
|
|
|
+ ]];
|
|
|
+
|
|
|
+ var rating_plot = $.plot($("#rating-plot"), [{
|
|
|
+ color: "#dc3545",
|
|
|
+ label: "<?= $user['username'] ?>",
|
|
|
+ data: rating_data[0]
|
|
|
+ }], {
|
|
|
+ series: {
|
|
|
+ lines: { show: true, lineWidth: 2 },
|
|
|
+ points: { show: true, radius: 3, fillColor: "#fff" }
|
|
|
+ },
|
|
|
+ xaxis: { mode: "time", color: "#6c757d" },
|
|
|
+ yaxis: {
|
|
|
+ min: <?= $user_rating_min ?>,
|
|
|
+ max: <?= $user_rating_max ?>,
|
|
|
+ color: "#6c757d"
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ labelFormatter: function(username) {
|
|
|
+ return getUserLink(username, <?= $user['rating'] ?>, false);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ clickable: true,
|
|
|
+ hoverable: true,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: "#e9ecef",
|
|
|
+ color: "#6c757d",
|
|
|
+ backgroundColor: null
|
|
|
+ },
|
|
|
+ hooks: {
|
|
|
+ drawBackground: [
|
|
|
+ function(plot, ctx) {
|
|
|
+ var plotOffset = plot.getPlotOffset();
|
|
|
+ for (var y = 0; y < plot.height(); y++) {
|
|
|
+ var rating = <?= $user_rating_max ?> - <?= $user_rating_max - $user_rating_min ?> * y / plot.height();
|
|
|
+ ctx.fillStyle = getColOfRating(rating);
|
|
|
+ ctx.globalAlpha = 0.10;
|
|
|
+ ctx.fillRect(plotOffset.left, plotOffset.top + y, plot.width(), Math.min(5, plot.height() - y));
|
|
|
+ }
|
|
|
+ ctx.globalAlpha = 1;
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ function showTooltip(x, y, contents) {
|
|
|
+ $('<div id="rating-tooltip">' + contents + '</div>').css({
|
|
|
+ position: 'absolute',
|
|
|
+ display: 'none',
|
|
|
+ top: y - 20,
|
|
|
+ left: x + 10
|
|
|
+ }).appendTo("body").fadeIn(200);
|
|
|
+ }
|
|
|
+
|
|
|
+ var prev = -1;
|
|
|
+ function onHoverRating(event, pos, item) {
|
|
|
+ if (prev != item.dataIndex) {
|
|
|
+ $("#rating-tooltip").remove();
|
|
|
+ var params = rating_data[item.seriesIndex][item.dataIndex];
|
|
|
+
|
|
|
+ var total = params[1];
|
|
|
+ var contestId = params[2];
|
|
|
+ if (contestId != 0) {
|
|
|
+ var change = params[5] > 0 ? "+" + params[5] : params[5];
|
|
|
+ var contestName = params[3];
|
|
|
+ var rank = params[4];
|
|
|
+ var html = "= " + total + " (" + change + "), <br/>"
|
|
|
+ + "Rank: " + rank + "<br/>"
|
|
|
+ + '<a href="' + '/contest/' + contestId + '">' + contestName + '</a>';
|
|
|
+ } else {
|
|
|
+ var html = "= " + total + "<br/>Unrated";
|
|
|
+ }
|
|
|
+ showTooltip(item.pageX, item.pageY, html);
|
|
|
+ prev = item.dataIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $("#rating-plot").bind("plothover", function (event, pos, item) {
|
|
|
+ if (item) onHoverRating(event, pos, item);
|
|
|
+ });
|
|
|
+ $("#rating-plot").bind("plotclick", function (event, pos, item) {
|
|
|
+ if (item && prev == -1) {
|
|
|
+ onHoverRating(event, pos, item);
|
|
|
+ } else {
|
|
|
+ $("#rating-tooltip").fadeOut(200);
|
|
|
+ prev = -1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ (() => {
|
|
|
+ var E = <?php echo $user['rating']; ?>, sigma = <?php echo $user['rating_rd']; ?>;
|
|
|
+ var data = [], step = 0.1;
|
|
|
+ for (var x = E - 6 * sigma; x <= E + 6 * sigma; x += step) {
|
|
|
+ var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
|
|
|
+ var pdf = (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(e);
|
|
|
+ data.push([x, pdf]);
|
|
|
+ }
|
|
|
+
|
|
|
+ var realratings = [
|
|
|
+ <?php
|
|
|
+ $result = DB::query("select contest_id, user_rating from contests_registrants where username = '{$user['username']}' and has_participated = 1");
|
|
|
+ foreach ($result as $r) echo "{$r['user_rating']}, ";
|
|
|
+ echo '0';
|
|
|
+ ?>
|
|
|
+ ];
|
|
|
+
|
|
|
+ var realdata = [];
|
|
|
+ realratings.forEach((x) => {
|
|
|
+ if (x >= E - 6 * sigma && x <= E + 6 * sigma) {
|
|
|
+ var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
|
|
|
+ var pdf = (1 / sigma / Math.sqrt(2 * Math.PI)) * Math.exp(e);
|
|
|
+ realdata.push([x, pdf]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ var maxY = 0;
|
|
|
+ data.forEach(p => { if (p[1] > maxY) maxY = p[1]; });
|
|
|
+
|
|
|
+ $.plot('#rating-dist-plot', [{
|
|
|
+ data: data,
|
|
|
+ color: '#dc3545',
|
|
|
+ lines: { show: true, lineWidth: 2 },
|
|
|
+ points: { show: false }
|
|
|
+ }, {
|
|
|
+ data: realdata,
|
|
|
+ color: '#6c757d',
|
|
|
+ lines: { show: false },
|
|
|
+ points: {
|
|
|
+ show: true,
|
|
|
+ radius: 3,
|
|
|
+ fillColor: '#fff',
|
|
|
+ symbol: 'circle'
|
|
|
+ }
|
|
|
+ }, {
|
|
|
+ data: [[E, 0], [E, maxY]],
|
|
|
+ color: '#dc3545',
|
|
|
+ lines: { show: true, lineWidth: 1 },
|
|
|
+ dashes: { show: true, lineWidth: 1, dashLength: 4 }
|
|
|
+ }], {
|
|
|
+ grid: {
|
|
|
+ hoverable: true,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: "#e9ecef",
|
|
|
+ color: "#6c757d",
|
|
|
+ backgroundColor: null,
|
|
|
+ markings: [{
|
|
|
+ xaxis: { from: E, to: E },
|
|
|
+ color: "rgba(220,53,69,0.10)"
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ xaxis: { color: "#6c757d" },
|
|
|
+ yaxis: { color: "#6c757d", show: false },
|
|
|
+ legend: { show: false }
|
|
|
+ });
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+
|
|
|
+<?php else: ?>
|
|
|
+ <?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
|
|
|
+ <div class="card border-danger">
|
|
|
+ <div class="card-header bg-danger">用户信息</div>
|
|
|
+ <div class="card-body">
|
|
|
+ <h4>不存在该用户</h4>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+<?php endif ?>
|
|
|
+
|
|
|
+<?php echoUOJPageFooter() ?>
|