web-上传组件、富文本组件

This commit is contained in:
2025-10-20 11:25:34 +08:00
parent f137d7d720
commit 2f1835bdbf
12 changed files with 1608 additions and 445 deletions

View File

@@ -15,43 +15,48 @@
<!-- 分类和标签 -->
<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-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 || ''"
/>
</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 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 || ''"
/>
</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>
<!-- 上传区域 - 只在没有封面图片时显示 -->
<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>
</el-form-item>
<!-- 文章内容 -->
@@ -93,28 +98,22 @@
</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>
<!-- 文章预览组件 -->
<ArticleShowView
v-model="previewVisible"
:as-dialog="true"
title="文章预览"
width="900px"
:article-data="articleForm"
:category-list="categoryList"
:show-edit-button="false"
@close="previewVisible = false"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
ElForm,
@@ -126,13 +125,14 @@ import {
ElRow,
ElCol,
ElCheckbox,
ElUpload,
ElIcon,
ElMessage,
ElDialog
ElMessage
} from 'element-plus';
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
import { ArrowLeft } from '@element-plus/icons-vue';
import { RichTextComponent } from '@/components/text';
import { FileUpload } from '@/components/file';
import { ArticleShowView } from './index';
import { resourceCategoryApi, resourceTagApi, resourceApi } from '@/apis/resource';
import { Resource, ResourceCategory, Tag } from '@/types/resource';
const router = useRouter();
const route = useRoute();
@@ -146,17 +146,25 @@ const previewVisible = ref(false);
// 是否编辑模式
const isEdit = ref(false);
// 数据状态
const categoryList = ref<ResourceCategory[]>([]);
const tagList = ref<Tag[]>([]);
const categoryLoading = ref(false);
const tagLoading = ref(false);
// 表单数据
const articleForm = reactive({
const articleForm = ref<Resource>({
title: '',
category: '',
tags: [] as string[],
summary: '',
cover: '',
content: '',
categoryID: '',
author: '',
source: '',
sourceUrl: '',
viewCount: 0,
coverImage: '',
tags: [] as Tag[],
allowComment: true,
isTop: false,
isRecommend: false
});
// 表单验证规则
@@ -165,7 +173,7 @@ const rules = {
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
],
category: [
categoryID: [
{ required: true, message: '请选择文章分类', trigger: 'change' }
],
content: [
@@ -173,19 +181,43 @@ const rules = {
]
};
onMounted(() => {
// 检查是否是编辑模式
const id = route.query.id;
if (id) {
isEdit.value = true;
loadArticle(id as string);
}
});
// 加载文章数据(编辑模式)
function loadArticle(id: string) {
// TODO: 调用API加载文章数据
console.log('加载文章:', id);
// 加载分类列表
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;
}
}
// 加载标签列表
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;
}
}
// 返回
@@ -235,8 +267,8 @@ async function handleSaveDraft() {
// 预览
function handlePreview() {
console.log(articleForm.content);
if (!articleForm.title) {
console.log(articleForm.value.content);
if (!articleForm.value.title) {
ElMessage.warning('请先输入文章标题');
return;
}
@@ -244,35 +276,46 @@ function handlePreview() {
}
// 封面上传成功
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('只能上传图片文件!');
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;
});
}
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;
// 删除封面
function removeCover() {
articleForm.value.coverImage = '';
}
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('加载文章失败');
}
}
});
</script>
<style lang="scss" scoped>
@@ -341,82 +384,16 @@ function getCategoryLabel(value: string): string {
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;
.cover-preview {
margin-top: 16px;
position: relative;
.cover {
width: 200px;
height: auto;
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;
}
display: block;
margin-bottom: 8px;
}
}
</style>