This commit is contained in:
2025-10-07 01:32:58 +08:00
parent ed3cfaacd4
commit 97baf85611
9 changed files with 207 additions and 185 deletions

View File

@@ -35,7 +35,12 @@
</n-flex> </n-flex>
</n-gi> </n-gi>
<n-gi :span="5"> <n-gi :span="5">
<AI v-if="aiStore.detailsData.solved.length > 0 && aiStore.detailsData.solved.length < 10" /> <AI
v-if="
aiStore.detailsData.solved.length > 0 &&
aiStore.detailsData.solved.length < 10
"
/>
</n-gi> </n-gi>
</n-grid> </n-grid>
</n-spin> </n-spin>

View File

@@ -66,4 +66,4 @@ watch(
:deep(.md-editor-preview h1) { :deep(.md-editor-preview h1) {
margin-top: 0; margin-top: 0;
} }
</style> </style>

View File

@@ -31,7 +31,9 @@ ChartJS.register(
const aiStore = useAIStore() const aiStore = useAIStore()
const show = computed(() => { const show = computed(() => {
return Object.values(aiStore.detailsData.difficulty).reduce((a, b) => a + b, 0) > 0 return (
Object.values(aiStore.detailsData.difficulty).reduce((a, b) => a + b, 0) > 0
)
}) })
const data = computed(() => { const data = computed(() => {

View File

@@ -138,11 +138,10 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
}, },
} }
}) })
</script> </script>
<style scoped> <style scoped>
.chart { .chart {
height: 300px; height: 300px;
width: 100%; width: 100%;
} }
</style> </style>

View File

