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

781 行
20 KiB
Vue

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

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

<template>
<view class="shop-detail fc-page">
<!-- 统一沉浸式深绿质感导航栏 -->
<fc-navbar :title="pageTitle" theme="gradient" :show-back="true" />
<!-- 骨架屏占位态 -->
<view v-if="loading && !loaded" class="shop-detail__skeleton">
<view class="shop-detail__skeleton-hero fc-skeleton"></view>
<view class="fc-card shop-detail__skeleton-card">
<view class="shop-detail__skeleton-line fc-skeleton" style="width: 60%; height: 36rpx;"></view>
<view class="shop-detail__skeleton-line fc-skeleton" style="width: 40%; height: 26rpx; margin-top: 18rpx;"></view>
</view>
<view class="fc-card shop-detail__skeleton-card">
<view class="shop-detail__skeleton-line fc-skeleton" style="width: 100%; height: 80rpx;"></view>
</view>
</view>
<!-- 真实内容视图 -->
<view v-else class="shop-detail__body">
<!-- 1. 商家 Hero 形象卡片 -->
<view class="shop-detail__hero-card fc-card">
<view class="shop-detail__hero-top">
<image
v-if="merchant.logo"
class="shop-detail__logo"
:src="assetUrl(merchant.logo)"
mode="aspectFill"
/>
<view v-else class="shop-detail__logo shop-detail__logo--empty">
<image class="fc-icon fc-icon--lg" :src="iconUrl('login-shop') || iconUrl('home-nearby')" mode="aspectFit" />
</view>
<view class="shop-detail__hero-info">
<view class="shop-detail__title-row">
<text class="shop-detail__name">{{ merchant.name || '同城认证门店' }}</text>
</view>
<view class="shop-detail__badges">
<view class="shop-detail__badge-pill shop-detail__badge-pill--green">
<image class="fc-icon fc-icon--xs" :src="iconUrl('check') || iconUrl('shield')" mode="aspectFit" />
<text>官方认证商家</text>
</view>
<text v-if="merchant.category" class="shop-detail__badge-pill shop-detail__badge-pill--gold">
{{ merchant.category }}
</text>
</view>
<text class="shop-detail__slogan">营业执照官方核验 · 实体门店信用保障</text>
</view>
</view>
<!-- 店铺数据栏 -->
<view class="shop-detail__stat-row">
<view class="shop-detail__stat-item">
<text class="shop-detail__stat-val">{{ merchant.postCount || postTotal || 0 }}</text>
<text class="shop-detail__stat-lbl">在招/在售</text>
</view>
<view class="shop-detail__stat-sep"></view>
<view class="shop-detail__stat-item">
<text class="shop-detail__stat-val">100%</text>
<text class="shop-detail__stat-lbl">实体真店</text>
</view>
<view class="shop-detail__stat-sep"></view>
<view class="shop-detail__stat-item">
<text class="shop-detail__stat-val">{{ distanceText || '同城' }}</text>
<text class="shop-detail__stat-lbl">到店距离</text>
</view>
</view>
</view>
<!-- 2. 店铺实体档案卡片(地址导航、咨询电话、营业公告) -->
<view class="fc-card shop-detail__section">
<text class="shop-detail__section-title">门店实体档案</text>
<!-- 门店地址行 -->
<view class="shop-detail__address-row" @click="openLocation">
<view class="shop-detail__address-left">
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-location')" mode="aspectFit" />
<text class="shop-detail__address-txt">{{ merchant.address || '贵州省同城实体门店' }}</text>
</view>
<view class="shop-detail__nav-btn" @click.stop="openLocation">
<text>{{ hasLocation ? '导航到店' : '复制地址' }}</text>
<text class="shop-detail__nav-arrow">›</text>
</view>
</view>
<!-- 门店电话行(支持未解锁受保护提示) -->
<view v-if="merchant.contact" class="shop-detail__cell-row" @click="callPhone">
<view class="shop-detail__cell-left">
<image class="fc-icon fc-icon--sm" :src="iconUrl('phone')" mode="aspectFit" />
<text class="shop-detail__cell-txt">联系咨询:{{ merchant.contact }}</text>
</view>
<view class="shop-detail__call-link" :class="{ 'shop-detail__call-link--locked': !merchant.unlocked }">
<text>{{ merchant.unlocked ? '立即拨打 ›' : '受保护 · 去解锁 ›' }}</text>
</view>
</view>
<!-- 店铺公告栏 -->
<view v-if="merchant.notice" class="shop-detail__notice-box">
<view class="shop-detail__notice-head">
<image class="fc-icon fc-icon--xs" :src="iconUrl('notice')" mode="aspectFit" />
<text class="shop-detail__notice-title">店铺公告</text>
</view>
<text class="shop-detail__notice-content">{{ merchant.notice }}</text>
</view>
</view>
<!-- 3. 工商资质认证背书卡片 -->
<view class="fc-card shop-detail__verify-card">
<view class="shop-detail__verify-item">
<image class="fc-icon fc-icon--sm" :src="iconUrl('check')" mode="aspectFit" />
<text class="shop-detail__verify-txt">工商执照认证已核验</text>
</view>
<view class="shop-detail__verify-item">
<image class="fc-icon fc-icon--sm" :src="iconUrl('check')" mode="aspectFit" />
<text class="shop-detail__verify-txt">线下实体门店经营</text>
</view>
<view class="shop-detail__verify-item">
<image class="fc-icon fc-icon--sm" :src="iconUrl('check')" mode="aspectFit" />
<text class="shop-detail__verify-txt">平台消费者权益保障</text>
</view>
</view>
<!-- 4. 商家全部在售/发布信息列表 -->
<view class="shop-detail__posts-wrap">
<view class="shop-detail__posts-head">
<text class="shop-detail__section-title">在售服务 / 发布信息 ({{ postTotal }})</text>
</view>
<view v-if="posts && posts.length" class="shop-detail__posts-list">
<fc-post-card
v-for="p in posts"
:key="p.id"
:post="p"
@click="goPostDetail(p)"
/>
<view class="shop-detail__more">
<text v-if="loadingMore">加载中…</text>
<text v-else-if="!hasMore">— 已经到底了 —</text>
<text v-else @click="loadMore">点击加载更多</text>
</view>
</view>
<fc-empty v-else text="该商家暂未发布其他信息" />
</view>
</view>
<!-- 底部留白 -->
<view class="shop-detail__bar-space"></view>
<!-- 固定底部快捷主控栏(直接呈现地图导航与联系店长) -->
<view class="shop-detail__bar">
<view class="shop-detail__btn-nav" @click="openLocation">
<image class="fc-icon fc-icon--xs" :src="iconUrl('home-location')" mode="aspectFit" />
<text>地图导航</text>
</view>
<view class="shop-detail__btn-call" :class="{ 'shop-detail__btn-call--locked': !merchant.unlocked }" @click="callPhone">
<image class="fc-icon fc-icon--xs" :src="iconUrl('phone')" mode="aspectFit" />
<text>{{ merchant.unlocked ? '联系店长' : '联系店长 (受保护)' }}</text>
</view>
</view>
<view class="fc-safe-bottom"></view>
</view>
</template>
<script>
/**
* 商家门店公开主页详情页(春绿生态设计主题)
* 核心功能:
* 1. 深度展示商家官方认证身份、门头形象、主营类目与信用资质;
* 2. 实体门店地址一键微信地图路线规划(openLocation)与电话一键呼叫;
* 3. 聚合展示该商家名下全部在售服务与信息流,支持点击跳转并无缝联通;
* 4. 严格遵循春绿 Spring Green 视觉规范,高对比度无障碍交互。
*/
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import FcPostCard from '@/components/fc-post-card/fc-post-card.vue'
import FcEmpty from '@/components/fc-empty/fc-empty.vue'
import { merchantApi } from '@/common/api/merchant.js'
import { iconUrl, loadUiConfig } from '@/common/store/ui.js'
import { assetUrl } from '@/common/utils/format.js'
export default {
components: { FcNavbar, FcPostCard, FcEmpty },
data() {
return {
merchantId: 0,
loading: true,
loaded: false,
merchant: {},
posts: [],
page: 1,
size: 10,
postTotal: 0,
hasMore: false,
loadingMore: false,
userLat: null,
userLng: null,
postId: 0,
}
},
computed: {
/**
* 页面顶部导航栏标题
* @returns {string}
*/
pageTitle() {
return (this.merchant && this.merchant.name) || '店铺主页'
},
/**
* 店铺是否具备真实经纬度坐标
* @returns {boolean}
*/
hasLocation() {
const m = this.merchant
return !!(m && m.lat != null && m.lng != null && Number(m.lat) !== 0 && Number(m.lng) !== 0)
},
/**
* 距离文本格式化
* @returns {string}
*/
distanceText() {
if (this.merchant && this.merchant.distance != null) {
const d = Number(this.merchant.distance)
return d < 1 ? `${Math.round(d * 1000)}m` : `${d.toFixed(1)}km`
}
return ''
},
},
/**
* 页面加载入参初始化
* @param {Object} query 路由入参
*/
onLoad(query) {
this.merchantId = Number(query.id || query.userId || 0)
this.postId = Number(query.postId || 0)
loadUiConfig()
this.fetchLocation()
this.loadDetail()
},
/**
* 触底加载下一页信息流
*/
onReachBottom() {
this.loadMore()
},
/**
* 下拉刷新
*/
async onPullDownRefresh() {
try {
await this.loadDetail(true)
} finally {
uni.stopPullDownRefresh()
}
},
/**
* 微信好友分享配置
*/
onShareAppMessage() {
return {
title: `【认证商家】${this.merchant.name || '同城实体门店'}`,
path: `/pages/merchant/detail?id=${this.merchantId}`,
imageUrl: this.merchant.logo ? assetUrl(this.merchant.logo) : undefined,
}
},
methods: {
iconUrl,
assetUrl,
/**
* 静默获取当前定位经纬度,便于计算到店物理距离
*/
fetchLocation() {
if (typeof uni === 'undefined' || !uni.getLocation) return
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.userLat = res.latitude
this.userLng = res.longitude
},
fail: () => {},
})
},
/**
* 加载商家门店详情主档案
* @param {boolean} refresh 是否为下拉刷新重载
*/
async loadDetail(refresh = false) {
if (!this.merchantId) {
uni.showToast({ title: '参数错误', icon: 'none' })
return
}
if (!refresh) this.loading = true
try {
const params = {}
if (this.postId > 0) {
params.postId = this.postId
}
if (this.userLat != null && this.userLng != null) {
params.lat = this.userLat
params.lng = this.userLng
}
const data = await merchantApi.detail(this.merchantId, params)
if (data) {
this.merchant = data
this.posts = Array.isArray(data.posts) ? data.posts : []
this.postTotal = data.postCount || this.posts.length
this.hasMore = !!data.hasMorePosts
this.page = 1
this.loaded = true
}
} catch (e) {
uni.showToast({ title: (e && e.message) || '店铺信息加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
/**
* 分页加载更多商家名下信息
*/
async loadMore() {
if (!this.hasMore || this.loadingMore) return
this.loadingMore = true
try {
const nextPage = this.page + 1
const res = await merchantApi.posts(this.merchantId, {
page: nextPage,
size: this.size,
})
if (res && Array.isArray(res.list)) {
this.posts = this.posts.concat(res.list)
this.page = nextPage
this.hasMore = !!res.hasMore
if (res.total != null) this.postTotal = res.total
}
} catch (e) {
uni.showToast({ title: '加载更多失败', icon: 'none' })
} finally {
this.loadingMore = false
}
},
/**
* 调起微信地图或复制地址
*/
openLocation() {
const m = this.merchant
const addr = (m && m.address) || ''
const lat = m && m.lat != null ? Number(m.lat) : null
const lng = m && m.lng != null ? Number(m.lng) : null
if (lat != null && lng != null && lat !== 0 && lng !== 0) {
uni.openLocation({
latitude: lat,
longitude: lng,
name: m.name || '商家门店',
address: addr || '实体店详细地址',
fail: () => {
if (addr) {
uni.setClipboardData({
data: addr,
success: () => uni.showToast({ title: '地址已复制', icon: 'none' }),
})
}
},
})
} else if (addr) {
uni.setClipboardData({
data: addr,
success: () => uni.showToast({ title: '门店地址已复制', icon: 'none' }),
})
} else {
uni.showToast({ title: '暂未提供详细地址', icon: 'none' })
}
},
/**
* 一键拨打商家咨询电话(未解锁/非会员状态下严格执行付费拦截)
*/
callPhone() {
const isProtected = !this.merchant || !this.merchant.unlocked || (this.merchant.contact && this.merchant.contact.includes('*'))
if (isProtected) {
uni.showModal({
title: '联系方式受保护',
content: this.postId ? '该商家门店电话受平台付费保护,请先解锁关联的发布信息或开通会员查看。' : '该商家门店联系方式受平台保护,开通会员即可直接联系全站商家。 ',
confirmText: this.postId ? '返回去解锁' : '去开通会员',
cancelText: '知道了',
confirmColor: '#00E070',
success: (r) => {
if (r.confirm) {
if (this.postId) {
uni.navigateBack()
} else {
uni.navigateTo({ url: '/pages/member/index' })
}
}
}
})
return
}
const phone = this.merchant && this.merchant.contact
if (!phone) {
uni.showToast({ title: '商家暂未公开联系电话', icon: 'none' })
return
}
uni.makePhoneCall({
phoneNumber: String(phone).trim(),
fail: () => {},
})
},
/**
* 跳转到信息详情页
* @param {Object} p 信息项
*/
goPostDetail(p) {
if (!p || !p.id) return
uni.navigateTo({
url: `/pages/detail/detail?id=${p.id}`,
})
},
/**
* 返回小程序首页
*/
goHome() {
uni.reLaunch({
url: '/pages/index/index',
})
},
/**
* 点击分享提示
*/
onShareTap() {
uni.showToast({
title: '点击右上角三个点转发给好友',
icon: 'none',
})
},
},
}
</script>
<style lang="scss" scoped>
.shop-detail {
min-height: 100vh;
background: $fc-bg;
}
.shop-detail__share-nav {
padding: 10rpx 14rpx;
display: flex;
align-items: center;
}
/* 骨架屏 */
.shop-detail__skeleton {
padding: 24rpx;
}
.shop-detail__skeleton-hero {
height: 240rpx;
border-radius: $fc-radius-lg;
margin-bottom: 24rpx;
}
.shop-detail__skeleton-card {
padding: 30rpx;
margin-bottom: 24rpx;
}
.shop-detail__skeleton-line {
border-radius: 8rpx;
}
/* 真实页面主体 */
.shop-detail__body {
padding: 20rpx 24rpx;
}
/* 1. Hero 形象卡片 */
.shop-detail__hero-card {
background: #ffffff;
border-radius: $fc-radius-lg;
padding: 30rpx 26rpx;
border: 1rpx solid #e2f4ea;
box-shadow: 0 4rpx 20rpx rgba(4, 48, 30, 0.04);
}
.shop-detail__hero-top {
display: flex;
align-items: center;
}
.shop-detail__logo {
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
border: 2rpx solid #eef6f1;
box-shadow: 0 4rpx 14rpx rgba(4, 48, 30, 0.08);
flex-shrink: 0;
background: #f8faf8;
&--empty {
background: #f0fbf5;
display: flex;
align-items: center;
justify-content: center;
border-color: #dcf3e5;
}
}
.shop-detail__hero-info {
flex: 1;
margin-left: 24rpx;
}
.shop-detail__title-row {
display: flex;
align-items: center;
}
.shop-detail__name {
font-size: 34rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.3;
}
.shop-detail__badges {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 10rpx;
}
.shop-detail__badge-pill {
display: inline-flex;
align-items: center;
gap: 6rpx;
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 6rpx;
font-weight: 600;
&--green {
background: #e6f9ef;
color: #04874a;
border: 1rpx solid #b8ebd0;
}
&--gold {
background: #fef7ec;
color: #d97706;
border: 1rpx solid #fde68a;
}
}
.shop-detail__slogan {
display: block;
font-size: 22rpx;
color: $fc-text-placeholder;
margin-top: 10rpx;
}
/* 店铺数据指标条 */
.shop-detail__stat-row {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 26rpx;
padding-top: 22rpx;
border-top: 1rpx solid #f0f5f2;
}
.shop-detail__stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.shop-detail__stat-val {
font-size: 32rpx;
font-weight: 700;
color: #04874a;
}
.shop-detail__stat-lbl {
font-size: 22rpx;
color: $fc-text-secondary;
margin-top: 4rpx;
}
.shop-detail__stat-sep {
width: 2rpx;
height: 40rpx;
background: #eef2ef;
}
/* 2. 档案与地址卡片 */
.shop-detail__section {
margin-top: 20rpx;
background: #ffffff;
border-radius: $fc-radius-lg;
padding: 26rpx;
border: 1rpx solid #eef4f0;
}
.shop-detail__section-title {
font-size: 30rpx;
font-weight: 700;
color: $fc-text;
margin-bottom: 20rpx;
display: block;
}
.shop-detail__address-row {
display: flex;
align-items: center;
justify-content: space-between;
background: #f8faf8;
border-radius: 14rpx;
padding: 18rpx 20rpx;
cursor: pointer;
}
.shop-detail__address-left {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
margin-right: 16rpx;
}
.shop-detail__address-txt {
font-size: 26rpx;
color: #1e293b;
font-weight: 500;
line-height: 1.4;
}
.shop-detail__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;
}
.shop-detail__nav-arrow {
font-size: 24rpx;
font-weight: 700;
}
.shop-detail__cell-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 4rpx 6rpx;
cursor: pointer;
}
.shop-detail__cell-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.shop-detail__cell-txt {
font-size: 26rpx;
color: #334155;
font-weight: 500;
}
.shop-detail__call-link {
font-size: 24rpx;
font-weight: 600;
color: #04874a;
}
.shop-detail__notice-box {
background: #fbfdfc;
border: 1rpx dashed #d1eedc;
border-radius: 12rpx;
padding: 16rpx 20rpx;
margin-top: 20rpx;
}
.shop-detail__notice-head {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 8rpx;
}
.shop-detail__notice-title {
font-size: 22rpx;
font-weight: 700;
color: #04874a;
}
.shop-detail__notice-content {
font-size: 24rpx;
color: #334155;
line-height: 1.5;
}
/* 3. 资质背书 */
.shop-detail__verify-card {
margin-top: 20rpx;
background: linear-gradient(135deg, #f0fbf5 0%, #f9fdfb 100%);
border-radius: $fc-radius-lg;
padding: 20rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border: 1rpx solid #dcf3e5;
}
.shop-detail__verify-item {
display: flex;
align-items: center;
gap: 6rpx;
}
.shop-detail__verify-txt {
font-size: 20rpx;
color: #04874a;
font-weight: 600;
}
/* 4. 商家信息流 */
.shop-detail__posts-wrap {
margin-top: 28rpx;
}
.shop-detail__posts-head {
padding: 0 4rpx 16rpx;
}
.shop-detail__posts-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.shop-detail__more {
text-align: center;
padding: 24rpx 0;
font-size: 24rpx;
color: $fc-text-placeholder;
}
/* 底部操作栏 */
.shop-detail__bar-space {
height: 130rpx;
}
.shop-detail__bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
border-top: 1rpx solid #eef2ef;
padding: 16rpx 28rpx calc(16rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
gap: 20rpx;
z-index: 99;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.shop-detail__btn-nav {
flex: 1;
height: 84rpx;
border-radius: $fc-radius-pill;
background: #edfbf3;
border: 1rpx solid #a8ebd0;
color: #04874a;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
cursor: pointer;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
.shop-detail__btn-call {
flex: 1;
height: 84rpx;
border-radius: $fc-radius-pill;
background: #00e070;
color: #04301e;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
cursor: pointer;
box-shadow: 0 6rpx 18rpx rgba(0, 224, 112, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
</style>