Files
guoxue-mp/pages/index/detail.vue
T
2026-03-25 15:59:24 +08:00

646 lines
15 KiB
Vue

<template>
<view class="detail-page" :style="{paddingTop: paddingTop}">
<view class="detail-bg"></view>
<view class="detail-header">
<view class="header-left" @click="goBack">
<u-icon name="arrow-left" size="32" color="#1b1b1b"></u-icon>
</view>
<text class="header-title">详情</text>
<!-- <view class="header-right">-->
<!-- <u-icon name="more-dot-fill" size="26" color="#1b1b1b"></u-icon>-->
<!-- <u-icon name="calendar-fill" size="26" color="#1b1b1b" class="header-icon-gap"></u-icon>-->
<!-- </view>-->
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="detail-info">
<view class="detail-label">{{ detailInfo.category }}</view>
<view class="detail-title">{{ detailInfo.title }}</view>
<view class="detail-subtitle">{{ detailInfo.source }}</view>
<view class="detail-author">
<!-- <view class="author-meta">-->
<!-- <text class="author-name">{{ detailInfo.author }}</text>-->
<!-- <text class="author-desc">{{ detailInfo.authorDesc }}</text>-->
<!-- </view>-->
<text class="detail-date">{{ dayjs(detailInfo.createTime).format('YYYY-MM-DD') }}</text>
</view>
</view>
<!-- <image class="detail-cover" :src="detailInfo.image" mode="aspectFill"></image>-->
<view class="detail-article">
<u-parse :content="detailInfo.content"/>
</view>
<u-gap height="200rpx"/>
</scroll-view>
<view class="detail-action">
<view class="flex-1 flex justify-center items-center">
<view class="action-item" @click="openCommentPopup" v-if="!authing">
<u-icon name="chat" size="28" color="#1b1b1b"></u-icon>
<text class="action-text">评论</text>
</view>
<view class="action-item" @click="toggleFavorite">
<u-icon :name="isFavorite ? 'star-fill' : 'star'" size="28" color="#f7b500"></u-icon>
<text class="action-text">收藏</text>
</view>
</view>
<button open-type="share" class="action-button" @click="handleShare">
<u-icon name="share" size="28" color="#fff"></u-icon>
<text class="action-btn-text">分享</text>
</button>
</view>
<u-popup
:show="showCommentPopup"
mode="bottom"
round="20"
close-on-click-overlay
:safe-area-inset-bottom="false"
@close="showCommentPopup = false"
>
<view class="comment-popup">
<view class="comment-popup-header">
<text class="comment-popup-title">评论 ({{ commentList.length }})</text>
<u-icon name="close" size="28" color="#9ca3af" @click="showCommentPopup = false"></u-icon>
</view>
<scroll-view scroll-y class="comment-list">
<view v-if="commentLoading" class="comment-placeholder">评论加载中...</view>
<view v-else-if="!commentList.length" class="comment-placeholder">暂时还没有评论</view>
<view v-else>
<view
class="comment-item"
v-for="(item, index) in commentList"
:key="item.commentId || item.id || index"
>
<view class="comment-item-header">
<text class="comment-user">{{ item.userName || item.nickname || '匿名用户' }}</text>
<text class="comment-time">{{ formatCommentTime(item.createTime || item.createdAt) }}</text>
</view>
<view class="comment-content">{{ item.content }}</view>
</view>
</view>
</scroll-view>
<view class="comment-footer">
<view class="comment-publish-btn" @click="openPublishPopup">发表评论</view>
</view>
</view>
</u-popup>
<u-popup
:show="showPublishPopup"
mode="center"
round="20"
close-on-click-overlay
:safe-area-inset-bottom="false"
@close="closePublishPopup"
>
<view class="publish-popup">
<view class="publish-header">写评论</view>
<textarea
class="publish-textarea"
v-model="newCommentContent"
maxlength="200"
auto-height
placeholder="请输入评论内容"
placeholder-style="color:#9ca3af"
></textarea>
<view class="publish-actions">
<view class="publish-cancel" @click="closePublishPopup">取消</view>
<view
class="publish-submit"
:class="{'is-disabled': commentSubmitting}"
@click="submitComment"
>
发表
</view>
</view>
</view>
</u-popup>
<Login ref="loginPopup" @done="onLoginDone"/>
</view>
</template>
<script>
import {
addArticleCollectReq,
articleInfoReq,
articleCommentListReq,
addArticleCommentReq, dictDataReq
} from "@/api/common";
import {dayjs} from '@/uni_modules/iRainna-dayjs/js_sdk/dayjs.min.js'
import Login from "@/components/Login.vue";
import {getUserInfo} from "@/util/user";
export default {
components: {Login},
data() {
return {
detailInfo: {
category: '',
title: '',
subtitle: '',
author: '',
authorDesc: '',
date: '',
image: '',
content: []
},
isFavorite: false,
articleImages: [
'/static/icon/article_1.png',
'/static/icon/article_2.png',
'/static/icon/article_3.png',
'/static/icon/article_4.png',
'/static/icon/article_5.png',
'/static/icon/article_6.png'
],
mockArticles: [
{
id: 5,
category: '',
title: '',
subtitle: '',
author: '',
authorDesc: '8年情感心理师 · 自由写作者',
date: '',
image: '',
content: ''
}
],
paddingTop: '40px',
showCommentPopup: false,
commentList: [],
commentLoading: false,
showPublishPopup: false,
newCommentContent: '',
commentSubmitting: false,
authing: true,
}
},
onLoad(options) {
const {id} = options || {}
this.getInfo(id)
this.paddingTop = uni.getSystemInfoSync().statusBarHeight + 'px'
},
onShow(){
this.getDictList()
},
methods: {
dayjs,
async getDictList() {
const {data} = await dictDataReq({dictId: '171'})
this.authing = parseInt(data[0].dictDataCode) === 1
},
async getInfo(id) {
const {data} = await articleInfoReq(id)
this.detailInfo = data
this.isFavorite = data.hasCollect
},
goBack() {
if (getCurrentPages().length === 1) uni.switchTab({url: '/pages/index/index'})
else uni.navigateBack()
},
toggleFavorite() {
this.isFavorite = !this.isFavorite
uni.showToast({
title: this.isFavorite ? '已收藏' : '已取消',
icon: 'none'
})
addArticleCollectReq({
articleId: this.detailInfo.articleId
})
},
handleShare() {
uni.showToast({
title: '已复制分享链接',
icon: 'none'
})
},
openCommentPopup() {
if (!this.detailInfo.articleId) return
this.showCommentPopup = true
this.fetchComments()
},
async fetchComments() {
if (!this.detailInfo.articleId) return
this.commentLoading = true
try {
const {data} = await articleCommentListReq({articleId: this.detailInfo.articleId})
this.commentList = this.normalizeCommentList(data)
} catch (error) {
console.log(error)
uni.showToast({
title: '评论加载失败',
icon: 'none'
})
} finally {
this.commentLoading = false
}
},
normalizeCommentList(data) {
if (Array.isArray(data)) return data
if (data && Array.isArray(data.records)) return data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
return []
},
openPublishPopup() {
if (!this.ensureLogin()) return
this.showPublishPopup = true
this.newCommentContent = ''
},
closePublishPopup() {
this.showPublishPopup = false
},
async submitComment() {
if (this.commentSubmitting) return
if (!this.ensureLogin()) return
const content = (this.newCommentContent || '').trim()
if (!content) {
return uni.showToast({
title: '请输入评论内容',
icon: 'none'
})
}
this.commentSubmitting = true
try {
await addArticleCommentReq({
content,
articleId: this.detailInfo.articleId
})
uni.showToast({
title: '发表成功',
icon: 'success'
})
this.newCommentContent = ''
this.showPublishPopup = false
this.fetchComments()
} catch (error) {
console.log(error)
const message = error && (error.msg || error.message)
uni.showToast({
title: message || '发表评论失败',
icon: 'none'
})
} finally {
this.commentSubmitting = false
}
},
ensureLogin() {
const {token} = getUserInfo()
if (!token) {
this.$refs.loginPopup && this.$refs.loginPopup.open()
return false
}
return true
},
onLoginDone() {
if (this.showCommentPopup) {
this.fetchComments()
}
},
formatCommentTime(time) {
if (!time) return '--'
const parsed = dayjs(time)
return typeof parsed.isValid === 'function' && parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : time
}
},
onShareTimeline() {
return {
title: this.detailInfo.title,
path: '/pages/index/detail?id=' + this.detailInfo.articleId
}
},
onShareAppMessage() {
return {
title: this.detailInfo.title,
path: '/pages/index/detail?id=' + this.detailInfo.articleId
}
},
}
</script>
<style scoped lang="scss">
.detail-page {
position: relative;
min-height: 100vh;
background: linear-gradient(180deg, #e5edff 0%, #f6f4ff 45%, #fef7ff 100%);
//padding: 60rpx 32rpx 140rpx;
box-sizing: border-box;
}
.detail-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 300rpx;
background: rgba(255, 255, 255, 0.72);
filter: blur(35rpx);
z-index: 0;
}
.detail-header {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 30rpx;
}
.header-left,
.header-right {
display: flex;
align-items: center;
}
.header-title {
font-size: 34rpx;
font-weight: 600;
color: #1b1b1b;
}
.header-icon-gap {
margin-left: 18rpx;
}
.detail-scroll {
position: relative;
z-index: 2;
max-height: calc(100vh - 260rpx);
padding-bottom: 220rpx;
}
.detail-info {
padding: 20rpx 16rpx;
}
.detail-label {
display: inline-flex;
padding: 8rpx 22rpx;
border-radius: 40rpx;
font-size: 24rpx;
color: #4a5568;
background: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.detail-title {
font-size: 40rpx;
font-weight: 600;
color: #16171d;
line-height: 1.4;
}
.detail-subtitle {
font-size: 26rpx;
color: #8b8f99;
margin-top: 16rpx;
line-height: 1.5;
}
.detail-author {
margin-top: 22rpx;
display: flex;
justify-content: space-between;
align-items: flex-end;
border-bottom: 1px solid rgba(255, 255, 255, 0.6);
padding-bottom: 20rpx;
}
.author-meta {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.author-name {
font-size: 28rpx;
color: #2c2c2c;
font-weight: 600;
}
.author-desc {
font-size: 24rpx;
color: #a0a3b0;
}
.detail-date {
font-size: 24rpx;
color: #babec6;
}
.detail-cover {
//width: 100%;
height: 360rpx;
border-radius: 30rpx;
margin: 30rpx;
box-shadow: 0 20rpx 40rpx rgba(128, 139, 255, 0.2);
}
.detail-article {
margin: 30rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 32rpx;
padding: 30rpx;
box-shadow: 0 20rpx 40rpx rgba(128, 139, 255, 0.15);
}
.detail-paragraph {
font-size: 26rpx;
color: #555c6b;
line-height: 1.8;
margin-bottom: 26rpx;
}
.detail-paragraph:last-child {
margin-bottom: 0;
}
.detail-action {
z-index: 999;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 26rpx 40rpx calc(26rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-top: 1px solid rgba(148, 163, 184, 0.2);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -12rpx 40rpx rgba(99, 102, 241, 0.12);
}
.action-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #1b1b1b;
font-size: 24rpx;
}
.action-text {
color: #1f2a37;
}
.action-button {
flex: 1;
margin-left: 40rpx;
height: 90rpx;
border-radius: 50rpx;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 500;
}
.action-btn-text {
color: #fff;
}
.comment-popup {
padding: 30rpx;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.comment-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.comment-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2933;
}
.comment-list {
flex: 1;
max-height: 60vh;
}
.comment-item {
padding: 20rpx 0;
border-bottom: 1px solid #eef2ff;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-item-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.comment-user {
font-size: 28rpx;
color: #111827;
font-weight: 600;
}
.comment-time {
font-size: 22rpx;
color: #9ca3af;
}
.comment-content {
margin-top: 12rpx;
font-size: 26rpx;
line-height: 1.6;
color: #4b5563;
white-space: pre-wrap;
word-break: break-all;
}
.comment-placeholder {
text-align: center;
color: #9ca3af;
padding: 80rpx 0;
font-size: 26rpx;
}
.comment-footer {
padding-top: 24rpx;
}
.comment-publish-btn {
width: 100%;
text-align: center;
padding: 22rpx 0;
border-radius: 50rpx;
color: #fff;
font-size: 28rpx;
font-weight: 500;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
}
.publish-popup {
width: 640rpx;
padding: 40rpx 32rpx 32rpx;
box-sizing: border-box;
}
.publish-header {
font-size: 32rpx;
font-weight: 600;
color: #111827;
margin-bottom: 24rpx;
}
.publish-textarea {
width: 100%;
min-height: 200rpx;
background: #f3f4f6;
border-radius: 24rpx;
padding: 24rpx;
font-size: 26rpx;
color: #1f2937;
box-sizing: border-box;
}
.publish-actions {
display: flex;
justify-content: flex-end;
gap: 24rpx;
margin-top: 24rpx;
}
.publish-cancel {
padding: 16rpx 32rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #6b7280;
background: #f5f5f5;
}
.publish-submit {
padding: 16rpx 40rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #fff;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
}
.publish-submit.is-disabled {
opacity: 0.6;
}
</style>