refactor: replace tag two-panel with unified n-select combobox
This commit is contained in:
@@ -91,16 +91,7 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
|||||||
// 从服务器来的tag列表
|
// 从服务器来的tag列表
|
||||||
const tagList = shallowRef<Tag[]>([])
|
const tagList = shallowRef<Tag[]>([])
|
||||||
|
|
||||||
interface Tags {
|
const tagValue = ref<string[]>([])
|
||||||
select: string[]
|
|
||||||
upload: string[]
|
|
||||||
}
|
|
||||||
// 从 tagList 中选择的 和 新上传的
|
|
||||||
const tags = useLocalStorage<Tags>(STORAGE_KEY.ADMIN_PROBLEM_TAGS, {
|
|
||||||
select: [],
|
|
||||||
upload: [],
|
|
||||||
})
|
|
||||||
const tagKeyword = ref("")
|
|
||||||
|
|
||||||
// 这几个用的少,就不缓存本地了
|
// 这几个用的少,就不缓存本地了
|
||||||
const [needTemplate, toggleNeedTemplate] = useToggle(false)
|
const [needTemplate, toggleNeedTemplate] = useToggle(false)
|
||||||
@@ -126,34 +117,9 @@ const languageOptions = [
|
|||||||
{ label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" },
|
{ label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const tagOptions = computed(() =>
|
||||||
const filteredTagList = computed(() => {
|
tagList.value.map((tag) => ({ label: tag.name, value: tag.name }))
|
||||||
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 = []
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProblemDetail() {
|
async function getProblemDetail() {
|
||||||
if (!props.problemID) {
|
if (!props.problemID) {
|
||||||
@@ -213,7 +179,7 @@ async function getProblemDetail() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 标签
|
// 标签
|
||||||
tags.value.select = data.tags
|
tagValue.value = data.tags
|
||||||
toggleReady(true)
|
toggleReady(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error("获取题目失败")
|
message.error("获取题目失败")
|
||||||
@@ -226,22 +192,6 @@ async function getTagList() {
|
|||||||
tagList.value = res.data
|
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() {
|
function addSample() {
|
||||||
problem.value.samples.push({ input: "", output: "" })
|
problem.value.samples.push({ input: "", output: "" })
|
||||||
}
|
}
|
||||||
@@ -291,7 +241,7 @@ async function validateProblem() {
|
|||||||
hasErrors = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 标签
|
// 标签
|
||||||
else if (tags.value.upload.length === 0 && tags.value.select.length === 0) {
|
else if (tagValue.value.length === 0) {
|
||||||
message.error("标签没有填写")
|
message.error("标签没有填写")
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
@@ -394,7 +344,7 @@ async function submit() {
|
|||||||
try {
|
try {
|
||||||
await api!(problem.value)
|
await api!(problem.value)
|
||||||
problem.value = null
|
problem.value = null
|
||||||
tags.value = null
|
tagValue.value = []
|
||||||
if (
|
if (
|
||||||
route.name === "admin problem create" ||
|
route.name === "admin problem create" ||
|
||||||
route.name === "admin contest problem create"
|
route.name === "admin contest problem create"
|
||||||
@@ -429,7 +379,7 @@ const showClear = computed(
|
|||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
problem.value = null
|
problem.value = null
|
||||||
tags.value = null
|
tagValue.value = []
|
||||||
// 为了给所有状态初始化,刷新页面
|
// 为了给所有状态初始化,刷新页面
|
||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
@@ -449,13 +399,9 @@ onMounted(() => {
|
|||||||
getProblemDetail()
|
getProblemDetail()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(tagValue, (val) => {
|
||||||
() => [tags.value.select, tags.value.upload],
|
problem.value.tags = val
|
||||||
(tags) => {
|
})
|
||||||
const uniqueTags = unique<string>(tags[0].concat(tags[1]))
|
|
||||||
problem.value.tags = uniqueTags
|
|
||||||
},
|
|
||||||
)
|
|
||||||
watch(
|
watch(
|
||||||
() => problem.value.languages,
|
() => problem.value.languages,
|
||||||
(langs) => {
|
(langs) => {
|
||||||
@@ -493,67 +439,17 @@ watch(
|
|||||||
<n-form-item label="可见">
|
<n-form-item label="可见">
|
||||||
<n-switch v-model:value="problem.visible" />
|
<n-switch v-model:value="problem.visible" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
<n-form-item label="标签">
|
||||||
<n-form label-placement="top" :show-feedback="false">
|
<n-select
|
||||||
<n-grid :cols="2" :x-gap="20" responsive="screen">
|
v-model:value="tagValue"
|
||||||
<n-gi>
|
multiple
|
||||||
<n-form-item>
|
filterable
|
||||||
<template #label>
|
tag
|
||||||
<n-flex align="center" justify="space-between" class="tag-label">
|
clearable
|
||||||
<span>现成的标签</span>
|
:options="tagOptions"
|
||||||
<n-text depth="3">已选 {{ tags.select.length }} 个</n-text>
|
placeholder="搜索或输入新标签(Enter 创建)"
|
||||||
</n-flex>
|
/>
|
||||||
</template>
|
</n-form-item>
|
||||||
<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>
|
</n-form>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
v-if="ready"
|
v-if="ready"
|
||||||
@@ -812,47 +708,4 @@ watch(
|
|||||||
.addSamples {
|
.addSamples {
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user