文件
fenleixinxi/uniapp/pages/detail/detail.vue

2581 行
69 KiB
Vue

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

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

<template>
<view class="detail fc-page">
<!-- 顶部统一导航 -->
<fc-navbar title="详情" theme="gradient" :show-back="true" />
<!-- 骨架屏加载占位态(首次进入数据未加载完成时呈现,1:1 对齐真实卡片) -->
<view v-if="loading && !loaded" class="detail__skeleton-wrap">
<!-- 顶部画廊封面骨架 -->
<view class="detail__skeleton-gallery fc-skeleton"></view>
<!-- 标题与价格卡片骨架 -->
<view class="fc-card detail__skeleton-card">
<view class="detail__skeleton-line fc-skeleton" style="width: 85%; height: 38rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 52%; height: 32rpx; margin-top: 18rpx;"></view>
<view class="detail__skeleton-row" style="margin-top: 32rpx;">
<view class="detail__skeleton-line fc-skeleton" style="width: 180rpx; height: 48rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 150rpx; height: 26rpx;"></view>
</view>
<view class="detail__skeleton-line fc-skeleton" style="width: 44%; height: 24rpx; margin-top: 24rpx;"></view>
</view>
<!-- 发布者卡片骨架 -->
<view class="fc-card detail__skeleton-card">
<view class="detail__skeleton-row">
<view class="detail__skeleton-avatar fc-skeleton"></view>
<view class="detail__skeleton-col" style="margin-left: 20rpx; flex: 1;">
<view class="detail__skeleton-line fc-skeleton" style="width: 190rpx; height: 32rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 270rpx; height: 24rpx; margin-top: 14rpx;"></view>
</view>
<view class="detail__skeleton-pill fc-skeleton" style="width: 124rpx; height: 56rpx;"></view>
</view>
</view>
<!-- 详细描述卡片骨架 -->
<view class="fc-card detail__skeleton-card">
<view class="detail__skeleton-line fc-skeleton" style="width: 140rpx; height: 32rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 100%; height: 26rpx; margin-top: 24rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 90%; height: 26rpx; margin-top: 14rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 62%; height: 26rpx; margin-top: 14rpx;"></view>
<view class="detail__skeleton-row" style="margin-top: 28rpx;">
<view class="detail__skeleton-pill fc-skeleton" style="width: 100rpx; height: 44rpx; margin-right: 14rpx;"></view>
<view class="detail__skeleton-pill fc-skeleton" style="width: 140rpx; height: 44rpx; margin-right: 14rpx;"></view>
<view class="detail__skeleton-pill fc-skeleton" style="width: 110rpx; height: 44rpx;"></view>
</view>
<view class="detail__skeleton-block fc-skeleton" style="height: 112rpx; margin-top: 28rpx;"></view>
</view>
<!-- 联系方式卡片骨架 -->
<view class="fc-card detail__skeleton-card">
<view class="detail__skeleton-line fc-skeleton" style="width: 140rpx; height: 32rpx;"></view>
<view class="detail__skeleton-row" style="margin-top: 24rpx; height: 72rpx;">
<view class="detail__skeleton-line fc-skeleton" style="width: 130rpx; height: 28rpx;"></view>
<view class="detail__skeleton-line fc-skeleton" style="width: 260rpx; height: 34rpx;"></view>
</view>
<view class="detail__skeleton-pill fc-skeleton" style="width: 100%; height: 80rpx; margin-top: 26rpx;"></view>
</view>
</view>
<!-- 真实内容视图(数据到达后平滑淡入呈现) -->
<view v-else class="detail__content-wrap">
<!-- 顶部实拍大图画廊 / 分类主题插画 -->
<view v-if="imageUrls.length" class="detail__gallery">
<swiper
class="detail__swiper"
circular
:autoplay="imageUrls.length > 1"
:interval="4000"
:duration="400"
@change="onSwiperChange"
>
<swiper-item v-for="(img, idx) in imageUrls" :key="idx" @click="previewImages(idx)">
<image class="detail__swiper-img" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<view v-if="imageUrls.length > 1" class="detail__swiper-badge">
<text>{{ activeSwiperIndex + 1 }} / {{ imageUrls.length }}</text>
</view>
<view class="detail__hero-badges detail__hero-badges--float">
<text class="fc-tag detail__badge">{{ post.categoryName }}</text>
<text v-if="post.isTop || post.isPaid" class="fc-tag detail__badge detail__badge--paid">置顶推荐</text>
</view>
</view>
<view v-else 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.isTop || post.isPaid" class="fc-tag detail__badge detail__badge--paid">置顶推荐</text>
</view>
</view>
<!-- 核心信息卡片:标题、价格、数据指标 -->
<view class="fc-card detail__head">
<view class="detail__title-row">
<text class="detail__title">{{ post.title }}</text>
</view>
<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 && post.priceUnit" 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 || 0 }}</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 }}<text v-if="post.address"> · {{ post.address }}</text></text>
</view>
</view>
<!-- 发布者专区 -->
<view class="fc-card detail__publisher">
<image
v-if="post.publisher && post.publisher.avatar"
class="detail__avatar detail__avatar--img"
:src="assetUrl(post.publisher.avatar)"
mode="aspectFill"
/>
<view v-else 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="detail__verified">已认证</text>
</view>
<text class="detail__publisher-sub">
{{ post.publisher && post.publisher.isMerchant ? '商家用户' : '个人用户' }} · {{ timeText }}发布
</text>
</view>
<view v-if="!isSelf" class="detail__follow" :class="{ 'detail__follow--active': followed }" @click="toggleFollow">
<text>{{ followed ? '已关注' : '+ 关注' }}</text>
</view>
</view>
<!-- 商家门店专区(仅商家发布或关联认证商户时展示) -->
<view v-if="isMerchantPost" class="fc-card detail__merchant" @click="goShopDetail">
<view class="detail__merchant-badge-bar">
<view class="detail__merchant-flag">
<image class="fc-icon fc-icon--xs" :src="iconUrl('shield')" mode="aspectFit" />
<text class="detail__merchant-flag-txt">认证商家官方门店</text>
</view>
<view class="detail__merchant-entry">
<text class="detail__merchant-entry-txt">进店逛逛</text>
<text class="detail__merchant-entry-arrow">›</text>
</view>
</view>
<view class="detail__merchant-main">
<image
v-if="merchantInfo && merchantInfo.logo"
class="detail__merchant-logo"
:src="assetUrl(merchantInfo.logo)"
mode="aspectFill"
/>
<view v-else class="detail__merchant-logo-placeholder">
<image class="fc-icon fc-icon--md" :src="iconUrl('login-shop') || iconUrl('home-nearby')" mode="aspectFit" />
</view>
<view class="detail__merchant-meta">
<view class="detail__merchant-name-row">
<text class="detail__merchant-name">{{ (merchantInfo && merchantInfo.name) || (post.publisher && post.publisher.name) || '同城认证门店' }}</text>
</view>
<view class="detail__merchant-tags">
<text v-if="merchantInfo && merchantInfo.category" class="detail__merchant-tag-item">{{ merchantInfo.category }}</text>
<text class="detail__merchant-tag-item detail__merchant-tag-item--credit">实体保障</text>
<text class="detail__merchant-tag-item detail__merchant-tag-item--credit">真实经营</text>
</view>
</view>
</view>
<!-- 店铺公告(若有) -->
<view v-if="merchantInfo && merchantInfo.notice" class="detail__merchant-notice">
<image class="fc-icon fc-icon--xs" :src="iconUrl('notice')" mode="aspectFit" />
<text class="detail__merchant-notice-txt">{{ merchantInfo.notice }}</text>
</view>
<!-- 实体门店地址与一键导航 -->
<view v-if="merchantAddress" class="detail__merchant-address-box" @click.stop="openMerchantLocation">
<view class="detail__merchant-address-left">
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-location')" mode="aspectFit" />
<text class="detail__merchant-address-txt">{{ merchantAddress }}</text>
</view>
<view class="detail__merchant-nav-btn">
<text>{{ hasMerchantCoord ? '导航到店' : '复制地址' }}</text>
<text class="detail__merchant-nav-arrow">›</text>
</view>
</view>
</view>
<!-- 详细内容与服务标签 -->
<view class="fc-card">
<text class="detail__section-title">详细描述</text>
<text class="detail__content">{{ post.content }}</text>
<view v-if="post.tags && post.tags.length" class="detail__tags">
<text v-for="t in post.tags" :key="t" class="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">
<view class="detail__contact-head">
<text class="detail__section-title">联系方式</text>
<text v-if="unlocked" class="detail__contact-badge">已解锁</text>
</view>
<!-- 已解锁态(会员/本人/已付费):清爽展示联系人与电话 -->
<view v-if="unlocked" class="detail__contact-unlocked">
<view v-if="contact.name" class="detail__contact-item">
<text class="detail__contact-key">联系人</text>
<text class="detail__contact-name">{{ contact.name }}</text>
</view>
<view class="detail__contact-item">
<text class="detail__contact-key">联系电话</text>
<text class="detail__contact-phone" @click="callPhone">{{ contact.phone }}</text>
</view>
<view v-if="contact.wechat" class="detail__contact-item">
<text class="detail__contact-key">微信号</text>
<text class="detail__contact-wechat" @click="copyWechat">{{ contact.wechat }}</text>
</view>
<view class="detail__contact-btns">
<view class="detail__btn-primary" @click="callPhone">
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
<text class="detail__btn-text">拨打电话</text>
</view>
<view v-if="contact.wechat" class="detail__btn-ghost" @click="copyWechat">
<text>复制微信号</text>
</view>
</view>
</view>
<!-- 未解锁态(普通非会员):极简纯净,无多余口号标签 -->
<view v-else class="detail__contact-locked">
<view class="detail__locked-row">
<text class="detail__contact-key">联系电话</text>
<text class="detail__locked-mask">{{ post.phone || post.contact || '188****5042' }}</text>
</view>
<view class="detail__locked-action-bar">
<view class="detail__btn-unlock" @click="openPayModal">
<text>解锁完整内容 (¥2)</text>
</view>
<text class="detail__vip-link" @click="goVip">开通会员免费看 ›</text>
</view>
</view>
</view>
<!-- 基础档案信息(信息编号与安全提示) -->
<view class="detail__archive">
<text class="detail__archive-id">信息编号:#{{ post.id }}</text>
<text class="detail__archive-safe">提示:交易时请仔细核对,防范网络风险</text>
</view>
<!-- 用户评价 -->
<view class="fc-card">
<view class="detail__cmt-head">
<text class="detail__section-title">用户评价({{ comments.length }})</text>
<text v-if="!isSelf" class="detail__cmt-write" @click="onOpenComment">写评价 ›</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>
<view v-if="c.reply" class="detail__cmt-reply-box">
<text class="detail__cmt-reply-tag">发布者回复</text>
<text class="detail__cmt-reply-text">{{ c.reply }}</text>
</view>
<view v-if="isSelf" class="detail__cmt-actions">
<view class="detail__cmt-action-btn" @click="onOpenReply(c)">
<image class="fc-icon fc-icon--xs" :src="iconUrl('edit')" mode="aspectFit" />
<text class="detail__cmt-action-text">{{ c.reply ? '修改回复' : '回复' }}</text>
</view>
</view>
</view>
<text v-if="!comments.length" class="detail__cmt-empty">暂无评价,快来抢沙发~</text>
</view>
</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" :class="{ 'detail__bar-text--active': faved }">{{ faved ? '已收藏' : '收藏' }}</text>
</view>
<!-- 微信原生转发好友按钮 -->
<button class="detail__bar-item detail__bar-item--btn" open-type="share">
<image class="fc-icon fc-icon--md" :src="iconUrl('bar-share')" mode="aspectFit" />
<text class="detail__bar-text">分享</text>
</button>
<!-- 生成海报按钮 -->
<view class="detail__bar-item" @click="openPosterModal">
<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="onMainAction">
<text>{{ loading && !loaded ? '加载中…' : (unlocked ? '拨打电话' : '解锁完整内容 (¥2)') }}</text>
</view>
</view>
<!-- 支付解锁收银弹窗 -->
<view v-if="showPayModal" class="detail__mask" @click="showPayModal = false">
<view class="detail__cashier" @click.stop>
<view class="detail__cashier-header">
<text class="detail__cashier-title">解锁完整内容</text>
<text class="detail__cashier-close" @click="showPayModal = false">✕</text>
</view>
<view class="detail__cashier-target">
<text class="detail__cashier-target-title fc-ellipsis">{{ post.title }}</text>
<view class="detail__cashier-price-row">
<text class="detail__cashier-price-symbol">¥</text>
<text class="detail__cashier-price-val">2.00</text>
</view>
</view>
<view class="detail__cashier-methods">
<view
class="detail__cashier-method"
:class="{ 'detail__cashier-method--active': payType === 'wxpay' }"
@click="payType = 'wxpay'"
>
<image class="fc-icon fc-icon--md" :src="iconUrl('paywechat')" mode="aspectFit" />
<view class="detail__cashier-method-info">
<text class="detail__cashier-method-name">微信支付</text>
<text class="detail__cashier-method-tip">微信安全支付</text>
</view>
<view class="detail__cashier-radio">
<view v-if="payType === 'wxpay'" class="detail__cashier-radio-inner"></view>
</view>
</view>
<view
class="detail__cashier-method"
:class="{ 'detail__cashier-method--active': payType === 'balance' }"
@click="payType = 'balance'"
>
<image class="fc-icon fc-icon--md" :src="iconUrl('wallet')" mode="aspectFit" />
<view class="detail__cashier-method-info">
<text class="detail__cashier-method-name">余额支付</text>
<text class="detail__cashier-method-tip">即时扣款</text>
</view>
<view class="detail__cashier-radio">
<view v-if="payType === 'balance'" class="detail__cashier-radio-inner"></view>
</view>
</view>
</view>
<view class="detail__cashier-vip-banner" @click="goVip">
<image class="fc-icon fc-icon--sm" :src="iconUrl('crown')" mode="aspectFit" />
<text class="detail__cashier-vip-text">开通会员,全站信息免费看</text>
<text class="detail__cashier-vip-arrow">去开通 ›</text>
</view>
<view class="detail__cashier-btn" :class="{ 'detail__cashier-btn--disabled': submitting }" @click="confirmPay">
<text>{{ submitting ? '处理中…' : '立即支付 ¥2 解锁' }}</text>
</view>
</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="showReplyModal" class="detail__mask" @click="showReplyModal = false">
<view class="detail__sheet" @click.stop>
<view class="detail__sheet-header">
<text class="detail__sheet-title">回复用户评价</text>
<text class="detail__sheet-close" @click="showReplyModal = false">✕</text>
</view>
<view v-if="activeReplyComment" class="detail__reply-target">
<text class="detail__reply-target-user">@{{ activeReplyComment.user }}:</text>
<text class="detail__reply-target-content">{{ activeReplyComment.content }}</text>
</view>
<textarea
v-model="replyText"
class="detail__sheet-input"
placeholder="输入您的回复内容(200字以内)…"
maxlength="200"
/>
<view
class="detail__sheet-btn"
:class="{ 'detail__sheet-btn--disabled': replySubmitting }"
@click="submitReply"
>
<text>{{ replySubmitting ? '提交中…' : '提交回复' }}</text>
</view>
</view>
</view>
<!-- 完善重构的真实海报弹层 -->
<view v-if="showPoster" class="detail__mask" @click="showPoster = false">
<view class="detail__poster" @click.stop>
<view class="detail__poster-header">
<text class="detail__poster-top-title">分享海报</text>
<text class="detail__poster-close" @click="showPoster = false">✕</text>
</view>
<!-- 海报正在生成态 -->
<view v-if="posterLoading" class="detail__poster-loading">
<view class="detail__poster-spinner"></view>
<text class="detail__poster-loading-text">海报绘制生成中…</text>
</view>
<!-- 真实海报预览图 -->
<view v-else class="detail__poster-preview">
<image
v-if="posterImagePath"
class="detail__poster-img"
:src="posterImagePath"
mode="widthFix"
@click="previewPoster"
/>
<text class="detail__poster-tip">点击大图可全屏预览,支持长按直接转发</text>
</view>
<!-- 底部三大快捷分享操作 -->
<view class="detail__poster-actions">
<view class="detail__poster-btn detail__poster-btn--primary" @click="savePoster">
<image class="fc-icon fc-icon--sm" :src="iconUrl('image-white')" mode="aspectFit" />
<text class="detail__poster-btn-text">保存到手机相册</text>
</view>
<button class="detail__poster-btn detail__poster-btn--share" open-type="share">
<image class="fc-icon fc-icon--sm" :src="iconUrl('bar-share')" mode="aspectFit" />
<text class="detail__poster-btn-text">发给微信好友</text>
</button>
<view class="detail__poster-btn detail__poster-btn--ghost" @click="copyPosterText">
<text>复制文案</text>
</view>
</view>
</view>
</view>
<!-- 隐藏的离屏 Canvas,用于高质量绘制海报(640 x 1020 逻辑分辨率) -->
<canvas
canvas-id="postPosterCanvas"
id="postPosterCanvas"
class="detail__poster-canvas"
style="width: 640px; height: 1020px; position: fixed; left: -9999px; top: -9999px;"
/>
</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, uiState } from '@/common/store/ui.js'
import { fromNow, assetUrl } 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'
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import QRCode from '@/common/utils/qrcode-generator.js'
export default {
components: { FcNavbar },
data() {
return {
id: 0,
post: { publisher: {}, tags: [] },
unlocked: false,
payType: config.defaultPayType || 'wxpay',
contact: { name: '', phone: '', wechat: '' },
comments: [],
followed: false,
faved: false,
favoriteCount: 0,
reportReasons: REPORT_REASONS,
showComment: false,
showReplyModal: false,
activeReplyComment: null,
replyText: '',
replySubmitting: false,
showPoster: false,
showPayModal: false,
score: 5,
commentText: '',
submitting: false,
activeSwiperIndex: 0,
// 数据加载中与首次加载完成标识(用于骨架屏与静默刷新判定)
loading: true,
loaded: false,
// 海报生成状态与临时图片路径
posterLoading: false,
posterImagePath: '',
}
},
computed: {
/**
* 相对发布时间
*/
/**
* 是否为商家发布的信息
* @returns {boolean}
*/
isMerchantPost() {
return !!(
this.post.identityType === 'merchant' ||
(this.post.merchant && this.post.merchant.name) ||
(this.post.publisher && this.post.publisher.isMerchant)
)
},
/**
* 商家门店详细信息
* @returns {Object|null}
*/
merchantInfo() {
if (this.post.merchant && typeof this.post.merchant === 'object') {
return this.post.merchant
}
if (this.post.publisher && this.post.publisher.isMerchant) {
return {
name: this.post.publisher.name || '同城认证商户',
logo: this.post.publisher.avatar || '',
address: this.post.address || '',
lat: this.post.lat,
lng: this.post.lng,
verified: true,
}
}
return null
},
/**
* 门店实体地址
* @returns {string}
*/
merchantAddress() {
return (this.merchantInfo && this.merchantInfo.address) || this.post.address || ''
},
/**
* 门店是否具备坐标
* @returns {boolean}
*/
hasMerchantCoord() {
const m = this.merchantInfo
if (m && m.lat != null && m.lng != null && Number(m.lat) !== 0 && Number(m.lng) !== 0) {
return true
}
return this.hasLocation
},
timeText() {
return fromNow(this.post.createdAt)
},
/**
* 媒体列表图片
*/
imageUrls() {
const list = []
if (this.post.media && Array.isArray(this.post.media)) {
this.post.media.forEach((m) => {
if (typeof m === 'string' && !m.match(/\.(mp4|mov|avi)$/i)) {
list.push(assetUrl(m))
}
})
}
if (!list.length && this.post.cover) {
list.push(assetUrl(this.post.cover))
}
return list
},
/**
* 是否有坐标
*/
hasLocation() {
return this.post.lat != null && this.post.lng != null
},
/**
* 当前登录用户
*/
currentUser() {
try {
const store = useUserStore()
const user = (store.userInfo && store.userInfo.value) || store.userInfo || (store.state && store.state.userInfo)
return user || null
} catch (e) {
return null
}
},
/**
* 是否为本人发布
*/
isSelf() {
try {
const user = this.currentUser
const curId = user && (user.id || user.userId)
const pubId = (this.post && this.post.publisher && this.post.publisher.id) || (this.post && (this.post.userId || this.post.user_id))
return !!(curId && pubId && Number(curId) === Number(pubId))
} catch (e) {
return false
}
},
},
onLoad(query) {
this.id = Number(query.id || 101)
loadUiConfig()
this.fetch()
},
onShow() {
if (this.loaded) this.refreshAccess()
},
/**
* 转发给微信好友(携带分销邀请人与精美封面图)
*/
onShareAppMessage() {
let inviterId = 0
try {
const user = this.currentUser
if (user && user.id) inviterId = user.id
} catch (e) {}
const shareTitle = this.post.title
? `【${this.post.categoryName || '同城信息'}】${this.post.title}`
: (uiState.shareTitle || uiState.siteName || '同城便民信息')
const sharePath = `/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
const shareImg = this.posterImagePath || (this.imageUrls && this.imageUrls.length ? this.imageUrls[0] : (this.post.cover ? assetUrl(this.post.cover) : ''))
return {
title: shareTitle,
path: sharePath,
imageUrl: shareImg || undefined,
}
},
/**
* 转发到微信朋友圈
*/
onShareTimeline() {
let inviterId = 0
try {
const user = this.currentUser
if (user && user.id) inviterId = user.id
} catch (e) {}
const shareTitle = this.post.title
? `【${this.post.categoryName || '同城信息'}】${this.post.title}`
: (uiState.shareTitle || uiState.siteName || '同城便民信息')
const shareImg = this.posterImagePath || (this.imageUrls && this.imageUrls.length ? this.imageUrls[0] : (this.post.cover ? assetUrl(this.post.cover) : ''))
return {
title: shareTitle,
query: `id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`,
imageUrl: shareImg || undefined,
}
},
methods: {
iconUrl,
assetUrl,
/**
* 获取分类插画
*/
catIcon() {
return categoryIconUrl(this.post.categoryId)
},
/**
* 轮播切换同步
* @param {Object} e 事件
*/
onSwiperChange(e) {
this.activeSwiperIndex = (e && e.detail && e.detail.current) || 0
},
/**
* 全屏图片预览
* @param {Number} idx 索引
*/
previewImages(idx) {
if (!this.imageUrls || !this.imageUrls.length) return
uni.previewImage({
current: idx,
urls: this.imageUrls,
})
},
/**
* 拉取详情数据(首次激活骨架屏态)
*/
async fetch() {
this.loading = true
try {
const res = await postApi.detail(this.id)
this.post = res
try {
const cs = await postApi.comments(this.id)
this.comments = Array.isArray(cs) ? cs : []
} catch (e) {
this.comments = []
}
this.applyAccess(res)
this.loaded = true
this.faved = !!res.faved
this.followed = !!res.followed
this.favoriteCount = Number(res.favoriteCount || 0)
if (this.isLogin()) await this.loadInteractStatus()
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
/**
* 检查登录状态
*/
isLogin() {
try {
return !!useUserStore().isLogin.value
} catch (e) {
return false
}
},
/**
* 登录拦截引导
*/
ensureLogin() {
if (this.isLogin()) return true
uni.showModal({
title: '提示',
content: '该功能需要先登录账号,是否前往?',
confirmText: '去登录',
cancelText: '取消',
confirmColor: '#00E070',
success: (r) => {
if (r.confirm) {
uni.navigateTo({ url: '/pages/auth/login' })
}
},
})
return false
},
/**
* 回显解锁数据与联系方式
* @param {Object} res 接口返回数据
*/
applyAccess(res) {
this.unlocked = !!(res && res.unlocked)
this.contact = this.unlocked
? {
name: (res && (res.contactName || res.publisher?.name)) || '',
phone: (res && (res.phone || res.contact)) || '',
wechat: (res && res.wechat) || '',
}
: { name: '', phone: '', wechat: '' }
},
/**
* 页面回显静默刷新解锁权限
*/
async refreshAccess() {
try {
const res = await postApi.detail(this.id, { accessOnly: 1 })
this.applyAccess(res)
} catch (e) {}
},
/**
* 加载互动状态
*/
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) {}
},
/**
* 打开解锁收银台
*/
openPayModal() {
if (!this.ensureLogin()) return
this.showPayModal = true
},
/**
* 底部主按钮响应
*/
onMainAction() {
if (this.loading && !this.loaded) return
if (this.unlocked) {
this.callPhone()
} else {
this.openPayModal()
}
},
/**
* 确认支付解锁
*/
async confirmPay() {
if (this.submitting) return
this.submitting = true
try {
const res = await postApi.payView(this.id, { payType: this.payType })
if (res.order) {
await pay({ ...res.order, payType: this.payType })
await this.waitUnlock()
this.showPayModal = false
return
}
this.applyAccess(res)
this.showPayModal = false
uni.showToast({ title: '已解锁联系方式', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message || '支付失败', icon: 'none' })
} finally {
this.submitting = false
}
},
/**
* 微信支付结果轮询
*/
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.applyAccess(res)
uni.showToast({ title: '已解锁联系方式', icon: 'none' })
return
}
} catch (e) {}
}
uni.showToast({ title: '支付成功,稍后生效', icon: 'none' })
},
/**
* 写评价点击拦截(禁止发布者自评)
*/
onOpenComment() {
if (!this.ensureLogin()) return
if (this.isSelf) {
uni.showToast({ title: '不能评价自己发布的信息', icon: 'none' })
return
}
this.showComment = true
},
/**
* 打开回复评价弹窗(仅发布者可操作)
* @param {Object} comment 被回复的评价对象
*/
onOpenReply(comment) {
if (!this.ensureLogin()) return
if (!this.isSelf) {
uni.showToast({ title: '只有发布者可以回复评价', icon: 'none' })
return
}
this.activeReplyComment = comment
this.replyText = (comment && comment.reply) || ''
this.showReplyModal = true
},
/**
* 提交发布者回复
*/
async submitReply() {
if (!this.replyText.trim()) {
uni.showToast({ title: '请输入回复内容', icon: 'none' })
return
}
if (!this.activeReplyComment || !this.activeReplyComment.id) {
uni.showToast({ title: '评价信息失效,请重试', icon: 'none' })
return
}
if (this.replySubmitting) return
this.replySubmitting = true
try {
const res = await postApi.replyComment(this.activeReplyComment.id, {
reply: this.replyText.trim()
})
// 同步更新页面中的评价回复
if (this.activeReplyComment) {
this.activeReplyComment.reply = (res && res.reply) ? res.reply : this.replyText.trim()
}
this.showReplyModal = false
this.replyText = ''
this.activeReplyComment = null
uni.showToast({ title: '回复成功', icon: 'none' })
} catch (e) {
uni.showToast({ title: e.message || '回复失败,请重试', icon: 'none' })
} finally {
this.replySubmitting = false
}
},
/**
* 提交评价
*/
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' })
}
},
/**
* 关注发布者
*/
async toggleFollow() {
if (!this.ensureLogin()) return
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' })
}
},
/**
* 收藏信息
*/
async toggleFav() {
if (!this.ensureLogin()) return
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' })
}
},
/**
* 举报
*/
openReport() {
if (!this.ensureLogin()) return
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' })
}
},
})
},
/**
* 前往会员中心
*/
goVip() {
this.showPayModal = false
uni.navigateTo({ url: '/pages/member/index' })
},
/**
* 导航或复制地址
*/
/**
* 跳转至商家店铺详情主页
*/
goShopDetail() {
const m = this.merchantInfo
const shopId = (m && (m.id || m.merchantId)) || (this.post.publisher && this.post.publisher.id) || (this.post.userId || this.post.user_id)
if (!shopId) {
uni.showToast({ title: '暂未获取到店铺信息', icon: 'none' })
return
}
uni.navigateTo({
url: '/pages/merchant/detail?id=' + shopId,
fail: (err) => {
console.warn('跳转商家详情失败:', err)
}
})
},
/**
* 导航前往商家门店或复制实体地址
*/
openMerchantLocation() {
const m = this.merchantInfo
const addr = this.merchantAddress
const lat = m && m.lat != null ? Number(m.lat) : (this.post.lat != null ? Number(this.post.lat) : null)
const lng = m && m.lng != null ? Number(m.lng) : (this.post.lng != null ? Number(this.post.lng) : null)
const name = (m && m.name) || this.post.title || '商家门店'
if (lat != null && lng != null && lat !== 0 && lng !== 0) {
uni.openLocation({
latitude: lat,
longitude: lng,
name: name,
address: addr || '商家实体门店',
fail: () => {
if (addr) {
uni.setClipboardData({
data: `${addr}`.trim(),
success: () => uni.showToast({ title: '门店地址已复制', icon: 'none' })
})
}
}
})
} else if (addr) {
uni.setClipboardData({
data: `${addr}`.trim(),
success: () => uni.showToast({ title: '门店地址已复制', icon: 'none' })
})
}
},
openMap() {
if (!this.hasLocation) {
uni.setClipboardData({
data: `${this.post.address || ''} ${this.post.area || ''}`.trim(),
success: () => 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.openPayModal()
const phone = this.contact.phone || this.post.phone || this.post.contact
if (!phone) return uni.showToast({ title: '暂未提供电话', icon: 'none' })
uni.makePhoneCall({ phoneNumber: String(phone) })
},
/**
* 复制微信
*/
copyWechat() {
if (!this.contact.wechat) return uni.showToast({ title: '暂未提供微信号', icon: 'none' })
uni.setClipboardData({
data: this.contact.wechat,
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
})
},
/**
* 打开海报弹窗并异步生成海报
*/
openPosterModal() {
this.showPoster = true
if (!this.posterImagePath) {
this.generatePoster()
}
},
/**
* 辅助函数:异步获取图片本地路径(网络图片需下载)
* @param {String} src 图片链接
*/
getImageLocalPath(src) {
return new Promise((resolve) => {
if (!src) return resolve('')
if (src.startsWith('/static/') || src.startsWith('static/')) {
return resolve(src.startsWith('/') ? src : '/' + src)
}
uni.getImageInfo({
src: src,
success: (res) => resolve(res.path),
fail: () => resolve(''),
})
})
},
/**
* 辅助函数:绘制圆角矩形
*/
drawRoundRect(ctx, x, y, width, height, radius, fillColor, strokeColor) {
ctx.save()
ctx.beginPath()
ctx.moveTo(x + radius, y)
ctx.lineTo(x + width - radius, y)
ctx.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI)
ctx.lineTo(x + width, y + height - radius)
ctx.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI)
ctx.lineTo(x + radius, y + height)
ctx.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI)
ctx.lineTo(x, y + radius)
ctx.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI)
ctx.closePath()
if (fillColor) {
ctx.setFillStyle(fillColor)
ctx.fill()
}
if (strokeColor) {
ctx.setStrokeStyle(strokeColor)
ctx.stroke()
}
ctx.restore()
},
/**
* 辅助函数:多行文本绘制并带省略号
*/
drawTextWrapped(ctx, text, x, y, maxWidth, lineHeight, maxLines = 2) {
if (!text) return y
const words = String(text).split('')
let line = ''
let lineCount = 0
let currentY = y
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i]
const metrics = ctx.measureText(testLine)
if (metrics.width > maxWidth && i > 0) {
lineCount++
if (lineCount >= maxLines) {
line = line.slice(0, -1) + '…'
ctx.fillText(line, x, currentY)
return currentY + lineHeight
}
ctx.fillText(line, x, currentY)
line = words[i]
currentY += lineHeight
} else {
line = testLine
}
}
if (line) {
ctx.fillText(line, x, currentY)
currentY += lineHeight
}
return currentY
},
/**
* 在 Canvas 上绘制真实的、符合国际标准且微信100%可识别的长按二维码
* @param {Object} ctx Canvas 上下文
* @param {String} text 二维码目标 URL
* @param {Number} x 起始 X
* @param {Number} y 起始 Y
* @param {Number} size 二维码尺寸(px)
*/
drawRealQRCode(ctx, text, x, y, size) {
try {
const qr = QRCode.create(text, { errorCorrectionLevel: 'M' })
const modules = qr.modules
const count = modules.size
const padding = 10
const qrRealSize = size - padding * 2
const cellSize = qrRealSize / count
// 1. 纯白底色(高对比度保障扫描率)
ctx.save()
this.drawRoundRect(ctx, x, y, size, size, 14, '#FFFFFF', '#DFF2E8')
// 2. 绘制深墨绿码元(#04301E),符合春绿规范且微信识别率100%
ctx.setFillStyle('#04301E')
for (let r = 0; r < count; r++) {
for (let c = 0; c < count; c++) {
if (modules.get(r, c) === 1) {
ctx.fillRect(
x + padding + c * cellSize,
y + padding + r * cellSize,
cellSize + 0.4,
cellSize + 0.4
)
}
}
}
// 3. 中心微质感同城 Logo 徽标
const centerSize = Math.floor(size * 0.22)
const cx = x + size / 2
const cy = y + size / 2
ctx.beginPath()
ctx.arc(cx, cy, centerSize / 2 + 3, 0, Math.PI * 2)
ctx.setFillStyle('#FFFFFF')
ctx.fill()
ctx.beginPath()
ctx.arc(cx, cy, centerSize / 2, 0, Math.PI * 2)
ctx.setFillStyle('#00E070')
ctx.fill()
ctx.setFontSize(Math.floor(centerSize * 0.58))
ctx.setFillStyle('#04301E')
ctx.setTextAlign('center')
ctx.setTextBaseline('middle')
ctx.fillText('同', cx, cy + 1)
ctx.restore()
} catch (e) {
console.error('QR draw error', e)
}
},
/**
* 真实商业级 Canvas 绘制高清同城分享海报
*/
async generatePoster() {
this.posterLoading = true
const W = 640
const H = 1020
const ctx = uni.createCanvasContext('postPosterCanvas', this)
// 1. 底板背景(薄荷微浅绿)
ctx.setFillStyle('#F2FEF7')
ctx.fillRect(0, 0, W, H)
// 2. 主体纯白高光卡片(圆角 24px,带微立体边框)
this.drawRoundRect(ctx, 24, 24, 592, 972, 26, '#FFFFFF', '#DDF2E7')
// 3. 头部 Hero 品牌渐变区
const grad = ctx.createLinearGradient(24, 24, 616, 210)
grad.addColorStop(0, '#04874A')
grad.addColorStop(1, '#023A21')
this.drawRoundRect(ctx, 24, 24, 592, 180, 26, grad, null)
// 补齐头部底部的直角以平滑承接内容
ctx.setFillStyle(grad)
ctx.fillRect(24, 160, 592, 44)
// 头部品牌与城市胶囊
this.drawRoundRect(ctx, 48, 50, 200, 36, 18, 'rgba(255, 255, 255, 0.22)', null)
ctx.setFontSize(20)
ctx.setFillStyle('#FFFFFF')
ctx.fillText('同城生活 · 便民严选', 64, 75)
const cityText = this.post.area ? `贵阳 · ${this.post.area}` : '贵阳同城'
const cityW = ctx.measureText(cityText).width + 30
this.drawRoundRect(ctx, 592 - cityW, 50, cityW, 36, 18, 'rgba(255, 255, 255, 0.16)', null)
ctx.setFontSize(20)
ctx.setFillStyle('#FFFFFF')
ctx.fillText(cityText, 607 - cityW, 75)
// 头部大标题(白字粗体,最多折 2 行)
ctx.setFontSize(32)
ctx.setFillStyle('#FFFFFF')
this.drawTextWrapped(ctx, this.post.title || '', 48, 128, 544, 40, 2)
// 4. 实拍大图 / 主题视觉区
let coverPath = ''
if (this.imageUrls && this.imageUrls.length) {
coverPath = await this.getImageLocalPath(this.imageUrls[0])
}
const picY = 224
if (coverPath) {
ctx.save()
this.drawRoundRect(ctx, 48, picY, 544, 290, 16, null, null)
ctx.clip()
ctx.drawImage(coverPath, 48, picY, 544, 290)
ctx.restore()
// 大图内右上方贴上“置顶推荐 / 实拍保真”标签
if (this.post.isTop || this.post.isPaid) {
this.drawRoundRect(ctx, 450, picY + 16, 126, 36, 18, '#F5A623', null)
ctx.setFontSize(18)
ctx.setFillStyle('#FFFFFF')
ctx.fillText('置顶推荐', 478, picY + 41)
}
// 大图左下方贴上分类胶囊
this.drawRoundRect(ctx, 64, picY + 236, 136, 36, 18, 'rgba(4, 48, 30, 0.75)', null)
ctx.setFontSize(20)
ctx.setFillStyle('#00E070')
ctx.fillText(this.post.categoryName || '同城便民', 80, picY + 261)
} else {
// 无实拍图时展示生态质感主题大卡片
this.drawRoundRect(ctx, 48, picY, 544, 220, 16, '#EBF9F2', '#D3EFE1')
ctx.setFontSize(32)
ctx.setFillStyle('#04874A')
ctx.fillText(this.post.categoryName || '同城便民服务', 72, picY + 90)
ctx.setFontSize(22)
ctx.setFillStyle('#5E8C78')
ctx.fillText('平台官方审核认证 · 放心联系', 72, picY + 140)
}
// 5. 价值展示行(价格 + 浏览量)
const valY = coverPath ? picY + 332 : picY + 262
ctx.setFontSize(26)
ctx.setFillStyle('#F5A623')
if (this.post.price > 0) {
ctx.fillText('¥', 48, valY)
ctx.setFontSize(48)
ctx.fillText(String(this.post.price), 68, valY)
if (this.post.priceUnit) {
ctx.setFontSize(22)
ctx.setFillStyle('#888888')
ctx.fillText(this.post.priceUnit, 72 + ctx.measureText(String(this.post.price)).width + 10, valY)
}
} else {
ctx.setFontSize(40)
ctx.fillText('面议', 48, valY)
}
// 右侧显示浏览人次
const viewsText = `🔥 ${this.post.views || 0} 人关注 · 平台认证`
ctx.setFontSize(20)
ctx.setFillStyle('#7A998A')
ctx.fillText(viewsText, 592 - ctx.measureText(viewsText).width - 48, valY - 6)
// 6. 详细描述摘录
const descY = valY + 44
ctx.setFontSize(22)
ctx.setFillStyle('#3E5246')
this.drawTextWrapped(ctx, this.post.content || '', 48, descY, 544, 34, 2)
// 7. 特色服务标签与地址信息
const tagY = descY + 76
if (this.post.tags && this.post.tags.length) {
let tagStartX = 48
this.post.tags.slice(0, 3).forEach((tag) => {
const tW = ctx.measureText(tag).width + 24
this.drawRoundRect(ctx, tagStartX, tagY, tW, 34, 8, '#EBF9F2', '#D3EFE1')
ctx.setFontSize(19)
ctx.setFillStyle('#04874A')
ctx.fillText(tag, tagStartX + 12, tagY + 24)
tagStartX += tW + 12
})
}
// 地址行
const addrY = tagY + 54
ctx.setFontSize(20)
ctx.setFillStyle('#6E8276')
const addrFull = `📍 ${this.post.address || this.post.area || '同城便民服务地点'}`
ctx.fillText(addrFull.slice(0, 28), 48, addrY)
// 8. 分割虚线
ctx.setStrokeStyle('#E2EBE5')
ctx.setLineWidth(1)
ctx.beginPath()
ctx.moveTo(48, 804)
ctx.lineTo(592, 804)
ctx.stroke()
// 9. 底部扫码交互区(真实高密度二维码 + 醒目指引)
let inviterId = 0
try {
const user = this.currentUser
if (user && user.id) inviterId = user.id
} catch (e) {}
const qrUrl = `https://fenleixinxi.gzyakj.com.cn/pages/detail/detail?id=${this.id}${inviterId ? '&inviterId=' + inviterId : ''}`
// 绘制真实合规的微信二维码
this.drawRealQRCode(ctx, qrUrl, 48, 826, 140)
// 右侧指引文案
ctx.setFontSize(26)
ctx.setFillStyle('#04301E')
ctx.fillText('微信长按识别小程序', 214, 866)
ctx.setFontSize(21)
ctx.setFillStyle('#04874A')
ctx.fillText('查看完整电话 · 微信 · 地图导航', 214, 904)
ctx.setFontSize(18)
ctx.setFillStyle('#88A395')
ctx.fillText('同城生活分类信息 · 官方便民服务保障', 214, 938)
// 10. 最终导出高保真图片
ctx.draw(false, () => {
setTimeout(() => {
uni.canvasToTempFilePath(
{
canvasId: 'postPosterCanvas',
width: W,
height: H,
destWidth: W,
destHeight: H,
fileType: 'png',
quality: 1,
success: (res) => {
this.posterImagePath = res.tempFilePath
this.posterLoading = false
},
fail: () => {
this.posterLoading = false
uni.showToast({ title: '海报绘制失败,请重试', icon: 'none' })
},
},
this
)
}, 260)
})
},
/**
* 预览海报大图
*/
previewPoster() {
if (!this.posterImagePath) return
uni.previewImage({
urls: [this.posterImagePath],
current: this.posterImagePath,
})
},
/**
* 保存海报到手机相册
*/
savePoster() {
if (!this.posterImagePath) {
uni.showToast({ title: '海报准备中,请稍候', icon: 'none' })
return
}
// #ifdef H5
uni.showToast({ title: '请长按上方海报保存图片', icon: 'none' })
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: this.posterImagePath,
success: () => {
uni.showToast({ title: '已保存到系统相册', icon: 'success' })
},
fail: (err) => {
if (err && err.errMsg && (err.errMsg.includes('auth') || err.errMsg.includes('deny'))) {
uni.showModal({
title: '授权提示',
content: '需要您的相册权限以保存海报,是否前往开启?',
confirmText: '去设置',
confirmColor: '#00E070',
success: (res) => {
if (res.confirm) uni.openSetting()
},
})
} else {
uni.showToast({ title: '保存失败,可点击大图长按保存', icon: 'none' })
}
},
})
// #endif
},
/**
* 一键复制海报文案
*/
copyPosterText() {
const text = `【${this.post.categoryName || '同城信息'}】${this.post.title}\n价格:${this.post.price > 0 ? '¥' + this.post.price : '面议'}\n位置:${this.post.address || ''}\n快来同城便民平台查看详情!`
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '文案已复制', icon: 'none' })
},
})
},
},
}
</script>
<style lang="scss" scoped>
.detail {
min-height: 100vh;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
/* 骨架屏结构(淡入微动画) */
.detail__skeleton-wrap {
animation: fadeIn 0.25s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0.6;
}
to {
opacity: 1;
}
}
.detail__content-wrap {
animation: fadeIn 0.25s ease-out;
}
.detail__skeleton-gallery {
width: 100%;
height: 440rpx;
border-radius: 0 0 $fc-radius-lg $fc-radius-lg;
}
.detail__skeleton-card {
padding: 28rpx 28rpx 32rpx;
margin-top: 20rpx;
}
.detail__skeleton-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__skeleton-col {
display: flex;
flex-direction: column;
}
.detail__skeleton-avatar {
width: 84rpx;
height: 84rpx;
border-radius: 50%;
}
.detail__skeleton-pill {
border-radius: $fc-radius-pill;
}
.detail__skeleton-block {
border-radius: $fc-radius;
width: 100%;
}
.detail__skeleton-line {
border-radius: 8rpx;
}
/* 实拍画廊 */
.detail__gallery {
position: relative;
width: 100%;
height: 480rpx;
overflow: hidden;
background: #000000;
}
.detail__swiper {
width: 100%;
height: 100%;
}
.detail__swiper-img {
width: 100%;
height: 100%;
cursor: pointer;
}
.detail__swiper-badge {
position: absolute;
right: 24rpx;
bottom: 24rpx;
padding: 4rpx 18rpx;
border-radius: 999rpx;
background: rgba(0, 0, 0, 0.55);
color: #ffffff;
font-size: 22rpx;
}
.detail__hero-badges--float {
position: absolute;
left: 24rpx;
bottom: 24rpx;
z-index: 2;
}
/* 无图时的插画封面 */
.detail__hero {
padding: 24rpx 24rpx 0;
}
.detail__hero-thumb {
height: 380rpx;
border-radius: $fc-radius-lg;
background: $fc-gradient-soft;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.detail__hero-img {
width: 220rpx;
height: 220rpx;
}
.detail__hero-badges {
display: flex;
margin-top: 18rpx;
}
.detail__badge {
margin-right: 12rpx;
}
.detail__badge--paid {
background: $fc-gradient-warm;
color: #ffffff;
}
/* 核心信息 */
.detail__head {
margin-top: 20rpx;
}
.detail__title {
font-size: 34rpx;
font-weight: 700;
color: $fc-text;
line-height: 1.4;
word-break: break-all;
}
.detail__price-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: 18rpx;
}
.detail__amount {
font-size: 46rpx;
}
.detail__unit {
margin-left: 6rpx;
font-size: $fc-font-xs;
color: $fc-text-secondary;
}
.detail__neg {
font-size: 38rpx;
}
.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: 14rpx;
font-size: $fc-font-sm;
color: $fc-text-secondary;
}
/* 商家门店卡片 */
.detail__merchant {
margin-top: 20rpx;
background: #ffffff;
border-radius: $fc-radius-lg;
padding: 24rpx;
border: 1rpx solid #dcf3e5;
box-shadow: 0 4rpx 16rpx rgba(0, 224, 112, 0.05);
transition: all 0.2s ease;
&:active {
transform: scale(0.995);
background: #fbfdfc;
}
}
.detail__merchant-entry {
display: flex;
align-items: center;
gap: 4rpx;
}
.detail__merchant-entry-txt {
font-size: 22rpx;
color: #04874a;
font-weight: 600;
}
.detail__merchant-entry-arrow {
font-size: 24rpx;
color: #04874a;
font-weight: 700;
}
.detail__merchant-badge-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(90deg, #edfbf3 0%, #f6fdf9 100%);
border-radius: 12rpx;
padding: 10rpx 16rpx;
margin-bottom: 20rpx;
border: 1rpx solid #d4f5e2;
}
.detail__merchant-flag {
display: flex;
align-items: center;
gap: 8rpx;
}
.detail__merchant-flag-txt {
font-size: 22rpx;
font-weight: 700;
color: #04874a;
}
.detail__merchant-security {
font-size: 20rpx;
color: #04874a;
}
.detail__merchant-main {
display: flex;
align-items: center;
}
.detail__merchant-logo {
width: 96rpx;
height: 96rpx;
border-radius: 16rpx;
border: 1rpx solid #eef2f0;
flex-shrink: 0;
background: #f8faf8;
}
.detail__merchant-logo-placeholder {
width: 96rpx;
height: 96rpx;
border-radius: 16rpx;
border: 1rpx solid #dcf3e5;
background: #f0fbf5;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.detail__merchant-meta {
flex: 1;
margin-left: 20rpx;
}
.detail__merchant-name-row {
display: flex;
align-items: center;
}
.detail__merchant-name {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.3;
}
.detail__merchant-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 10rpx;
}
.detail__merchant-tag-item {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 6rpx;
background: #e6f9ef;
color: #04874a;
font-weight: 600;
&--credit {
background: #fef7ec;
color: #d97706;
}
}
.detail__merchant-notice {
display: flex;
align-items: flex-start;
gap: 10rpx;
background: #fbfdfc;
border: 1rpx dashed #d1eedc;
border-radius: 12rpx;
padding: 14rpx 18rpx;
margin-top: 20rpx;
}
.detail__merchant-notice-txt {
flex: 1;
font-size: 24rpx;
color: #334155;
line-height: 1.5;
}
.detail__merchant-address-box {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8faf8;
border-radius: 14rpx;
padding: 16rpx 20rpx;
margin-top: 20rpx;
cursor: pointer;
}
.detail__merchant-address-left {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
margin-right: 16rpx;
}
.detail__merchant-address-txt {
font-size: 26rpx;
color: #1e293b;
font-weight: 500;
line-height: 1.4;
}
.detail__merchant-nav-btn {
background: #00e070;
color: #04301e;
font-size: 22rpx;
font-weight: 700;
padding: 8rpx 18rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
gap: 4rpx;
flex-shrink: 0;
}
.detail__merchant-nav-arrow {
font-size: 24rpx;
font-weight: 700;
}
/* 发布者 */
.detail__publisher {
display: flex;
align-items: center;
}
.detail__avatar {
width: 84rpx;
height: 84rpx;
border-radius: 50%;
background: $fc-gradient-warm;
color: #ffffff;
font-size: 34rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--img {
background: #f0f0f0;
}
}
.detail__publisher-info {
flex: 1;
margin-left: 20rpx;
}
.detail__publisher-name-row {
display: flex;
align-items: center;
}
.detail__publisher-name {
font-size: 30rpx;
font-weight: 600;
color: $fc-text;
}
.detail__verified {
margin-left: 12rpx;
background: #e4fdf0;
border: 1rpx solid rgba(0, 224, 112, 0.4);
color: #04874a;
font-weight: 600;
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: $fc-radius-pill;
}
.detail__publisher-sub {
display: block;
margin-top: 6rpx;
font-size: $fc-font-xs;
color: $fc-text-secondary;
}
.detail__follow {
padding: 10rpx 26rpx;
border-radius: $fc-radius-pill;
border: 2rpx solid $fc-primary-text;
color: $fc-primary-text;
font-size: 24rpx;
font-weight: 600;
&--active {
border-color: $fc-border;
color: $fc-text-placeholder;
background: $fc-bg;
}
}
/* 详细内容 */
.detail__section-title {
font-size: 30rpx;
font-weight: 700;
color: $fc-text;
}
.detail__content {
display: block;
margin-top: 16rpx;
font-size: $fc-font;
color: $fc-text-regular;
line-height: 1.65;
word-break: break-all;
white-space: pre-wrap;
}
.detail__tags {
display: flex;
flex-wrap: wrap;
margin-top: 16rpx;
}
.detail__tag {
margin: 0 12rpx 12rpx 0;
background: #eefbf4;
color: #04874a;
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.detail__map {
margin-top: 20rpx;
display: flex;
align-items: center;
background: $fc-bg;
border-radius: $fc-radius;
padding: 18rpx 20rpx;
}
.detail__map-bg {
width: 96rpx;
height: 76rpx;
border-radius: 10rpx;
background: linear-gradient(135deg, #d8f1e8, #93d6c2);
display: flex;
align-items: center;
justify-content: center;
}
.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: 6rpx;
font-size: $fc-font-xs;
color: $fc-primary-text;
}
.detail__report {
margin-top: 14rpx;
display: flex;
justify-content: flex-end;
}
.detail__report-text {
font-size: $fc-font-xs;
color: $fc-text-placeholder;
}
/* 联系方式专区(极简规范) */
.detail__contact-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__contact-badge {
font-size: 20rpx;
color: #04874a;
background: #e4fdf0;
padding: 4rpx 14rpx;
border-radius: $fc-radius-pill;
font-weight: 600;
}
.detail__contact-unlocked {
margin-top: 16rpx;
}
.detail__contact-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 2rpx solid $fc-border;
}
.detail__contact-key {
font-size: $fc-font;
color: $fc-text-secondary;
}
.detail__contact-name {
font-size: $fc-font;
font-weight: 600;
color: $fc-text;
}
.detail__contact-phone {
font-size: 32rpx;
font-weight: 700;
color: #04874a;
letter-spacing: 1rpx;
}
.detail__contact-wechat {
font-size: 28rpx;
font-weight: 600;
color: $fc-text;
}
.detail__contact-btns {
display: flex;
margin-top: 24rpx;
}
.detail__btn-primary {
flex: 1;
height: 80rpx;
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;
font-weight: 700;
box-shadow: 0 6rpx 18rpx rgba(0, 224, 112, 0.25);
}
.detail__btn-ghost {
margin-left: 20rpx;
width: 220rpx;
height: 80rpx;
border-radius: $fc-radius-pill;
background: #ffffff;
border: 2rpx solid $fc-primary-text;
color: $fc-primary-text;
display: flex;
align-items: center;
justify-content: center;
font-size: $fc-font;
font-weight: 600;
}
.detail__btn-text {
margin-left: 8rpx;
}
/* 未解锁态(极简) */
.detail__contact-locked {
margin-top: 16rpx;
}
.detail__locked-row {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
border-bottom: 2rpx solid $fc-border;
}
.detail__locked-mask {
font-size: 32rpx;
font-weight: 700;
color: $fc-text-placeholder;
letter-spacing: 2rpx;
}
.detail__locked-action-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
}
.detail__btn-unlock {
flex: 1;
height: 80rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 18rpx rgba(0, 224, 112, 0.28);
}
.detail__vip-link {
margin-left: 24rpx;
font-size: 24rpx;
color: #f5a623;
font-weight: 600;
}
/* 档案与编号 */
.detail__archive {
padding: 10rpx 36rpx 20rpx;
display: flex;
flex-direction: column;
}
.detail__archive-id {
font-size: 22rpx;
color: $fc-text-placeholder;
}
.detail__archive-safe {
margin-top: 6rpx;
font-size: 20rpx;
color: $fc-text-placeholder;
}
/* 评价互动 */
.detail__cmt-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__cmt-write {
font-size: $fc-font-sm;
color: $fc-primary-text;
font-weight: 600;
}
.detail__cmt {
margin-top: 20rpx;
padding-top: 18rpx;
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__cmt-score {
display: flex;
align-items: center;
}
.detail__cmt-content {
display: block;
margin-top: 10rpx;
font-size: $fc-font;
color: $fc-text-regular;
line-height: 1.6;
}
.detail__cmt-reply-box {
margin-top: 14rpx;
padding: 14rpx 18rpx;
border-radius: $fc-radius-sm;
background: #f4faf6;
border-left: 4rpx solid $fc-primary;
}
.detail__cmt-reply-tag {
display: inline-block;
font-size: 20rpx;
font-weight: 700;
color: #04874a;
background: rgba(0, 224, 112, 0.18);
padding: 2rpx 12rpx;
border-radius: 6rpx;
margin-right: 12rpx;
vertical-align: middle;
}
.detail__cmt-reply-text {
font-size: $fc-font-sm;
color: $fc-text-regular;
line-height: 1.5;
vertical-align: middle;
}
.detail__cmt-actions {
display: flex;
justify-content: flex-end;
margin-top: 14rpx;
}
.detail__cmt-action-btn {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 20rpx;
background: #eefaf2;
border-radius: $fc-radius-pill;
border: 1rpx solid rgba(0, 224, 112, 0.35);
}
.detail__cmt-action-text {
font-size: 24rpx;
color: #04874a;
font-weight: 600;
}
.detail__cmt-empty {
display: block;
margin-top: 20rpx;
text-align: center;
font-size: $fc-font-sm;
color: $fc-text-placeholder;
}
/* 底部操作栏 */
.detail__bar-space {
height: calc(150rpx + env(safe-area-inset-bottom));
}
.detail__bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
align-items: center;
padding: 14rpx 24rpx calc(14rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-top: 2rpx solid rgba(4, 48, 30, 0.06);
box-shadow: 0 -6rpx 24rpx rgba(4, 48, 30, 0.05);
}
.detail__bar-item {
width: 100rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&--btn {
background: transparent;
border: none;
outline: none;
padding: 0;
margin: 0;
line-height: 1;
&::after {
display: none;
}
}
}
.detail__bar-text {
margin-top: 6rpx;
font-size: 20rpx;
color: $fc-text-secondary;
&--active {
color: #04874a;
font-weight: 700;
}
}
.detail__bar-main {
flex: 1;
height: 84rpx;
margin-left: 12rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.35);
}
/* 统一收银弹层 */
.detail__mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 200;
background: rgba(4, 48, 30, 0.55);
backdrop-filter: blur(4rpx);
display: flex;
align-items: flex-end;
}
.detail__cashier {
width: 100%;
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 36rpx 36rpx calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.detail__cashier-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__cashier-title {
font-size: 32rpx;
font-weight: 700;
color: $fc-text;
}
.detail__cashier-close {
font-size: 36rpx;
color: $fc-text-placeholder;
padding: 8rpx;
}
.detail__cashier-target {
margin-top: 20rpx;
padding: 20rpx;
background: $fc-bg;
border-radius: $fc-radius;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__cashier-target-title {
flex: 1;
font-size: $fc-font;
color: $fc-text-regular;
margin-right: 20rpx;
}
.detail__cashier-price-row {
display: flex;
align-items: baseline;
color: #04874a;
}
.detail__cashier-price-symbol {
font-size: 24rpx;
font-weight: 700;
}
.detail__cashier-price-val {
font-size: 38rpx;
font-weight: 800;
}
.detail__cashier-methods {
margin-top: 20rpx;
}
.detail__cashier-method {
display: flex;
align-items: center;
padding: 22rpx 20rpx;
border-radius: $fc-radius;
border: 2rpx solid $fc-border;
margin-bottom: 14rpx;
&--active {
border-color: #00e070;
background: #eefbf4;
}
}
.detail__cashier-method-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
}
.detail__cashier-method-name {
font-size: $fc-font;
font-weight: 600;
color: $fc-text;
}
.detail__cashier-method-tip {
font-size: 20rpx;
color: $fc-text-placeholder;
margin-top: 2rpx;
}
.detail__cashier-radio {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
border: 2rpx solid $fc-border;
display: flex;
align-items: center;
justify-content: center;
}
.detail__cashier-method--active .detail__cashier-radio {
border-color: #00e070;
}
.detail__cashier-radio-inner {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #00e070;
}
.detail__cashier-vip-banner {
margin-top: 8rpx;
display: flex;
align-items: center;
padding: 16rpx 20rpx;
background: #fff8e8;
border-radius: $fc-radius;
}
.detail__cashier-vip-text {
flex: 1;
margin-left: 12rpx;
font-size: 24rpx;
color: #925800;
}
.detail__cashier-vip-arrow {
font-size: 24rpx;
font-weight: 700;
color: #f5a623;
}
.detail__cashier-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;
box-shadow: 0 6rpx 20rpx rgba(0, 224, 112, 0.35);
&--disabled {
opacity: 0.6;
}
}
/* 评价输入弹层 */
.detail__sheet {
width: 100%;
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 36rpx 32rpx calc(40rpx + env(safe-area-inset-bottom));
}
.detail__sheet-title {
display: block;
text-align: center;
font-size: 32rpx;
font-weight: 700;
color: $fc-text;
}
.detail__stars {
display: flex;
justify-content: center;
margin: 24rpx 0;
}
.detail__star-img {
width: 52rpx;
height: 52rpx;
margin: 0 10rpx;
}
.detail__sheet-input {
width: 100%;
height: 180rpx;
background: $fc-bg;
border-radius: $fc-radius;
padding: 20rpx 24rpx;
font-size: $fc-font;
box-sizing: border-box;
}
.detail__sheet-btn {
margin-top: 24rpx;
height: 84rpx;
border-radius: $fc-radius-pill;
background: $fc-gradient-primary;
color: $fc-primary-ink;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.detail__sheet-btn--disabled {
opacity: 0.6;
pointer-events: none;
}
.detail__sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
position: relative;
}
.detail__sheet-close {
font-size: 36rpx;
color: $fc-text-placeholder;
line-height: 1;
padding: 8rpx;
}
.detail__reply-target {
padding: 16rpx 20rpx;
background: $fc-bg;
border-radius: $fc-radius-sm;
margin-bottom: 20rpx;
font-size: $fc-font-sm;
line-height: 1.5;
}
.detail__reply-target-user {
font-weight: 600;
color: #04874a;
}
.detail__reply-target-content {
color: $fc-text-secondary;
}
/* 海报弹层 */
.detail__poster {
width: 100%;
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 32rpx 36rpx calc(40rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.detail__poster-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail__poster-top-title {
font-size: 32rpx;
font-weight: 700;
color: $fc-text;
}
.detail__poster-close {
font-size: 36rpx;
color: $fc-text-placeholder;
padding: 8rpx;
}
/* 海报加载动画 */
.detail__poster-loading {
height: 480rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.detail__poster-spinner {
width: 54rpx;
height: 54rpx;
border: 6rpx solid #eefbf4;
border-top-color: #00e070;
border-radius: 50%;
animation: posterSpin 0.8s linear infinite;
}
@keyframes posterSpin {
to {
transform: rotate(360deg);
}
}
.detail__poster-loading-text {
margin-top: 24rpx;
font-size: 26rpx;
color: $fc-text-secondary;
}
/* 海报真实预览图 */
.detail__poster-preview {
margin-top: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.detail__poster-img {
width: 480rpx;
max-height: 780rpx;
border-radius: 20rpx;
box-shadow: 0 12rpx 36rpx rgba(4, 48, 30, 0.16);
}
.detail__poster-tip {
margin-top: 14rpx;
font-size: 22rpx;
color: $fc-text-placeholder;
}
/* 底部操作区 */
.detail__poster-actions {
display: flex;
align-items: center;
margin-top: 28rpx;
}
.detail__poster-btn {
flex: 1;
height: 80rpx;
border-radius: $fc-radius-pill;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
&--primary {
flex: 1.4;
background: $fc-gradient-primary;
color: $fc-primary-ink;
box-shadow: 0 6rpx 18rpx rgba(0, 224, 112, 0.3);
}
&--share {
margin-left: 14rpx;
background: #eefbf4;
color: #04874a;
border: none;
outline: none;
line-height: 1;
&::after {
display: none;
}
}
&--ghost {
margin-left: 14rpx;
flex: 0.8;
background: $fc-bg;
color: $fc-text;
border: 2rpx solid $fc-border;
}
}
.detail__poster-btn-text {
margin-left: 6rpx;
}
</style>