add tag
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<t-popup v-model="popupVisible" placement="bottom">
|
||||
<div class="tag-selector bg-white">
|
||||
<div class="title-bar">
|
||||
<t-button variant="text" size="small" @click="cancelSelection">取消</t-button>
|
||||
<h3 class="title">选择标签</h3>
|
||||
<t-button theme="primary" variant="text" size="small" @click="confirmSelection">确定</t-button>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<t-input
|
||||
class="input"
|
||||
size="small"
|
||||
v-model.trim="keyword"
|
||||
borderless
|
||||
clearable
|
||||
placeholder="搜索标签"
|
||||
/>
|
||||
<template v-if="canCreateTag">
|
||||
<t-input
|
||||
class="input"
|
||||
size="small"
|
||||
v-model.trim="newTagName"
|
||||
borderless
|
||||
clearable
|
||||
placeholder="新增标签名称"
|
||||
/>
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="small"
|
||||
:loading="isCreating"
|
||||
:disabled="isCreating"
|
||||
@click="createTag"
|
||||
>新增</t-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="list">
|
||||
<t-tag
|
||||
v-for="item in visibleTagList"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
:theme="isSelected(item) ? 'primary' : 'default'"
|
||||
size="extra-large"
|
||||
:variant="isSelected(item) ? 'dark' : 'light-outline'"
|
||||
@click="toggleTag(item)"
|
||||
>
|
||||
<span v-text="getTagName(item)" />
|
||||
</t-tag>
|
||||
<div v-if="!visibleTagList.length" class="empty" v-text="emptyText" />
|
||||
</div>
|
||||
</div>
|
||||
</t-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { useUserStore } from "timi-web";
|
||||
import TagAPI from "@/api/TagAPI";
|
||||
import type { Tag } from "@/types/Tag";
|
||||
import { corePermissionCodes } from "@/utils/permission";
|
||||
|
||||
interface Props {
|
||||
modelValue: string[];
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string[]];
|
||||
"update:visible": [value: boolean];
|
||||
change: [value: Tag[]];
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const pageSize = 100;
|
||||
const keyword = ref("");
|
||||
const tagList = ref<Tag[]>([]);
|
||||
const newTagName = ref("");
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const draftSelectedIdList = ref<string[]>([]);
|
||||
|
||||
const canCreateTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.create));
|
||||
const visibleTagList = computed(() => {
|
||||
const value = keyword.value.trim().toLowerCase();
|
||||
if (!value) {
|
||||
return tagList.value;
|
||||
}
|
||||
return tagList.value.filter(item => getTagName(item).toLowerCase().includes(value));
|
||||
});
|
||||
const emptyText = computed(() => {
|
||||
if (isLoading.value) {
|
||||
return "正在加载标签";
|
||||
}
|
||||
return keyword.value.trim() ? "未找到匹配标签" : "暂无标签";
|
||||
});
|
||||
const popupVisible = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(value: boolean) {
|
||||
emit("update:visible", value);
|
||||
}
|
||||
});
|
||||
|
||||
function getTagName(tag: Tag): string {
|
||||
return tag.name || tag.zhCN || "未命名标签";
|
||||
}
|
||||
|
||||
function isSelected(tag: Tag): boolean {
|
||||
return !!tag.id && draftSelectedIdList.value.includes(tag.id);
|
||||
}
|
||||
|
||||
function syncDraftSelection(): void {
|
||||
draftSelectedIdList.value = [...props.modelValue];
|
||||
}
|
||||
|
||||
function toggleTag(tag: Tag): void {
|
||||
const id = tag.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
if (draftSelectedIdList.value.includes(id)) {
|
||||
draftSelectedIdList.value = draftSelectedIdList.value.filter(item => item !== id);
|
||||
return;
|
||||
}
|
||||
draftSelectedIdList.value = [...draftSelectedIdList.value, id];
|
||||
}
|
||||
|
||||
async function loadTags(): Promise<void> {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const result = await TagAPI.list({
|
||||
index: 0,
|
||||
size: pageSize
|
||||
});
|
||||
tagList.value = result.list || [];
|
||||
} catch {
|
||||
Toast.error("标签加载失败");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag(): Promise<void> {
|
||||
const name = newTagName.value.trim();
|
||||
if (!name) {
|
||||
Toast.warning("请输入标签名称");
|
||||
return;
|
||||
}
|
||||
if (tagList.value.some(item => getTagName(item) === name)) {
|
||||
Toast.warning("标签已存在");
|
||||
return;
|
||||
}
|
||||
isCreating.value = true;
|
||||
try {
|
||||
const tag = await TagAPI.create({
|
||||
name,
|
||||
zhCN: name
|
||||
});
|
||||
newTagName.value = "";
|
||||
tagList.value = [tag, ...tagList.value];
|
||||
if (tag.id) {
|
||||
draftSelectedIdList.value = [...draftSelectedIdList.value, tag.id];
|
||||
}
|
||||
Toast.success("标签已新增");
|
||||
} catch {
|
||||
Toast.error("标签新增失败");
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelSelection(): void {
|
||||
syncDraftSelection();
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
function confirmSelection(): void {
|
||||
const selectedIdList = [...draftSelectedIdList.value];
|
||||
emit("update:modelValue", selectedIdList);
|
||||
emit("change", tagList.value.filter(item => item.id && selectedIdList.includes(item.id)));
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
watch(() => props.visible, async (value) => {
|
||||
if (!value) {
|
||||
syncDraftSelection();
|
||||
keyword.value = "";
|
||||
newTagName.value = "";
|
||||
return;
|
||||
}
|
||||
syncDraftSelection();
|
||||
await loadTags();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tag-selector {
|
||||
display: flex;
|
||||
height: 78vh;
|
||||
padding-top: 1rem;
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
flex-direction: column;
|
||||
|
||||
.title-bar {
|
||||
padding: 0 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: .5rem 1rem;
|
||||
align-items: center;
|
||||
|
||||
.input {
|
||||
--td-input-vertical-padding: 6px;
|
||||
|
||||
border: 1px solid var(--td-component-border);
|
||||
border-radius: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: .75rem;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
padding: 0 1rem calc(1rem + env(safe-area-inset-bottom, 0)) 1rem;
|
||||
|
||||
.item {
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
min-width: 5rem;
|
||||
max-width: 100%;
|
||||
border-radius: .75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #52607a;
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.tag-selector {
|
||||
background: rgba(15, 23, 42, .96);
|
||||
|
||||
.empty {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user