340 lines
7.4 KiB
Vue
340 lines
7.4 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-info">
|
||
<div class="meta-item" v-if="articleData.publishTime || articleData.createTime">
|
||
发布时间:{{ formatDateSimple(articleData.publishTime || articleData.createTime || '') }}
|
||
</div>
|
||
<div class="meta-item" v-if="articleData.viewCount !== undefined">
|
||
浏览次数:{{ articleData.viewCount }}
|
||
</div>
|
||
<div class="meta-item" v-if="articleData.source">
|
||
来源:{{ articleData.source }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分隔线 -->
|
||
<div class="separator"></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-info">
|
||
<div class="meta-item" v-if="articleData.publishTime || articleData.createTime">
|
||
发布时间:{{ formatDateSimple(articleData.publishTime || articleData.createTime || '') }}
|
||
</div>
|
||
<div class="meta-item" v-if="articleData.viewCount !== undefined">
|
||
浏览次数:{{ articleData.viewCount }}
|
||
</div>
|
||
<div class="meta-item" v-if="articleData.source">
|
||
来源:{{ articleData.source }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分隔线 -->
|
||
<div class="separator"></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';
|
||
import { ResourceCategory, Resource, Tag } from '@/types';
|
||
|
||
|
||
interface Props {
|
||
modelValue?: boolean; // Dialog 模式下的显示状态
|
||
asDialog?: boolean; // 是否作为 Dialog 使用
|
||
title?: string; // Dialog 标题
|
||
width?: string; // Dialog 宽度
|
||
articleData?: Resource; // 文章数据
|
||
categoryList?: Array<ResourceCategory>; // 分类列表
|
||
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
|
||
});
|
||
|
||
|
||
// 格式化日期(简单格式:YYYY-MM-DD)
|
||
function formatDateSimple(date: string | Date): string {
|
||
if (!date) return '';
|
||
|
||
const d = new Date(date);
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
// 关闭处理
|
||
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;
|
||
background: #fff;
|
||
}
|
||
|
||
.article-header {
|
||
margin-bottom: 36px;
|
||
}
|
||
|
||
.article-title {
|
||
font-family: 'PingFang SC';
|
||
font-weight: 600;
|
||
font-size: 28px;
|
||
line-height: 28px;
|
||
color: #141F38;
|
||
margin: 0 0 30px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.article-meta-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 48px;
|
||
margin-top: 30px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.meta-item {
|
||
font-family: 'PingFang SC';
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
line-height: 28px;
|
||
color: #979797;
|
||
}
|
||
|
||
.separator {
|
||
width: 100%;
|
||
height: 1px;
|
||
background: #E9E9E9;
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.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 {
|
||
font-family: 'PingFang SC';
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
line-height: 30px;
|
||
color: #334155;
|
||
|
||
// 继承富文本编辑器的样式
|
||
: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>
|