视图路径修改
This commit is contained in:
889
schoolNewsWeb/src/views/public/article/ArticleShowView.vue
Normal file
889
schoolNewsWeb/src/views/public/article/ArticleShowView.vue
Normal file
@@ -0,0 +1,889 @@
|
||||
<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 v-if="loading" class="loading-state">
|
||||
<el-skeleton :rows="8" animated />
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentArticleData">
|
||||
<!-- 文章头部信息 -->
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ currentArticleData.title }}</h1>
|
||||
<div class="article-meta-info">
|
||||
<div class="meta-item" v-if="currentArticleData.publishTime || currentArticleData.createTime">
|
||||
发布时间:{{ formatDateSimple(currentArticleData.publishTime || currentArticleData.createTime || '') }}
|
||||
</div>
|
||||
<div class="meta-item" v-if="currentArticleData.viewCount !== undefined">
|
||||
浏览次数:{{ currentArticleData.viewCount }}
|
||||
</div>
|
||||
<div class="meta-item" v-if="currentArticleData.source">
|
||||
来源:{{ currentArticleData.source }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<div class="article-content ql-editor" v-html="currentArticleData.content"></div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="加载文章失败" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button v-if="showEditButton" type="primary" @click="handleEdit">编辑</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 路由页面模式 -->
|
||||
<div v-else class="article-page-view">
|
||||
<!-- 返回按钮 -->
|
||||
<div v-if="showBackButton" class="back-header">
|
||||
<el-button @click="handleBack" :icon="ArrowLeft">{{ backButtonText }}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</div>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<div v-else-if="currentArticleData" class="article-wrapper">
|
||||
<div class="article-show-container">
|
||||
<!-- 文章头部信息 -->
|
||||
<div class="article-header">
|
||||
<h1 class="article-title">{{ currentArticleData.title }}</h1>
|
||||
<div class="article-meta-info">
|
||||
<div class="meta-item" v-if="currentArticleData.publishTime || currentArticleData.createTime">
|
||||
发布时间:{{ formatDateSimple(currentArticleData.publishTime || currentArticleData.createTime || '') }}
|
||||
</div>
|
||||
<div class="meta-item" v-if="currentArticleData.viewCount !== undefined">
|
||||
浏览次数:{{ currentArticleData.viewCount }}
|
||||
</div>
|
||||
<div class="meta-item" v-if="currentArticleData.source">
|
||||
来源:{{ currentArticleData.source }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<div class="article-content ql-editor" v-html="currentArticleData.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载失败 -->
|
||||
<div v-else class="error-container">
|
||||
<el-empty description="加载文章失败" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { learningRecordApi, learningHistoryApi } from '@/apis/study';
|
||||
import { useStore } from 'vuex';
|
||||
import type { ResourceCategory, Resource, ResourceVO, LearningRecord, TbLearningHistory } from '@/types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ArticleShowView'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
modelValue?: boolean; // Dialog 模式下的显示状态
|
||||
asDialog?: boolean; // 是否作为 Dialog 使用
|
||||
title?: string; // Dialog 标题
|
||||
width?: string; // Dialog 宽度
|
||||
articleData?: Resource; // 文章数据(Dialog 模式使用)
|
||||
resourceID?: string; // 资源ID(路由模式使用)
|
||||
categoryList?: Array<ResourceCategory>; // 分类列表
|
||||
showEditButton?: boolean; // 是否显示编辑按钮
|
||||
showBackButton?: boolean; // 是否显示返回按钮(路由模式)
|
||||
backButtonText?: string; // 返回按钮文本
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
asDialog: false, // 默认作为路由页面使用
|
||||
title: '文章预览',
|
||||
width: '900px',
|
||||
articleData: () => ({}),
|
||||
categoryList: () => [],
|
||||
showEditButton: false,
|
||||
showBackButton: true,
|
||||
backButtonText: '返回'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
'close': [];
|
||||
'edit': [];
|
||||
'back': [];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const loadedArticleData = ref<Resource | null>(null);
|
||||
|
||||
// 学习记录相关
|
||||
const learningRecord = ref<LearningRecord | null>(null);
|
||||
const learningStartTime = ref(0);
|
||||
const learningTimer = ref<number | null>(null);
|
||||
const hasVideoCompleted = ref(false);
|
||||
const totalVideos = ref(0); // 视频总数
|
||||
const completedVideos = ref<Set<number>>(new Set()); // 已完成的视频索引
|
||||
const userInfo = computed(() => store.getters['auth/user']);
|
||||
|
||||
// 学习历史记录相关
|
||||
const learningHistory = ref<TbLearningHistory | null>(null);
|
||||
const historyStartTime = ref(0);
|
||||
const historyTimer = ref<number | null>(null);
|
||||
|
||||
// 当前显示的文章数据
|
||||
const currentArticleData = computed(() => {
|
||||
// Dialog 模式使用传入的 articleData
|
||||
if (props.asDialog) {
|
||||
return props.articleData;
|
||||
}
|
||||
// 路由模式:优先使用传入的 articleData,否则使用加载的数据
|
||||
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
||||
return props.articleData;
|
||||
}
|
||||
return loadedArticleData.value;
|
||||
});
|
||||
|
||||
// Dialog 显示状态
|
||||
const visible = computed({
|
||||
get: () => props.asDialog ? props.modelValue : false,
|
||||
set: (val) => props.asDialog ? emit('update:modelValue', val) : undefined
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 路由模式下,从路由参数加载文章
|
||||
if (!props.asDialog) {
|
||||
const articleId = route.query.articleId as string;
|
||||
const taskId = route.query.taskId as string;
|
||||
|
||||
// 如果传入了 articleData,则不需要从路由加载
|
||||
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
||||
loadedArticleData.value = props.articleData;
|
||||
|
||||
const resourceID = props.articleData.resourceID;
|
||||
if (resourceID) {
|
||||
// 创建学习历史记录(每次进入都创建新记录)
|
||||
createHistoryRecord(resourceID);
|
||||
|
||||
// 如果有taskId,还要创建学习记录
|
||||
if (taskId || route.query.taskId) {
|
||||
loadLearningRecord(resourceID);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化视频监听
|
||||
nextTick().then(() => {
|
||||
setTimeout(() => {
|
||||
initVideoListeners();
|
||||
}, 300);
|
||||
});
|
||||
} else if (articleId) {
|
||||
// 从路由参数加载
|
||||
loadArticle(articleId);
|
||||
// 如果有 taskId,表示是任务学习,需要创建/加载学习记录
|
||||
if (taskId) {
|
||||
loadLearningRecord(articleId);
|
||||
}
|
||||
} else {
|
||||
// 既没有传入数据,也没有路由参数,显示错误
|
||||
ElMessage.error('文章ID不存在');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 组件销毁前保存学习进度
|
||||
if (learningRecord.value && !learningRecord.value.isComplete) {
|
||||
saveLearningProgress();
|
||||
}
|
||||
stopLearningTimer();
|
||||
|
||||
// 保存学习历史记录
|
||||
if (learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
}
|
||||
stopHistoryTimer();
|
||||
});
|
||||
|
||||
// 监听 articleData 变化(用于 ResourceArticle 切换文章)
|
||||
watch(() => props.articleData, async (newData, oldData) => {
|
||||
if (!props.asDialog) {
|
||||
// 如果从有数据变成null,或者切换到新文章,都需要保存当前历史
|
||||
if (learningHistory.value && (oldData && (!newData || oldData.resourceID !== newData.resourceID))) {
|
||||
await saveHistoryRecord();
|
||||
stopHistoryTimer();
|
||||
}
|
||||
|
||||
// 加载新文章数据
|
||||
if (newData && Object.keys(newData).length > 0) {
|
||||
loadedArticleData.value = newData;
|
||||
|
||||
// 为新文章创建学习历史记录
|
||||
const resourceID = newData.resourceID;
|
||||
if (resourceID) {
|
||||
await createHistoryRecord(resourceID);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化视频监听
|
||||
nextTick().then(() => {
|
||||
setTimeout(() => {
|
||||
initVideoListeners();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 加载文章数据
|
||||
async function loadArticle(resourceID: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await resourceApi.getResourceById(resourceID);
|
||||
if (res.success && res.data) {
|
||||
// ResourceVO 包含 resource 对象
|
||||
const resourceVO = res.data as ResourceVO;
|
||||
loadedArticleData.value = resourceVO.resource || res.data as Resource;
|
||||
|
||||
// 增加浏览次数
|
||||
await resourceApi.incrementViewCount(resourceID);
|
||||
|
||||
// 创建学习历史记录(每次进入都创建新记录)
|
||||
await createHistoryRecord(resourceID);
|
||||
|
||||
// 等待 DOM 更新后监听视频(增加延迟确保 DOM 完全渲染)
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
initVideoListeners();
|
||||
}, 300); // 延迟 300ms 确保 DOM 完全渲染
|
||||
} else {
|
||||
ElMessage.error(res.message || '加载文章失败');
|
||||
loadedArticleData.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载文章失败:', error);
|
||||
ElMessage.error('加载文章失败');
|
||||
loadedArticleData.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载学习记录
|
||||
async function loadLearningRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const res = await learningRecordApi.getRecordList({
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 1, // 资源类型:文章
|
||||
resourceID: resourceID
|
||||
});
|
||||
|
||||
if (res.success && res.dataList && res.dataList.length > 0) {
|
||||
learningRecord.value = res.dataList[0];
|
||||
|
||||
// 如果已完成,不需要启动计时器
|
||||
if (learningRecord.value.isComplete) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 没有记录,创建新的
|
||||
await createLearningRecord(resourceID);
|
||||
}
|
||||
|
||||
// 开始学习计时
|
||||
startLearningTimer();
|
||||
} catch (error) {
|
||||
console.error('加载学习记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建学习记录
|
||||
async function createLearningRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const taskId = route.query.taskId as string;
|
||||
const res = await learningRecordApi.createRecord({
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 1, // 资源类型:文章
|
||||
resourceID: resourceID,
|
||||
taskID: taskId || undefined,
|
||||
duration: 0,
|
||||
progress: 0,
|
||||
isComplete: false
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningRecord.value = res.data;
|
||||
ElMessage.success('开始学习');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建学习记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始学习计时
|
||||
function startLearningTimer() {
|
||||
learningStartTime.value = Date.now();
|
||||
|
||||
// 每10秒保存一次学习进度
|
||||
learningTimer.value = window.setInterval(() => {
|
||||
// 如果文章已完成,停止定时器
|
||||
if (learningRecord.value?.isComplete) {
|
||||
stopLearningTimer();
|
||||
return;
|
||||
}
|
||||
saveLearningProgress();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// 停止学习计时
|
||||
function stopLearningTimer() {
|
||||
if (learningTimer.value) {
|
||||
clearInterval(learningTimer.value);
|
||||
learningTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存学习进度
|
||||
async function saveLearningProgress() {
|
||||
if (!userInfo.value?.id || !learningRecord.value) return;
|
||||
|
||||
// 如果文章已完成,不再保存进度
|
||||
if (learningRecord.value.isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const duration = Math.floor((currentTime - learningStartTime.value) / 1000);
|
||||
|
||||
try {
|
||||
const updatedRecord = {
|
||||
id: learningRecord.value.id,
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 1,
|
||||
resourceID: route.query.articleId as string,
|
||||
duration: (learningRecord.value.duration || 0) + duration,
|
||||
progress: hasVideoCompleted.value ? 100 : 50, // 如果视频播放完成,进度100%
|
||||
isComplete: hasVideoCompleted.value
|
||||
};
|
||||
|
||||
await learningRecordApi.updateRecord(updatedRecord);
|
||||
|
||||
// 更新本地记录
|
||||
learningRecord.value.duration = updatedRecord.duration;
|
||||
learningRecord.value.progress = updatedRecord.progress;
|
||||
learningRecord.value.isComplete = updatedRecord.isComplete;
|
||||
|
||||
// 重置开始时间
|
||||
learningStartTime.value = currentTime;
|
||||
|
||||
// 如果已完成,标记完成
|
||||
if (hasVideoCompleted.value) {
|
||||
await markArticleComplete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存学习进度失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 标记文章完成
|
||||
async function markArticleComplete() {
|
||||
if (!userInfo.value?.id || !learningRecord.value) return;
|
||||
|
||||
try {
|
||||
await learningRecordApi.markComplete({
|
||||
id: learningRecord.value.id,
|
||||
taskID: route.query.taskId as string,
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 1,
|
||||
resourceID: route.query.articleId as string,
|
||||
isComplete: true
|
||||
});
|
||||
|
||||
ElMessage.success('文章学习完成!');
|
||||
stopLearningTimer();
|
||||
} catch (error) {
|
||||
console.error('标记完成失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化视频监听器
|
||||
function initVideoListeners() {
|
||||
|
||||
// 尝试多种选择器查找文章内容区域
|
||||
const selectors = [
|
||||
'.article-show-container .article-content.ql-editor',
|
||||
'.article-wrapper .article-content.ql-editor',
|
||||
'.article-content.ql-editor',
|
||||
'.article-show-container .article-content',
|
||||
'.article-wrapper .article-content',
|
||||
'.article-content'
|
||||
];
|
||||
|
||||
let articleContent: Element | null = null;
|
||||
|
||||
for (const selector of selectors) {
|
||||
articleContent = document.querySelector(selector);
|
||||
if (articleContent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!articleContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videos = articleContent.querySelectorAll('video');
|
||||
|
||||
if (videos.length === 0) {
|
||||
// 没有视频,默认阅读即完成
|
||||
hasVideoCompleted.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化视频数量和完成状态
|
||||
totalVideos.value = videos.length;
|
||||
completedVideos.value.clear();
|
||||
|
||||
// 监听所有视频的播放结束事件
|
||||
videos.forEach((video, index) => {
|
||||
const videoElement = video as HTMLVideoElement;
|
||||
|
||||
// 移除旧的监听器
|
||||
videoElement.removeEventListener('ended', () => handleVideoEnded(index));
|
||||
// 添加新的监听器,传递视频索引
|
||||
videoElement.addEventListener('ended', () => handleVideoEnded(index));
|
||||
});
|
||||
}
|
||||
|
||||
// 处理视频播放结束
|
||||
function handleVideoEnded(videoIndex: number) {
|
||||
// 标记该视频已完成
|
||||
completedVideos.value.add(videoIndex);
|
||||
|
||||
const completedCount = completedVideos.value.size;
|
||||
console.log(`✅ 视频 ${videoIndex + 1} 播放完成 (${completedCount}/${totalVideos.value})`);
|
||||
|
||||
// 检查是否所有视频都已完成
|
||||
if (completedCount >= totalVideos.value) {
|
||||
if (!hasVideoCompleted.value) {
|
||||
hasVideoCompleted.value = true;
|
||||
ElMessage.success(`所有视频播放完成 (${totalVideos.value}/${totalVideos.value})`);
|
||||
// 立即保存学习进度并标记完成
|
||||
saveLearningProgress();
|
||||
}
|
||||
} else {
|
||||
ElMessage.info(`视频 ${videoIndex + 1} 播放完成 (${completedCount}/${totalVideos.value})`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 学习历史记录功能 ====================
|
||||
|
||||
// 创建学习历史记录
|
||||
async function createHistoryRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const res = await learningHistoryApi.recordResourceView(
|
||||
userInfo.value.id,
|
||||
resourceID,
|
||||
0 // 初始时长为0
|
||||
);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log('✅ 学习历史记录创建成功:', learningHistory.value);
|
||||
|
||||
// 开始计时
|
||||
startHistoryTimer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 创建学习历史记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始学习历史计时
|
||||
function startHistoryTimer() {
|
||||
historyStartTime.value = Date.now();
|
||||
|
||||
// 每30秒保存一次学习历史
|
||||
historyTimer.value = window.setInterval(() => {
|
||||
saveHistoryRecord();
|
||||
}, 30000); // 30秒
|
||||
}
|
||||
|
||||
// 停止学习历史计时
|
||||
function stopHistoryTimer() {
|
||||
if (historyTimer.value) {
|
||||
clearInterval(historyTimer.value);
|
||||
historyTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存学习历史记录
|
||||
async function saveHistoryRecord() {
|
||||
if (!userInfo.value?.id || !learningHistory.value) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const duration = Math.floor((currentTime - historyStartTime.value) / 1000); // 秒
|
||||
|
||||
// 如果时长太短(小于1秒),不保存
|
||||
if (duration < 1) return;
|
||||
|
||||
try {
|
||||
const updatedHistory: TbLearningHistory = {
|
||||
...learningHistory.value,
|
||||
duration: (learningHistory.value.duration || 0) + duration,
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 调用API更新学习历史
|
||||
const res = await learningHistoryApi.recordLearningHistory(updatedHistory);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log(`💾 学习历史已保存 - 累计时长: ${learningHistory.value.duration}秒`);
|
||||
}
|
||||
|
||||
// 重置开始时间
|
||||
historyStartTime.value = currentTime;
|
||||
} catch (error) {
|
||||
console.error('❌ 保存学习历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期(简单格式: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() {
|
||||
// 非Dialog模式下关闭时保存学习历史
|
||||
if (!props.asDialog && learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
stopHistoryTimer();
|
||||
}
|
||||
|
||||
if (props.asDialog) {
|
||||
visible.value = false;
|
||||
}
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// 编辑处理
|
||||
function handleEdit() {
|
||||
emit('edit');
|
||||
}
|
||||
|
||||
// 返回处理
|
||||
function handleBack() {
|
||||
// 返回前保存学习进度
|
||||
if (learningRecord.value && !learningRecord.value.isComplete) {
|
||||
saveLearningProgress();
|
||||
}
|
||||
stopLearningTimer();
|
||||
|
||||
const taskId = route.query.taskId as string;
|
||||
// 如果有 taskId,返回任务详情
|
||||
if (taskId) {
|
||||
router.push({
|
||||
path: '/study-plan/task-detail',
|
||||
query: { taskId }
|
||||
});
|
||||
} else {
|
||||
emit('back');
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
open: () => {
|
||||
if (props.asDialog) {
|
||||
visible.value = true;
|
||||
}
|
||||
},
|
||||
close: handleClose
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 路由页面模式样式
|
||||
.article-page-view {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.back-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.article-wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
padding: 80px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
// Dialog 和路由模式共用的文章内容样式
|
||||
.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>
|
||||
Reference in New Issue
Block a user