feat(个人主页): 主页改成名片 + 两栏,做对的题按天带题名;设置并进主页,点头像换头像、点笔改签名

- 名片:头像、班级、签名、做对几道 / 本班第几 / 成就,底下一排四个时间(第一次提交、最近一次提交、距上次提交、学习天数)
- 做对的题按日期或按知识点,能搜;最上面一行是交过还没做对的
- 知识点本人和老师看得到;同学看多一张「你和他」
- 老师看学生:看他的智能分析 / 看他的提交 /「⋯」不计入排名、清空签名
- 换头像弹框:拖圆圈选一块,看领奖台和排名里多大,可退回名字的字
- 去掉「我的设置」页和下拉菜单里那一项;手机三个页签

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 00:03:37 +08:00
1 parent cbf5b9d922
commit e368ed3eca
10 files changed
+1983 -370

No files matched your search

@@ -0,0 +1,133 @@
<script setup lang="ts">
import type { ProfileOverview } from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import AchievementIcon from "shared/components/AchievementIcon.vue"
import { RARITY_COLOR } from "utils/constants"
import { parseTime } from "utils/functions"
/** 拿到的成就一个一个摆出来,新的在前;全部 41 个(含没拿到的)在成就页 */
const props = defineProps<{
achievements: ProfileOverview["achievements"]
username: string
/** 看的是别人:「全部成就」带上他的名字 */
other: boolean
compact?: boolean
}>()
const theme = useThemeVars()
const allLink = computed(() => ({
path: "/achievement",
query: props.other ? { name: props.username } : {},
}))
</script>
<template>
<section class="card" :class="{ compact }">
<h3>
成就 <small>{{ achievements.unlocked.length }} / {{ achievements.total }}</small>
<span class="grow"></span>
<router-link :to="allLink" class="link">全部成就 ›</router-link>
</h3>
<div v-if="achievements.unlocked.length" class="grid">
<n-tooltip v-for="item in achievements.unlocked" :key="item.id" :delay="200">
<template #trigger>
<span class="medal">
<span class="ring" :style="{ '--rarity': RARITY_COLOR[item.rarity] }">
<AchievementIcon :icon="item.icon" :size="26" />
</span>
<span class="name">{{ item.name }}</span>
</span>
</template>
{{ item.description }} · {{ parseTime(item.unlockTime, "YYYY年M月D日") }}
</n-tooltip>
</div>
<div v-else class="empty">还没有成就</div>
</section>
</template>
<style scoped>
.card {
padding: 14px 18px 16px;
border: 1px solid v-bind("theme.borderColor");
border-radius: var(--oj-radius);
background: v-bind("theme.cardColor");
}
h3 {
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
font-size: var(--oj-fs-h2);
}
h3 small {
font-size: var(--oj-fs-sec);
font-weight: 400;
color: v-bind("theme.textColor3");
}
.grow {
flex: 1;
}
.link {
font-size: var(--oj-fs-meta);
font-weight: 400;
color: #18a058;
text-decoration: none;
}
.grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
row-gap: 14px;
margin-top: 12px;
}
.medal {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: default;
}
.ring {
width: 40px;
height: 40px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid var(--rarity);
display: flex;
align-items: center;
justify-content: center;
}
.name {
max-width: 100%;
font-size: 12px;
line-height: 16px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.empty {
padding: 16px 0 4px;
font-size: var(--oj-fs-sec);
color: v-bind("theme.textColor3");
}
.compact {
padding: 4px 0 0;
border: 0;
border-radius: 0;
background: none;
}
.compact .grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
</style>