Files
nl-blogs/client/src/components/admin/PostRelationModal.vue

363 lines
11 KiB
Vue
Raw Normal View History

2026-01-19 16:14:08 +08:00
<template>
2026-01-19 21:19:35 +08:00
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
2026-01-19 16:14:08 +08:00
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden animate-reveal flex flex-col">
<div class="p-4 border-b border-white/10 flex justify-between items-center shrink-0">
<h3 class="text-lg font-serif italic text-white">管理文章关联</h3>
<button @click="close" class="text-white/50 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="p-6 space-y-6 overflow-y-auto flex-1">
<div>
<p class="text-sm text-art-muted">文章: <span class="text-white font-medium">{{ post?.title }}</span></p>
</div>
<!-- Category Selection -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-white">分类</label>
<button @click="showCreateCategory = true" class="text-xs text-art-accent hover:text-white transition-colors">
+ 新建分类
</button>
</div>
2026-01-19 20:21:09 +08:00
<CustomSelect
v-model="selectedCategoryId"
:options="categoryOptions"
placeholder="请选择分类"
/>
2026-01-19 16:14:08 +08:00
<!-- Quick Create Category -->
<div v-if="showCreateCategory" class="admin-card p-3 space-y-2">
<input
v-model="newCategory.name"
placeholder="分类名称"
class="admin-input text-sm"
@keyup.enter="createCategory"
/>
<div class="flex gap-2">
<button @click="createCategory" class="admin-btn-primary text-xs px-3 py-1">创建</button>
<button @click="showCreateCategory = false" class="admin-btn-secondary text-xs px-3 py-1">取消</button>
</div>
</div>
</div>
<!-- Column Selection -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-white">专栏 <span class="text-art-muted text-xs">(可选)</span></label>
<button @click="showCreateColumn = true" class="text-xs text-art-accent hover:text-white transition-colors">
+ 新建专栏
</button>
</div>
2026-01-19 20:21:09 +08:00
<CustomSelect
v-model="selectedColumnId"
:options="columnOptions"
placeholder="不选择专栏"
/>
2026-01-19 16:14:08 +08:00
<!-- Quick Create Column -->
<div v-if="showCreateColumn" class="admin-card p-3 space-y-2">
<input
v-model="newColumn.name"
placeholder="专栏名称"
class="admin-input text-sm"
@keyup.enter="createColumn"
/>
2026-01-23 16:05:23 +08:00
<div>
<label class="block text-xs text-art-muted mb-2">封面图片</label>
<ImageUpload
v-model="newColumn.cover"
:categoryId="1"
/>
</div>
2026-01-19 16:14:08 +08:00
<textarea
v-model="newColumn.description"
placeholder="描述"
rows="2"
class="admin-input text-sm"
/>
<div class="flex gap-2">
<button @click="createColumn" class="admin-btn-primary text-xs px-3 py-1">创建</button>
<button @click="showCreateColumn = false" class="admin-btn-secondary text-xs px-3 py-1">取消</button>
</div>
</div>
</div>
<!-- Tags Selection -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-white">标签 <span class="text-art-muted text-xs">(可多选)</span></label>
<button @click="showCreateTag = true" class="text-xs text-art-accent hover:text-white transition-colors">
+ 新建标签
</button>
</div>
<div class="flex flex-wrap gap-2 min-h-[60px] p-3 bg-white/5 rounded border border-white/10">
<button
v-for="tag in tags"
:key="tag.id"
@click="toggleTag(tag.id)"
class="px-3 py-1 text-sm rounded border transition-colors"
:class="selectedTagIds.includes(tag.id)
? 'bg-art-accent/20 border-art-accent text-art-accent'
: 'bg-white/5 border-white/10 text-art-muted hover:border-white/30'"
>
{{ tag.name }}
</button>
<div v-if="tags.length === 0" class="text-xs text-art-muted italic">
暂无标签
</div>
</div>
<!-- Quick Create Tag -->
<div v-if="showCreateTag" class="admin-card p-3 space-y-2">
<input
v-model="newTag.name"
placeholder="标签名称"
class="admin-input text-sm"
@keyup.enter="createTag"
/>
<div class="flex gap-2">
<button @click="createTag" class="admin-btn-primary text-xs px-3 py-1">创建</button>
<button @click="showCreateTag = false" class="admin-btn-secondary text-xs px-3 py-1">取消</button>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-white/10 flex justify-end gap-3 bg-white/5 shrink-0">
<button @click="close" class="px-4 py-2 text-sm text-white/70 hover:text-white transition-colors">取消</button>
<button @click="save" :disabled="saving" class="admin-btn-primary py-1.5 px-4 text-sm">
{{ saving ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
2026-01-19 21:19:35 +08:00
</Teleport>
2026-01-19 16:14:08 +08:00
</template>
<script setup lang="ts">
2026-01-19 20:21:09 +08:00
import { ref, watch, onMounted, computed } from 'vue'
2026-01-19 16:14:08 +08:00
import {
fetchCategories,
fetchColumns,
fetchTags,
createCategory as createCategoryApi,
createColumn as createColumnApi,
createTag as createTagApi,
2026-01-23 16:05:23 +08:00
updatePostRelations,
2026-01-19 16:14:08 +08:00
Category,
Column,
Tag,
Post
} from '../../services/api'
import { useToast } from '../../composables/useToast'
2026-01-19 20:21:09 +08:00
import CustomSelect from '../CustomSelect.vue'
2026-01-23 16:05:23 +08:00
import ImageUpload from './ImageUpload.vue'
2026-01-19 16:14:08 +08:00
const props = defineProps<{
isOpen: boolean
post: Post | null
}>()
const emit = defineEmits<{
'update:isOpen': [value: boolean]
'saved': []
}>()
const toast = useToast()
const categories = ref<Category[]>([])
const columns = ref<Column[]>([])
const tags = ref<Tag[]>([])
const selectedCategoryId = ref(0)
const selectedColumnId = ref(0)
const selectedTagIds = ref<number[]>([])
const showCreateCategory = ref(false)
const showCreateColumn = ref(false)
const showCreateTag = ref(false)
const newCategory = ref({ name: '', slug: '', description: '' })
const newColumn = ref({ name: '', cover: '', description: '', isActive: 1, sortOrder: 0 })
const newTag = ref({ name: '', slug: '' })
const saving = ref(false)
2026-01-19 20:21:09 +08:00
// 计算属性:分类选项
const categoryOptions = computed(() => {
return [
{ value: 0, label: '请选择分类' },
...categories.value.map(cat => ({ value: cat.id, label: cat.name }))
]
})
// 计算属性:专栏选项
const columnOptions = computed(() => {
return [
{ value: 0, label: '不选择专栏' },
...columns.value.map(col => ({ value: col.id, label: col.name }))
]
})
2026-01-19 16:14:08 +08:00
const close = () => {
emit('update:isOpen', false)
showCreateCategory.value = false
showCreateColumn.value = false
showCreateTag.value = false
}
const loadData = async () => {
try {
const [cats, cols, tagList] = await Promise.all([
fetchCategories(),
fetchColumns(),
fetchTags()
])
categories.value = cats
columns.value = cols
tags.value = tagList
} catch (err: any) {
toast.showToast('加载数据失败', 'error')
}
}
const initForm = () => {
if (props.post) {
selectedCategoryId.value = props.post.categoryId || 0
selectedColumnId.value = props.post.columnId || 0
selectedTagIds.value = props.post.tags?.map(t => t.id) || []
}
}
const toggleTag = (tagId: number) => {
const index = selectedTagIds.value.indexOf(tagId)
if (index === -1) {
selectedTagIds.value.push(tagId)
} else {
selectedTagIds.value.splice(index, 1)
}
}
const createCategory = async () => {
if (!newCategory.value.name) {
toast.showToast('请输入分类名称', 'error')
return
}
try {
await createCategoryApi({
name: newCategory.value.name,
slug: newCategory.value.slug || newCategory.value.name,
2026-01-20 09:13:51 +08:00
description: newCategory.value.description || '',
sortOrder: 0
2026-01-19 16:14:08 +08:00
})
toast.showToast('分类创建成功', 'success')
const createdName = newCategory.value.name
newCategory.value = { name: '', slug: '', description: '' }
showCreateCategory.value = false
await loadData()
// 自动选择新创建的分类
const newCat = categories.value.find(c => c.name === createdName)
if (newCat) {
selectedCategoryId.value = newCat.id
}
} catch (err: any) {
toast.showToast(err.message || '创建失败', 'error')
}
}
const createColumn = async () => {
if (!newColumn.value.name) {
toast.showToast('请输入专栏名称', 'error')
return
}
try {
await createColumnApi(newColumn.value)
toast.showToast('专栏创建成功', 'success')
const createdName = newColumn.value.name
newColumn.value = { name: '', cover: '', description: '', isActive: 1, sortOrder: 0 }
showCreateColumn.value = false
await loadData()
// 自动选择新创建的专栏
const newCol = columns.value.find(c => c.name === createdName)
if (newCol) {
selectedColumnId.value = newCol.id
}
} catch (err: any) {
toast.showToast(err.message || '创建失败', 'error')
}
}
const createTag = async () => {
if (!newTag.value.name) {
toast.showToast('请输入标签名称', 'error')
return
}
try {
await createTagApi({
name: newTag.value.name,
slug: newTag.value.slug || newTag.value.name
})
toast.showToast('标签创建成功', 'success')
const createdName = newTag.value.name
newTag.value = { name: '', slug: '' }
showCreateTag.value = false
await loadData()
// 自动选择新创建的标签
const newTagItem = tags.value.find(t => t.name === createdName)
if (newTagItem && !selectedTagIds.value.includes(newTagItem.id)) {
selectedTagIds.value.push(newTagItem.id)
}
} catch (err: any) {
toast.showToast(err.message || '创建失败', 'error')
}
}
const save = async () => {
if (!props.post) return
saving.value = true
try {
2026-01-23 16:05:23 +08:00
// 只更新关联关系,不更新内容
await updatePostRelations(props.post.id, {
2026-01-19 16:14:08 +08:00
categoryId: selectedCategoryId.value,
2026-01-23 16:05:23 +08:00
columnId: selectedColumnId.value > 0 ? selectedColumnId.value : null,
tagIds: selectedTagIds.value
})
2026-01-19 16:14:08 +08:00
toast.showToast('保存成功', 'success')
emit('saved')
close()
} catch (err: any) {
toast.showToast(err.message || '保存失败', 'error')
} finally {
saving.value = false
}
}
watch(() => props.isOpen, (open) => {
if (open) {
loadData()
initForm()
}
})
watch(() => props.post, () => {
if (props.isOpen) {
initForm()
}
})
onMounted(() => {
if (props.isOpen) {
loadData()
initForm()
}
})
</script>