| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- <?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() ?>
|