article-detail.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. <template>
  2. <view class="detail-container">
  3. <!-- 登录浮层(全局组件,页面内弹出底部登录) -->
  4. <login-sheet ref="loginSheet" @success="onSheetLoginSuccess" />
  5. <view v-if="loading" class="loading-wrap">
  6. <view class="loading-spinner"></view>
  7. <text class="loading-text">加载中...</text>
  8. </view>
  9. <view v-else-if="error" class="error-wrap">
  10. <text class="error-icon">📄</text>
  11. <text class="error-text">{{ errorMsg }}</text>
  12. <button class="retry-btn" @click="loadDetail(articleId)">重新加载</button>
  13. </view>
  14. <template v-else-if="article">
  15. <scroll-view scroll-y class="content-scroll" @scrolltolower="onScrollToBottom">
  16. <image v-if="article.coverImage" class="detail-cover" :src="article.coverImage" mode="widthFix" />
  17. <text class="detail-title">{{ article.title }}</text>
  18. <view class="detail-meta">
  19. <text class="meta-author">{{ article.author || '浠艾福' }}</text>
  20. <text class="meta-sep">|</text>
  21. <text class="meta-date">{{ formatDate(article.publishedAt) }}</text>
  22. <text class="meta-sep">|</text>
  23. <text class="meta-readtime">{{ article.readTime || 3 }}分钟阅读</text>
  24. <text class="reading-time-badge" v-if="isLogin && !readingCompleted">{{ formatReadingTime(readingSeconds) }}</text>
  25. <text class="reading-time-badge completed" v-else-if="isLogin && readingCompleted">✅ 阅读完成</text>
  26. <text class="meta-sep" v-if="isLogin">|</text>
  27. <text class="meta-readcount">{{ article.viewCount || 0 }}人阅读</text>
  28. </view>
  29. <view class="detail-category-row">
  30. <text class="detail-category">{{ article.categoryName || '' }}</text>
  31. </view>
  32. <view v-if="article.relatedDimensions" class="detail-dimensions">
  33. <view v-for="dim in parseDimensions(article)" :key="dim.code" class="dim-bar-item">
  34. <view class="dim-bar" :style="{ background: dim.color, width: dim.weight + '%' }"></view>
  35. <text class="dim-label">{{ dim.name }}</text>
  36. </view>
  37. </view>
  38. <view class="detail-summary" v-if="article.summary">
  39. <text class="detail-summary-text">{{ article.summary }}</text>
  40. </view>
  41. <view class="divider"></view>
  42. <view class="detail-body"><mp-html :content="article.content" :domain="config.API_BASE_URL" :tag-style="contentTagStyle" /></view>
  43. <!-- 关联商品(文章标签匹配推荐) -->
  44. <view class="related-section" v-if="relatedProducts.length > 0">
  45. <view class="related-header">
  46. <text class="related-title">相关商品</text>
  47. <text class="related-sub">与本文内容相关</text>
  48. </view>
  49. <scroll-view scroll-x class="related-scroll" :show-scrollbar="false">
  50. <view class="related-list">
  51. <view class="related-item" v-for="p in relatedProducts" :key="p.id" @click="goProduct(p)">
  52. <image class="related-img" :src="getImageUrl(p.coverImage)" mode="aspectFill" />
  53. <view class="related-info">
  54. <text class="related-name">{{ p.name }}</text>
  55. <view class="related-price-row">
  56. <text class="related-price">¥{{ p.memberPrice }}</text>
  57. <text class="related-match">匹配{{ p.tagMatchCount }}标签</text>
  58. </view>
  59. </view>
  60. </view>
  61. </view>
  62. </scroll-view>
  63. </view>
  64. <!-- 评论区(已屏蔽) -->
  65. <view style="height: 200rpx;"></view>
  66. </scroll-view>
  67. <!-- 底部 -->
  68. <view class="detail-footer">
  69. <view class="share-btn" @click="generatePoster">
  70. <text class="share-btn-icon">🖼</text>
  71. <text class="share-btn-text">生成海报</text>
  72. </view>
  73. </view>
  74. </template>
  75. <ContentSharePoster :show="showPoster" :title="(article && article.title) || ''" :coverImage="(article && article.coverImage) || ''" :qrCodeBase64="posterQrCode || ''" typeLabel="好文推荐" :dimensionCode="posterDimension || ''" :summary="posterSummary || ''" :inviteText="posterInviteText || ''" @close="closePoster" />
  76. <!-- 浠宝答题弹窗 -->
  77. <view class="quiz-mask" v-if="showQuiz" @click="closeQuiz">
  78. <view class="quiz-dialog" @click.stop>
  79. <view class="quiz-mascot">
  80. <text class="mascot-icon">{{ mascotIcon }}</text>
  81. <text class="mascot-name">{{ mascotName }}</text>
  82. </view>
  83. <text class="quiz-intro">阅读完成!考考你三个问题~</text>
  84. <view class="quiz-question" v-for="(q, qi) in quizQuestions" :key="qi">
  85. <text class="q-title">问题{{ qi+1 }}: {{ q.question }}</text>
  86. <view class="q-options">
  87. <text v-for="(opt, oi) in q.options" :key="oi" class="q-option" :class="{ selected: quizAnswers[qi] === String.fromCharCode(65 + oi) }" @click="selectQuizAnswer(qi, oi)">{{ opt }}</text>
  88. </view>
  89. </view>
  90. <button class="quiz-submit" @click="submitQuiz">提交答案</button>
  91. </view>
  92. </view>
  93. <!-- 答题结果 -->
  94. <view class="result-mask" v-if="showResult">
  95. <view class="result-dialog">
  96. <text class="result-icon">{{ resultIcon }}</text>
  97. <text class="result-text">答对 {{ quizResult.correctCount }}/{{ quizResult.totalQuestions }} 题</text>
  98. <text class="result-energy">获得 {{ quizResult.cfEarned }} CF值 + {{ quizResult.energyEarned }} 能量 ⚡</text>
  99. <button class="result-btn" @click="closeResult">知道了</button>
  100. </view>
  101. </view>
  102. </view>
  103. </template>
  104. <script>
  105. import { getArticleDetail, reportReadingTime, getShareQrCode, completeArticleRead, generateQuiz, submitQuiz, getArticleRelatedProducts } from '@/utils/api.js'
  106. import shareMixin from '../../components/share-mixin.js'
  107. import ContentSharePoster from '@/components/ContentSharePoster.vue'
  108. import MpHtml from '@/components/mp-html/mp-html.vue'
  109. import config from '@/config.js'
  110. export default {
  111. mixins: [shareMixin],
  112. components: { ContentSharePoster, MpHtml },
  113. data() {
  114. return {
  115. articleId: '', article: null, loading: true, error: false, errorMsg: '',
  116. relatedProducts: [],
  117. readingSeconds: 0, lastReportedSeconds: 0, readingCompleted: false,
  118. isTimerRunning: false, timerHandle: null, syncHandle: null,
  119. currentChildId: null, isLogin: false,
  120. showPoster: false, posterQrCode: '',
  121. posterDimension: '', posterSummary: '', posterInviteText: '',
  122. contentTagStyle: { p: 'font-size:32rpx;color:#444;line-height:1.8;', div: 'font-size:32rpx;color:#444;line-height:1.8;', section: 'font-size:32rpx;color:#444;line-height:1.8;', li: 'font-size:32rpx;color:#444;line-height:1.8;', blockquote: 'font-size:32rpx;color:#444;line-height:1.8;' },
  123. // 答题
  124. showQuiz: false, quizQuestions: [], quizAnswers: [], showResult: false, quizResult: {}, quizRecordId: null,
  125. mascotList: [{ icon: '🐶', name: '浠宝' }, { icon: '🐼', name: '福宝' }],
  126. mascotIcon: '🐶', mascotName: '浠宝'
  127. }
  128. },
  129. computed: {},
  130. onLoad(options) {
  131. this.mascotIcon = this.mascotList[Math.floor(Math.random() * 2)].icon
  132. this.mascotName = this.mascotList[Math.floor(Math.random() * 2)].name
  133. // 兼容旧分享链接:refCode / inviteCode 参数(shareMixin onLoad 也会处理,此处显式兜底)
  134. if (options && (options.refCode || options.inviteCode)) {
  135. this.autoBindInviteCode(options.refCode || options.inviteCode)
  136. }
  137. var aid = ''
  138. if (options && options.id) {
  139. aid = options.id
  140. } else if (options && options.scene) {
  141. try {
  142. var sceneDecoded = decodeURIComponent(options.scene)
  143. var idMatch = sceneDecoded.match(/id=(\d+)/)
  144. if (idMatch && idMatch[1]) aid = idMatch[1]
  145. var refMatch = sceneDecoded.match(/ref=([^&]+)/)
  146. if (refMatch && refMatch[1]) this.autoBindInviteCode(refMatch[1])
  147. } catch (e) {}
  148. }
  149. if (aid) { this.articleId = aid; this.loadDetail(aid) }
  150. else { this.error = true; this.errorMsg = '参数错误'; this.loading = false }
  151. this.currentChildId = uni.getStorageSync('currentChildId') || null
  152. this.isLogin = !!uni.getStorageSync('token')
  153. },
  154. onShow() {
  155. var memberId = uni.getStorageSync('currentChildId')
  156. if (!this.article || this.error) return
  157. // 已登录用户(家长/成员/规划师)均可计时,匿名用户不计时
  158. if (this.isLogin) this.startNewReadingSession()
  159. },
  160. onHide() { this.pauseReadingTimer() },
  161. onUnload() { this.pauseReadingTimer() },
  162. methods: {
  163. async loadDetail(id) {
  164. this.loading = true; this.error = false
  165. try {
  166. var res = await getArticleDetail({ id: id })
  167. if (res.code === 200 && res.data) {
  168. this.article = res.data
  169. this.article.coverImage = this.getImageUrl(this.article.coverImage)
  170. this.article.content = this.resolveContentUrls(this.article.content)
  171. this.setShareInfo('推荐阅读: ' + (res.data.title || ''), '/pages/article-center/article-detail?id=' + id)
  172. this.startReadingTimer()
  173. this.loadRelatedProducts(id)
  174. } else { this.error = true; this.errorMsg = '文章不存在或无权限查看' }
  175. } catch (e) { this.error = true; this.errorMsg = '加载失败' }
  176. finally { this.loading = false }
  177. },
  178. formatDate(d) { return d ? d.slice(0, 10) : '' },
  179. loadRelatedProducts: function(id) {
  180. var self = this
  181. getArticleRelatedProducts({ articleId: id, size: 6 }).then(function(res) {
  182. if (res && res.code === 200 && res.data) {
  183. var list = res.data
  184. var i
  185. for (i = 0; i < list.length; i++) {
  186. list[i].coverImage = self.getImageUrl(list[i].coverImage)
  187. }
  188. self.relatedProducts = list
  189. }
  190. }).catch(function() {})
  191. },
  192. goProduct: function(p) {
  193. if (p && p.id) {
  194. uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + p.id })
  195. }
  196. },
  197. getImageUrl: function(path) {
  198. if (!path) return ''
  199. if (path.indexOf('http://') === 0 || path.indexOf('https://') === 0) return path
  200. if (!path || path === 'null' || path === 'undefined' || path === '/null') return ''
  201. var s = String(path).trim()
  202. if (!s || s === 'null' || s === '/null') return ''
  203. if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
  204. return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
  205. },
  206. resolveContentUrls: function(html) {
  207. if (!html) return html
  208. // 将富文本内容中的相对图片路径(/uploads/...)拼接为绝对 URL
  209. return html.replace(/(src|href)=["']\/(uploads\/[^"']+)/g, function(match, attr, path) {
  210. return attr + '="' + config.API_BASE_URL + '/' + path + '"'
  211. })
  212. },
  213. parseDimensions: function(article) {
  214. var dimMap = { body: { code: 'body', color: '#FF8C42', name: '身' }, mind: { code: 'mind', color: '#FF6B9D', name: '心' }, wisdom: { code: 'wisdom', color: '#6366F1', name: '智' }, action: { code: 'action', color: '#10B981', name: '行' }, wealth: { code: 'wealth', color: '#F59E0B', name: '富' } }
  215. var raw = article && article.relatedDimensions ? article.relatedDimensions.split(',').map(function(s) { return s.trim().toLowerCase() }) : []
  216. var weights = null
  217. if (article && article.dimensionWeights) try { var w = JSON.parse(article.dimensionWeights); if (w && typeof w === 'object') weights = w } catch (e) {}
  218. if (!weights) {
  219. var n = raw.filter(function(c) { return dimMap[c] }).length
  220. if (n > 0) { var base = Math.floor(100 / n); var rem = 100 - base * n; weights = {}; var i = 0; raw.forEach(function(c) { if (!dimMap[c]) return; weights[c] = i < rem ? base + 1 : base; i++ }) }
  221. }
  222. return raw.filter(function(c) { return dimMap[c] }).map(function(c) { return { code: dimMap[c].code, name: dimMap[c].name, color: dimMap[c].color, weight: (weights && weights[c]) ? weights[c] : 20 } })
  223. },
  224. getMainDimension: function() {
  225. var dims = this.article ? this.parseDimensions(this.article) : []
  226. var best = null
  227. var i
  228. for (i = 0; i < dims.length; i++) {
  229. if (!best || dims[i].weight > best.weight) best = dims[i]
  230. }
  231. return best ? best.code : ''
  232. },
  233. onScrollToBottom() {},
  234. startNewReadingSession: function() {
  235. // 每次进入文章详情页都从 0 开始新会话,时长不延续上一次
  236. this.readingSeconds = 0
  237. this.lastReportedSeconds = 0
  238. this.startReadingTimer()
  239. },
  240. // 获取当前操作对象的 memberId:优先 currentChildId(切换视角),否则用 userId
  241. getActiveMemberId: function() {
  242. var mid = uni.getStorageSync('currentChildId')
  243. if (mid) return parseInt(mid)
  244. var uid = uni.getStorageSync('userId')
  245. return uid ? parseInt(uid) : null
  246. },
  247. startReadingTimer: function() {
  248. // 无登录态时不计时(匿名浏览只显示时长要求,不计时)
  249. if (!this.isLogin) return
  250. if (this.isTimerRunning || this.readingCompleted) return
  251. this.isTimerRunning = true
  252. var self = this
  253. this.timerHandle = setInterval(function() { self.readingSeconds++ }, 1000)
  254. // 每 10 秒自动记录一次该用户已读时长
  255. this.syncHandle = setInterval(function() { self.reportReadingTime() }, 10000)
  256. },
  257. pauseReadingTimer: function() {
  258. if (!this.isTimerRunning) return
  259. this.isTimerRunning = false
  260. if (this.timerHandle) { clearInterval(this.timerHandle); this.timerHandle = null }
  261. if (this.syncHandle) { clearInterval(this.syncHandle); this.syncHandle = null }
  262. this.reportReadingTime()
  263. },
  264. async reportReadingTime() {
  265. var delta = this.readingSeconds - this.lastReportedSeconds
  266. if (delta <= 0) return
  267. this.lastReportedSeconds = this.readingSeconds
  268. var memberId = this.getActiveMemberId()
  269. if (!this.isLogin || !this.article) return
  270. reportReadingTime({ articleId: this.article.id, memberId: memberId || 0, durationSeconds: delta })
  271. // 达到阅读时长自动完成
  272. var targetSeconds = (this.article.readTime || 3) * 60
  273. if (!this.readingCompleted && this.readingSeconds >= targetSeconds) {
  274. this.readingCompleted = true
  275. this.pauseReadingTimer()
  276. try { await completeArticleRead({ articleId: this.article.id, memberId: memberId || 0, durationSeconds: this.readingSeconds }) } catch (e) {}
  277. uni.showToast({ title: '阅读完成 +5能量', icon: 'success' })
  278. // 弹出答题(登录用户)
  279. this.startQuiz()
  280. }
  281. },
  282. formatReadingTime: function(seconds) {
  283. if (seconds < 60) return '已读 ' + seconds + '秒'
  284. var min = Math.floor(seconds / 60); var sec = seconds % 60
  285. return '已读 ' + min + '分' + (sec > 0 ? sec + '秒' : '')
  286. },
  287. async generatePoster() {
  288. if (!this.articleId) return
  289. uni.showLoading({ title: '生成海报中...' })
  290. try {
  291. if (!this.referralCode) {
  292. await this.loadReferralCode()
  293. }
  294. var scene = 'id=' + this.articleId + (this.referralCode ? '&ref=' + this.referralCode : '')
  295. if (scene.length > 32) {
  296. scene = scene.substring(0, 32)
  297. }
  298. var res = await getShareQrCode('pages/article-center/article-detail', scene)
  299. if (res && res.data) {
  300. this.posterQrCode = res.data.qrCodeBase64 || ''
  301. this.posterDimension = this.getMainDimension()
  302. this.posterSummary = (this.article && this.article.summary) || ''
  303. this.posterInviteText = '「' + (uni.getStorageSync('nickname') || '好友') + '」邀请你一起阅读这篇文章'
  304. this.showPoster = true
  305. }
  306. } catch (e) { uni.showToast({ title: '生成失败', icon: 'none' }) }
  307. finally { uni.hideLoading() }
  308. },
  309. // 答题
  310. async startQuiz() {
  311. if (!this.isLogin) {
  312. uni.showToast({ title: '登录后即可完成知识测评', icon: 'none' })
  313. setTimeout(() => { this.openLoginSheet() }, 1500)
  314. return
  315. }
  316. var memberId = this.getActiveMemberId()
  317. try {
  318. var res = await generateQuiz({ articleId: this.articleId, memberId: memberId || 0 })
  319. if (res.code === 200 && res.data && res.data.questions && res.data.questions.length > 0) {
  320. this.quizRecordId = res.data.recordId
  321. this.quizQuestions = res.data.questions
  322. this.quizAnswers = []
  323. this.showQuiz = true
  324. }
  325. } catch (e) {}
  326. },
  327. async submitQuiz() {
  328. var memberId = this.getActiveMemberId()
  329. var answers = []
  330. for (var i = 0; i < this.quizQuestions.length; i++) {
  331. answers.push(this.quizAnswers[i] || '')
  332. }
  333. try {
  334. var res = await submitQuiz({ recordId: this.quizRecordId, memberId: parseInt(memberId || 0), answers: answers })
  335. if (res.code === 200) {
  336. this.quizResult = res.data || {}
  337. this.showQuiz = false
  338. this.showResult = true
  339. }
  340. } catch (e) {}
  341. },
  342. closePoster: function() {
  343. this.showPoster = false
  344. },
  345. closeQuiz: function() {
  346. this.showQuiz = false
  347. },
  348. closeResult: function() {
  349. this.showResult = false
  350. },
  351. selectQuizAnswer: function(qi, oi) {
  352. this.quizAnswers[qi] = String.fromCharCode(65 + oi)
  353. }
  354. }
  355. }
  356. </script>
  357. <style scoped>
  358. .detail-container { min-height: 100vh; background: #fff; }
  359. .loading-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
  360. .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; }
  361. @keyframes spin { 0% { transform: rotate(0deg); } 360% { transform: rotate(360deg); } }
  362. .loading-text { font-size: 26rpx; color: #999; }
  363. .error-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 300rpx; }
  364. .error-icon { font-size: 100rpx; margin-bottom: 24rpx; }
  365. .error-text { font-size: 28rpx; color: #999; margin-bottom: 30rpx; }
  366. .retry-btn { width: 240rpx; height: 72rpx; line-height: 72rpx; background: #5B9BD5; color: #fff; font-size: 28rpx; border-radius: 36rpx; text-align: center; border: none; }
  367. .content-scroll { height: calc(100vh - 120rpx); }
  368. .detail-cover { width: 100%; display: block; }
  369. .detail-title { display: block; font-size: 36rpx; font-weight: bold; color: #333; line-height: 1.4; padding: 30rpx 30rpx 0; }
  370. .detail-meta { display: flex; align-items: center; padding: 16rpx 30rpx 0; font-size: 22rpx; color: #999; flex-wrap: wrap; }
  371. .meta-author { color: #5B9BD5; }
  372. .meta-sep { margin: 0 12rpx; color: #ddd; }
  373. .reading-time-badge { margin-left: auto; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.08); padding: 4rpx 12rpx; border-radius: 20rpx; white-space: nowrap; }
  374. .reading-time-badge.completed { color: #10B981; background: rgba(16,185,129,0.1); }
  375. .meta-readcount {
  376. font-size: 20rpx;
  377. color: #bbb;
  378. }
  379. .detail-category-row { padding: 16rpx 30rpx 0; }
  380. .detail-category { display: inline-block; font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.1); padding: 4rpx 16rpx; border-radius: 8rpx; }
  381. .detail-dimensions { padding: 16rpx 30rpx 0; display: flex; gap: 12rpx; }
  382. .dim-bar-item { flex: 1; }
  383. .dim-bar { height: 8rpx; border-radius: 4rpx; }
  384. .dim-label { font-size: 18rpx; color: #999; text-align: center; display: block; margin-top: 4rpx; }
  385. .detail-summary { padding: 20rpx 30rpx; }
  386. .detail-summary-text { font-size: 26rpx; color: #888; line-height: 1.7; display: block; border-left: 4rpx solid #F97316; padding-left: 20rpx; }
  387. .divider { height: 1rpx; background: #eee; margin: 24rpx 30rpx; }
  388. .detail-body { padding: 0 30rpx; font-size: 32rpx; color: #444; line-height: 1.8; }
  389. /* 关联商品 */
  390. .related-section { padding: 30rpx 0 20rpx; }
  391. .related-header { display: flex; align-items: baseline; padding: 0 30rpx; margin-bottom: 20rpx; }
  392. .related-title { font-size: 30rpx; font-weight: 700; color: #333; }
  393. .related-sub { font-size: 22rpx; color: #999; margin-left: 16rpx; }
  394. .related-scroll { white-space: nowrap; padding-left: 30rpx; }
  395. .related-list { display: inline-flex; }
  396. .related-item { width: 240rpx; margin-right: 20rpx; background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06); display: inline-block; vertical-align: top; }
  397. .related-img { width: 240rpx; height: 240rpx; display: block; background: #f5f5f5; }
  398. .related-info { padding: 14rpx 16rpx 18rpx; }
  399. .related-name { font-size: 24rpx; color: #333; line-height: 1.4; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; white-space: normal; min-height: 68rpx; }
  400. .related-price-row { display: flex; align-items: center; justify-content: space-between; margin-top: 10rpx; }
  401. .related-price { font-size: 26rpx; font-weight: 700; color: #F97316; }
  402. .related-match { font-size: 18rpx; color: #999; }
  403. .detail-footer { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 30rpx; display: flex; align-items: center; gap: 16rpx; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.06); z-index: 10; }
  404. .share-btn { height: 72rpx; padding: 0 30rpx; background: linear-gradient(135deg, #F97316, #FB923C); border-radius: 36rpx; display: flex; align-items: center; justify-content: center; border: none; }
  405. .share-btn::after { border: none; }
  406. .share-btn-icon { font-size: 30rpx; margin-right: 8rpx; }
  407. .share-btn-text { font-size: 26rpx; color: #fff; font-weight: 600; }
  408. /* 答题弹窗 */
  409. .quiz-mask, .result-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: center; justify-content: center; }
  410. .quiz-dialog, .result-dialog { background: #fff; border-radius: 24rpx; width: 650rpx; padding: 32rpx; }
  411. .quiz-mascot { display: flex; align-items: center; justify-content: center; gap: 12rpx; margin-bottom: 16rpx; }
  412. .mascot-icon { font-size: 60rpx; }
  413. .mascot-name { font-size: 32rpx; font-weight: 700; color: #F97316; }
  414. .quiz-intro { text-align: center; font-size: 28rpx; color: #666; margin-bottom: 24rpx; }
  415. .quiz-question { margin-bottom: 20rpx; }
  416. .q-title { font-size: 26rpx; font-weight: 600; color: #333; margin-bottom: 12rpx; }
  417. .q-options { display: flex; flex-direction: column; gap: 8rpx; }
  418. .q-option { padding: 14rpx 20rpx; border: 2rpx solid #E5E7EB; border-radius: 12rpx; font-size: 24rpx; color: #333; }
  419. .q-option.selected { border-color: #F97316; background: #FFF7ED; color: #92400E; }
  420. .quiz-submit, .result-btn { width: 100%; padding: 20rpx; background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 28rpx; border-radius: 12rpx; border: none; margin-top: 20rpx; }
  421. .result-dialog { text-align: center; }
  422. .result-icon { font-size: 80rpx; }
  423. .result-text { font-size: 32rpx; font-weight: 700; color: #333; margin: 16rpx 0; }
  424. .result-energy { font-size: 28rpx; color: #F97316; }
  425. </style>