文章详情
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章详情',
|
||||
navigationStyle: 'custom',
|
||||
})
|
||||
@@ -0,0 +1,316 @@
|
||||
/* ========================================
|
||||
文章详情页
|
||||
======================================== */
|
||||
|
||||
.article-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========== 导航栏 ========== */
|
||||
.article-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.nav-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
position: relative;
|
||||
|
||||
.nav-back {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 滚动区域 ========== */
|
||||
.article-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ========== 文章内容 ========== */
|
||||
.article-content {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
display: block;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26px;
|
||||
color: #969799;
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
.meta-source {
|
||||
color: #1989fa;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.meta-time {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.meta-read {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.article-body {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.article-paragraph {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
line-height: 1.85;
|
||||
color: #323233;
|
||||
margin-bottom: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.article-images {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.article-img {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 点赞/评论/分享栏 ========== */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 28px 32px;
|
||||
margin: 0 32px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 16px;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.action-text {
|
||||
font-size: 28px;
|
||||
color: #646566;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 评论区域 ========== */
|
||||
.comment-section {
|
||||
margin-top: 32px;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.comment-title {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
font-size: 28px;
|
||||
color: #969799;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 评论列表 */
|
||||
.comment-list {
|
||||
.comment-item {
|
||||
display: flex;
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
|
||||
.comment-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.comment-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.comment-name {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 24px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
color: #323233;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.comment-like {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
.like-count {
|
||||
font-size: 24px;
|
||||
color: #969799;
|
||||
|
||||
&.liked {
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 子回复 */
|
||||
.reply-list {
|
||||
margin-top: 16px;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.reply-item {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
|
||||
& + .reply-item {
|
||||
border-top: 1px solid #ebedf0;
|
||||
}
|
||||
|
||||
.reply-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
/* ========== 底部评论输入框 ========== */
|
||||
.comment-input-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f2f3f5;
|
||||
gap: 16px;
|
||||
|
||||
.input-wrap {
|
||||
flex: 1;
|
||||
background: #f7f8fa;
|
||||
border-radius: 40px;
|
||||
padding: 12px 24px;
|
||||
|
||||
.comment-input {
|
||||
font-size: 28px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 28px;
|
||||
background: #1989fa;
|
||||
border-radius: 40px;
|
||||
|
||||
.send-text {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部安全区占位 */
|
||||
.comment-bottom-safe {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* 文章不存在 */
|
||||
.empty-article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-size: 30px;
|
||||
color: #969799;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView, Input } from '@tarojs/components'
|
||||
import { Icon } from '@antmjs/vantui'
|
||||
import { getArticleDetail } from '@/mock/articleDetail'
|
||||
import { comments } from '@/mock/comments'
|
||||
import type { Comment } from '@/types/comment'
|
||||
import './index.less'
|
||||
|
||||
/** 状态栏和导航栏高度 */
|
||||
function getStatusBarH(): number {
|
||||
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||
catch { return 20 }
|
||||
}
|
||||
|
||||
export default function ArticleDetail() {
|
||||
// useRouter 在部分 Taro 版本首次渲染 params 为空,用 getCurrentInstance 兜底
|
||||
const router = useRouter()
|
||||
const id = router.params.id || Taro.getCurrentInstance().router?.params?.id
|
||||
const article = useMemo(() => (id ? getArticleDetail(id) : null), [id])
|
||||
const [commentText, setCommentText] = useState('')
|
||||
const [commentList, setCommentList] = useState<Comment[]>(comments)
|
||||
|
||||
const statusBarH = useMemo(getStatusBarH, [])
|
||||
|
||||
/** 返回 */
|
||||
const handleBack = useCallback(() => {
|
||||
Taro.navigateBack()
|
||||
}, [])
|
||||
|
||||
/** 点赞评论 */
|
||||
const handleLikeComment = useCallback((commentId: string) => {
|
||||
setCommentList((prev) =>
|
||||
prev.map((c) => {
|
||||
if (c.id === commentId) {
|
||||
return { ...c, liked: !c.liked, likes: c.liked ? c.likes - 1 : c.likes + 1 }
|
||||
}
|
||||
if (c.replies) {
|
||||
return {
|
||||
...c,
|
||||
replies: c.replies.map((r) =>
|
||||
r.id === commentId ? { ...r, liked: !r.liked, likes: r.liked ? r.likes - 1 : r.likes + 1 } : r,
|
||||
),
|
||||
}
|
||||
}
|
||||
return c
|
||||
}),
|
||||
)
|
||||
}, [])
|
||||
|
||||
/** 发送评论 */
|
||||
const handleSendComment = useCallback(() => {
|
||||
if (!commentText.trim()) return
|
||||
const newComment: Comment = {
|
||||
id: `c${Date.now()}`,
|
||||
author: { name: '我', avatar: 'https://picsum.photos/seed/me/100/100' },
|
||||
content: commentText.trim(),
|
||||
time: '刚刚',
|
||||
likes: 0,
|
||||
}
|
||||
setCommentList((prev) => [newComment, ...prev])
|
||||
setCommentText('')
|
||||
Taro.showToast({ title: '评论成功', icon: 'success' })
|
||||
}, [commentText])
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
<View className='article-page'>
|
||||
<View className='empty-article'>文章不存在</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 正文段落
|
||||
const paragraphs = article.content.split('\n\n').filter(Boolean)
|
||||
|
||||
return (
|
||||
<View className='article-page'>
|
||||
{/* ========== 自定义导航栏 ========== */}
|
||||
<View className='article-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='nav-content'>
|
||||
<View className='nav-back' onClick={handleBack}>
|
||||
<Icon name='arrow-left' size={22} color='#323233' />
|
||||
</View>
|
||||
<Text className='nav-title'>文章详情</Text>
|
||||
<View className='nav-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 占位 */}
|
||||
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||
|
||||
<ScrollView className='article-scroll' scrollY enhanced showScrollbar={false}>
|
||||
{/* ========== 文章内容 ========== */}
|
||||
<View className='article-content'>
|
||||
{/* 标题 */}
|
||||
<Text className='article-title'>{article.title}</Text>
|
||||
|
||||
{/* Meta 信息 */}
|
||||
<View className='article-meta'>
|
||||
<Text className='meta-source'>{article.source}</Text>
|
||||
<Text className='meta-time'>{article.time}</Text>
|
||||
<Text className='meta-read'>{article.readCount} 阅读</Text>
|
||||
</View>
|
||||
|
||||
{/* 正文 */}
|
||||
<View className='article-body'>
|
||||
{paragraphs.map((p, idx) => (
|
||||
<Text key={idx} className='article-paragraph'>{p}</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 图片 */}
|
||||
{article.images && article.images.length > 0 && (
|
||||
<View className='article-images'>
|
||||
{article.images.map((img, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
className='article-img'
|
||||
src={img}
|
||||
mode='widthFix'
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* ========== 点赞/评论/分享栏 ========== */}
|
||||
<View className='action-bar'>
|
||||
<View className='action-item'>
|
||||
<Icon name='like-o' size={20} color='#646566' />
|
||||
<Text className='action-text'>点赞</Text>
|
||||
</View>
|
||||
<View className='action-item'>
|
||||
<Icon name='comment-o' size={20} color='#646566' />
|
||||
<Text className='action-text'>{commentList.length}</Text>
|
||||
</View>
|
||||
<View className='action-item'>
|
||||
<Icon name='share-o' size={20} color='#646566' />
|
||||
<Text className='action-text'>分享</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 评论区域 ========== */}
|
||||
<View className='comment-section'>
|
||||
<View className='comment-header'>
|
||||
<Text className='comment-title'>全部评论</Text>
|
||||
<Text className='comment-count'>({commentList.length})</Text>
|
||||
</View>
|
||||
|
||||
{commentList.length > 0 ? (
|
||||
<View className='comment-list'>
|
||||
{commentList.map((comment) => (
|
||||
<View key={comment.id} className='comment-item'>
|
||||
<Image
|
||||
className='comment-avatar'
|
||||
src={comment.author.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='comment-body'>
|
||||
<View className='comment-top'>
|
||||
<Text className='comment-name'>{comment.author.name}</Text>
|
||||
<Text className='comment-time'>{comment.time}</Text>
|
||||
</View>
|
||||
<Text className='comment-text'>{comment.content}</Text>
|
||||
<View className='comment-actions'>
|
||||
<View
|
||||
className='comment-like'
|
||||
onClick={() => handleLikeComment(comment.id)}
|
||||
>
|
||||
<Icon
|
||||
name={comment.liked ? 'like' : 'like-o'}
|
||||
size={16}
|
||||
color={comment.liked ? '#ee0a24' : '#969799'}
|
||||
/>
|
||||
<Text className={`like-count ${comment.liked ? 'liked' : ''}`}>
|
||||
{comment.likes || '赞'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 子回复 */}
|
||||
{comment.replies && comment.replies.length > 0 && (
|
||||
<View className='reply-list'>
|
||||
{comment.replies.map((reply) => (
|
||||
<View key={reply.id} className='reply-item'>
|
||||
<Image
|
||||
className='reply-avatar'
|
||||
src={reply.author.avatar}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='comment-body'>
|
||||
<View className='comment-top'>
|
||||
<Text className='comment-name'>{reply.author.name}</Text>
|
||||
<Text className='comment-time'>{reply.time}</Text>
|
||||
</View>
|
||||
<Text className='comment-text'>{reply.content}</Text>
|
||||
<View className='comment-actions'>
|
||||
<View
|
||||
className='comment-like'
|
||||
onClick={() => handleLikeComment(reply.id)}
|
||||
>
|
||||
<Icon
|
||||
name={reply.liked ? 'like' : 'like-o'}
|
||||
size={16}
|
||||
color={reply.liked ? '#ee0a24' : '#969799'}
|
||||
/>
|
||||
<Text className={`like-count ${reply.liked ? 'liked' : ''}`}>
|
||||
{reply.likes || '赞'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View className='no-comments'>暂无评论,快来抢沙发吧~</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部占位(防止被输入框遮挡) */}
|
||||
<View className='comment-bottom-safe' />
|
||||
</ScrollView>
|
||||
|
||||
{/* ========== 底部评论输入框(固定) ========== */}
|
||||
<View className='comment-input-bar'>
|
||||
<View className='input-wrap'>
|
||||
<Input
|
||||
className='comment-input'
|
||||
value={commentText}
|
||||
placeholder='写下你的评论...'
|
||||
placeholderStyle='color: #c8c9cc;'
|
||||
onInput={(e) => setCommentText(e.detail.value)}
|
||||
confirmType='send'
|
||||
onConfirm={handleSendComment}
|
||||
/>
|
||||
</View>
|
||||
<View className='send-btn' onClick={handleSendComment}>
|
||||
<Text className='send-text'>发送</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user