feat(排名): 个性签名直接写在名单上,不用再指着小引号才看得到
- 桌面:写了签名的同学名字下面多一行,把签名写出来(一行二十来个字,长的指上去看全文);两栏按高度对半分 - 手机:签名从名字后面(只剩三五个字)挪到名字下面一行 - 自己还没写的,名单里自己那行有「写一句个性签名」,和「你」卡点开的是同一个框 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e946c99fb
commit
28a09831b5
4 files changed
+283
-177
No files matched your search
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MOOD_MAX } from "@oj2/contract"
|
||||||
|
import { updateProfile } from "oj/api"
|
||||||
|
import { useUserStore } from "shared/store/user"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 就地改个性签名:「你」卡上的签名、赛道里自己那行的「写一句个性签名」点开的是同一个框。
|
||||||
|
* 触发按钮由调用方放进默认插槽。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{ mood: string | null }>()
|
||||||
|
const emit = defineEmits<{ saved: [mood: string | null] }>()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const inputId = useId()
|
||||||
|
|
||||||
|
const editing = ref(false)
|
||||||
|
const draft = ref("")
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
watch(editing, (open) => {
|
||||||
|
if (open) draft.value = props.mood ?? ""
|
||||||
|
})
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const mood = draft.value.trim()
|
||||||
|
if (mood.length > MOOD_MAX) {
|
||||||
|
message.warning(`个性签名最多 ${MOOD_MAX} 个字`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await updateProfile({ mood })
|
||||||
|
if (userStore.profile) userStore.profile.mood = mood || null
|
||||||
|
emit("saved", mood || null)
|
||||||
|
editing.value = false
|
||||||
|
message.success(mood ? "签名改好了" : "签名清空了")
|
||||||
|
} catch {
|
||||||
|
message.error("没保存上,再试一次")
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-popover
|
||||||
|
v-model:show="editing"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-start"
|
||||||
|
:style="{ width: '300px' }"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<form class="mood-form" @submit.prevent="save">
|
||||||
|
<label class="mood-label" :for="inputId">个性签名</label>
|
||||||
|
<n-input
|
||||||
|
v-model:value="draft"
|
||||||
|
:input-props="{ id: inputId }"
|
||||||
|
:maxlength="MOOD_MAX"
|
||||||
|
show-count
|
||||||
|
clearable
|
||||||
|
placeholder="写点什么,让大家认识你"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<div class="mood-actions">
|
||||||
|
<n-button size="small" @click="editing = false">取消</n-button>
|
||||||
|
<n-button size="small" type="primary" attr-type="submit" :loading="saving"> 保存 </n-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mood-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mood-label {
|
||||||
|
font-size: var(--oj-fs-sec);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mood-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MOOD_MAX, type RankRow } from "@oj2/contract"
|
import type { RankRow } from "@oj2/contract"
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { updateProfile } from "oj/api"
|
|
||||||
import { useUserStore } from "shared/store/user"
|
|
||||||
import UserName from "shared/components/UserName.vue"
|
import UserName from "shared/components/UserName.vue"
|
||||||
import { gapText, toPass } from "../utils"
|
import { gapText, toPass } from "../utils"
|
||||||
|
import MoodPopover from "./MoodPopover.vue"
|
||||||
import RankAvatar from "./RankAvatar.vue"
|
import RankAvatar from "./RankAvatar.vue"
|
||||||
|
|
||||||
export interface MiniRank {
|
export interface MiniRank {
|
||||||
@@ -23,7 +22,7 @@ export interface MiniRank {
|
|||||||
* 用词只说位置(前一名 / 后一名 / 进步最大),不用「追」「对手」「冲」:用户指出
|
* 用词只说位置(前一名 / 后一名 / 进步最大),不用「追」「对手」「冲」:用户指出
|
||||||
* 「要追的」「追你」会被中学生读成追求谁,「冲得最猛」也容易误会。
|
* 「要追的」「追你」会被中学生读成追求谁,「冲得最猛」也容易误会。
|
||||||
*
|
*
|
||||||
* 个性签名(设计稿「签名 A」):自己的签名挂在大字下面,「改」就地改,不用跳去设置页;
|
* 个性签名:自己的签名挂在大字下面,「改」就地改,不用跳去设置页;
|
||||||
* 没写的给一句「写一句个性签名」。前一名、后一名的签名挂在他们名字下面。
|
* 没写的给一句「写一句个性签名」。前一名、后一名的签名挂在他们名字下面。
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -45,36 +44,6 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{ moodSaved: [mood: string | null] }>()
|
const emit = defineEmits<{ moodSaved: [mood: string | null] }>()
|
||||||
|
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const message = useMessage()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const editing = ref(false)
|
|
||||||
const draft = ref("")
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
watch(editing, (open) => {
|
|
||||||
if (open) draft.value = props.me.mood ?? ""
|
|
||||||
})
|
|
||||||
|
|
||||||
async function saveMood() {
|
|
||||||
const mood = draft.value.trim()
|
|
||||||
if (mood.length > MOOD_MAX) {
|
|
||||||
message.warning(`个性签名最多 ${MOOD_MAX} 个字`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await updateProfile({ mood })
|
|
||||||
if (userStore.profile) userStore.profile.mood = mood || null
|
|
||||||
emit("moodSaved", mood || null)
|
|
||||||
editing.value = false
|
|
||||||
message.success(mood ? "签名改好了" : "签名清空了")
|
|
||||||
} catch {
|
|
||||||
message.error("没保存上,再试一次")
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const beyond = computed(() => !!props.cap && props.me.rank > props.cap)
|
const beyond = computed(() => !!props.cap && props.me.rank > props.cap)
|
||||||
|
|
||||||
@@ -126,37 +95,14 @@ function threatNote(me: RankRow, behind: RankRow) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-popover
|
<MoodPopover :mood="me.mood" @saved="emit('moodSaved', $event)">
|
||||||
v-model:show="editing"
|
<button v-if="me.mood" class="mood" aria-label="改个性签名">
|
||||||
trigger="click"
|
<span class="quote-mark" aria-hidden="true">“</span>
|
||||||
placement="bottom-start"
|
<n-ellipsis class="mood-text" :line-clamp="2" :tooltip="false">{{ me.mood }}</n-ellipsis>
|
||||||
:style="{ width: '300px' }"
|
<span class="mood-edit">
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<button v-if="me.mood" class="mood" aria-label="改个性签名">
|
|
||||||
<span class="quote-mark" aria-hidden="true">“</span>
|
|
||||||
<n-ellipsis class="mood-text" :line-clamp="2" :tooltip="false">{{ me.mood }}</n-ellipsis>
|
|
||||||
<span class="mood-edit">
|
|
||||||
<svg
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M4 20h4L19 9l-4-4L4 16z" />
|
|
||||||
</svg>
|
|
||||||
改
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button v-else class="mood empty">
|
|
||||||
<svg
|
<svg
|
||||||
width="13"
|
width="12"
|
||||||
height="13"
|
height="12"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -167,28 +113,26 @@ function threatNote(me: RankRow, behind: RankRow) {
|
|||||||
>
|
>
|
||||||
<path d="M4 20h4L19 9l-4-4L4 16z" />
|
<path d="M4 20h4L19 9l-4-4L4 16z" />
|
||||||
</svg>
|
</svg>
|
||||||
写一句个性签名,上了领奖台全班都看得到
|
改
|
||||||
</button>
|
</span>
|
||||||
</template>
|
</button>
|
||||||
<form class="mood-form" @submit.prevent="saveMood">
|
<button v-else class="mood empty">
|
||||||
<label class="mood-label" for="rank-mood-input">个性签名</label>
|
<svg
|
||||||
<n-input
|
width="13"
|
||||||
v-model:value="draft"
|
height="13"
|
||||||
:input-props="{ id: 'rank-mood-input' }"
|
viewBox="0 0 24 24"
|
||||||
:maxlength="MOOD_MAX"
|
fill="none"
|
||||||
show-count
|
stroke="currentColor"
|
||||||
clearable
|
stroke-width="2"
|
||||||
placeholder="写点什么,让大家认识你"
|
stroke-linecap="round"
|
||||||
autofocus
|
stroke-linejoin="round"
|
||||||
/>
|
aria-hidden="true"
|
||||||
<div class="mood-actions">
|
>
|
||||||
<n-button size="small" @click="editing = false">取消</n-button>
|
<path d="M4 20h4L19 9l-4-4L4 16z" />
|
||||||
<n-button size="small" type="primary" attr-type="submit" :loading="saving">
|
</svg>
|
||||||
保存
|
写一句个性签名,全班都看得到
|
||||||
</n-button>
|
</button>
|
||||||
</div>
|
</MoodPopover>
|
||||||
</form>
|
|
||||||
</n-popover>
|
|
||||||
<div v-if="headline" class="headline">
|
<div v-if="headline" class="headline">
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
@@ -427,23 +371,6 @@ function threatNote(me: RankRow, behind: RankRow) {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mood-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mood-label {
|
|
||||||
font-size: var(--oj-fs-sec);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mood-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rival-name {
|
.rival-name {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useThemeVars, type DropdownOption } from "naive-ui"
|
|||||||
import UserName from "shared/components/UserName.vue"
|
import UserName from "shared/components/UserName.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { useRankPalette } from "../palette"
|
import { useRankPalette } from "../palette"
|
||||||
|
import MoodPopover from "./MoodPopover.vue"
|
||||||
import RankAvatar from "./RankAvatar.vue"
|
import RankAvatar from "./RankAvatar.vue"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,8 +20,10 @@ import RankAvatar from "./RankAvatar.vue"
|
|||||||
* 桌面的条只有百来像素(老师那边多一个「⋯」,七十来像素),压不下文字标签:「前一名」
|
* 桌面的条只有百来像素(老师那边多一个「⋯」,七十来像素),压不下文字标签:「前一名」
|
||||||
* 「后一名」靠条的颜色认(和右边「你」卡里两个标签同色),「进步最大」改成涨跌那一格的橙底徽章。手机每行有空,标签照旧。
|
* 「后一名」靠条的颜色认(和右边「你」卡里两个标签同色),「进步最大」改成涨跌那一格的橙底徽章。手机每行有空,标签照旧。
|
||||||
*
|
*
|
||||||
* 个性签名(设计稿「签名 A」):桌面一行只有一百来像素的条,放不下一句话,所以有签名的
|
* 个性签名(设计稿「签名直接写在赛道上」T1):写了签名的人名字下面多一行,直接写出来,
|
||||||
* 名字后面挂一个引号,指上去看;手机每行本来就有空,签名直接写在名字后面。
|
* 不再藏在引号后面指上去才看得到(用户:排名上看不出签名)。桌面这一行从名字底下一直伸到
|
||||||
|
* 本栏末尾,能露二十多个字;手机挪到名字下面一行。你还没写的话,你那行(桌面)挂一个
|
||||||
|
* 「写一句个性签名」,和「你」卡点开的是同一个框。
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rows: RankRow[]
|
rows: RankRow[]
|
||||||
@@ -48,6 +51,7 @@ const emit = defineEmits<{
|
|||||||
analysis: [username: string]
|
analysis: [username: string]
|
||||||
hide: [row: RankRow]
|
hide: [row: RankRow]
|
||||||
clearMood: [row: RankRow]
|
clearMood: [row: RankRow]
|
||||||
|
moodSaved: [mood: string | null]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
@@ -109,11 +113,31 @@ function expandGap() {
|
|||||||
if (!props.complete) emit("expand")
|
if (!props.complete) emit("expand")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 两栏:前一半、后一半。手机一栏 */
|
/** 桌面每一项大约多高:带签名(或「写一句」)的行多一行,两栏按高度对半分,不按人数 */
|
||||||
|
function heightOf(item: Item) {
|
||||||
|
if (item.kind === "gap") return 48
|
||||||
|
return hasSecondLine(item.row) ? 56 : 36
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSecondLine(row: RankRow) {
|
||||||
|
return !!row.mood || row.user.id === props.meId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 两栏:前一半、后一半(按高度)。手机一栏 */
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
if (!isDesktop.value) return [items.value]
|
if (!isDesktop.value) return [items.value]
|
||||||
const half = Math.ceil(items.value.length / 2)
|
const total = items.value.reduce((sum, item) => sum + heightOf(item), 0)
|
||||||
return [items.value.slice(0, half), items.value.slice(half)]
|
let acc = 0
|
||||||
|
let cut = items.value.length
|
||||||
|
for (const [index, item] of items.value.entries()) {
|
||||||
|
const h = heightOf(item)
|
||||||
|
if (acc + h / 2 > total / 2) {
|
||||||
|
cut = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
acc += h
|
||||||
|
}
|
||||||
|
return [items.value.slice(0, cut), items.value.slice(cut)]
|
||||||
})
|
})
|
||||||
|
|
||||||
function barColor(row: RankRow) {
|
function barColor(row: RankRow) {
|
||||||
@@ -175,7 +199,7 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
<div
|
<div
|
||||||
v-else-if="!isDesktop"
|
v-else-if="!isDesktop"
|
||||||
class="prow"
|
class="prow"
|
||||||
:class="{ me: item.row.user.id === meId, zero: !item.row.solved }"
|
:class="{ me: item.row.user.id === meId, zero: !item.row.solved, tall: !!item.row.mood }"
|
||||||
>
|
>
|
||||||
<span class="fill" :style="{ width: width(item.row), background: fillColor(item.row) }" />
|
<span class="fill" :style="{ width: width(item.row), background: fillColor(item.row) }" />
|
||||||
<span class="rank">{{ item.row.solved ? item.row.rank : "—" }}</span>
|
<span class="rank">{{ item.row.solved ? item.row.rank : "—" }}</span>
|
||||||
@@ -185,13 +209,17 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
:size="20"
|
:size="20"
|
||||||
:me="item.row.user.id === meId"
|
:me="item.row.user.id === meId"
|
||||||
/>
|
/>
|
||||||
<button class="who" @click="emit('open', item.row.user.username)">
|
<div class="pmain">
|
||||||
<UserName :username="item.row.user.username" />
|
<div class="pname">
|
||||||
</button>
|
<button class="who" @click="emit('open', item.row.user.username)">
|
||||||
<span class="mood">{{ item.row.mood }}</span>
|
<UserName :username="item.row.user.username" />
|
||||||
<span v-if="item.row.user.id === chaseId" class="tag chase">前一名</span>
|
</button>
|
||||||
<span v-else-if="item.row.user.id === threatId" class="tag threat">后一名</span>
|
<span v-if="item.row.user.id === chaseId" class="tag chase">前一名</span>
|
||||||
<span v-else-if="item.row.user.id === hotId" class="tag hot">进步最大</span>
|
<span v-else-if="item.row.user.id === threatId" class="tag threat">后一名</span>
|
||||||
|
<span v-else-if="item.row.user.id === hotId" class="tag hot">进步最大</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="item.row.mood" class="sig">“{{ item.row.mood }}”</span>
|
||||||
|
</div>
|
||||||
<span class="solved">{{ item.row.solved }}</span>
|
<span class="solved">{{ item.row.solved }}</span>
|
||||||
<span class="change">
|
<span class="change">
|
||||||
<span v-if="(item.row.change ?? 0) > 0" class="up">↑{{ item.row.change }}</span>
|
<span v-if="(item.row.change ?? 0) > 0" class="up">↑{{ item.row.change }}</span>
|
||||||
@@ -209,58 +237,73 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
<button class="more" aria-label="更多">⋯</button>
|
<button class="more" aria-label="更多">⋯</button>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="lane" :class="{ me: item.row.user.id === meId, zero: !item.row.solved }">
|
<div
|
||||||
<span class="rank">{{ item.row.solved ? item.row.rank : "—" }}</span>
|
v-else
|
||||||
<RankAvatar
|
class="entry"
|
||||||
:username="item.row.user.username"
|
:class="{ me: item.row.user.id === meId, zero: !item.row.solved }"
|
||||||
:avatar="item.row.avatar"
|
>
|
||||||
:size="20"
|
<div class="lane">
|
||||||
:me="item.row.user.id === meId"
|
<span class="rank">{{ item.row.solved ? item.row.rank : "—" }}</span>
|
||||||
/>
|
<RankAvatar
|
||||||
<n-tooltip :disabled="!item.row.mood" :style="{ maxWidth: '300px' }">
|
:username="item.row.user.username"
|
||||||
<template #trigger>
|
:avatar="item.row.avatar"
|
||||||
<button class="who" @click="emit('open', item.row.user.username)">
|
:size="20"
|
||||||
<UserName :username="item.row.user.username" />
|
:me="item.row.user.id === meId"
|
||||||
|
/>
|
||||||
|
<button class="who" @click="emit('open', item.row.user.username)">
|
||||||
|
<UserName :username="item.row.user.username" />
|
||||||
|
</button>
|
||||||
|
<span class="bar-box" :style="{ background: palette.track }">
|
||||||
|
<span
|
||||||
|
class="bar"
|
||||||
|
:style="{ width: width(item.row), background: barColor(item.row) }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="solved">{{ item.row.solved }}</span>
|
||||||
|
<span class="change">
|
||||||
|
<span v-if="item.row.user.id === hotId" class="up hot" title="进步最大"
|
||||||
|
>↑{{ item.row.change }}</span
|
||||||
|
>
|
||||||
|
<span v-else-if="(item.row.change ?? 0) > 0" class="up">↑{{ item.row.change }}</span>
|
||||||
|
<span v-else-if="(item.row.change ?? 0) < 0" class="down"
|
||||||
|
>↓{{ -item.row.change! }}</span
|
||||||
|
>
|
||||||
|
<span v-else-if="item.row.change === 0" class="flat">—</span>
|
||||||
|
</span>
|
||||||
|
<n-dropdown
|
||||||
|
v-if="teacher"
|
||||||
|
trigger="click"
|
||||||
|
:options="menuOf(item.row)"
|
||||||
|
@select="(key: string) => onMenu(key, item.row)"
|
||||||
|
>
|
||||||
|
<button class="more" aria-label="更多">⋯</button>
|
||||||
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.row.mood" class="sig line">
|
||||||
|
<n-ellipsis :tooltip="{ contentStyle: { maxWidth: '300px' } }"
|
||||||
|
>“{{ item.row.mood }}”</n-ellipsis
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.row.user.id === meId" class="line">
|
||||||
|
<MoodPopover :mood="null" @saved="emit('moodSaved', $event)">
|
||||||
|
<button class="write">
|
||||||
<svg
|
<svg
|
||||||
v-if="item.row.mood"
|
width="12"
|
||||||
class="quote"
|
height="12"
|
||||||
width="11"
|
|
||||||
height="11"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="none"
|
||||||
role="img"
|
stroke="currentColor"
|
||||||
aria-label="有个性签名"
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M4 20h4L19 9l-4-4L4 16z" />
|
||||||
d="M4 18v-5c0-4 2-7 6-8l1 2c-2 1-3 2.5-3 4h3v7zm9 0v-5c0-4 2-7 6-8l1 2c-2 1-3 2.5-3 4h3v7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
写一句个性签名
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</MoodPopover>
|
||||||
{{ item.row.mood }}
|
</div>
|
||||||
</n-tooltip>
|
|
||||||
<span class="bar-box" :style="{ background: palette.track }">
|
|
||||||
<span class="bar" :style="{ width: width(item.row), background: barColor(item.row) }" />
|
|
||||||
</span>
|
|
||||||
<span class="solved">{{ item.row.solved }}</span>
|
|
||||||
<span class="change">
|
|
||||||
<span v-if="item.row.user.id === hotId" class="up hot" title="进步最大"
|
|
||||||
>↑{{ item.row.change }}</span
|
|
||||||
>
|
|
||||||
<span v-else-if="(item.row.change ?? 0) > 0" class="up">↑{{ item.row.change }}</span>
|
|
||||||
<span v-else-if="(item.row.change ?? 0) < 0" class="down"
|
|
||||||
>↓{{ -item.row.change! }}</span
|
|
||||||
>
|
|
||||||
<span v-else-if="item.row.change === 0" class="flat">—</span>
|
|
||||||
</span>
|
|
||||||
<n-dropdown
|
|
||||||
v-if="teacher"
|
|
||||||
trigger="click"
|
|
||||||
:options="menuOf(item.row)"
|
|
||||||
@select="(key: string) => onMenu(key, item.row)"
|
|
||||||
>
|
|
||||||
<button class="more" aria-label="更多">⋯</button>
|
|
||||||
</n-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +344,8 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
|
|
||||||
.prow {
|
.prow {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 38px;
|
min-height: 38px;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -331,22 +375,69 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prow .who {
|
.prow.tall {
|
||||||
width: auto;
|
padding-top: 5px;
|
||||||
max-width: 120px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机:签名跟在名字后面,一行放不下就省略 */
|
/* 手机:名字一行,签名在名字下面一行 */
|
||||||
.mood {
|
.pmain {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pname {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prow .who {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig {
|
||||||
|
display: block;
|
||||||
font-size: var(--oj-fs-meta);
|
font-size: var(--oj-fs-meta);
|
||||||
color: v-bind("theme.textColor3");
|
line-height: 18px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 桌面:签名那一行从名字底下(名次 28 + 头像 20 + 两个 8 的间距)伸到本栏末尾 */
|
||||||
|
.line {
|
||||||
|
margin: -6px 0 4px 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line :deep(.n-ellipsis) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write {
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border: 1px dashed rgba(24, 160, 88, 0.6);
|
||||||
|
border-radius: 11px;
|
||||||
|
background: v-bind("theme.cardColor");
|
||||||
|
color: #18a058;
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--oj-fs-meta);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.write:hover {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
.prow .rank {
|
.prow .rank {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
@@ -364,14 +455,14 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
color: #0c7a43;
|
color: #0c7a43;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lane.me {
|
.entry.me {
|
||||||
margin: 0 -8px;
|
margin: 0 -8px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(24, 160, 88, 0.12);
|
background: rgba(24, 160, 88, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lane.zero {
|
.entry.zero {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,11 +495,6 @@ function onMenu(key: string, row: RankRow) {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: v-bind("theme.textColor3");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-box {
|
.bar-box {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ async function restore(userId: number) {
|
|||||||
@analysis="openAnalysis"
|
@analysis="openAnalysis"
|
||||||
@hide="hide"
|
@hide="hide"
|
||||||
@clear-mood="confirmClearMood"
|
@clear-mood="confirmClearMood"
|
||||||
|
@mood-saved="onMoodSaved"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="!loading" class="empty">排名没取到,刷新一下试试</div>
|
<div v-else-if="!loading" class="empty">排名没取到,刷新一下试试</div>
|
||||||
|
|||||||
Reference in new issue
Block a user