2025-10-18 18:19:19 +08:00
|
|
|
|
<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">
|
2025-10-20 11:25:34 +08:00
|
|
|
|
<el-form-item label="文章分类" prop="categoryID">
|
|
|
|
|
|
<el-select v-model="articleForm.categoryID" placeholder="请选择分类" style="width: 100%" :loading="categoryLoading">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="category in categoryList"
|
|
|
|
|
|
:key="category.categoryID || category.id"
|
|
|
|
|
|
:label="category.name"
|
|
|
|
|
|
:value="category.categoryID || category.id || ''"
|
|
|
|
|
|
/>
|
2025-10-18 18:19:19 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="标签" prop="tags">
|
2025-10-20 11:25:34 +08:00
|
|
|
|
<el-select v-model="articleForm.tags" multiple placeholder="请选择标签" style="width: 100%" :loading="tagLoading">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="tag in tagList"
|
|
|
|
|
|
:key="tag.id || tag.tagID"
|
|
|
|
|
|
:label="tag.name"
|
|
|
|
|
|
:value="tag.id || tag.tagID || ''"
|
|
|
|
|
|
/>
|
2025-10-18 18:19:19 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 封面图 -->
|
|
|
|
|
|
<el-form-item label="封面图片">
|
2025-10-20 11:25:34 +08:00
|
|
|
|
<!-- 上传区域 - 只在没有封面图片时显示 -->
|
|
|
|
|
|
<FileUpload
|
|
|
|
|
|
v-if="!articleForm.coverImage"
|
|
|
|
|
|
:as-dialog="false"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
:max-size="2"
|
|
|
|
|
|
:multiple="false"
|
|
|
|
|
|
tip="建议尺寸:800x450px,支持jpg、png格式"
|
|
|
|
|
|
@success="handleCoverUploadSuccess"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<!-- 封面预览 - 只在有封面图片时显示 -->
|
|
|
|
|
|
<div v-if="articleForm.coverImage" class="cover-preview">
|
|
|
|
|
|
<img :src="articleForm.coverImage" class="cover" />
|
|
|
|
|
|
<el-button type="danger" size="small" @click="removeCover">删除封面</el-button>
|
|
|
|
|
|
</div>
|
2025-10-18 18:19:19 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
<!-- 文章预览组件 -->
|
|
|
|
|
|
<ArticleShowView
|
|
|
|
|
|
v-model="previewVisible"
|
|
|
|
|
|
:as-dialog="true"
|
|
|
|
|
|
title="文章预览"
|
|
|
|
|
|
width="900px"
|
|
|
|
|
|
:article-data="articleForm"
|
|
|
|
|
|
:category-list="categoryList"
|
|
|
|
|
|
:show-edit-button="false"
|
|
|
|
|
|
@close="previewVisible = false"
|
|
|
|
|
|
/>
|
2025-10-18 18:19:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-10-20 11:25:34 +08:00
|
|
|
|
import { ref, onMounted } from 'vue';
|
2025-10-18 18:19:19 +08:00
|
|
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ElForm,
|
|
|
|
|
|
ElFormItem,
|
|
|
|
|
|
ElInput,
|
|
|
|
|
|
ElSelect,
|
|
|
|
|
|
ElOption,
|
|
|
|
|
|
ElButton,
|
|
|
|
|
|
ElRow,
|
|
|
|
|
|
ElCol,
|
|
|
|
|
|
ElCheckbox,
|
2025-10-20 11:25:34 +08:00
|
|
|
|
ElMessage
|
2025-10-18 18:19:19 +08:00
|
|
|
|
} from 'element-plus';
|
2025-10-20 11:25:34 +08:00
|
|
|
|
import { ArrowLeft } from '@element-plus/icons-vue';
|
2025-10-18 18:19:19 +08:00
|
|
|
|
import { RichTextComponent } from '@/components/text';
|
2025-10-20 11:25:34 +08:00
|
|
|
|
import { FileUpload } from '@/components/file';
|
|
|
|
|
|
import { ArticleShowView } from './index';
|
|
|
|
|
|
import { resourceCategoryApi, resourceTagApi, resourceApi } from '@/apis/resource';
|
|
|
|
|
|
import { Resource, ResourceCategory, Tag } from '@/types/resource';
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
// 数据状态
|
|
|
|
|
|
const categoryList = ref<ResourceCategory[]>([]);
|
|
|
|
|
|
const tagList = ref<Tag[]>([]);
|
|
|
|
|
|
const categoryLoading = ref(false);
|
|
|
|
|
|
const tagLoading = ref(false);
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
// 表单数据
|
2025-10-20 11:25:34 +08:00
|
|
|
|
const articleForm = ref<Resource>({
|
2025-10-18 18:19:19 +08:00
|
|
|
|
title: '',
|
|
|
|
|
|
content: '',
|
2025-10-20 11:25:34 +08:00
|
|
|
|
categoryID: '',
|
|
|
|
|
|
author: '',
|
|
|
|
|
|
source: '',
|
|
|
|
|
|
sourceUrl: '',
|
|
|
|
|
|
viewCount: 0,
|
|
|
|
|
|
coverImage: '',
|
|
|
|
|
|
tags: [] as Tag[],
|
2025-10-18 18:19:19 +08:00
|
|
|
|
allowComment: true,
|
|
|
|
|
|
isTop: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 表单验证规则
|
|
|
|
|
|
const rules = {
|
|
|
|
|
|
title: [
|
|
|
|
|
|
{ required: true, message: '请输入文章标题', trigger: 'blur' },
|
|
|
|
|
|
{ min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
|
|
|
|
|
|
],
|
2025-10-20 11:25:34 +08:00
|
|
|
|
categoryID: [
|
2025-10-18 18:19:19 +08:00
|
|
|
|
{ required: true, message: '请选择文章分类', trigger: 'change' }
|
|
|
|
|
|
],
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载分类列表
|
|
|
|
|
|
async function loadCategoryList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
categoryLoading.value = true;
|
|
|
|
|
|
const result = await resourceCategoryApi.getCategoryList();
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
// 数组数据从 dataList 获取
|
|
|
|
|
|
categoryList.value = result.dataList || [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result.message || '加载分类失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载分类失败:', error);
|
|
|
|
|
|
ElMessage.error('加载分类失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
categoryLoading.value = false;
|
2025-10-18 18:19:19 +08:00
|
|
|
|
}
|
2025-10-20 11:25:34 +08:00
|
|
|
|
}
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
// 加载标签列表
|
|
|
|
|
|
async function loadTagList() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
tagLoading.value = true;
|
|
|
|
|
|
const result = await resourceTagApi.getTagList();
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
// 数组数据从 dataList 获取
|
|
|
|
|
|
tagList.value = result.dataList || [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result.message || '加载标签失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载标签失败:', error);
|
|
|
|
|
|
ElMessage.error('加载标签失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
tagLoading.value = false;
|
|
|
|
|
|
}
|
2025-10-18 18:19:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回
|
|
|
|
|
|
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() {
|
2025-10-20 11:25:34 +08:00
|
|
|
|
console.log(articleForm.value.content);
|
|
|
|
|
|
if (!articleForm.value.title) {
|
2025-10-18 18:19:19 +08:00
|
|
|
|
ElMessage.warning('请先输入文章标题');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
previewVisible.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 封面上传成功
|
2025-10-20 11:25:34 +08:00
|
|
|
|
function handleCoverUploadSuccess(files: any[]) {
|
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
|
const file = files[0];
|
|
|
|
|
|
// 使用文件下载URL构建完整路径
|
|
|
|
|
|
import('@/config').then(config => {
|
|
|
|
|
|
articleForm.value.coverImage = config.FILE_DOWNLOAD_URL + file.id;
|
|
|
|
|
|
});
|
2025-10-18 18:19:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
// 删除封面
|
|
|
|
|
|
function removeCover() {
|
|
|
|
|
|
articleForm.value.coverImage = '';
|
2025-10-18 18:19:19 +08:00
|
|
|
|
}
|
2025-10-20 11:25:34 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
// 并行加载分类和标签数据
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
loadCategoryList(),
|
|
|
|
|
|
loadTagList()
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是编辑模式,加载文章数据
|
|
|
|
|
|
const id = route.query.id;
|
|
|
|
|
|
if (id) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
isEdit.value = true;
|
|
|
|
|
|
const result = await resourceApi.getResourceById(id as string);
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
articleForm.value = result.data;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result.message || '加载文章失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载文章失败:', error);
|
|
|
|
|
|
ElMessage.error('加载文章失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-10-18 18:19:19 +08:00
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 11:25:34 +08:00
|
|
|
|
.cover-preview {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
.cover {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
height: auto;
|
2025-10-18 18:19:19 +08:00
|
|
|
|
border-radius: 4px;
|
2025-10-20 11:25:34 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8px;
|
2025-10-18 18:19:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|