340 lines
9.9 KiB
Vue
340 lines
9.9 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="isEdit ? '编辑文章' : '新增文章'"
|
||
width="80%"
|
||
:close-on-click-modal="false"
|
||
@open="handleDialogOpen"
|
||
v-loading="loading"
|
||
@close="handleClose"
|
||
>
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="space-y-6">
|
||
<!-- 基本信息 -->
|
||
<div class="bg-gray-50 p-4 rounded-lg">
|
||
<h3 class="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="文章标题" prop="title">
|
||
<el-input v-model="form.title" placeholder="请输入文章标题" maxlength="200" show-word-limit />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="12">
|
||
<el-form-item label="文章分类" prop="category_id">
|
||
<el-select v-model="form.category_id" placeholder="选择分类" clearable class="w-full">
|
||
<el-option v-for="category in categories" :key="category.id" :label="category.name"
|
||
:value="category.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="文章摘要" prop="summary">
|
||
<el-input v-model="form.summary" type="textarea" :rows="3" placeholder="请输入文章摘要" maxlength="500"
|
||
show-word-limit />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="封面图片" prop="cover_image">
|
||
<el-input v-model="form.cover_image" placeholder="请输入封面图片URL" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="文章标签" prop="tag_ids">
|
||
<el-select v-model="form.tag_ids" multiple placeholder="选择标签" clearable class="w-full">
|
||
<el-option v-for="tag in tags" :key="tag.id" :label="tag.name" :value="tag.id">
|
||
<div class="flex items-center">
|
||
<div class="w-4 h-4 rounded mr-2" :style="{ backgroundColor: tag.color }"></div>
|
||
{{ tag.name }}
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="推荐状态" prop="is_featured">
|
||
<el-switch v-model="form.is_featured" active-text="推荐" inactive-text="普通" />
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<!-- 文章内容 -->
|
||
<div class="bg-gray-50 p-4 rounded-lg">
|
||
<h3 class="text-lg font-medium text-gray-900 mb-4">文章内容</h3>
|
||
|
||
<el-form-item label="文章内容" prop="content">
|
||
<Editor style="width: 100%;" v-model="form.content" :init="editorInit"
|
||
tinymceScriptSrc="https://cdn.jsdelivr.net/npm/tinymce@7.9.1/tinymce.min.js" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="flex justify-end space-x-3">
|
||
<el-button @click="handleClose">取消</el-button>
|
||
<el-button type="primary" :loading="loading" @click="handleSubmit">
|
||
{{ isEdit ? '更新' : '创建' }}
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { articleApi } from '@/api';
|
||
import Editor from '@tinymce/tinymce-vue';
|
||
import { ElMessage } from 'element-plus';
|
||
import { computed, reactive, ref, watch } from 'vue';
|
||
|
||
// Props
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
article: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
categories: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
tags: {
|
||
type: Array,
|
||
default: () => []
|
||
}
|
||
})
|
||
|
||
// Emits
|
||
const emit = defineEmits(['update:modelValue', 'success'])
|
||
|
||
// 响应式数据
|
||
const formRef = ref(null)
|
||
const loading = ref(false)
|
||
const articleDetail = ref(null)
|
||
|
||
// 表单数据
|
||
const form = reactive({
|
||
title: '',
|
||
content: '',
|
||
summary: '',
|
||
cover_image: '',
|
||
category_id: '',
|
||
tag_ids: [],
|
||
is_featured: false
|
||
})
|
||
|
||
// 表单验证规则
|
||
const rules = {
|
||
title: [
|
||
{ required: true, message: '请输入文章标题', trigger: 'blur' },
|
||
{ min: 1, max: 200, message: '标题长度在 1 到 200 个字符', trigger: 'blur' }
|
||
],
|
||
content: [
|
||
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
||
],
|
||
summary: [
|
||
{ max: 500, message: '摘要长度不能超过 500 个字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
// TinyMCE 配置
|
||
const editorInit = {
|
||
menubar: false,
|
||
dragdrop: true, // 启用拖拽图片功能
|
||
valid_elements: '*[*]',
|
||
valid_elements: 'section[*],*[*]', // 允许 section 及其属性
|
||
valid_children: '+body[section],+section[p,div,span]', // 允许 body 包含 section,section 包含段落等
|
||
file_picker_types: 'image',
|
||
invalid_elements: 'script',
|
||
statusbar: false,
|
||
placeholder: '开始编写吧', // 占位符
|
||
theme: 'silver', // 主题 必须引入
|
||
license_key: 'gpl', // 使用开源许可
|
||
paste_as_text: false, // 允许 HTML 粘贴
|
||
paste_enable_default_filters: false, // 禁用默认 HTML 过滤
|
||
paste_webkit_styles: 'all', // 允许所有 Webkit 内联样式
|
||
paste_retain_style_properties: 'all', // 保留所有 inline style
|
||
extended_valid_elements: '*[*]', // 确保所有 HTML 属性都被保留
|
||
promotion: false, // 移除 Upgrade 按钮
|
||
branding: false, // 移除 TinyMCE 品牌信息
|
||
toolbar_mode: 'wrap',
|
||
contextmenu: 'styleControls | insertBefore insertAfter | copyElement | removeIndent | deleteElement | image link',
|
||
content_style: `
|
||
body::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
body::-webkit-scrollbar-track {
|
||
background: #f8fafc;
|
||
}
|
||
body::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1;
|
||
border-radius: 4px;
|
||
}
|
||
body::-webkit-scrollbar-thumb:hover {
|
||
background: #94a3b8;
|
||
}
|
||
.element-highlight {
|
||
outline: 1px solid #3b82f6 !important;
|
||
}
|
||
|
||
|
||
`,
|
||
// 设置工具栏样式
|
||
toolbar_location: 'top',
|
||
|
||
// 如果需要固定工具栏
|
||
toolbar_sticky: true,
|
||
toolbar: [
|
||
'undo redo bold italic underline forecolor backcolor alignleft aligncenter alignright image insertBeforeSection insertSection styleControls mySaveBtn help_article'
|
||
|
||
]
|
||
,
|
||
plugins: [
|
||
'anchor', 'autolink', 'charmap', 'codesample', 'emoticons', 'image', 'link',
|
||
'lists', 'media', 'searchreplace', 'table', 'visualblocks', 'wordcount'
|
||
],
|
||
setup: function (editor) {
|
||
editor.on('paste', function (e) {
|
||
e.preventDefault(); // 阻止默认粘贴行为
|
||
const clipboardData = e.clipboardData || window.clipboardData;
|
||
if (clipboardData && clipboardData.types.indexOf('text/html') > -1) {
|
||
// 获取原始HTML内容
|
||
const htmlContent = clipboardData.getData('text/html');
|
||
// 直接插入原始HTML,避免修改
|
||
editor.insertContent(htmlContent);
|
||
} else {
|
||
// 如果没有HTML,回退到纯文本
|
||
const text = clipboardData.getData('text/plain');
|
||
editor.insertContent(text);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 计算属性
|
||
const dialogVisible = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value)
|
||
})
|
||
|
||
const isEdit = computed(() => !!props.article)
|
||
|
||
// 获取文章详情
|
||
const fetchArticleDetail = async (articleId) => {
|
||
if (!articleId) return
|
||
|
||
loading.value = true
|
||
try {
|
||
const response = await articleApi.getArticleDetail(articleId)
|
||
articleDetail.value = response.data
|
||
// 使用详情数据填充表单
|
||
fillFormWithDetail(articleDetail.value)
|
||
} catch (error) {
|
||
ElMessage.error('获取文章详情失败')
|
||
console.error('获取文章详情失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 使用详情数据填充表单
|
||
const fillFormWithDetail = (detail) => {
|
||
if (!detail) return
|
||
|
||
Object.assign(form, {
|
||
title: detail.title || '',
|
||
content: detail.content || '',
|
||
summary: detail.summary || '',
|
||
cover_image: detail.cover_image || '',
|
||
category_id: detail.category_id || '',
|
||
tag_ids: detail.tags ? detail.tags.map(tag => tag.id) : [],
|
||
is_featured: detail.is_featured || false
|
||
})
|
||
}
|
||
|
||
// 对话框打开时获取详情
|
||
const handleDialogOpen = () => {
|
||
if (props.article?.id && isEdit.value) {
|
||
fetchArticleDetail(props.article.id)
|
||
}
|
||
}
|
||
|
||
// 监听文章数据变化,初始化表单
|
||
watch(() => props.article, (newArticle) => {
|
||
if (newArticle && isEdit.value) {
|
||
// 编辑模式,如果有详情数据则使用详情数据,否则使用props数据
|
||
if (articleDetail.value) {
|
||
fillFormWithDetail(articleDetail.value)
|
||
} else {
|
||
// 使用props数据作为临时填充
|
||
Object.assign(form, {
|
||
title: newArticle.title || '',
|
||
content: newArticle.content || '',
|
||
summary: newArticle.summary || '',
|
||
cover_image: newArticle.cover_image || '',
|
||
category_id: newArticle.category_id || '',
|
||
tag_ids: newArticle.tag_ids || [],
|
||
is_featured: newArticle.is_featured || false
|
||
})
|
||
}
|
||
} else {
|
||
// 新增模式,重置表单
|
||
resetForm()
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 重置表单(使用函数声明,避免提升前调用报错)
|
||
function resetForm() {
|
||
Object.assign(form, {
|
||
title: '',
|
||
content: '',
|
||
summary: '',
|
||
cover_image: '',
|
||
category_id: '',
|
||
tag_ids: [],
|
||
is_featured: false
|
||
})
|
||
|
||
if (formRef.value) {
|
||
formRef.value.clearValidate()
|
||
}
|
||
}
|
||
|
||
// 关闭对话框
|
||
const handleClose = () => {
|
||
dialogVisible.value = false
|
||
resetForm()
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
try {
|
||
await formRef.value.validate()
|
||
|
||
loading.value = true
|
||
|
||
if (isEdit.value) {
|
||
// 编辑模式
|
||
await articleApi.updateArticle(props.article.id, form)
|
||
ElMessage.success('文章更新成功')
|
||
} else {
|
||
// 新增模式
|
||
await articleApi.createArticle(form)
|
||
ElMessage.success('文章创建成功')
|
||
}
|
||
|
||
emit('success')
|
||
handleClose()
|
||
} catch (error) {
|
||
if (error.message) {
|
||
ElMessage.error(error.message)
|
||
} else {
|
||
ElMessage.error(isEdit.value ? '更新文章失败' : '创建文章失败')
|
||
}
|
||
console.error('提交表单失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|