From 66401507bf4de3af6b57f328bc0f183aedabbd3e Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Wed, 1 Jul 2026 22:33:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E7=AB=A0=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.config.ts | 1 + src/mock/articleDetail.ts | 37 ++++ src/mock/comments.ts | 57 ++++++ src/pages/article/index.config.ts | 4 + src/pages/article/index.less | 316 ++++++++++++++++++++++++++++++ src/pages/article/index.tsx | 250 +++++++++++++++++++++++ src/types/comment.ts | 14 ++ 7 files changed, 679 insertions(+) create mode 100644 src/mock/articleDetail.ts create mode 100644 src/mock/comments.ts create mode 100644 src/pages/article/index.config.ts create mode 100644 src/pages/article/index.less create mode 100644 src/pages/article/index.tsx create mode 100644 src/types/comment.ts diff --git a/src/app.config.ts b/src/app.config.ts index ef57de9..ccfc8c5 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -4,6 +4,7 @@ export default defineAppConfig({ 'pages/tasks/index', 'pages/messages/index', 'pages/profile/index', + 'pages/article/index', ], window: { backgroundTextStyle: 'light', diff --git a/src/mock/articleDetail.ts b/src/mock/articleDetail.ts new file mode 100644 index 0000000..7e58874 --- /dev/null +++ b/src/mock/articleDetail.ts @@ -0,0 +1,37 @@ +import { articles } from './articles' + +/** 文章详情额外字段 */ +interface ArticleDetail { + id: string + content: string + images: string[] +} + +/** 用一段长文本来模拟文章正文 */ +const FULL_CONTENT = `近日,学校召开了2024年秋季学期教学工作会议。会议总结了上学期教学工作取得的成绩,并对新学期教学工作进行了全面部署。 + +教务处处长在会议上指出,上学期全校教师积极探索教学改革,推行线上线下混合式教学模式,取得了显著成效。学生在各类学科竞赛中斩获国家级奖项12项、省级奖项35项,创历史新高。 + +会议特别强调了以下几点: + +第一,持续推进课程思政建设。各学院要深入挖掘专业课程中蕴含的思政元素,将价值引领融入知识传授全过程,培养德才兼备的高素质人才。 + +第二,深化产教融合。学校将进一步加强与企业的合作,建设一批高水平的校外实践教学基地,为学生提供更多实习实训机会。目前已与30余家知名企业达成合作协议。 + +第三,优化课程体系。根据社会需求和学科发展趋势,及时调整课程设置,增加人工智能、大数据等前沿领域的课程内容,提升学生的核心竞争力。 + +第四,强化教学质量监控。完善教学督导制度,建立学生评教、同行评议、专家评价相结合的教学质量评价体系,确保教学质量持续提升。 + +会上,多位优秀教师代表分享了教学经验。来自计算机学院的张老师介绍了翻转课堂的实践经验,引起与会教师的广泛共鸣。 + +会议最后,校领导对新学期教学工作提出了殷切期望,希望全体教师以更加饱满的热情投入到教学工作中,为培养新时代优秀人才贡献力量。` + +export function getArticleDetail(id: string) { + const article = articles.find((a) => a.id === id) + if (!article) return null + return { + ...article, + content: article.excerpt + '\n\n' + FULL_CONTENT, + images: article.images, + } as ArticleDetail & typeof article +} diff --git a/src/mock/comments.ts b/src/mock/comments.ts new file mode 100644 index 0000000..de35eae --- /dev/null +++ b/src/mock/comments.ts @@ -0,0 +1,57 @@ +import type { Comment } from '@/types/comment' + +/** 模拟评论数据 */ +export const comments: Comment[] = [ + { + id: 'c1', + author: { name: '张同学', avatar: 'https://picsum.photos/seed/c1/100/100' }, + content: '写得太好了!对新学期的工作重点有了更清晰的了解,期待学校的教学改革能带来更好的学习体验。', + time: '2小时前', + likes: 28, + liked: true, + }, + { + id: 'c2', + author: { name: '李老师', avatar: 'https://picsum.photos/seed/c2/100/100' }, + content: '作为一线教师,深切感受到学校对教学工作的重视。翻转课堂确实能激发学生的学习主动性,推荐更多老师尝试。', + time: '3小时前', + likes: 15, + }, + { + id: 'c3', + author: { name: '王同学', avatar: 'https://picsum.photos/seed/c3/100/100' }, + content: '产教融合这个方向真的很赞,希望能多一些实习机会,让我们在校期间就能接触真实项目。', + time: '5小时前', + likes: 42, + replies: [ + { + id: 'c3r1', + author: { name: '教务处回复', avatar: 'https://picsum.photos/seed/admin/100/100' }, + content: '感谢同学的反馈!学校正在积极拓展合作企业,预计下学期将新增20个实习基地,请关注后续通知。', + time: '4小时前', + likes: 18, + }, + ], + }, + { + id: 'c4', + author: { name: '赵同学', avatar: 'https://picsum.photos/seed/c4/100/100' }, + content: '希望课程设置能多加入一些AI相关的内容,现在就业市场对这方面的需求很大。', + time: '6小时前', + likes: 35, + }, + { + id: 'c5', + author: { name: '陈同学', avatar: 'https://picsum.photos/seed/c5/100/100' }, + content: '选修课的种类能不能再多一些?特别是跨学科的课程,感觉很有意思。', + time: '8小时前', + likes: 12, + }, + { + id: 'c6', + author: { name: '刘同学', avatar: 'https://picsum.photos/seed/c6/100/100' }, + content: '教学质量监控很重要,希望能真实反映课堂情况,不要流于形式。', + time: '昨天', + likes: 20, + }, +] diff --git a/src/pages/article/index.config.ts b/src/pages/article/index.config.ts new file mode 100644 index 0000000..7062634 --- /dev/null +++ b/src/pages/article/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '文章详情', + navigationStyle: 'custom', +}) diff --git a/src/pages/article/index.less b/src/pages/article/index.less new file mode 100644 index 0000000..2725d67 --- /dev/null +++ b/src/pages/article/index.less @@ -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; +} diff --git a/src/pages/article/index.tsx b/src/pages/article/index.tsx new file mode 100644 index 0000000..b266366 --- /dev/null +++ b/src/pages/article/index.tsx @@ -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(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 ( + + 文章不存在 + + ) + } + + // 正文段落 + const paragraphs = article.content.split('\n\n').filter(Boolean) + + return ( + + {/* ========== 自定义导航栏 ========== */} + + + + + + 文章详情 + + + + + {/* 占位 */} + + + + {/* ========== 文章内容 ========== */} + + {/* 标题 */} + {article.title} + + {/* Meta 信息 */} + + {article.source} + {article.time} + {article.readCount} 阅读 + + + {/* 正文 */} + + {paragraphs.map((p, idx) => ( + {p} + ))} + + + {/* 图片 */} + {article.images && article.images.length > 0 && ( + + {article.images.map((img, idx) => ( + + ))} + + )} + + + {/* ========== 点赞/评论/分享栏 ========== */} + + + + 点赞 + + + + {commentList.length} + + + + 分享 + + + + {/* ========== 评论区域 ========== */} + + + 全部评论 + ({commentList.length}) + + + {commentList.length > 0 ? ( + + {commentList.map((comment) => ( + + + + + {comment.author.name} + {comment.time} + + {comment.content} + + handleLikeComment(comment.id)} + > + + + {comment.likes || '赞'} + + + + + {/* 子回复 */} + {comment.replies && comment.replies.length > 0 && ( + + {comment.replies.map((reply) => ( + + + + + {reply.author.name} + {reply.time} + + {reply.content} + + handleLikeComment(reply.id)} + > + + + {reply.likes || '赞'} + + + + + + ))} + + )} + + + ))} + + ) : ( + 暂无评论,快来抢沙发吧~ + )} + + + {/* 底部占位(防止被输入框遮挡) */} + + + + {/* ========== 底部评论输入框(固定) ========== */} + + + setCommentText(e.detail.value)} + confirmType='send' + onConfirm={handleSendComment} + /> + + + 发送 + + + + ) +} diff --git a/src/types/comment.ts b/src/types/comment.ts new file mode 100644 index 0000000..589d74f --- /dev/null +++ b/src/types/comment.ts @@ -0,0 +1,14 @@ +/** 评论 */ +export interface Comment { + id: string + author: { + name: string + avatar: string + } + content: string + time: string + likes: number + liked?: boolean + /** 子回复 */ + replies?: Comment[] +}