refactor: replace tag two-panel with unified n-select combobox
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-05-17 08:09:13 -06:00
parent d25f126710
commit 3c90bedff6

View File

@@ -91,16 +91,7 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
// 从服务器来的tag列表
const tagList = shallowRef<Tag[]>([])
interface Tags {
select: string[]
upload: string[]
}
// 从 tagList 中选择的 和 新上传的
const tags = useLocalStorage<Tags>(STORAGE_KEY.ADMIN_PROBLEM_TAGS, {
select: [],
upload: [],
})
const tagKeyword = ref("")
const tagValue = ref<string[]>([])
// 这几个用的少,就不缓存本地了
const [needTemplate, toggleNeedTemplate] = useToggle(false)
@@ -126,34 +117,9 @@ const languageOptions = [
{ label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" },
]
const filteredTagList = computed(() => {
const keyword = tagKeyword.value.trim().toLowerCase()
if (!keyword) return tagList.value
return tagList.value.filter((tag) => tag.name.toLowerCase().includes(keyword))
})
const selectedTagSet = computed(() => new Set(tags.value.select))
function toggleExistingTag(tagName: string) {
const selected = new Set(tags.value.select)
if (selected.has(tagName)) {
selected.delete(tagName)
} else {
selected.add(tagName)
}
tags.value.select = Array.from(selected)
}
function selectFilteredTags() {
const selected = new Set(tags.value.select)
filteredTagList.value.forEach((tag) => selected.add(tag.name))
tags.value.select = Array.from(selected)
}
function clearExistingTags() {
tags.value.select = []
}
const tagOptions = computed(() =>
tagList.value.map((tag) => ({ label: tag.name, value: tag.name }))
)
async function getProblemDetail() {
if (!props.problemID) {
@@ -213,7 +179,7 @@ async function getProblemDetail() {
}
})
// 标签
tags.value.select = data.tags
tagValue.value = data.tags
toggleReady(true)
} catch (error) {
message.error("获取题目失败")
@@ -226,22 +192,6 @@ async function getTagList() {
tagList.value = res.data
}
function updateNewTags(v: string[]) {
const blanks = []
const uniqueTags = unique(v)
const items = tagList.value.map((t) => t.name)
for (let i = 0; i < uniqueTags.length; i++) {
const tag = uniqueTags[i]
if (items.indexOf(tag) < 0) {
blanks.push(tag)
} else {
message.error("已经存在标签:" + tag)
break
}
}
tags.value.upload = blanks
}
function addSample() {
problem.value.samples.push({ input: "", output: "" })
}
@@ -291,7 +241,7 @@ async function validateProblem() {
hasErrors = true
}
// 标签
else if (tags.value.upload.length === 0 && tags.value.select.length === 0) {
else if (tagValue.value.length === 0) {
message.error("标签没有填写")
hasErrors = true
}
@@ -394,7 +344,7 @@ async function submit() {
try {
await api!(problem.value)
problem.value = null
tags.value = null
tagValue.value = []
if (
route.name === "admin problem create" ||
route.name === "admin contest problem create"
@@ -429,7 +379,7 @@ const showClear = computed(
function clear() {
problem.value = null
tags.value = null
tagValue.value = []
// 为了给所有状态初始化,刷新页面
location.reload()
}
@@ -449,13 +399,9 @@ onMounted(() => {
getProblemDetail()
})
watch(
() => [tags.value.select, tags.value.upload],
(tags) => {
const uniqueTags = unique<string>(tags[0].concat(tags[1]))
problem.value.tags = uniqueTags
},
)
watch(tagValue, (val) => {
problem.value.tags = val
})
watch(
() => problem.value.languages,
(langs) => {
@@ -493,67 +439,17 @@ watch(
<n-form-item label="可见">
<n-switch v-model:value="problem.visible" />
</n-form-item>
</n-form>
<n-form label-placement="top" :show-feedback="false">
<n-grid :cols="2" :x-gap="20" responsive="screen">
<n-gi>
<n-form-item>
<template #label>
<n-flex align="center" justify="space-between" class="tag-label">
<span>现成的标签</span>
<n-text depth="3">已选 {{ tags.select.length }} </n-text>
</n-flex>
</template>
<div class="tag-picker">
<n-flex align="center" justify="space-between" class="tag-toolbar">
<n-input
v-model:value="tagKeyword"
clearable
placeholder="搜索现成标签"
/>
<n-button size="small" tertiary @click="selectFilteredTags">
全选当前
</n-button>
<n-button size="small" tertiary @click="clearExistingTags">
清空
</n-button>
</n-flex>
<n-scrollbar class="tag-scroll">
<n-empty
v-if="filteredTagList.length === 0"
description="没有匹配的现成标签"
/>
<n-flex v-else size="small" class="tag-cloud">
<n-tag
v-for="tag in filteredTagList"
:key="tag.id"
checkable
:checked="selectedTagSet.has(tag.name)"
:bordered="!selectedTagSet.has(tag.name)"
:type="selectedTagSet.has(tag.name) ? 'success' : 'default'"
@click="toggleExistingTag(tag.name)"
>
{{ tag.name }}
</n-tag>
</n-flex>
</n-scrollbar>
</div>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="新增的标签">
<div class="tag-picker new-tag-panel">
<n-dynamic-tags
v-model:value="tags.upload"
@update:value="updateNewTags"
/>
<n-text depth="3" class="tag-help">
只填写标签库里还没有的标签已有标签请在左侧点击选择
</n-text>
</div>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="标签">
<n-select
v-model:value="tagValue"
multiple
filterable
tag
clearable
:options="tagOptions"
placeholder="搜索或输入新标签Enter 创建)"
/>
</n-form-item>
</n-form>
<TextEditor
v-if="ready"
@@ -812,47 +708,4 @@ watch(
.addSamples {
width: 100%;
}
.tag-label {
width: 100%;
}
.tag-picker {
width: 100%;
min-width: 0;
border: 1px solid var(--n-border-color);
border-radius: 8px;
padding: 12px;
}
.tag-toolbar {
margin-bottom: 10px;
}
.tag-toolbar :deep(.n-input) {
flex: 1 1 220px;
min-width: 160px;
}
.tag-scroll {
max-height: 156px;
}
.tag-cloud {
padding-right: 8px;
}
.tag-cloud :deep(.n-tag) {
cursor: pointer;
user-select: none;
}
.new-tag-panel {
min-height: 220px;
}
.tag-help {
display: block;
margin-top: 10px;
}
</style>