| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749 |
- <template>
- <view class="detail-container">
- <!-- 加载状态 -->
- <view v-if="loading" class="loading-wrap">
- <view class="loading-spinner"></view>
- <text class="loading-text">加载中...</text>
- </view>
- <!-- 错误状态 -->
- <view v-else-if="error" class="error-wrap">
- <text class="error-icon">📄</text>
- <text class="error-text">{{ errorMsg }}</text>
- <button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
- </view>
- <!-- 文章内容 -->
- <view v-else-if="article" class="detail-content">
- <!-- 封面图 -->
- <image
- v-if="article.coverImage"
- class="detail-cover"
- :src="article.coverImage"
- mode="widthFix"
- />
- <!-- 文章标题 -->
- <text class="detail-title">{{ article.title }}</text>
- <!-- 元信息 -->
- <view class="detail-meta">
- <text class="meta-author">{{ article.author || '浠艾福' }}</text>
- <text class="meta-separator">|</text>
- <text class="meta-date">{{ getFormattedDate() }}</text>
- <text class="meta-separator">|</text>
- <text class="meta-read">{{ article.readTime || 3 }} 分钟阅读</text>
- </view>
- <!-- 分类标签 + 内容类型/难度 -->
- <view class="detail-category-row">
- <text class="detail-category">{{ article.categoryName || '' }}</text>
- <text v-if="article.contentType" class="detail-type-badge" :class="'type-' + article.contentType">{{ contentTypeLabel(article.contentType) }}</text>
- <text v-if="article.difficultyLevel && article.difficultyLevel > 0" class="detail-difficulty">{{ '★'.repeat(article.difficultyLevel) }}{{ '☆'.repeat(5 - article.difficultyLevel) }}</text>
- </view>
- <!-- 维度标签 -->
- <view v-if="dimensionList.length" class="detail-dimensions">
- <text v-for="(dim, idx) in dimensionList" :key="idx" class="dimension-tag" :style="{ color: dim.color, background: dim.bgColor }">{{ dim.label }}</text>
- </view>
- <!-- 知识标签 -->
- <view v-if="tagList.length" class="detail-tags">
- <text v-for="(tag, idx) in tagList" :key="idx" class="tag-item">{{ tag }}</text>
- </view>
- <!-- 分割线 -->
- <view class="divider"></view>
- <!-- 文章正文(富文本) -->
- <view class="detail-body">
- <rich-text :nodes="article.content" />
- </view>
- <!-- 收藏按钮 -->
- <view class="action-bar">
- <view class="action-btn" @click="toggleFavorite">
- <text class="action-icon">{{ isFavorited ? '❤️' : '🤍' }}</text>
- <text class="action-text">{{ isFavorited ? '已收藏' : '收藏' }}</text>
- </view>
- <view class="action-btn" @click="handleShare">
- <text class="action-icon">📤</text>
- <text class="action-text">分享</text>
- </view>
- <view class="action-btn">
- <text class="action-icon">👁</text>
- <text class="action-text">{{ article.viewCount || 0 }}</text>
- </view>
- </view>
- <!-- 相关文章 -->
- <view class="related-section" v-if="relatedArticles.length > 0">
- <view class="related-header">
- <text class="related-title">📖 相关推荐</text>
- </view>
- <view class="related-list">
- <view
- v-for="item in relatedArticles"
- :key="item.id"
- class="related-card"
- @click="goRelatedArticle(item.id)"
- >
- <image
- class="related-cover"
- :src="item.coverImage || '/static/default-article.png'"
- mode="aspectFill"
- />
- <view class="related-info">
- <text class="related-name">{{ item.title }}</text>
- <text class="related-summary">{{ item.summary || '' }}</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 底部完成按钮 -->
- <view class="detail-footer">
- <!-- 定时器显示 -->
- <view class="reading-timer" v-if="readingSeconds >= 0">
- <text class="timer-icon">⏱</text>
- <text class="timer-text">{{ formatTime(readingSeconds) }}</text>
- <text class="timer-hint" v-if="readingSeconds < 60">阅读{{ 60 - readingSeconds }}秒可获取积分</text>
- <text class="timer-hint timer-ready" v-else>已达标,可领取积分</text>
- </view>
- <!-- 完成按钮 -->
- <button
- class="read-btn"
- :class="{ 'read-btn-active': readingSeconds >= 60 }"
- @click="onReadComplete"
- >阅读完成</button>
- </view>
- </view>
- <!-- 空状态 -->
- <view v-else class="error-wrap">
- <text class="error-icon">📄</text>
- <text class="error-text">文章不存在</text>
- </view>
- <!-- 分享弹窗 -->
- <view class="modal-mask" v-if="showShareModal" @click="showShareModal = false">
- <view class="share-modal" @click.stop="">
- <text class="share-modal-title">分享到</text>
- <view class="share-modal-items">
- <view class="share-modal-item" @click="shareToWechat">
- <text class="share-modal-icon">💬</text>
- <text class="share-modal-label">微信好友</text>
- </view>
- <view class="share-modal-item" @click="shareToTimeline">
- <text class="share-modal-icon">📱</text>
- <text class="share-modal-label">朋友圈</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getArticleDetail, getFeaturedArticles, recordArticleRead } from '../../utils/api.js'
- var DIMENSION_MAP = {
- body: { label: '身', color: '#FF8C42', bgColor: 'rgba(255,140,66,0.1)' },
- mind: { label: '心', color: '#FF6B9D', bgColor: 'rgba(255,107,157,0.1)' },
- wisdom: { label: '智', color: '#6366F1', bgColor: 'rgba(99,102,241,0.1)' },
- action: { label: '行', color: '#10B981', bgColor: 'rgba(16,185,129,0.1)' },
- wealth: { label: '富', color: '#F59E0B', bgColor: 'rgba(245,158,11,0.1)' }
- }
- export default {
- data() {
- return {
- articleId: '',
- article: null,
- loading: true,
- error: false,
- errorMsg: '',
- tagList: [],
- dimensionList: [],
- relatedArticles: [],
- readingSeconds: 0,
- readingTimer: null,
- readRecorded: false,
- showShareModal: false,
- isFavorited: false
- }
- },
- onLoad(options) {
- if (options && options.id) {
- this.articleId = options.id
- this.token = uni.getStorageSync('token')
- this.baseUrl = this.token ? '' : ''
- this.loadDetail(options.id)
- this.startReadingTimer()
- } else {
- this.error = true
- this.errorMsg = '参数错误'
- this.loading = false
- }
- },
- onHide() {
- this.stopReadingTimer()
- },
- onUnload() {
- this.stopReadingTimer()
- },
- methods: {
- contentTypeLabel: function(type) {
- var labels = { article: '文章', knowledge: '知识点', course: '课程', tip: '贴士' }
- return labels[type] || type || ''
- },
- async loadDetail(id) {
- this.loading = true
- this.error = false
- try {
- const res = await getArticleDetail({ id: id })
- if (res.code === 200 && res.data) {
- this.article = res.data
- // 解析标签 JSON 字符串
- if (res.data.tags) {
- try {
- let parsed = JSON.parse(res.data.tags)
- this.tagList = Array.isArray(parsed) ? parsed : []
- } catch (e) {
- this.tagList = []
- }
- }
- // 解析维度 ID JSON
- if (res.data.dimensionIds) {
- try {
- var dimIds = JSON.parse(res.data.dimensionIds)
- if (Array.isArray(dimIds)) {
- this.dimensionList = dimIds.map(function(d) {
- return DIMENSION_MAP[d] || { label: d, color: '#64748B', bgColor: 'rgba(100,116,139,0.1)' }
- })
- }
- } catch (e) {
- this.dimensionList = []
- }
- }
- // 加载相关文章
- this.loadRelatedArticles()
- } else {
- this.error = true
- this.errorMsg = '文章不存在或无权限查看'
- }
- } catch (e) {
- this.error = true
- this.errorMsg = '加载失败,请稍后重试'
- } finally {
- this.loading = false
- }
- },
- async loadRelatedArticles() {
- try {
- var dimensionCode = null
- if (this.article && this.article.dimensionIds) {
- try {
- var dimIds = JSON.parse(this.article.dimensionIds)
- if (Array.isArray(dimIds) && dimIds.length > 0) {
- dimensionCode = dimIds[0]
- }
- } catch (e) {}
- }
- var params = { size: 3 }
- if (dimensionCode) {
- params.dimensionCode = dimensionCode
- }
- const res = await getFeaturedArticles(params)
- if (res.code === 200 && res.data) {
- var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
- // 排除当前文章
- this.relatedArticles = list.filter(function(item) {
- return item.id !== (this.article && this.article.id)
- }.bind(this)).slice(0, 3)
- }
- } catch (e) {
- // 静默处理
- }
- },
- getFormattedDate() {
- if (this.article && this.article.publishedAt) {
- return this.article.publishedAt.slice(0, 10)
- }
- return ''
- },
- startReadingTimer() {
- var self = this
- this.readingTimer = setInterval(function() {
- self.readingSeconds = self.readingSeconds + 1
- }, 1000)
- },
- stopReadingTimer() {
- if (this.readingTimer) {
- clearInterval(this.readingTimer)
- this.readingTimer = null
- }
- },
- formatTime(seconds) {
- var m = Math.floor(seconds / 60)
- var s = seconds % 60
- return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
- },
- toggleFavorite: function() {
- this.isFavorited = !this.isFavorited
- uni.showToast({ title: this.isFavorited ? '已收藏' : '已取消收藏', icon: 'none' })
- },
- handleShare: function() {
- var self = this
- uni.showActionSheet({
- itemList: ['微信好友', '朋友圈', '保存图片'],
- success: function(res) {
- var channel = 'wechat'
- if (res.tapIndex === 1) channel = 'friend'
- uni.request({
- url: self.baseUrl + '/api/share/create',
- method: 'POST',
- header: { 'Authorization': 'Bearer ' + self.token },
- data: {
- targetType: 'article',
- targetId: self.articleId,
- shareChannel: channel,
- title: self.article && self.article.title ? self.article.title : ''
- },
- success: function(createRes) {
- if (createRes.data && createRes.data.code === 200) {
- var shareEvent = createRes.data.data
- uni.showToast({ title: '分享成功', icon: 'success' })
- uni.request({
- url: self.baseUrl + '/api/share/click',
- method: 'POST',
- header: { 'Authorization': 'Bearer ' + self.token },
- data: { shareEventId: shareEvent.id }
- })
- self.showShareModal = true
- }
- }
- })
- }
- })
- },
- shareToWechat: function() {
- this.showShareModal = false
- uni.showToast({ title: '请使用微信分享', icon: 'none' })
- },
- shareToTimeline: function() {
- this.showShareModal = false
- uni.showToast({ title: '请使用微信朋友圈', icon: 'none' })
- },
- async onReadComplete() {
- if (this.readRecorded) {
- uni.navigateBack()
- return
- }
- this.stopReadingTimer()
- try {
- var childId = uni.getStorageSync('currentChildId')
- var res = await recordArticleRead({
- id: this.article.id,
- durationSeconds: this.readingSeconds,
- childId: childId || undefined
- })
- this.readRecorded = true
- if (res.data && res.data.pointsEarned > 0) {
- uni.showToast({ title: '获得' + res.data.pointsEarned + '积分!', icon: 'success' })
- } else if (res.data && res.data.belowThreshold) {
- uni.showToast({ title: '阅读时间未达标', icon: 'none' })
- } else if (res.data && res.data.alreadyCompleted) {
- uni.showToast({ title: '已读过此文', icon: 'none' })
- } else {
- uni.showToast({ title: '阅读记录已保存', icon: 'success' })
- }
- } catch (e) {
- uni.showToast({ title: '记录保存失败', icon: 'none' })
- }
- var self = this
- setTimeout(function() {
- uni.navigateBack()
- }, 1500)
- },
- goRelatedArticle: function(id) {
- uni.redirectTo({ url: '/pages/mind/article-detail?id=' + id })
- }
- }
- }
- </script>
- <style scoped>
- .detail-container {
- min-height: 100vh;
- background: #fff;
- padding-bottom: 120rpx;
- }
- /* ===== 加载状态 ===== */
- .loading-wrap {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding-top: 300rpx;
- }
- .loading-spinner {
- width: 60rpx;
- height: 60rpx;
- border: 4rpx solid #e0e0e0;
- border-top-color: #5B9BD5;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin-bottom: 20rpx;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .loading-text {
- font-size: 26rpx;
- color: #999;
- }
- /* ===== 错误状态 ===== */
- .error-wrap {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding-top: 300rpx;
- }
- .error-icon {
- font-size: 100rpx;
- margin-bottom: 24rpx;
- }
- .error-text {
- font-size: 28rpx;
- color: #999;
- margin-bottom: 30rpx;
- }
- .retry-btn {
- width: 240rpx;
- height: 72rpx;
- line-height: 72rpx;
- background: #5B9BD5;
- color: #fff;
- font-size: 28rpx;
- border-radius: 36rpx;
- text-align: center;
- border: none;
- }
- .retry-btn::after {
- border: none;
- }
- /* ===== 内容区 ===== */
- .detail-cover {
- width: 100%;
- max-height: 500rpx;
- display: block;
- }
- .detail-title {
- display: block;
- font-size: 36rpx;
- font-weight: bold;
- color: #333;
- line-height: 1.4;
- padding: 30rpx 30rpx 0;
- }
- /* ===== 元信息 ===== */
- .detail-meta {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 16rpx 30rpx 0;
- font-size: 22rpx;
- color: #999;
- }
- .meta-author {
- color: #5B9BD5;
- }
- .meta-separator {
- margin: 0 12rpx;
- color: #ddd;
- }
- .meta-date,
- .meta-read {
- color: #999;
- }
- /* ===== 分类标签 ===== */
- .detail-category-row {
- padding: 16rpx 30rpx 0;
- display: flex;
- align-items: center;
- gap: 12rpx;
- flex-wrap: wrap;
- }
- .detail-category {
- display: inline-block;
- font-size: 20rpx;
- color: #5B9BD5;
- background: rgba(91, 155, 213, 0.1);
- padding: 4rpx 16rpx;
- border-radius: 8rpx;
- }
- .detail-type-badge {
- display: inline-block;
- font-size: 18rpx;
- padding: 2rpx 12rpx;
- border-radius: 6rpx;
- background: #f0f0f0;
- color: #666;
- }
- .detail-type-badge.type-article {
- background: #e8f4fd;
- color: #2b7fc4;
- }
- .detail-type-badge.type-knowledge {
- background: #e8f8ee;
- color: #2a9055;
- }
- .detail-type-badge.type-course {
- background: #fef3e2;
- color: #c47a2b;
- }
- .detail-type-badge.type-tip {
- background: #f3e8fd;
- color: #7b2bc4;
- }
- .detail-difficulty {
- font-size: 18rpx;
- color: #f59e0b;
- margin-left: 4rpx;
- }
- /* ===== 维度标签 ===== */
- .detail-dimensions {
- display: flex;
- flex-direction: row;
- padding: 8rpx 30rpx 0;
- gap: 10rpx;
- }
- .dimension-tag {
- font-size: 18rpx;
- padding: 2rpx 14rpx;
- border-radius: 6rpx;
- }
- /* ===== 标签 ===== */
- .detail-tags {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- padding: 12rpx 30rpx 0;
- gap: 12rpx;
- }
- .tag-item {
- font-size: 20rpx;
- color: #8B5CF6;
- background: rgba(139, 92, 246, 0.08);
- padding: 4rpx 14rpx;
- border-radius: 8rpx;
- }
- /* ===== 分割线 ===== */
- .divider {
- height: 1rpx;
- background: #eee;
- margin: 24rpx 30rpx;
- }
- /* ===== 正文 ===== */
- .detail-body {
- padding: 0 30rpx;
- font-size: 28rpx;
- color: #444;
- line-height: 1.8;
- }
- .detail-body rich-text {
- word-break: break-word;
- }
- /* ===== 定时器 ===== */
- .reading-timer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- padding: 16rpx 0 20rpx;
- }
- .timer-icon {
- font-size: 32rpx;
- margin-right: 8rpx;
- }
- .timer-text {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- margin-right: 16rpx;
- }
- .timer-hint {
- font-size: 22rpx;
- color: #999;
- }
- .timer-ready {
- color: #22c55e;
- font-weight: bold;
- }
- /* ===== 底部按钮 ===== */
- .detail-footer {
- padding: 40rpx 60rpx;
- }
- .read-btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- background: #ddd;
- color: #fff;
- font-size: 32rpx;
- font-weight: bold;
- border-radius: 44rpx;
- text-align: center;
- border: none;
- transition: background 0.3s;
- }
- .read-btn-active {
- background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
- }
- .read-btn::after {
- border: none;
- }
- /* ===== 操作栏 ===== */
- .action-bar {
- display: flex;
- flex-direction: row;
- justify-content: space-around;
- padding: 24rpx 30rpx;
- border-bottom: 1rpx solid #eee;
- }
- .action-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 8rpx 24rpx;
- }
- .action-icon {
- font-size: 36rpx;
- margin-bottom: 6rpx;
- }
- .action-text {
- font-size: 22rpx;
- color: #666;
- }
- /* ===== 相关推荐 ===== */
- .related-section {
- padding: 30rpx;
- }
- .related-header {
- margin-bottom: 20rpx;
- }
- .related-title {
- font-size: 30rpx;
- font-weight: bold;
- color: #333;
- }
- .related-list {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .related-card {
- display: flex;
- flex-direction: row;
- background: #fff;
- border-radius: 16rpx;
- overflow: hidden;
- box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
- }
- .related-cover {
- width: 180rpx;
- height: 140rpx;
- flex-shrink: 0;
- background: linear-gradient(135deg, #667eea, #764ba2);
- }
- .related-info {
- flex: 1;
- padding: 16rpx;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
- .related-name {
- font-size: 26rpx;
- font-weight: bold;
- color: #333;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- margin-bottom: 8rpx;
- }
- .related-summary {
- font-size: 22rpx;
- color: #999;
- display: -webkit-box;
- -webkit-line-clamp: 1;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- }
- .share-btn::after {
- border: none;
- }
- /* ===== 分享弹窗 ===== */
- .modal-mask {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- z-index: 1000;
- }
- .share-modal {
- background: #fff;
- border-radius: 24rpx 24rpx 0 0;
- padding: 40rpx;
- }
- .share-modal-title {
- display: block;
- text-align: center;
- font-size: 32rpx;
- font-weight: bold;
- margin-bottom: 32rpx;
- }
- .share-modal-items {
- display: flex;
- justify-content: space-around;
- }
- .share-modal-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .share-modal-icon {
- font-size: 48rpx;
- margin-bottom: 12rpx;
- }
- .share-modal-label {
- font-size: 24rpx;
- color: #64748B;
- }
- </style>
|