first commit

This commit is contained in:
2025-11-24 16:06:44 +08:00
commit e57d497751
165 changed files with 59349 additions and 0 deletions

View 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 包含 sectionsection 包含段落等
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>