383 lines
8.5 KiB
Vue
383 lines
8.5 KiB
Vue
|
|
<template>
|
|||
|
|
<!-- Dialog 模式 -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-if="asDialog"
|
|||
|
|
v-model="visible"
|
|||
|
|
:title="title"
|
|||
|
|
:width="width"
|
|||
|
|
:close-on-click-modal="false"
|
|||
|
|
@close="handleClose"
|
|||
|
|
>
|
|||
|
|
<div class="article-show-container">
|
|||
|
|
<!-- 文章头部信息 -->
|
|||
|
|
<div class="article-header">
|
|||
|
|
<h1 class="article-title">{{ articleData.title }}</h1>
|
|||
|
|
<div class="article-meta">
|
|||
|
|
<span v-if="articleData.category" class="meta-item">
|
|||
|
|
分类:{{ getCategoryLabel(articleData.category) }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.tags && articleData.tags.length" class="meta-item">
|
|||
|
|
标签:{{ getTagsString(articleData.tags) }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.author" class="meta-item">
|
|||
|
|
作者:{{ articleData.author }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.createTime" class="meta-item">
|
|||
|
|
发布时间:{{ formatDate(articleData.createTime) }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 封面图片 -->
|
|||
|
|
<div v-if="articleData.coverImage" class="article-cover">
|
|||
|
|
<img :src="articleData.coverImage" class="cover-image" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 文章内容 -->
|
|||
|
|
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button @click="handleClose">关闭</el-button>
|
|||
|
|
<el-button v-if="showEditButton" type="primary" @click="handleEdit">编辑</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-dialog>
|
|||
|
|
|
|||
|
|
<!-- 非 Dialog 模式 -->
|
|||
|
|
<div v-else class="article-show-container">
|
|||
|
|
<!-- 文章头部信息 -->
|
|||
|
|
<div class="article-header">
|
|||
|
|
<h1 class="article-title">{{ articleData.title }}</h1>
|
|||
|
|
<div class="article-meta">
|
|||
|
|
<span v-if="articleData.category" class="meta-item">
|
|||
|
|
分类:{{ getCategoryLabel(articleData.category) }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.tags && articleData.tags.length" class="meta-item">
|
|||
|
|
标签:{{ getTagsString(articleData.tags) }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.author" class="meta-item">
|
|||
|
|
作者:{{ articleData.author }}
|
|||
|
|
</span>
|
|||
|
|
<span v-if="articleData.createTime" class="meta-item">
|
|||
|
|
发布时间:{{ formatDate(articleData.createTime) }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 封面图片 -->
|
|||
|
|
<div v-if="articleData.coverImage" class="article-cover">
|
|||
|
|
<img :src="articleData.coverImage" class="cover-image" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 文章内容 -->
|
|||
|
|
<div class="article-content ql-editor" v-html="articleData.content"></div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { computed } from 'vue';
|
|||
|
|
import { ElDialog, ElButton } from 'element-plus';
|
|||
|
|
|
|||
|
|
interface ArticleData {
|
|||
|
|
title?: string;
|
|||
|
|
content?: string;
|
|||
|
|
coverImage?: string;
|
|||
|
|
category?: string;
|
|||
|
|
tags?: Array<{ name?: string }> | string[]; // 支持对象数组或字符串数组
|
|||
|
|
author?: string;
|
|||
|
|
createTime?: string | Date;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
modelValue?: boolean; // Dialog 模式下的显示状态
|
|||
|
|
asDialog?: boolean; // 是否作为 Dialog 使用
|
|||
|
|
title?: string; // Dialog 标题
|
|||
|
|
width?: string; // Dialog 宽度
|
|||
|
|
articleData?: ArticleData; // 文章数据
|
|||
|
|
categoryList?: Array<{ id?: string; categoryID?: string; name?: string }>; // 分类列表
|
|||
|
|
showEditButton?: boolean; // 是否显示编辑按钮
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
modelValue: false,
|
|||
|
|
asDialog: true,
|
|||
|
|
title: '文章预览',
|
|||
|
|
width: '900px',
|
|||
|
|
articleData: () => ({}),
|
|||
|
|
categoryList: () => [],
|
|||
|
|
showEditButton: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
'update:modelValue': [value: boolean];
|
|||
|
|
'close': [];
|
|||
|
|
'edit': [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
// Dialog 显示状态
|
|||
|
|
const visible = computed({
|
|||
|
|
get: () => props.asDialog ? props.modelValue : false,
|
|||
|
|
set: (val) => props.asDialog ? emit('update:modelValue', val) : undefined
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 获取标签字符串
|
|||
|
|
function getTagsString(tags: Array<{ name?: string }> | string[]): string {
|
|||
|
|
if (!tags || tags.length === 0) return '';
|
|||
|
|
|
|||
|
|
if (typeof tags[0] === 'string') {
|
|||
|
|
return (tags as string[]).join(', ');
|
|||
|
|
} else {
|
|||
|
|
return (tags as Array<{ name?: string }>)
|
|||
|
|
.map(tag => tag.name || '')
|
|||
|
|
.filter(name => name)
|
|||
|
|
.join(', ');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取分类标签
|
|||
|
|
function getCategoryLabel(categoryId: string): string {
|
|||
|
|
if (!props.categoryList || !categoryId) return '';
|
|||
|
|
|
|||
|
|
const category = props.categoryList.find(cat =>
|
|||
|
|
cat.id === categoryId || cat.categoryID === categoryId
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return category?.name || categoryId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
function formatDate(date: string | Date): string {
|
|||
|
|
if (!date) return '';
|
|||
|
|
|
|||
|
|
const d = new Date(date);
|
|||
|
|
return d.toLocaleDateString('zh-CN', {
|
|||
|
|
year: 'numeric',
|
|||
|
|
month: '2-digit',
|
|||
|
|
day: '2-digit',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关闭处理
|
|||
|
|
function handleClose() {
|
|||
|
|
if (props.asDialog) {
|
|||
|
|
visible.value = false;
|
|||
|
|
}
|
|||
|
|
emit('close');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 编辑处理
|
|||
|
|
function handleEdit() {
|
|||
|
|
emit('edit');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 暴露方法
|
|||
|
|
defineExpose({
|
|||
|
|
open: () => {
|
|||
|
|
if (props.asDialog) {
|
|||
|
|
visible.value = true;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
close: handleClose
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.article-show-container {
|
|||
|
|
max-width: 100%;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.article-header {
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
padding-bottom: 16px;
|
|||
|
|
border-bottom: 1px solid #ebeef5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.article-title {
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #303133;
|
|||
|
|
margin: 0 0 16px 0;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.article-meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 16px;
|
|||
|
|
color: #909399;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.meta-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
&::before {
|
|||
|
|
content: '';
|
|||
|
|
width: 4px;
|
|||
|
|
height: 4px;
|
|||
|
|
background: #909399;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
margin-right: 8px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.article-cover {
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cover-image {
|
|||
|
|
max-width: 100%;
|
|||
|
|
max-height: 400px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.article-content {
|
|||
|
|
line-height: 1.8;
|
|||
|
|
color: #303133;
|
|||
|
|
font-size: 16px;
|
|||
|
|
|
|||
|
|
// 继承富文本编辑器的样式
|
|||
|
|
:deep(img) {
|
|||
|
|
max-width: 100%;
|
|||
|
|
height: auto;
|
|||
|
|
display: inline-block;
|
|||
|
|
vertical-align: bottom;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(video),
|
|||
|
|
:deep(iframe) {
|
|||
|
|
max-width: 100%;
|
|||
|
|
height: auto;
|
|||
|
|
display: inline-block;
|
|||
|
|
vertical-align: bottom;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 对齐方式样式 - 图片和视频分别处理
|
|||
|
|
:deep(.ql-align-center) {
|
|||
|
|
text-align: center !important;
|
|||
|
|
|
|||
|
|
// 视频始终居中显示
|
|||
|
|
video, .custom-video {
|
|||
|
|
display: block !important;
|
|||
|
|
margin-left: auto !important;
|
|||
|
|
margin-right: auto !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片跟随文字对齐
|
|||
|
|
img, .custom-image {
|
|||
|
|
display: inline-block !important;
|
|||
|
|
vertical-align: bottom !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ql-align-right) {
|
|||
|
|
text-align: right !important;
|
|||
|
|
|
|||
|
|
// 视频始终居中显示
|
|||
|
|
video, .custom-video {
|
|||
|
|
display: block !important;
|
|||
|
|
margin-left: auto !important;
|
|||
|
|
margin-right: auto !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片跟随文字对齐
|
|||
|
|
img, .custom-image {
|
|||
|
|
display: inline-block !important;
|
|||
|
|
vertical-align: bottom !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.ql-align-left) {
|
|||
|
|
text-align: left !important;
|
|||
|
|
|
|||
|
|
// 视频始终居中显示
|
|||
|
|
video, .custom-video {
|
|||
|
|
display: block !important;
|
|||
|
|
margin-left: auto !important;
|
|||
|
|
margin-right: auto !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片跟随文字对齐
|
|||
|
|
img, .custom-image {
|
|||
|
|
display: inline-block !important;
|
|||
|
|
vertical-align: bottom !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他富文本样式
|
|||
|
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
|||
|
|
margin: 24px 0 16px 0;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #303133;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(p) {
|
|||
|
|
margin: 16px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(blockquote) {
|
|||
|
|
margin: 16px 0;
|
|||
|
|
padding: 16px;
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
border-left: 4px solid #409eff;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(code) {
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
padding: 2px 6px;
|
|||
|
|
border-radius: 3px;
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(pre) {
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
margin: 16px 0;
|
|||
|
|
|
|||
|
|
code {
|
|||
|
|
background: none;
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(ul), :deep(ol) {
|
|||
|
|
margin: 16px 0;
|
|||
|
|
padding-left: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(li) {
|
|||
|
|
margin: 8px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(table) {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
margin: 16px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(th), :deep(td) {
|
|||
|
|
border: 1px solid #ebeef5;
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(th) {
|
|||
|
|
background: #f5f7fa;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|