@@ -14,7 +14,12 @@
</g> </g>
<g v-for="(day, i) in WEEK_DAYS" :key="i"> <g v-for="(day, i) in WEEK_DAYS" :key="i">
<text :x="0" :y="MONTH_HEIGHT + i * CELL_TOTAL + 8" class="label" font-size="9"> <text
:x="0"
:y="MONTH_HEIGHT + i * CELL_TOTAL + 8"
class="label"
font-size="9"
>
{{ day }} {{ day }}
</text> </text>
</g> </g>
@@ -39,7 +44,11 @@
<div class="legend"> <div class="legend">
<span></span> <span></span>
<div class="legend-colors"> <div class="legend-colors">
<div v-for="(color, i) in COLORS" :key="i" :style="{ backgroundColor: color }" /> <div
v-for="(color, i) in COLORS"
:key="i"
:style="{ backgroundColor: color }"
/>
</div> </div>
<span></span> <span></span>
</div> </div>
@@ -72,13 +81,18 @@ const LEGEND_HEIGHT = 20
const COLORS = ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"] const COLORS = ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"]
const WEEK_DAYS = ["", "一", "", "三", "", "五", ""] const WEEK_DAYS = ["", "一", "", "三", "", "五", ""]
const getColor = (count: number) => const getColor = (count: number) =>
count === 0 ? COLORS[0] : count === 0
count <= 2 ? COLORS[1] : ? COLORS[0]
count <= 4 ? COLORS[2] : : count <= 2
count <= 7 ? COLORS[3] : COLORS[4] ? COLORS[1]
: count <= 4
? COLORS[2]
: count <= 7
? COLORS[3]
: COLORS[4]
const cells = computed(() => const cells = computed(() =>
aiStore.heatmapData.map((item, i) => ({ aiStore.heatmapData.map((item, i) => ({
date: new Date(item.timestamp), date: new Date(item.timestamp),
count: item.value, count: item.value,
@@ -87,17 +101,17 @@ const cells = computed(() =>
day: i % 7, day: i % 7,
x: Math.floor(i / 7) * CELL_TOTAL, x: Math.floor(i / 7) * CELL_TOTAL,
y: (i % 7) * CELL_TOTAL, y: (i % 7) * CELL_TOTAL,
})) })),
) )
const monthLabels = computed(() => { const monthLabels = computed(() => {
const labels: { text: string; x: number }[] = [] const labels: { text: string; x: number }[] = []
let lastMonth = -1 let lastMonth = -1
cells.value.forEach((cell, i) => { cells.value.forEach((cell, i) => {
const month = cell.date.getMonth() const month = cell.date.getMonth()
const isWeekStart = cell.date.getDay() === 0 || i === 0 const isWeekStart = cell.date.getDay() === 0 || i === 0
if (month !== lastMonth && (isWeekStart || cell.date.getDay() <= 3)) { if (month !== lastMonth && (isWeekStart || cell.date.getDay() <= 3)) {
labels.push({ labels.push({
text: `${month + 1}`, text: `${month + 1}`,
@@ -106,17 +120,16 @@ const monthLabels = computed(() => {
lastMonth = month lastMonth = month
} }
}) })
return labels return labels
}) })
const svgWidth = computed(() => const svgWidth = computed(
DAY_WIDTH + Math.ceil(cells.value.length / 7) * CELL_TOTAL + RIGHT_PADDING () =>
DAY_WIDTH + Math.ceil(cells.value.length / 7) * CELL_TOTAL + RIGHT_PADDING,
) )
const svgHeight = computed(() => const svgHeight = computed(() => MONTH_HEIGHT + 7 * CELL_TOTAL + LEGEND_HEIGHT)
MONTH_HEIGHT + 7 * CELL_TOTAL + LEGEND_HEIGHT
)
interface Cell { interface Cell {
date: Date date: Date
@@ -141,13 +154,13 @@ const tooltipStyle = computed(() => ({
top: `${tooltip.value?.y}px`, top: `${tooltip.value?.y}px`,
})) }))
const getTooltipText = (count: number) => const getTooltipText = (count: number) =>
count === 0 ? "没有提交记录" : `提交了 ${count}` count === 0 ? "没有提交记录" : `提交了 ${count}`
const showTooltip = (e: MouseEvent, cell: Cell) => { const showTooltip = (e: MouseEvent, cell: Cell) => {
const rect = (e.target as HTMLElement).getBoundingClientRect() const rect = (e.target as HTMLElement).getBoundingClientRect()
const containerRect = containerRef.value?.getBoundingClientRect() const containerRect = containerRef.value?.getBoundingClientRect()
if (containerRect) { if (containerRect) {
tooltip.value = { tooltip.value = {
x: rect.left - containerRect.left + rect.width / 2, x: rect.left - containerRect.left + rect.width / 2,

View File

@@ -130,42 +130,40 @@ function rowProps(row: Contest) {
</script> </script>
<template> <template>
<n-flex vertical size="large"> <n-flex vertical size="large">
<n-card embedded> <n-space>
<n-space> <n-form :show-feedback="false" label-placement="left" inline>
<n-form :show-feedback="false" label-placement="left" inline> <n-form-item label="比赛状态">
<n-form-item label="比赛状态"> <n-select
<n-select style="width: 120px"
style="width: 120px" :options="options"
:options="options" v-model:value="query.status"
v-model:value="query.status" />
/> </n-form-item>
</n-form-item> <n-form-item label="标签">
<n-form-item label="标签"> <n-select
<n-select style="width: 120px"
style="width: 120px" :options="tags"
:options="tags" v-model:value="query.tag"
v-model:value="query.tag" />
/> </n-form-item>
</n-form-item> </n-form>
</n-form> <n-form :show-feedback="false" label-placement="left" inline>
<n-form :show-feedback="false" label-placement="left" inline> <n-form-item>
<n-form-item> <n-input
<n-input style="width: 180px"
style="width: 200px" clearable
clearable v-model:value="query.keyword"
v-model:value="query.keyword" placeholder="比赛标题"
placeholder="比赛标题" />
/> </n-form-item>
</n-form-item> <n-form-item>
<n-form-item> <n-flex :wrap="false">
<n-flex> <n-button @click="search(query.keyword)">搜索</n-button>
<n-button @click="search(query.keyword)">搜索</n-button> <n-button @click="clear" quaternary>重置</n-button>
<n-button @click="clear" quaternary>重置</n-button> </n-flex>
</n-flex> </n-form-item>
</n-form-item> </n-form>
</n-form> </n-space>
</n-space>
</n-card>
<n-data-table <n-data-table
:bordered="false" :bordered="false"
:columns="columns" :columns="columns"

View File

@@ -195,54 +195,52 @@ function rowProps(row: ProblemFiltered) {
<template> <template>
<n-flex vertical size="large"> <n-flex vertical size="large">
<n-card embedded> <n-flex justify="space-between">
<n-flex justify="space-between"> <n-space>
<n-space> <n-form :show-feedback="false" inline label-placement="left">
<n-form :show-feedback="false" inline label-placement="left"> <n-form-item label="难度">
<n-form-item label="难度"> <n-select
<n-select style="width: 120px"
style="width: 120px" v-model:value="query.difficulty"
v-model:value="query.difficulty" :options="difficultyOptions"
:options="difficultyOptions" />
/> </n-form-item>
</n-form-item> <n-form-item label="出题者">
<n-form-item label="出题者"> <AuthorSelect v-model:value="query.author" />
<AuthorSelect v-model:value="query.author" /> </n-form-item>
</n-form-item> </n-form>
</n-form> <n-form :show-feedback="false" inline label-placement="left">
<n-form :show-feedback="false" inline label-placement="left"> <n-form-item>
<n-form-item> <n-input
<n-input clearable
clearable style="width: 200px"
style="width: 200px" v-model:value="query.keyword"
v-model:value="query.keyword" placeholder="编号或者标题"
placeholder="编号或者标题" />
/> </n-form-item>
</n-form-item> <n-form-item>
<n-form-item> <n-button @click="clearQuery" quaternary>重置</n-button>
<n-button @click="clearQuery" quaternary>重置</n-button> </n-form-item>
</n-form-item> <!-- <n-form-item>
<!-- <n-form-item>
<n-button @click="getRandom" quaternary>随机</n-button> <n-button @click="getRandom" quaternary>随机</n-button>
</n-form-item> --> </n-form-item> -->
<n-form-item> <n-form-item>
<n-button <n-button
@click="toggleShowTag()" @click="toggleShowTag()"
quaternary quaternary
icon-placement="right" icon-placement="right"
> >
<template #icon> <template #icon>
<Icon v-if="showTag" icon="ph:caret-down"></Icon> <Icon v-if="showTag" icon="ph:caret-down"></Icon>
<Icon v-else icon="ph:caret-up"></Icon> <Icon v-else icon="ph:caret-up"></Icon>
</template> </template>
标签 标签
</n-button> </n-button>
</n-form-item> </n-form-item>
</n-form> </n-form>
</n-space> </n-space>
<Hitokoto v-if="isDesktop" /> <Hitokoto v-if="isDesktop" />
</n-flex> </n-flex>
</n-card>
<n-collapse-transition :show="showTag"> <n-collapse-transition :show="showTag">
<n-flex> <n-flex>
<n-tag <n-tag

View File

@@ -51,7 +51,11 @@ export const useAIStore = defineStore("ai", () => {
} }
// 统一获取分析数据details + duration // 统一获取分析数据details + duration
async function fetchAnalysisData(start: string, end: string, duration: string) { async function fetchAnalysisData(
start: string,
end: string,
duration: string,
) {
loading.fetching = true loading.fetching = true
try { try {
await Promise.all([ await Promise.all([

View File

@@ -240,84 +240,87 @@ const columns = computed(() => {
</script> </script>
<template> <template>
<n-flex vertical size="large"> <n-flex vertical size="large">
<n-card embedded> <n-space>
<n-space> <n-form :show-feedback="false" inline label-placement="left">
<n-form :show-feedback="false" inline label-placement="left"> <n-form-item v-if="isDesktop && userStore.isAuthed" label="只看自己">
<n-form-item v-if="isDesktop && userStore.isAuthed" label="只看自己"> <n-switch
<n-switch v-model:value="query.myself"
v-model:value="query.myself" checked-value="1"
checked-value="1" unchecked-value="0"
unchecked-value="0" />
/> </n-form-item>
</n-form-item> <n-form-item label="状态">
<n-form-item label="状态"> <n-select
<n-select class="select"
class="select" v-model:value="query.result"
v-model:value="query.result" :options="resultOptions"
:options="resultOptions" />
/> </n-form-item>
</n-form-item> <n-form-item label="语言" v-if="route.name !== 'contest submissions'">
<n-form-item label="语言" v-if="route.name !== 'contest submissions'"> <n-select
<n-select class="select"
class="select" v-model:value="query.language"
v-model:value="query.language" :options="languageOptions"
:options="languageOptions" />
/> </n-form-item>
</n-form-item> </n-form>
</n-form> <n-form :show-feedback="false" inline label-placement="left">
<n-form :show-feedback="false" inline label-placement="left"> <n-form-item>
<n-form-item> <n-input
<n-input class="input"
class="input" clearable
clearable v-model:value="query.username"
v-model:value="query.username" placeholder="用户"
placeholder="用户" />
/> </n-form-item>
</n-form-item> <n-form-item>
<n-form-item> <n-input
<n-input class="input"
class="input" clearable
clearable v-model:value="query.problem"
v-model:value="query.problem" placeholder="题号"
placeholder="题号" />
/> </n-form-item>
</n-form-item> </n-form>
</n-form> <n-form :show-feedback="false" inline label-placement="left">
<n-form :show-feedback="false" inline label-placement="left"> <n-form-item v-if="isMobile && userStore.isAuthed" label="只看自己">
<n-form-item v-if="isMobile && userStore.isAuthed" label="只看自己"> <n-switch
<n-switch v-model:value="query.myself"
v-model:value="query.myself" checked-value="1"
checked-value="1" unchecked-value="0"
unchecked-value="0" />
/> </n-form-item>
</n-form-item> <n-form-item>
<n-form-item> <n-button @click="search(query.username, query.problem)">
<n-button @click="search(query.username, query.problem)"> 搜索
搜索 </n-button>
</n-button> </n-form-item>
</n-form-item> <n-form-item>
<n-form-item> <n-button @click="clear" quaternary>重置</n-button>
<n-button @click="clear" quaternary>重置</n-button> </n-form-item>
</n-form-item> <n-form-item
<n-form-item v-if="userStore.isSuperAdmin && route.name === 'submissions'"
v-if="userStore.isSuperAdmin && route.name === 'submissions'" >
> <IconButton
<IconButton icon="streamline-emojis:bar-chart"
icon="streamline-emojis:bar-chart" tip="数据统计"
tip="数据统计" @click="toggleStatisticPanel(true)"
@click="toggleStatisticPanel(true)" />
/> </n-form-item>
</n-form-item> </n-form>
</n-form> <n-form
<n-form :show-feedback="false" inline label-placement="left"> :show-feedback="false"
<n-form-item v-if="todayCount > 0"> inline
<component :is="isDesktop ? NH2 : NText" class="todayCount"> label-placement="left"
<n-gradient-text>今日提交数{{ todayCount }}</n-gradient-text> v-if="todayCount > 0"
</component> >
</n-form-item> <n-form-item>
</n-form> <component :is="isDesktop ? NH2 : NText" class="todayCount">
</n-space> <n-gradient-text>今日提交数{{ todayCount }}</n-gradient-text>
</n-card> </component>
</n-form-item>
</n-form>
</n-space>
<n-data-table :bordered="false" :columns="columns" :data="submissions" /> <n-data-table :bordered="false" :columns="columns" :data="submissions" />
</n-flex> </n-flex>
<Pagination <Pagination