Files
zhiyin/zhiyin-app/src/pages/user/user.vue
T
yuzhiran 4180eae944 feat: 意见反馈功能
新增完整反馈闭环:
- 后端: Feedback 模块 (schema+service+controller+module)
  - POST /api/feedback (用户提交)
  - GET /api/feedback (管理员列表)
  - PATCH /api/feedback/:id/resolve (管理员标记已处理)
- 前端: pages/feedback/feedback.vue
  - 三种反馈类型: 问题反馈/改进建议/点赞鼓励
  - 文本输入 + 联系方式选填
  - 提交后显示成功提示
- 用户页新增'意见反馈'菜单入口
- 管理后台可通过 API 查看和管理反馈
2026-07-06 11:13:08 +08:00

494 lines
23 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page fade-in">
<!-- 个人中心 -->
<view class="header" v-if="isLoggedIn">
<view class="profile-section">
<image class="avatar" :src="userInfo.avatar || '/static/avatar-default.png'" mode="aspectFill" />
<view class="profile-info">
<text class="nickname">{{ userInfo.nickname || '未设置昵称' }}</text>
<text class="user-id">ID: {{ maskedId }}</text>
<view class="plan-badge">{{ userInfo.plan || '免费版' }}</view>
</view>
<text class="header-arrow"></text>
</view>
<view class="stats-bar">
<view class="stat">
<text class="stat-num">{{ stats.interviewCount || 0 }}</text>
<text class="stat-label">模拟面试</text>
</view>
<view class="stat-divider"></view>
<view class="stat">
<text class="stat-num">{{ stats.avgScore || '--' }}</text>
<text class="stat-label">平均得分</text>
</view>
<view class="stat-divider"></view>
<view class="stat">
<text class="stat-num">{{ stats.completedCount || 0 }}</text>
<text class="stat-label">已完成</text>
</view>
</view>
</view>
<view class="header header-guest" v-else @click="goLogin">
<view class="guest-box">
<view class="guest-avatar"><text class="guest-icon">👤</text></view>
<view class="guest-info">
<text class="guest-name">未登录 / 点击登录</text>
</view>
<text class="header-arrow"></text>
</view>
</view>
<!-- 引力值卡片代替原配额卡片 -->
<view class="gravity-card" v-if="isLoggedIn">
<view class="gravity-card-inner">
<view class="gravity-top-row">
<view class="gravity-header">
<text class="gravity-icon"></text>
<text class="gravity-label">我的引力值</text>
</view>
<text class="gravity-num">{{ memberInfo.gravity ?? 0 }}</text>
</view>
<text class="gravity-hint">每次面试消耗 5 引力值 · 分享好友贡献面经可获得更多</text>
<view class="gravity-actions">
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
<text class="gravity-btn h5buy" @click="goH5Buy">购买引力值</text>
<text class="gravity-btn detail" @click="showGravityDetail = true">明细</text>
</view>
</view>
</view>
<!-- 菜单列表 -->
<view class="menu-area">
<view class="menu-group">
<view class="menu-item" @click="requireLogin(goCareer, '择业顾问')">
<view class="menu-icon-wrap wrap-teal"><text class="menu-icon">🧭</text></view>
<text class="menu-text">择业顾问</text>
<text class="menu-tag">NEW</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="requireLogin(goHistory, '面试记录')">
<view class="menu-icon-wrap wrap-blue"><text class="menu-icon">📋</text></view>
<text class="menu-text">面试记录</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="requireLogin(goReviewReview, '面试复盘')">
<view class="menu-icon-wrap wrap-orange"><text class="menu-icon">🎙</text></view>
<text class="menu-text">面试复盘</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="goVip">
<view class="menu-icon-wrap wrap-purple"><text class="menu-icon">💎</text></view>
<text class="menu-text">会员中心</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="requireLogin(goResume, '我的简历')">
<view class="menu-icon-wrap wrap-green"><text class="menu-icon">📄</text></view>
<text class="menu-text">我的简历</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="requireLogin(goSharePage, '我的分享')">
<view class="menu-icon-wrap wrap-orange"><text class="menu-icon">📤</text></view>
<text class="menu-text">我的分享</text>
<text class="menu-arrow"></text>
</view>
</view>
<view class="menu-group">
<!-- #ifdef MP-WEIXIN -->
<view class="menu-item">
<button class="contact-btn-inner" open-type="contact">
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon">💬</text></view>
<text class="menu-text">联系客服</text>
<text class="menu-arrow"></text>
</button>
</view>
<!-- #endif -->
<view class="menu-item" @click="goFeedback">
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon">📝</text></view>
<text class="menu-text">意见反馈</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" @click="goAbout">
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon"></text></view>
<text class="menu-text">关于</text>
<text class="menu-arrow"></text>
</view>
<view class="menu-item" v-if="isAdmin" @click="goAdmin">
<view class="menu-icon-wrap wrap-gray"><text class="menu-icon"></text></view>
<text class="menu-text">管理后台</text>
<text class="menu-arrow"></text>
</view>
</view>
<view class="logout-wrap" v-if="isLoggedIn">
<button class="logout-btn" @click="doLogout">退出登录</button>
</view>
</view>
<!-- 引力值获取方式 -->
<view class="modal-overlay" v-if="showGetGravityModal" @click="showGetGravityModal = false">
<view class="modal-content" @click.stop>
<text class="modal-title">获取引力值</text>
<text class="modal-hint">引力值可用于面试简历优化下载等</text>
<!-- 分享得引力值 -->
<view class="gp-get-method" @click="goSharePage">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🎁</text></view>
<view class="gp-method-info">
<text class="gp-method-name">分享给好友</text>
<text class="gp-method-desc">每成功邀请一位好友注册可得 5 引力值上不封顶</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<!-- 贡献面经 -->
<view class="gp-get-method" @click="goContributePage">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">📝</text></view>
<view class="gp-method-info">
<text class="gp-method-name">贡献面经</text>
<text class="gp-method-desc">每发布一篇面经可获得 3 引力值助力他人也提升自己</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<!-- 购买引力值 -->
<view class="gp-get-method" @click="goH5Buy">
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
<view class="gp-method-info">
<text class="gp-method-name">购买引力值</text>
<text class="gp-method-desc">¥5 一份微信支付安全便捷</text>
</view>
<text class="gp-method-arrow"></text>
</view>
<text class="modal-close" @click="showGetGravityModal = false">关闭</text>
</view>
</view>
<!-- 引力值明细 -->
<view class="modal-overlay" v-if="showGravityDetail" @click="showGravityDetail = false">
<view class="modal-content detail-content" @click.stop>
<text class="modal-title">引力值明细</text>
<text class="modal-hint">购买消耗补给的引力值变动记录</text>
<scroll-view class="detail-list" scroll-y>
<view v-if="gravityTxs.length === 0" class="detail-empty">暂无记录</view>
<view v-for="tx in gravityTxs" :key="tx._id" class="detail-item">
<view class="detail-item-left">
<text class="detail-item-desc">{{ tx.description }}</text>
<text class="detail-item-time">{{ formatTime(tx.createdAt) }}</text>
</view>
<view class="detail-item-right">
<text :class="['detail-item-amount', tx.amount > 0 ? 'amount-positive' : 'amount-negative']">
{{ tx.amount > 0 ? '+' : '' }}{{ tx.amount }}
</text>
<text class="detail-item-balance">余额 {{ tx.balance }}</text>
</view>
</view>
</scroll-view>
<view v-if="gravityTxTotalPages > 1" class="detail-pagination">
<text class="detail-page-btn" @click="loadGravityTxs(gravityTxPage - 1)" v-if="gravityTxPage > 1">上一页</text>
<text class="detail-page-info">{{ gravityTxPage }} / {{ gravityTxTotalPages }}</text>
<text class="detail-page-btn" @click="loadGravityTxs(gravityTxPage + 1)" v-if="gravityTxPage < gravityTxTotalPages">下一页</text>
</view>
<text class="modal-close" @click="showGravityDetail = false">关闭</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// #ifdef MP-WEIXIN
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
// #endif
// #ifndef MP-WEIXIN
import { onShow } from '@dcloudio/uni-app'
// #endif
import { api } from '../../config'
import { checkAuth } from '../../utils/auth'
// #ifdef MP-WEIXIN
const shareCodeRef = ref('')
onShareAppMessage(() => {
const path = shareCodeRef.value
? `/pages/user/user?share=${shareCodeRef.value}`
: '/pages/user/user'
return { title: '职引 - 个人中心 | AI模拟面试', path }
})
onShareTimeline(() => ({ title: '职引 - 个人中心 | AI模拟面试' }))
// #endif
const userInfo = ref({})
const isAdmin = ref(false)
const stats = ref({ interviewCount: 0, avgScore: '--', completedCount: 0 })
const token = ref('')
const memberInfo = ref({ plan: 'free', planName: '免费版', remaining: 0, gravity: 0 })
const isLoggedIn = computed(() => !!token.value)
const maskedId = computed(() => {
const id = userInfo.value.id || ''
return id.length > 6 ? `****${id.slice(-6)}` : id || '--'
})
const refreshState = () => {
token.value = uni.getStorageSync('token') || ''
if (!token.value) return
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s) } catch(e) {}
loadStats()
loadMemberStatus()
checkAdmin()
fetchUserInfo()
// #ifdef MP-WEIXIN
preCreateShare()
// #endif
}
// #ifdef MP-WEIXIN
async function preCreateShare() {
if (shareCodeRef.value) return // already have one
try {
const res = await uni.request({
url: api('/share/create'), method: 'POST',
data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' },
header: { Authorization: `Bearer ${token.value}` },
})
if (res.statusCode < 200 || res.statusCode >= 300) {
checkAuth(res)
return
}
const data = res.data?.data || res.data
if (data.shareCode) {
shareCodeRef.value = data.shareCode
}
} catch(e) { /* silent */ }
}
// #endif
const fetchUserInfo = async () => {
try {
const res = await uni.request({ url: api('/user/info'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
if (res.statusCode === 200 && res.data) {
userInfo.value = res.data
uni.setStorageSync('userInfo', JSON.stringify(res.data))
} else if (checkAuth(res)) {
return // token 过期,已清除并跳转登录
}
} catch(e) { /* silent */ }
}
onMounted(refreshState)
onShow(refreshState)
const loadMemberStatus = async () => {
if (!token.value) return
try {
const res = await uni.request({ url: api('/member/status'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
memberInfo.value = { plan: res.data.plan || 'free', planName: res.data.planName || '免费版', remaining: res.data.remaining ?? 0, gravity: res.data.gravity ?? 0 }
} else if (checkAuth(res)) {
return
}
} catch(e) { /* silent */ }
}
const loadStats = async () => {
try {
const res = await uni.request({ url: api('/interview/stats/mine'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
if (res.statusCode === 200) {
stats.value = res.data
} else if (checkAuth(res)) {
return
}
} catch(e) { console.error(e) }
}
const requireLogin = (action, name) => {
if (isLoggedIn.value) { action(); return }
uni.showModal({
title: '请先登录',
content: `需要登录后才能使用${name}功能`,
confirmText: '去登录',
success: (r) => { if (r.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
})
}
const checkAdmin = () => {
isAdmin.value = userInfo.value.role === 'admin'
}
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
// 引力值获取
const showGetGravityModal = ref(false)
const openBuyModal = () => { showGetGravityModal.value = true }
const goH5Buy = () => {
uni.navigateTo({ url: '/pages/member/member' })
}
// 引力值明细
const showGravityDetail = ref(false)
const gravityTxs = ref([])
const gravityTxPage = ref(1)
const gravityTxTotalPages = ref(1)
const loadGravityTxs = async (page = 1) => {
try {
const res = await uni.request({
url: api(`/user/gravity-transactions?page=${page}&limit=20`),
method: 'GET',
header: { Authorization: `Bearer ${token.value}` },
})
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
gravityTxs.value = res.data.items || []
gravityTxPage.value = res.data.page || 1
gravityTxTotalPages.value = res.data.totalPages || 1
} else if (checkAuth(res)) {
return
}
} catch(e) { /* silent */ }
}
const formatTime = (t) => {
if (!t) return ''
const d = new Date(t)
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
// 点击明细时打开弹窗并加载数据
watch(showGravityDetail, (v) => { if (v) loadGravityTxs() })
const goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
const goHistory = () => uni.switchTab({ url: '/pages/history/history' })
const goReviewReview = () => uni.navigateTo({ url: '/pages/review/review' })
const goVip = () => uni.navigateTo({ url: '/pages/member/member' })
const goResume = () => uni.navigateTo({ url: '/pages/resume/resume' })
const goSharePage = () => uni.navigateTo({ url: '/pages/share/share' })
const goContributePage = () => uni.navigateTo({ url: '/pages/contribute/contribute' })
const goAdmin = () => uni.navigateTo({ url: '/pages/admin/admin' })
const goFeedback = () => uni.navigateTo({ url: '/pages/feedback/feedback' })
const goAbout = () => uni.navigateTo({ url: '/pages/about/about' })
const doLogout = () => {
uni.showModal({
title: '退出登录', content: '确定要退出登录吗?',
success: (r) => { if (r.confirm) { uni.removeStorageSync('token'); uni.removeStorageSync('userInfo'); token.value = '' } }
})
}
</script>
<style scoped>
.page { height: 100%; overflow-y: auto; background: var(--color-bg); }
.header {
background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-mid) 50%, var(--color-gradient-end) 100%);
padding: 48rpx 32rpx 72rpx; border-radius: 0 0 48rpx 48rpx; min-height: 90rpx;
}
.profile-section { display: flex; align-items: center; margin-bottom: 36rpx; }
.avatar { width: 104rpx; height: 104rpx; border-radius: 50%; margin-right: 24rpx; border: 3rpx solid rgba(255,255,255,0.4); flex-shrink: 0; }
.profile-info { flex: 1; display: flex; flex-direction: column; }
.nickname { font-size: 34rpx; font-weight: 700; color: #FFFFFF; }
.plan-badge { font-size: 20rpx; color: rgba(255,255,255,0.9); background: rgba(255,255,255,0.2); padding: 4rpx 14rpx; border-radius: 8rpx; align-self: flex-start; margin-top: 8rpx; }
.user-id { font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; }
.header-arrow { font-size: 36rpx; color: rgba(255,255,255,0.5); }
.stats-bar { display: flex; align-items: center; background: rgba(255,255,255,0.15); border-radius: var(--radius-lg); padding: 24rpx 0; }
.stat { flex: 1; display: flex; flex-direction: column; align-items: center; }
.stat-num { font-size: 34rpx; font-weight: 700; color: #FFFFFF; }
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.7); margin-top: 6rpx; }
.stat-divider { width: 1rpx; height: 44rpx; background: rgba(255,255,255,0.2); }
.header-guest { padding: 36rpx 32rpx 72rpx; min-height: 90rpx; }
.guest-box { display: flex; align-items: center; }
.guest-avatar { width: 96rpx; height: 96rpx; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
.guest-icon { font-size: 40rpx; }
.guest-info { flex: 1; }
.guest-name { font-size: 30rpx; font-weight: 600; color: #FFFFFF; }
/* 引力值卡片(代替原配额卡片) */
.gravity-card { margin: -48rpx 32rpx 16rpx; position: relative; z-index: 1; }
.gravity-card-inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius-xl);
padding: 28rpx 24rpx 24rpx;
box-shadow: 0 8rpx 32rpx rgba(102,126,234,0.25);
}
.gravity-top-row { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8rpx; }
.gravity-header { display: flex; align-items: center; gap: 8rpx; }
.gravity-icon { font-size: 32rpx; }
.gravity-label { font-size: 24rpx; color: rgba(255,255,255,0.8); font-weight: 500; }
.gravity-num { font-size: 48rpx; font-weight: 800; color: #FFFFFF; line-height: 1; }
.gravity-hint { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-bottom: 20rpx; }
.gravity-actions { display: flex; gap: 16rpx; }
.gravity-btn {
flex: 1; text-align: center; padding: 18rpx 0; border-radius: var(--radius-md);
font-size: 26rpx; font-weight: 600; white-space: nowrap;
}
.gravity-btn.share { background: rgba(255,255,255,0.2); color: #FFFFFF; border: 2rpx solid rgba(255,255,255,0.3); }
.gravity-btn.h5buy { background: #FFFFFF; color: #667eea; }
.gravity-btn.contribute { background: rgba(255,255,255,0.15); color: #FFFFFF; border: 2rpx solid rgba(255,255,255,0.2); }
.gravity-btn.detail { background: rgba(255,255,255,0.15); color: #FFFFFF; border: 2rpx solid rgba(255,255,255,0.2); font-size: 22rpx; padding: 18rpx 16rpx; flex: 0.5; }
.gravity-btn:active { transform: scale(0.96); }
.menu-area { padding: 0 32rpx 32rpx; margin-top: 8rpx; }
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
.menu-item { display: flex; align-items: center; padding: 28rpx 32rpx; border-bottom: 1rpx solid var(--color-border); }
.contact-btn-inner { width: 100%; background: transparent; border: none; border-radius: 0; padding: 0; margin: 0; line-height: inherit; font-size: inherit; text-align: left; display: flex; align-items: center; min-height: auto; }
.contact-btn-inner::after { border: none; }
.menu-item:last-child { border-bottom: none; }
.menu-item:active { background: #F9FAFB; }
.menu-icon-wrap { width: 60rpx; height: 60rpx; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
.menu-icon { font-size: 28rpx; }
.menu-text { flex: 1; font-size: 28rpx; color: var(--color-text); font-weight: 500; }
.menu-tag { font-size: 18rpx; color: #fff; background: var(--color-primary); padding: 2rpx 12rpx; border-radius: 20rpx; margin-right: 12rpx; }
.menu-arrow { font-size: 32rpx; color: #D1D5DB; }
.wrap-blue { background: #EEF2FF; }
.wrap-purple { background: #F5F3FF; }
.wrap-green { background: #ECFDF5; }
.wrap-orange { background: #FFF7ED; }
.wrap-gray { background: #F3F4F6; }
.wrap-teal { background: #E6FFFA; }
.logout-wrap { margin-top: 8rpx; }
.logout-btn { background: #FFFFFF; color: var(--color-error); font-size: 28rpx; font-weight: 500; border-radius: var(--radius-md); height: 88rpx; line-height: 88rpx; border: 2rpx solid #FECACA; }
.logout-btn:active { background: #FEF2F2; transform: scale(0.96); }
/* 弹窗通用 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal-content { background: #FFF; border-radius: var(--radius-xl); padding: 40rpx 32rpx; width: 600rpx; max-height: 80vh; overflow-y: auto; display: flex; flex-direction: column; align-items: center; gap: 16rpx; }
.modal-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); }
.modal-hint { font-size: 22rpx; color: #6B7280; text-align: center; }
.modal-close { font-size: 24rpx; color: #9CA3AF; padding: 12rpx 24rpx; }
/* 获取引力值弹窗 - 分享/贡献方法 */
.gp-get-method {
width: 100%; display: flex; align-items: center; gap: 16rpx;
background: #F9FAFB; border-radius: var(--radius-md); padding: 20rpx;
border: 2rpx solid #E5E7EB; margin-top: 8rpx;
}
.gp-get-method:active { transform: scale(0.97); background: #F3F4F6; }
.gp-method-icon-wrap {
width: 64rpx; height: 64rpx; border-radius: var(--radius-md);
background: #EEF2FF; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.gp-method-icon { font-size: 32rpx; }
.gp-method-info { flex: 1; display: flex; flex-direction: column; gap: 4rpx; }
.gp-method-name { font-size: 26rpx; font-weight: 600; color: var(--color-text); }
.gp-method-desc { font-size: 20rpx; color: #6B7280; line-height: 1.4; }
.gp-method-arrow { font-size: 32rpx; color: #D1D5DB; }
/* 引力值明细弹窗 */
.detail-content { width: 650rpx; max-height: 70vh; }
.detail-list { width: 100%; max-height: 500rpx; }
.detail-empty { text-align: center; padding: 40rpx 0; font-size: 24rpx; color: #9CA3AF; }
.detail-item { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 20rpx 0; border-bottom: 1rpx solid #F3F4F6; }
.detail-item:last-child { border-bottom: none; }
.detail-item-left { display: flex; flex-direction: column; gap: 4rpx; flex: 1; min-width: 0; }
.detail-item-desc { font-size: 26rpx; color: var(--color-text); font-weight: 500; }
.detail-item-time { font-size: 20rpx; color: #9CA3AF; }
.detail-item-amount { font-size: 30rpx; font-weight: 700; }
.detail-item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; flex-shrink: 0; margin-left: 16rpx; }
.detail-item-balance { font-size: 20rpx; color: #9CA3AF; font-weight: 400; }
.amount-positive { color: #10B981; }
.amount-negative { color: #EF4444; }
.detail-pagination { display: flex; align-items: center; justify-content: center; gap: 24rpx; width: 100%; margin-top: 8rpx; }
.detail-page-btn { font-size: 24rpx; color: var(--color-primary); padding: 8rpx 16rpx; }
.detail-page-btn:active { opacity: 0.6; }
.detail-page-info { font-size: 24rpx; color: #6B7280; }
</style>