文件
fenleixinxi/uniapp/pages/detail/detail.vue
MiAn 81a48feaad feat: 对接真实数据并移除前端假数据
本次提交完成多模块真实数据对接,移除前端硬编码假数据:
- 新增后端接口:区域列表、未读消息数、详情页评价列表、最低提现金额
- 前端对应修改:移除首页/列表/详情页硬编码区域选项、未读消息数、评价内容,分销页最低提现金额
- 修复登录页微信授权不伪造code、营业执照不伪造文件名,避免绕过校验
- 新增集成测试用例,验证真实数据对接正确性
2026-10-19 11:48:41 +08:00

900 行
24 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="detail fc-page">
<!-- 顶部视觉 -->
<view class="detail__hero">
<view class="detail__hero-thumb">
<image class="detail__hero-img" :src="catIcon()" mode="aspectFit" />
</view>
<view class="detail__hero-badges">
<text class="fc-tag detail__badge">{{ post.categoryName }}</text>
<text v-if="post.isPaid" class="fc-tag detail__badge detail__badge--paid">置顶推荐</text>
</view>
</view>
<!-- 标题与价格 -->
<view class="fc-card detail__head">
<text class="detail__title">{{ post.title }}</text>
<view class="detail__price-row">
<view class="fc-price">
<text v-if="post.price > 0" class="fc-price__symbol">¥</text>
<text v-if="post.price > 0" class="detail__amount">{{ post.price }}</text>
<text v-if="post.price > 0" class="detail__unit">{{ post.priceUnit }}</text>
<text v-if="!(post.price > 0)" class="detail__neg">面议</text>
</view>
<view class="detail__stats">
<view class="fc-inline">
<image class="fc-icon fc-icon--sm" :src="iconUrl('eye')" mode="aspectFit" />
<text class="detail__stat-num">{{ post.views }}</text>
</view>
<view class="fc-inline detail__stat-gap">
<image class="fc-icon fc-icon--sm" :src="iconUrl('comment-gray')" mode="aspectFit" />
<text class="detail__stat-num">{{ post.commentCount || 0 }}</text>
</view>
</view>
</view>
<view class="detail__meta">
<text>{{ post.area }} · {{ post.address }}</text>
</view>
</view>
<!-- 发布者 -->
<view class="fc-card detail__publisher">
<view class="detail__avatar">{{ (post.publisher && post.publisher.name || '同').slice(0, 1) }}</view>
<view class="detail__publisher-info">
<view class="detail__publisher-name-row">
<text class="detail__publisher-name">{{ post.publisher && post.publisher.name }}</text>
<text v-if="post.publisher && post.publisher.verified" class="fc-tag detail__verified">已认证</text>
</view>
<text class="detail__publisher-sub">
{{ post.publisher && post.publisher.isMerchant ? '商家用户' : '个人用户' }} · {{ timeText }}发布
</text>
</view>
<view class="detail__follow" @click="toggleFollow">
<text>{{ followed ? '已关注' : '+ 关注' }}</text>
</view>
</view>
<!-- 内容展示 -->
<view class="fc-card">
<text class="detail__section-title">详细描述</text>
<text class="detail__content">{{ post.content }}</text>
<view class="detail__tags">
<text v-for="t in post.tags || []" :key="t" class="fc-tag detail__tag">{{ t }}</text>
</view>
<view v-if="post.address" class="detail__map" @click="openMap">
<view class="detail__map-bg">
<image class="fc-icon fc-icon--md" :src="iconUrl('home-location')" mode="aspectFit" />
</view>
<view class="detail__map-info">
<text class="detail__map-name">{{ post.address }}</text>
<text class="detail__map-action">{{ hasLocation ? '地图导航 ›' : '暂无定位信息' }}</text>
</view>
</view>
<view class="detail__report" @click="openReport">
<text class="detail__report-text">举报该信息 ›</text>
</view>
</view>
<!-- 付费查看联系方式 -->
<view class="fc-card detail__contact">
<text class="detail__section-title">联系方式</text>
<view v-if="unlocked" class="detail__contact-inner">
<view class="detail__contact-row">
<text class="detail__contact-label">电话</text>
<text class="detail__contact-value" @click="callPhone">{{ contact.phone }}</text>
</view>
<view class="detail__contact-row">
<text class="detail__contact-label">微信</text>
<text class="detail__contact-value">{{ contact.wechat }}</text>
</view>
<view class="detail__contact-actions">
<view class="detail__contact-btn" @click="callPhone">
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
<text class="detail__btn-text">拨打电话</text>
</view>
<view class="detail__contact-btn detail__contact-btn--ghost" @click="copyWechat">
<text>复制微信号</text>
</view>
</view>
</view>
<view v-else class="detail__locked">
<image class="fc-icon fc-icon--xl" :src="iconUrl('lock')" mode="aspectFit" />
<text class="detail__locked-text">非会员查看完整联系方式需付费或开通会员</text>
<view class="detail__pay-type">
<view class="detail__pay-item" :class="{ 'detail__pay-item--active': payType === 'balance' }"
@click="payType = 'balance'"><text>余额支付</text></view>
<view class="detail__pay-item" :class="{ 'detail__pay-item--active': payType === 'wxpay' }"
@click="payType = 'wxpay'"><text>微信支付</text></view>
</view>
<view class="detail__locked-actions">
<view class="detail__locked-btn" @click="payView(payType)"><text>¥2 付费查看</text></view>
<view class="detail__locked-btn detail__locked-btn--vip" @click="goVip"><text>开通会员免费看</text></view>
</view>
</view>
</view>
<!-- 评价互动 -->
<view class="fc-card">
<view class="detail__cmt-head">
<text class="detail__section-title">用户评价({{ comments.length }})</text>
<text class="detail__cmt-write" @click="showComment = true">写评价 ›</text>
</view>
<view v-for="c in comments" :key="c.id" class="detail__cmt">
<view class="detail__cmt-top">
<text class="detail__cmt-user">{{ c.user }}</text>
<view class="detail__cmt-score">
<image v-for="n in 5" :key="n" class="fc-icon fc-icon--xs"
:src="iconUrl(n <= c.score ? 'star' : 'star-o')" mode="aspectFit" />
</view>
</view>
<text class="detail__cmt-content">{{ c.content }}</text>
<text v-if="c.reply" class="detail__cmt-reply">商家回复:{{ c.reply }}</text>
</view>
<text v-if="!comments.length" class="detail__cmt-empty">暂无评价,快来抢沙发~</text>
</view>
<view class="detail__bar-space"></view>
<!-- 底部操作栏 -->
<view class="detail__bar">
<view class="detail__bar-item" @click="toggleFav">
<image class="fc-icon fc-icon--md" :src="iconUrl(faved ? 'bar-fav-on' : 'bar-fav')" mode="aspectFit" />
<text class="detail__bar-text">收藏</text>
</view>
<view class="detail__bar-item" @click="onShare">
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-share')" mode="aspectFit" />
<text class="detail__bar-text">分享</text>
</view>
<view class="detail__bar-item" @click="showPoster = true">
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-poster')" mode="aspectFit" />
<text class="detail__bar-text">海报</text>
</view>
<view class="detail__bar-main" @click="unlocked ? callPhone() : payView(payType)">
<text>{{ unlocked ? '立即联系' : '付费查看' }}</text>
</view>
</view>
<!-- 写评价弹层 -->
<view v-if="showComment" class="detail__mask" @click="showComment = false">
<view class="detail__sheet" @click.stop>
<text class="detail__sheet-title">发表评价</text>
<view class="detail__stars">
<image v-for="n in 5" :key="n" class="detail__star-img"
:src="iconUrl(n <= score ? 'star' : 'star-o')" mode="aspectFit" @click="score = n" />
</view>
<textarea v-model="commentText" class="detail__sheet-input" placeholder="说说你的体验…" maxlength="200" />
<view class="detail__sheet-btn" @click="submitComment"><text>提交评价</text></view>
</view>
</view>
<!-- 海报弹层 -->
<view v-if="showPoster" class="detail__mask" @click="showPoster = false">
<view class="detail__poster" @click.stop>
<view class="detail__poster-head">
<text class="detail__poster-title">{{ post.title }}</text>
<text class="detail__poster-price">¥{{ post.price > 0 ? post.price : '面议' }}</text>
</view>
<text class="detail__poster-tip">长按识别 / 保存海报分享给好友</text>
<view class="fc-inline detail__poster-qr">
<image class="fc-icon fc-icon--sm" :src="iconUrl('qrcode')" mode="aspectFit" />
<text class="detail__poster-qr-text">扫码查看同城详情</text>
</view>
</view>
</view>
</view>
</template>
<script>
import postApi from '@/common/api/post.js'
import favoriteApi from '@/common/api/favorite.js'
import reportApi from '@/common/api/report.js'
import { REPORT_REASONS } from '@/common/constants/report.js'
import { iconUrl, categoryIconUrl, loadUiConfig } from '@/common/store/ui.js'
import { fromNow, maskPhone } from '@/common/utils/format.js'
import { pay } from '@/common/utils/pay.js'
import { useUserStore } from '@/common/store/index.js'
import config from '@/common/config/index.js'
export default {
data() {
return {
id: 0,
post: { publisher: {}, tags: [] },
unlocked: false,
payType: config.defaultPayType,
contact: { phone: '', wechat: '' },
comments: [],
followed: false,
faved: false,
favoriteCount: 0,
reportReasons: REPORT_REASONS,
showComment: false,
showPoster: false,
score: 5,
commentText: '',
// 支付/提交中:防连点重复下单(余额支付是即时扣款)
submitting: false,
}
},
computed: {
timeText() {
return fromNow(this.post.createdAt)
},
/** 详情是否带真实坐标(契约新增 lat/lng) */
hasLocation() {
return this.post.lat != null && this.post.lng != null
},
},
onLoad(query) {
this.id = Number(query.id || 101)
loadUiConfig()
this.fetch()
},
methods: {
iconUrl,
catIcon() {
return categoryIconUrl(this.post.categoryId)
},
async fetch() {
try {
const res = await postApi.detail(this.id)
this.post = res
// 用户评价:来自后端 /post/:id/comments(原先写死两条假评价)
try {
const cs = await postApi.comments(this.id)
this.comments = Array.isArray(cs) ? cs : []
} catch (e) {
this.comments = []
}
this.contact = { phone: maskPhone(res.contact), wechat: '' }
// 契约:详情出参已带 faved/followed,先用返回值回显;
// 兼容旧后端:缺失时再用 /favorite/status 批量回显。
this.faved = !!res.faved
this.followed = !!res.followed
this.favoriteCount = Number(res.favoriteCount || 0)
// /favorite/status 强制登录(游客调用会 401 → 被拦截器清 token 并跳登录页),
// 因此只在已登录时用它兜底回显;未登录直接用详情返回的 faved/followed。
if (this.isLogin()) await this.loadInteractStatus()
} catch (e) {
uni.showToast({ title: '信息加载失败', icon: 'none' })
}
},
/** 是否已登录(未登录时不请求需要登录态的接口) */
isLogin() {
try {
return !!useUserStore().isLogin.value
} catch (e) {
return false
}
},
/** 进入详情时回显收藏 / 关注状态(失败静默,保持默认状态) */
async loadInteractStatus() {
const publisherId = this.post.publisher && this.post.publisher.id
try {
const res = await favoriteApi.status({ postIds: [this.id], userIds: publisherId ? [publisherId] : [] })
if (res && res.faved) this.faved = !!res.faved[this.id]
if (res && res.followed && publisherId) this.followed = !!res.followed[publisherId]
} catch (e) {
// 状态回显失败不影响详情可用性
}
},
async payView(payType) {
if (this.submitting) return
this.submitting = true
try {
const res = await postApi.payView(this.id, { payType })
// 微信支付:下单只返回 {order}(**不含 contact**),解锁由支付回调 /pay/notify 写 fc_pay_view 完成
if (res.order) {
await pay({ ...res.order, payType })
await this.waitUnlock()
return
}
this.applyContact(res)
uni.showToast({ title: '已解锁完整联系方式', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message || '支付失败', icon: 'none' })
} finally {
this.submitting = false
}
},
/** 写入已解锁的联系方式(余额/会员/重新拉取时复用) */
applyContact(res) {
const c = (res && res.contact) || {}
this.unlocked = true
this.contact = { phone: c.phone || '', wechat: c.wechat || '' }
},
/**
* 微信支付成功后轮询解锁结果:回调写 fc_pay_view 有延迟,
* 再次调用 /pay-view 只会命中后端「已解锁直接返回」分支(不会重复扣费),最多试 4 次。
*/
async waitUnlock() {
for (let i = 0; i < 4; i++) {
await new Promise((resolve) => setTimeout(resolve, i === 0 ? 800 : 1200))
try {
const res = await postApi.payView(this.id, { payType: config.constants.PAY_TYPE.WXPAY })
if (res && res.unlocked) {
this.applyContact(res)
uni.showToast({ title: '已解锁完整联系方式', icon: 'none' })
return
}
} catch (e) {
// 回调尚未到达:继续重试
}
}
uni.showToast({ title: '支付成功,解锁结果稍后生效', icon: 'none' })
},
async submitComment() {
if (!this.commentText.trim()) {
uni.showToast({ title: '请输入评价内容', icon: 'none' })
return
}
try {
await postApi.comment(this.id, { score: this.score, content: this.commentText })
this.commentText = ''
// 重新拉取评价列表,确保展示后台真实数据(含昵称)
try {
const cs = await postApi.comments(this.id)
this.comments = Array.isArray(cs) ? cs : this.comments
} catch (e) {}
this.showComment = false
uni.showToast({ title: '评价成功', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message, icon: 'none' })
}
},
/** 关注:调接口,失败回退本地状态并 Toast(保持可用) */
async toggleFollow() {
const publisherId = this.post.publisher && this.post.publisher.id
if (!publisherId) return uni.showToast({ title: '发布者信息缺失', icon: 'none' })
const prev = this.followed
this.followed = !prev
try {
const res = await favoriteApi.follow({ userId: publisherId })
this.followed = !!(res && res.followed)
uni.showToast({ title: this.followed ? '已关注' : '已取消关注', icon: 'none' })
} catch (e) {
this.followed = prev
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
},
/** 收藏:调接口,失败回退本地状态并 Toast(保持可用) */
async toggleFav() {
const prev = this.faved
this.faved = !prev
try {
const res = await favoriteApi.toggle({ postId: this.id })
this.faved = !!(res && res.faved)
if (res && typeof res.count === 'number') this.favoriteCount = res.count
uni.showToast({ title: this.faved ? '已收藏' : '已取消收藏', icon: 'none' })
} catch (e) {
this.faved = prev
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
},
/** 举报:原因取自常量(与后端 report.reasons 默认值一致) */
openReport() {
uni.showActionSheet({
itemList: this.reportReasons,
success: async (r) => {
const reason = this.reportReasons[r.tapIndex]
if (!reason) return
try {
await reportApi.create({ targetType: 'post', targetId: this.id, reason })
uni.showToast({ title: '举报已提交,我们会尽快核实', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message || '举报失败', icon: 'none' })
}
},
fail: () => {},
})
},
goVip() {
uni.navigateTo({ url: '/pages/member/index' })
},
openMap() {
if (!this.hasLocation) {
uni.showToast({ title: '暂无定位信息', icon: 'none' })
return
}
uni.openLocation({
latitude: Number(this.post.lat),
longitude: Number(this.post.lng),
name: this.post.address || this.post.title,
address: this.post.area || '',
fail: () => uni.showToast({ title: '导航需在小程序内使用', icon: 'none' }),
})
},
callPhone() {
if (!this.unlocked) return this.payView(this.payType)
uni.makePhoneCall({ phoneNumber: this.contact.phone })
},
copyWechat() {
uni.setClipboardData({ data: this.contact.wechat })
uni.showToast({ title: '微信号已复制', icon: 'none' })
},
onShare() {
uni.showToast({ title: '已生成分享链接', icon: 'none' })
},
},
}
</script>
<style lang="scss" scoped>
.detail {
padding-bottom: 40rpx;
}
.detail__hero {
padding: 32rpx 24rpx 0;
}
.detail__hero-thumb {
height: 420rpx;
border-radius: $fc-radius-lg;
background: $fc-gradient-soft;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.detail__hero-img {
width: 240rpx;
height: 240rpx;
}
.detail__hero-badges {
display: flex;
margin-top: 20rpx;
}
.detail__badge {
margin-right: 12rpx;
}
.detail__badge--paid {
background: $fc-gradient-warm;
color: #ffffff;
}
.detail__head {
margin-top: 20rpx;
}
.detail__title {
font-size: $fc-font-title;
font-weight: 700;
color: $fc-text;
line-height: 1.35;
}
.detail__price-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: 20rpx;
}
.detail__amount {
font-size: 48rpx;
}
.detail__unit {
margin-left: 6rpx;
font-size: $fc-font-xs;
color: $fc-text-secondary;
font-weight: 400;
}
.detail__neg {
font-size: 40rpx;
}
.detail__stats {
display: flex;
font-size: $fc-font-xs;
color: $fc-text-placeholder;
}
.detail__stat-num {
margin-left: 6rpx;
font-size: $fc-font-xs;
color: $fc-text-placeholder;
}
.detail__stat-gap {
margin-left: 18rpx;
}
.detail__meta {
margin-top: 16rpx;
font-size: $fc-font-sm;
color: $fc-text-secondary;
}
.detail__publisher {
display: flex;
align-items: center;
}
.detail__avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: $fc-gradient-warm;
color: #ffffff;
font-size: 36rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.detail__publisher-info {
flex: 1;
margin-left: 20rpx;
}
.detail__publisher-name-row {
display: flex;
align-items: center;
}
.detail__publisher-name {
font-size: $fc-font-lg;
font-weight: 600;
color: $fc-text;
}
.detail__verified {
margin-left: 12rpx;
background: #e9f9ee;
color: $fc-success;
}
.detail__publisher-sub {
display: block;
margin-top: 6rpx;
font-size: $fc-font-xs;
color: $fc-text-secondary;
}
.detail__follow {
padding: 14rpx 28rpx;
border-radius: $fc-radius-pill;
border: 2rpx solid $fc-primary-text;
color: $fc-primary-text;
font-size: $fc-font-sm;
}
.detail__section-title {
font-size: $fc-font-lg;
font-weight: 700;
color: $fc-text;
}
.detail__content {
display: block;
margin-top: 18rpx;
font-size: $fc-font;
color: $fc-text-regular;
line-height: 1.7;
}
.detail__tags {
display: flex;
flex-wrap: wrap;
margin-top: 18rpx;
}
.detail__tag {
margin: 0 12rpx 12rpx 0;
}
.detail__map {
margin-top: 24rpx;
display: flex;
align-items: center;
background: $fc-bg;
border-radius: $fc-radius;
padding: 20rpx;
}
.detail__map-bg {
width: 120rpx;
height: 90rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, #d8f1e8, #93d6c2);
display: flex;
align-items: center;
justify-content: center;
}
.detail__map-pin {
font-size: 36rpx;
}
.detail__map-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
}
.detail__map-name {
font-size: $fc-font;
color: $fc-text;
}
.detail__map-action {
margin-top: 8rpx;
font-size: $fc-font-xs;
color: $fc-primary-text;
}
/* 举报入口:低调,不抢主操作 */
.detail__report {
margin-top: 16rpx;
display: flex;
justify-content: flex-end;
}
.detail__report-text {
font-size: $fc-font-xs;
color: $fc-text-placeholder;
}
.detail__locked {
display: flex;
flex-direction: column;
align-items: center;
padding: 32rpx 0 8rpx;
}
.detail__locked-icon {
font-size: 64rpx;
}
.detail__locked-text {
margin-top: 16rpx;
font-size: $fc-font-sm;
color: $fc-text-secondary;
text-align: center;
}
.detail__locked-actions {
display: flex;
margin-top: 28rpx;
}
/* 支付方式选择(付费查看) */
.detail__pay-type {
display: flex;
margin-top: 24rpx;
}
.detail__pay-item {
min-width: 168rpx;
height: 66rpx;
padding: 0 24rpx;
margin-right: 16rpx;
border-radius: $fc-radius;
border: 2rpx solid $fc-border;
background: $fc-bg;
display: flex;
align-items: center;
justify-content: center;
font-size: $fc-font-sm;
color: $fc-text-regular;
&--active {
border-color: $fc-primary-text;
background: $fc-primary-lighter;
color: $fc-primary-text;
font-weight: 600;
}
}
.detail__locked-btn {
padding: 18rpx 40rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: $fc-font-sm;
font-weight: 600;
&--vip {
margin-left: 20rpx;
background: $fc-gradient-warm;
}
}
.detail__contact-row {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 2rpx solid $fc-border;
}
.detail__contact-label {
font-size: $fc-font;
color: $fc-text-secondary;
}
.detail__contact-value {
font-size: $fc-font-lg;
font-weight: 600;
color: $fc-primary-text;
}
.detail__contact-actions {
display: flex;
margin-top: 24rpx;
}
.detail__contact-btn {
flex: 1;
height: 84rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
display: flex;
align-items: center;
justify-content: center;
font-size: $fc-font;
&--ghost {
margin-left: 20rpx;
background: #ffffff;
border: 2rpx solid $fc-primary-text;
color: $fc-primary-text;
}
}
.detail__cmt-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__cmt-write {
font-size: $fc-font-sm;
color: $fc-primary-text;
}
.detail__cmt {
margin-top: 24rpx;
padding-top: 20rpx;
border-top: 2rpx solid $fc-border;
}
.detail__cmt-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__cmt-user {
font-size: $fc-font;
font-weight: 600;
color: $fc-text;
}
.detail__btn-text {
margin-left: 10rpx;
}
.detail__cmt-score {
display: flex;
align-items: center;
}
.detail__cmt-content {
display: block;
margin-top: 12rpx;
font-size: $fc-font;
color: $fc-text-regular;
line-height: 1.6;
}
.detail__cmt-reply {
display: block;
margin-top: 12rpx;
padding: 14rpx 20rpx;
border-radius: $fc-radius-sm;
background: $fc-bg;
font-size: $fc-font-sm;
color: $fc-text-secondary;
}
.detail__cmt-empty {
display: block;
margin-top: 24rpx;
text-align: center;
font-size: $fc-font-sm;
color: $fc-text-placeholder;
}
.detail__bar-space {
height: 160rpx;
}
.detail__bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
padding: 14rpx 24rpx;
background: #ffffff;
border-top: 2rpx solid $fc-border;
box-shadow: 0 -6rpx 24rpx rgba(4, 48, 30, 0.08);
}
.detail__bar-item {
width: 110rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.detail__bar-text {
margin-top: 4rpx;
font-size: 20rpx;
color: $fc-text-secondary;
}
.detail__bar-main {
flex: 1;
height: 88rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 30rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(4, 48, 30, 0.3);
}
.detail__mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 200;
background: rgba(4, 48, 42, 0.5);
display: flex;
align-items: flex-end;
}
.detail__sheet {
width: 100%;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
padding: 36rpx 32rpx 60rpx;
}
.detail__sheet-title {
display: block;
text-align: center;
font-size: $fc-font-lg;
font-weight: 700;
}
.detail__stars {
display: flex;
justify-content: center;
margin: 28rpx 0;
}
.detail__star-img {
width: 56rpx;
height: 56rpx;
margin: 0 10rpx;
}
.detail__sheet-input {
width: 100%;
height: 180rpx;
background: $fc-bg;
border-radius: $fc-radius;
padding: 22rpx 24rpx;
font-size: $fc-font;
}
.detail__sheet-btn {
margin-top: 28rpx;
height: 88rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 30rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.detail__poster {
width: 100%;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
padding: 44rpx 40rpx 60rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.detail__poster-head {
width: 100%;
border-radius: $fc-radius-lg;
background: $fc-gradient-hero;
padding: 40rpx 32rpx;
display: flex;
flex-direction: column;
}
.detail__poster-title {
color: #ffffff;
font-size: $fc-font-xl;
font-weight: 700;
}
.detail__poster-price {
margin-top: 20rpx;
color: #ffffff;
font-size: 52rpx;
font-weight: 700;
}
.detail__poster-tip {
margin-top: 28rpx;
font-size: $fc-font-sm;
color: $fc-text-secondary;
}
.detail__poster-qr {
margin-top: 24rpx;
width: 100%;
height: 140rpx;
border-radius: $fc-radius;
background: $fc-bg;
display: flex;
align-items: center;
justify-content: center;
color: $fc-primary-text;
font-size: $fc-font;
}
.detail__poster-qr-text {
margin-left: 12rpx;
}
</style>