hn-hlw-app/pages/article/article-detail.vue

258 lines
5.9 KiB
Vue
Raw Permalink Normal View History

2026-07-27 11:26:39 +08:00
<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>