423 lines
13 KiB
Vue
423 lines
13 KiB
Vue
<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,支持jpg、png格式</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>
|