feat(reaction): 学生端表情条组件
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
109
src/oj/problem/components/ProblemReaction.vue
Normal file
109
src/oj/problem/components/ProblemReaction.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<n-alert v-if="!userStore.isAuthed" type="error" title="请先登录" />
|
||||||
|
<div v-else ref="container" class="reactions">
|
||||||
|
<n-tooltip v-for="item in REACTIONS" :key="item.key" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="isDisabled(item.key)"
|
||||||
|
:type="mine.includes(item.key) ? 'primary' : 'default'"
|
||||||
|
:ghost="mine.includes(item.key)"
|
||||||
|
@click="toggle(item.key)"
|
||||||
|
>
|
||||||
|
<Icon :icon="item.icon" :width="18" />
|
||||||
|
<span v-if="showLabel" class="label">{{ item.label }}</span>
|
||||||
|
<span v-if="counts" class="count">{{ counts[item.key] }}</span>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ tooltipOf(item.key, item.label) }}
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
|
import { storeToRefs } from "pinia"
|
||||||
|
import { getReaction, setReaction } from "oj/api"
|
||||||
|
import { useProblemStore } from "oj/store/problem"
|
||||||
|
import { useUserStore } from "shared/store/user"
|
||||||
|
import { MAX_REACTIONS, REACTIONS } from "utils/constants"
|
||||||
|
import type { ReactionCounts, ReactionKey } from "utils/types"
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const problemStore = useProblemStore()
|
||||||
|
const { problem } = storeToRefs(problemStore)
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const container = ref<HTMLElement | null>(null)
|
||||||
|
const { width } = useElementSize(container)
|
||||||
|
// 七个按钮带中文标签大约需要 560px,放不下就只留图标和计数
|
||||||
|
const showLabel = computed(() => width.value >= 560)
|
||||||
|
|
||||||
|
const mine = ref<ReactionKey[]>([])
|
||||||
|
const counts = ref<ReactionCounts | null>(null)
|
||||||
|
|
||||||
|
const solved = computed(() => problem.value?.my_status === 0)
|
||||||
|
|
||||||
|
function isDisabled(key: ReactionKey) {
|
||||||
|
if (!solved.value) return true
|
||||||
|
if (mine.value.includes(key)) return false
|
||||||
|
return mine.value.length >= MAX_REACTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
function tooltipOf(key: ReactionKey, label: string) {
|
||||||
|
if (!solved.value) return "完成本题后可以评价"
|
||||||
|
if (isDisabled(key)) return `最多选 ${MAX_REACTIONS} 个,先取消一个`
|
||||||
|
if (counts.value) return `${counts.value[key]} 人选了「${label}」`
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggle(key: ReactionKey) {
|
||||||
|
if (!problem.value) return
|
||||||
|
const prevMine = [...mine.value]
|
||||||
|
const prevCounts = counts.value ? { ...counts.value } : null
|
||||||
|
const selected = mine.value.includes(key)
|
||||||
|
const next = selected
|
||||||
|
? mine.value.filter((k) => k !== key)
|
||||||
|
: [...mine.value, key]
|
||||||
|
|
||||||
|
mine.value = next
|
||||||
|
if (counts.value) counts.value[key] += selected ? -1 : 1
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await setReaction(problem.value.id, next)
|
||||||
|
mine.value = res.data.mine
|
||||||
|
counts.value = res.data.counts
|
||||||
|
} catch {
|
||||||
|
mine.value = prevMine
|
||||||
|
counts.value = prevCounts
|
||||||
|
message.error("操作失败,请重试")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!problem.value) return
|
||||||
|
const res = await getReaction(problem.value.id)
|
||||||
|
mine.value = res.data.mine
|
||||||
|
counts.value = res.data.counts
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.isAuthed) load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reactions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
margin-left: 6px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,8 +23,8 @@ import {
|
|||||||
} from "oj/problem/utils/pythonSyntaxCheck"
|
} from "oj/problem/utils/pythonSyntaxCheck"
|
||||||
|
|
||||||
// ==================== 异步组件 ====================
|
// ==================== 异步组件 ====================
|
||||||
const ProblemComment = defineAsyncComponent(
|
const ProblemReaction = defineAsyncComponent(
|
||||||
() => import("./ProblemComment.vue"),
|
() => import("./ProblemReaction.vue"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== 基础状态 ====================
|
// ==================== 基础状态 ====================
|
||||||
@@ -264,6 +264,6 @@ watch(
|
|||||||
:style="{ maxWidth: isDesktop && '50vw', maxHeight: '80vh' }"
|
:style="{ maxWidth: isDesktop && '50vw', maxHeight: '80vh' }"
|
||||||
v-model:show="commentPanel"
|
v-model:show="commentPanel"
|
||||||
>
|
>
|
||||||
<ProblemComment :showStatistics="false" />
|
<ProblemReaction />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const ProblemInfo = defineAsyncComponent(
|
|||||||
const ProblemSubmission = defineAsyncComponent(
|
const ProblemSubmission = defineAsyncComponent(
|
||||||
() => import("./components/ProblemSubmission.vue"),
|
() => import("./components/ProblemSubmission.vue"),
|
||||||
)
|
)
|
||||||
const ProblemComment = defineAsyncComponent(
|
const ProblemReaction = defineAsyncComponent(
|
||||||
() => import("./components/ProblemComment.vue"),
|
() => import("./components/ProblemReaction.vue"),
|
||||||
)
|
)
|
||||||
const ProblemFlowchart = defineAsyncComponent(
|
const ProblemFlowchart = defineAsyncComponent(
|
||||||
() => import("./components/ProblemFlowchart.vue"),
|
() => import("./components/ProblemFlowchart.vue"),
|
||||||
@@ -174,7 +174,7 @@ watch(
|
|||||||
tab="题目点评"
|
tab="题目点评"
|
||||||
:disabled="!!problemSetId"
|
:disabled="!!problemSetId"
|
||||||
>
|
>
|
||||||
<ProblemComment />
|
<ProblemReaction />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-if="myFlowchartStore.showing"
|
v-if="myFlowchartStore.showing"
|
||||||
@@ -226,7 +226,7 @@ watch(
|
|||||||
tab="题目点评"
|
tab="题目点评"
|
||||||
:disabled="!!problemSetId"
|
:disabled="!!problemSetId"
|
||||||
>
|
>
|
||||||
<ProblemComment />
|
<ProblemReaction />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-if="myFlowchartStore.showing"
|
v-if="myFlowchartStore.showing"
|
||||||
@@ -266,7 +266,7 @@ watch(
|
|||||||
tab="点评"
|
tab="点评"
|
||||||
:disabled="!!problemSetId"
|
:disabled="!!problemSetId"
|
||||||
>
|
>
|
||||||
<ProblemComment />
|
<ProblemReaction />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-if="myFlowchartStore.showing"
|
v-if="myFlowchartStore.showing"
|
||||||
|
|||||||
Reference in New Issue
Block a user