258 lines
5.9 KiB
Vue
258 lines
5.9 KiB
Vue
<template>
|
||
<full-page pageStyle="background:#fff">
|
||
<scroll-view class="article-detail" scroll-y :style="elderVarStyle">
|
||
<!-- 加载中 -->
|
||
<view v-if="loading" class="loading-container">
|
||
<uni-load-more status="loading" />
|
||
</view>
|
||
|
||
<!-- 文章内容 -->
|
||
<view v-else-if="article" class="article-container">
|
||
<view class="article-header">
|
||
<view class="article-title">{{ article.title }}</view>
|
||
<view class="article-meta">
|
||
<text class="article-time">{{ formatTime(article.createTime) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 文章封面 -->
|
||
<!-- <image
|
||
v-if="article.cover"
|
||
class="article-cover"
|
||
:src="article.cover"
|
||
mode="widthFix"
|
||
/> -->
|
||
|
||
<!-- 文章摘要 -->
|
||
<!-- <view v-if="article.summary" class="article-summary">
|
||
{{ article.summary }}
|
||
</view> -->
|
||
|
||
<!-- 文章正文 -->
|
||
<view class="article-body">
|
||
<rich-text :nodes="article.content" />
|
||
</view>
|
||
|
||
<!-- 关键词标签 -->
|
||
<view v-if="article.keyword" class="article-keywords">
|
||
<view class="keyword-label">关键词:</view>
|
||
<view class="keyword-list">
|
||
<view
|
||
v-for="(keyword, index) in keywordList"
|
||
:key="index"
|
||
class="keyword-tag"
|
||
>
|
||
{{ keyword }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<empty v-else text="文章不存在或已删除" />
|
||
</scroll-view>
|
||
</full-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from "vue";
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
import fullPage from "@/components/full-page.vue";
|
||
import empty from "@/components/empty.vue";
|
||
import useElder from "@/hooks/useElder";
|
||
import useUser from "@/hooks/useUser";
|
||
import { getArticle, addArticleReadRecord } from "@/utils/api";
|
||
|
||
const { elderVarStyle } = useElder();
|
||
const { logined, userInfo } = useUser();
|
||
|
||
const loading = ref(true);
|
||
const article = ref(null);
|
||
const articleId = ref("");
|
||
|
||
// 关键词列表
|
||
const keywordList = computed(() => {
|
||
if (!article.value?.keyword) return [];
|
||
return article.value.keyword.split(/[,,、\s]+/).filter(Boolean);
|
||
});
|
||
|
||
onLoad((options) => {
|
||
if (options.id) {
|
||
articleId.value = options.id;
|
||
fetchArticle();
|
||
} else {
|
||
loading.value = false;
|
||
}
|
||
});
|
||
|
||
async function fetchArticle() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getArticle(articleId.value);
|
||
if (res.success && res.data) {
|
||
article.value = res.data;
|
||
// 处理富文本中的图片样式
|
||
if (article.value.content) {
|
||
article.value.content = article.value.content.replace(/<img/gi, '<img style="max-width:100%;height:auto;display:block;"');
|
||
}
|
||
// 设置页面标题
|
||
uni.setNavigationBarTitle({
|
||
title: res.data.title || "文章详情",
|
||
});
|
||
|
||
// 阅读打点(不阻塞渲染)
|
||
recordArticleRead();
|
||
}
|
||
} catch (e) {
|
||
console.error("获取文章详情失败", e);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function getArticleVisitorId() {
|
||
let visitorId = uni.getStorageSync("articleVisitorId");
|
||
if (!visitorId) {
|
||
visitorId = `mp_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
||
uni.setStorageSync("articleVisitorId", visitorId);
|
||
}
|
||
return visitorId;
|
||
}
|
||
|
||
async function recordArticleRead() {
|
||
try {
|
||
if (!articleId.value) return;
|
||
const accountId = logined.value && userInfo.value && userInfo.value.userId ? String(userInfo.value.userId) : "";
|
||
await addArticleReadRecord(
|
||
accountId
|
||
? { articleId: articleId.value, accountId }
|
||
: { articleId: articleId.value, unionid: getArticleVisitorId() }
|
||
);
|
||
} catch (e) {
|
||
// 统计失败不影响用户阅读
|
||
console.warn("文章阅读打点失败", e);
|
||
}
|
||
}
|
||
|
||
// 格式化时间
|
||
function formatTime(timestamp) {
|
||
if (!timestamp) return "";
|
||
const date = new Date(timestamp);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
const hour = String(date.getHours()).padStart(2, "0");
|
||
const minute = String(date.getMinutes()).padStart(2, "0");
|
||
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.article-detail {
|
||
height: 100%;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 400rpx;
|
||
}
|
||
|
||
.article-container {
|
||
padding: 30rpx;
|
||
}
|
||
|
||
.article-header {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.article-title {
|
||
font-size: 40rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.article-meta {
|
||
margin-top: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.article-time {
|
||
font-size: var(--text-sm);
|
||
color: #999;
|
||
}
|
||
|
||
.article-cover {
|
||
width: 100%;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.article-summary {
|
||
padding: 24rpx;
|
||
margin-bottom: 30rpx;
|
||
background: #f8f9fa;
|
||
border-radius: 12rpx;
|
||
font-size: var(--text-sm);
|
||
color: #666;
|
||
line-height: 1.8;
|
||
border-left: 6rpx solid $theme-brown-primary;
|
||
}
|
||
|
||
.article-body {
|
||
font-size: var(--text-base);
|
||
color: #333;
|
||
line-height: 1.8;
|
||
|
||
:deep(img) {
|
||
max-width: 100%;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
:deep(p) {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
:deep(h1),
|
||
:deep(h2),
|
||
:deep(h3),
|
||
:deep(h4) {
|
||
font-weight: 600;
|
||
margin: 30rpx 0 20rpx;
|
||
}
|
||
}
|
||
|
||
.article-keywords {
|
||
margin-top: 40rpx;
|
||
padding-top: 30rpx;
|
||
border-top: 1px solid #eee;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.keyword-label {
|
||
flex-shrink: 0;
|
||
font-size: var(--text-sm);
|
||
color: #999;
|
||
margin-right: 16rpx;
|
||
line-height: 48rpx;
|
||
}
|
||
|
||
.keyword-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.keyword-tag {
|
||
padding: 8rpx 20rpx;
|
||
background: $theme-brown-light;
|
||
color: $theme-brown-primary;
|
||
font-size: var(--text-xs);
|
||
border-radius: 20rpx;
|
||
}
|
||
</style>
|