Files
schoolNews/schoolNewsWeb/src/views/article/ArticleAddView.vue
2025-10-18 18:19:19 +08:00

423 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="article-add-view">
<div class="page-header">
<el-button @click="handleBack" :icon="ArrowLeft">返回</el-button>
<h1 class="page-title">{{ isEdit ? '编辑文章' : '创建文章' }}</h1>
</div>
<div class="article-form">
<el-form ref="formRef" :model="articleForm" :rules="rules" label-width="100px" label-position="top">
<!-- 标题 -->
<el-form-item label="文章标题" prop="title">
<el-input v-model="articleForm.title" placeholder="请输入文章标题" maxlength="100" show-word-limit />
</el-form-item>
<!-- 分类和标签 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文章分类" prop="category">
<el-select v-model="articleForm.category" placeholder="请选择分类" style="width: 100%">
<el-option label="新闻资讯" value="news" />
<el-option label="技术文章" value="tech" />
<el-option label="学习资料" value="study" />
<el-option label="通知公告" value="notice" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="标签" prop="tags">
<el-select v-model="articleForm.tags" multiple placeholder="请选择标签" style="width: 100%">
<el-option label="重要" value="important" />
<el-option label="推荐" value="recommend" />
<el-option label="热门" value="hot" />
<el-option label="原创" value="original" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 摘要 -->
<el-form-item label="文章摘要" prop="summary">
<el-input v-model="articleForm.summary" type="textarea" :rows="3" placeholder="请输入文章摘要(选填)"
maxlength="200" show-word-limit />
</el-form-item>
<!-- 封面图 -->
<el-form-item label="封面图片">
<el-upload class="cover-uploader" :show-file-list="false" :on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload" action="#">
<img v-if="articleForm.cover" :src="articleForm.cover" class="cover" />
<el-icon v-else class="cover-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<div class="upload-tip">建议尺寸800x450px支持jpgpng格式</div>
</el-form-item>
<!-- 文章内容 -->
<el-form-item label="文章内容" prop="content">
<RichTextComponent ref="editorRef" v-model="articleForm.content" height="500px"
placeholder="请输入文章内容..." />
</el-form-item>
<!-- 发布设置 -->
<el-form-item label="发布设置">
<el-row :gutter="20">
<el-col :span="8">
<el-checkbox v-model="articleForm.allowComment">允许评论</el-checkbox>
</el-col>
<el-col :span="8">
<el-checkbox v-model="articleForm.isTop">置顶文章</el-checkbox>
</el-col>
<el-col :span="8">
<el-checkbox v-model="articleForm.isRecommend">推荐文章</el-checkbox>
</el-col>
</el-row>
</el-form-item>
<!-- 操作按钮 -->
<el-form-item>
<el-button type="primary" @click="handlePublish" :loading="publishing">
{{ isEdit ? '保存修改' : '立即发布' }}
</el-button>
<el-button @click="handleSaveDraft" :loading="savingDraft">
保存草稿
</el-button>
<el-button @click="handlePreview">
预览
</el-button>
<el-button @click="handleBack">
取消
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 预览对话框 -->
<el-dialog v-model="previewVisible" title="文章预览" width="900px" :close-on-click-modal="false">
<div class="article-preview">
<h1 class="preview-title">{{ articleForm.title }}</h1>
<div class="preview-meta">
<span>分类{{ getCategoryLabel(articleForm.category) }}</span>
<span v-if="articleForm.tags.length">
标签{{ articleForm.tags.join(', ') }}
</span>
</div>
<div class="preview-summary" v-if="articleForm.summary">
{{ articleForm.summary }}
</div>
<img v-if="articleForm.cover" :src="articleForm.cover" class="preview-cover" />
<div class="preview-content ql-editor" v-html="articleForm.content"></div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElButton,
ElRow,
ElCol,
ElCheckbox,
ElUpload,
ElIcon,
ElMessage,
ElDialog
} from 'element-plus';
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
import { RichTextComponent } from '@/components/text';
const router = useRouter();
const route = useRoute();
const formRef = ref();
const editorRef = ref();
const publishing = ref(false);
const savingDraft = ref(false);
const previewVisible = ref(false);
// 是否编辑模式
const isEdit = ref(false);
// 表单数据
const articleForm = reactive({
title: '',
category: '',
tags: [] as string[],
summary: '',
cover: '',
content: '',
allowComment: true,
isTop: false,
isRecommend: false
});
// 表单验证规则
const rules = {
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
],
category: [
{ required: true, message: '请选择文章分类', trigger: 'change' }
],
content: [
{ required: true, message: '请输入文章内容', trigger: 'blur' }
]
};
onMounted(() => {
// 检查是否是编辑模式
const id = route.query.id;
if (id) {
isEdit.value = true;
loadArticle(id as string);
}
});
// 加载文章数据(编辑模式)
function loadArticle(id: string) {
// TODO: 调用API加载文章数据
console.log('加载文章:', id);
}
// 返回
function handleBack() {
router.back();
}
// 发布文章
async function handlePublish() {
try {
await formRef.value?.validate();
publishing.value = true;
// TODO: 调用API发布文章
console.log('发布文章:', articleForm);
await new Promise(resolve => setTimeout(resolve, 1000));
ElMessage.success(isEdit.value ? '修改成功' : '发布成功');
router.push('/admin/manage/resource/articles');
} catch (error) {
console.error('发布失败:', error);
} finally {
publishing.value = false;
}
}
// 保存草稿
async function handleSaveDraft() {
savingDraft.value = true;
try {
// TODO: 调用API保存草稿
console.log('保存草稿:', articleForm);
await new Promise(resolve => setTimeout(resolve, 1000));
ElMessage.success('草稿已保存');
} catch (error) {
console.error('保存失败:', error);
ElMessage.error('保存失败');
} finally {
savingDraft.value = false;
}
}
// 预览
function handlePreview() {
console.log(articleForm.content);
if (!articleForm.title) {
ElMessage.warning('请先输入文章标题');
return;
}
previewVisible.value = true;
}
// 封面上传成功
function handleCoverSuccess(response: any) {
// TODO: 处理上传成功的响应
articleForm.cover = response.url;
}
// 上传前验证
function beforeCoverUpload(file: File) {
const isImage = file.type.startsWith('image/');
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isImage) {
ElMessage.error('只能上传图片文件!');
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!');
}
return isImage && isLt2M;
}
// 获取分类标签
function getCategoryLabel(value: string): string {
const map: Record<string, string> = {
news: '新闻资讯',
tech: '技术文章',
study: '学习资料',
notice: '通知公告'
};
return map[value] || value;
}
</script>
<style lang="scss" scoped>
.article-add-view {
min-height: 100vh;
background: #f5f7fa;
padding: 24px;
}
.page-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin: 0;
}
}
.article-form {
background: white;
border-radius: 8px;
padding: 32px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.cover-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.2s;
&:hover {
border-color: #409eff;
}
}
}
.cover-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
display: flex;
align-items: center;
justify-content: center;
}
.cover {
width: 178px;
height: 178px;
display: block;
object-fit: cover;
}
.upload-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.article-preview {
padding: 20px;
:deep(.ql-code-block-container) {
margin: 12px 0; // 上下间距
}
:deep(.ql-code-block) {
background: #282c34; // 代码块背景色(类似深色主题)
color: #abb2bf; // 代码文字颜色
padding: 12px; // 内边距
border-radius: 4px; // 圆角
overflow-x: auto; // 横向滚动
font-family: 'Courier New', monospace; // 等宽字体
white-space: pre; // 保留空格和换行
}
.preview-title {
font-size: 28px;
font-weight: 600;
color: #303133;
margin: 0 0 16px 0;
}
.preview-meta {
display: flex;
gap: 24px;
color: #909399;
font-size: 14px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.preview-summary {
background: #f5f7fa;
padding: 16px;
border-radius: 4px;
color: #606266;
line-height: 1.6;
margin-bottom: 20px;
}
.preview-cover {
width: 100%;
max-height: 400px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 20px;
}
.preview-content {
// ql-editor 类会自动应用 Quill 的默认样式
// 这里只添加必要的自定义样式覆盖
// 图片和视频样式(保留用户设置的尺寸)
:deep(img[width]),
:deep(video[width]),
:deep(img[style*="width"]),
:deep(video[style*="width"]) {
// 如果有 width 属性或 style 中包含 width使用用户设置的尺寸
max-width: 100%;
// 不强制设置 height: auto保留用户设置的固定尺寸
display: block;
margin: 12px auto;
}
// 没有 width 属性的图片和视频使用默认样式
:deep(img:not([width]):not([style*="width"])),
:deep(video:not([width]):not([style*="width"])),
:deep(iframe) {
max-width: 100%;
height: auto;
display: block;
margin: 12px auto;
}
}
}
</style>