first commit
This commit is contained in:
339
src/pages/admin/articles/components/ArticleEditDialog.vue
Normal file
339
src/pages/admin/articles/components/ArticleEditDialog.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user