Files
medical-mall/pages/mall/consumer/shop-detail.uvue

777 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="shop-detail-page">
<scroll-view class="page-scroll" scroll-y="true" @scrolltolower="onScrollToLower" refresher-enabled="true" @refresherrefresh="onRefresherRefresh" :refresher-triggered="isRefresherTriggered">
<!-- 店铺头部信息 -->
<view class="shop-header">
<image :src="merchant.shop_banner != '' ? merchant.shop_banner : '/static/default-banner.png'" class="shop-banner" mode="aspectFill" />
<view class="shop-info-card">
<image :src="merchant.shop_logo != '' ? merchant.shop_logo : '/static/default-shop.png'" class="shop-logo" />
<view class="shop-basic-info">
<text class="shop-name">{{ merchant.shop_name }}</text>
<view class="shop-stats">
<text class="stat-item">⭐ {{ merchant.rating.toFixed(1) }}</text>
<text class="stat-item">销量 {{ merchant.total_sales }}</text>
</view>
</view>
<view class="shop-actions">
<view class="action-btn chat-btn" @click="contactService">
<text class="action-text">客服</text>
</view>
<view class="action-btn follow-btn" @click="toggleFollow">
<text class="action-text" :class="{ followed: isFollowed }">{{ isFollowed ? '已关注' : '+ 关注' }}</text>
</view>
</view>
</view>
<text class="shop-desc">{{ merchant.shop_description != '' ? merchant.shop_description : '这家店很懒,什么都没写~' }}</text>
<!-- 优惠券列表 (新增) -->
<view class="shop-coupons" v-if="coupons.length > 0">
<scroll-view scroll-x="true" class="coupon-scroll" show-scrollbar="false">
<view class="coupon-wrapper">
<view class="coupon-card" v-for="coupon in coupons" :key="coupon.id" @click="claimCoupon(coupon)">
<view class="coupon-left">
<text class="coupon-amount"><text style="font-size:10px">¥</text>{{ coupon.discount_value }}</text>
<text class="coupon-cond" v-if="parseFloat(String(coupon.min_order_amount)) > 0">满{{ coupon.min_order_amount }}</text>
<text class="coupon-cond" v-else>无门槛</text>
</view>
<view class="coupon-right">
<text class="coupon-btn-label">领取</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 商品列表 -->
<view class="product-section">
<view class="section-title">全部商品</view>
<view class="product-grid">
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="price-row">
<view class="price-left">
<text class="product-price">¥{{ product.price }}</text>
<text class="product-sales">已售 {{ product.sales }}</text>
</view>
<view class="cart-btn" @click.stop="addToCart(product)">
<text class="cart-icon">🛒</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { MerchantType, ProductType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
// 分页相关状态
const currentPage = ref(1)
const pageSize = ref(6) // 默认显示六个
const hasMore = ref(true)
const isLoading = ref(false)
const currentMerchantId = ref('')
const merchant = ref<MerchantType>({
id: '',
user_id: '',
shop_name: '',
shop_logo: '',
shop_banner: '',
shop_description: '',
contact_name: '',
contact_phone: '',
shop_status: 0,
rating: 0,
total_sales: 0,
created_at: ''
} as MerchantType)
const products = ref<ProductType[]>([])
const isFollowed = ref(false)
const coupons = ref<any[]>([]) // 新增优惠券
const isRefresherTriggered = ref(false)
onMounted(() => {
const pages = getCurrentPages()
const options = pages[pages.length - 1].options as any
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
const mId = options['merchantId']
const pId = options['id']
const paramId = (mId != null ? mId : pId) as string
if (paramId) {
console.log('Page mounted with params:', paramId)
// 优先加载店铺信息
loadShopData(paramId).then(() => {
// 加载成功后,使用确定的 merchant_id 来查询关联数据 (商品/优惠券通常是关联在 merchant_id 上的)
const realMerchantId = merchant.value.user_id // 这里 user_id 映射了 DB 中的 merchant_id
if (realMerchantId && realMerchantId !== '') {
console.log('Chain loading products for Corrected Merchant ID:', realMerchantId)
currentMerchantId.value = realMerchantId // 更新当前上下文ID
loadShopProducts(realMerchantId)
loadCoupons(realMerchantId)
} else {
// 防御性策略:如果没能获取 merchant_id尝试用传入 ID
console.warn('Shop load failed or id empty, fallback using original id:', paramId)
currentMerchantId.value = paramId
loadShopProducts(paramId)
loadCoupons(paramId)
}
})
} else {
console.error('No ID passed to shop-detail')
uni.showToast({title: '参数错误', icon: 'error'})
}
})
const onRefresherRefresh = () => {
isRefresherTriggered.value = true
currentPage.value = 1
hasMore.value = true
isLoading.value = false
if (currentMerchantId.value != '') {
const id = currentMerchantId.value
Promise.all([
loadShopData(id),
loadCoupons(id),
loadShopProducts(id)
]).then(() => {
isRefresherTriggered.value = false
})
} else {
setTimeout(() => {
isRefresherTriggered.value = false
}, 500)
}
}
const onScrollToLower = () => {
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Scroll to lower, loading more...')
loadShopProducts(currentMerchantId.value)
}
}
onPullDownRefresh(() => {
onRefresherRefresh()
})
onReachBottom(() => {
onScrollToLower()
})
const loadShopData = async (id: string) => {
console.log('Loading shop data for:', id)
const shop = await supabaseService.getShopByMerchantId(id)
if (shop) {
console.log('Shop loaded successfully:', shop.shop_name)
merchant.value = {
id: shop.id,
user_id: shop.merchant_id, // 映射关系
shop_name: shop.shop_name,
shop_logo: shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png',
shop_banner: shop.shop_banner != null ? shop.shop_banner : '/static/default-banner.png',
shop_description: shop.description != null ? shop.description : '',
contact_name: shop.contact_name != null ? shop.contact_name : '',
contact_phone: shop.contact_phone != null ? shop.contact_phone : '',
shop_status: 1, // 默认正常
rating: shop.rating_avg != null ? shop.rating_avg : 5.0,
total_sales: shop.total_sales != null ? shop.total_sales : 0,
created_at: shop.created_at != null ? shop.created_at : ''
}
// 检查关注状态
checkFollowStatus(shop.id)
} else {
console.warn('Shop data is null for ID:', id)
uni.showToast({
title: '未找到店铺信息',
icon: 'none',
duration: 3000
})
}
}
const loadCoupons = async (id: string) => {
// 安全检查,防止因编译器可以缓存导致的方法未定义错误
// @ts-ignore
if (typeof supabaseService.fetchShopCoupons === 'function') {
coupons.value = await supabaseService.fetchShopCoupons(id)
} else if (typeof supabaseService.getAvailableCoupons === 'function') {
// Fallback to old name
coupons.value = await supabaseService.getAvailableCoupons(id)
} else {
console.warn('SupabaseService.fetchShopCoupons method missing. Please rebuild project.')
}
}
const claimCoupon = async (coupon: any) => {
const userId = supabaseService.getCurrentUserId()
if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
uni.showLoading({ title: '领取中' })
let success = false
// @ts-ignore
if (typeof supabaseService.claimShopCoupon === 'function') {
success = await supabaseService.claimShopCoupon(coupon.id, userId)
} else if (typeof supabaseService.claimCoupon === 'function') {
success = await supabaseService.claimCoupon(coupon.id, userId)
} else {
console.warn('claimCoupon not found')
}
uni.hideLoading()
if (success) {
uni.showToast({ title: '领取成功', icon: 'success' })
} else {
uni.showToast({ title: '领取失败', icon: 'none' })
}
}
const loadShopProducts = async (id: string) => {
if (isLoading.value) return
isLoading.value = true
// 保存当前使用的MerchantID供下拉/触底使用
if (currentPage.value === 1) {
currentMerchantId.value = id
}
console.log(`shop-detail loadShopProducts for: ${id} page: ${currentPage.value}`)
// @ts-ignore
if (typeof supabaseService.getProductsByMerchantId !== 'function') {
console.error('getProductsByMerchantId missing')
isLoading.value = false
uni.stopPullDownRefresh()
return
}
// 传入分页参数
const res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
console.log(`shop-detail getProductsByMerchantId result count: ${res.data?.length}`)
const rawList = res.data
if (rawList != null && Array.isArray(rawList) && rawList.length > 0) {
const list = rawList.map((item): ProductType => {
// 解析图片数组
let images: string[] = []
// 注意类型转换UTS中 any 到具体的类型转换
// 1. 尝试 main_image_url
if (item.main_image_url != null && item.main_image_url !== '') {
images.push(item.main_image_url!)
}
// 2. 尝试 image_urls (如果 main 为空,或者需要展示多图)
if (item.image_urls != null) {
try {
const rawUrl = item.image_urls
if (Array.isArray(rawUrl)) {
// 已经是数组
const arr = rawUrl as string[]
if (arr.length > 0) {
// 如果目前没有图,就全加进去;如果有图(main_image),考虑是否去重
if (images.length == 0) images.push(...arr)
}
} else if (typeof rawUrl === 'string') {
if (rawUrl.startsWith('[')) {
const parsed = JSON.parse(rawUrl)
if (Array.isArray(parsed)) {
const arr = parsed as string[]
if (images.length == 0) images.push(...arr)
}
} else {
// 单个图片路径字符串,如果跟 main_image 不一样才加
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
}
}
} catch(e) {
console.error('解析图片数组失败:', e)
}
}
// 没有任何图片则使用默认
if (images.length === 0) {
images.push('/static/default-product.png')
}
// 安全获取属性的方式,处理字段名称不一样的问题
const safeItem = item as any
let safePrice = safeItem['base_price'] as number
if (safePrice == null) {
const p = safeItem['price'] as number
safePrice = p != null ? p : 0
}
let safeMarketPrice = safeItem['market_price'] as number
if (safeMarketPrice == null) {
const mp = safeItem['original_price'] as number
safeMarketPrice = mp != null ? mp : safePrice
}
let safeStock = safeItem['total_stock'] as number
if (safeStock == null) {
let as_ = safeItem['available_stock'] as number
if (as_ == null) {
const s = safeItem['stock'] as number
safeStock = s != null ? s : 0
} else {
safeStock = as_
}
}
let safeSales = safeItem['sale_count'] as number
if (safeSales == null) {
const s = safeItem['sales'] as number
safeSales = s != null ? s : 0
}
return {
id: item.id,
merchant_id: item.merchant_id,
category_id: item.category_id,
name: item.name,
description: item.description != null ? item.description : '',
images: images,
price: safePrice,
original_price: safeMarketPrice,
stock: safeStock,
sales: safeSales,
status: 1,
created_at: item.created_at != null ? item.created_at : '',
updated_at: item.updated_at != null ? item.updated_at : ''
}
})
if (currentPage.value === 1) {
products.value = list
} else {
products.value.push(...list)
}
// 判断是否还有更多
if (list.length < pageSize.value) {
hasMore.value = false
} else {
hasMore.value = true
currentPage.value++ // 准备下一页
}
} else {
console.log('未加载到店铺商品 (本页为空)')
if (currentPage.value === 1) {
products.value = []
}
hasMore.value = false
}
isLoading.value = false
uni.stopPullDownRefresh()
}
const checkFollowStatus = async (shopId: string) => {
const userId = supabaseService.getCurrentUserId()
if (userId) {
// @ts-ignore
if (typeof supabaseService.isShopFollowed === 'function') {
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
}
}
}
const toggleFollow = async () => {
const userId = supabaseService.getCurrentUserId()
if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
// 这里的 merchant.value.id 假如是 ML_SHOPS.id
const shopId = merchant.value.id
if (shopId == null || shopId == '') return
uni.showLoading({ title: '处理中' })
// @ts-ignore
if (isFollowed.value) {
// 取消关注
// @ts-ignore
const success = await supabaseService.unfollowShop(shopId, userId)
if (success) {
isFollowed.value = false
uni.showToast({ title: '已取消关注', icon: 'none' })
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
} else {
// 关注
// @ts-ignore
const success = await supabaseService.followShop(shopId, userId)
if (success) {
isFollowed.value = true
uni.showToast({ title: '关注成功', icon: 'success' })
} else {
uni.showToast({ title: '关注失败', icon: 'none' })
}
}
uni.hideLoading()
}
const contactService = () => {
const currentUser = supabaseService.getCurrentUserId()
if (currentUser == null) {
uni.navigateTo({ url: '/pages/user/login' })
return
}
if (merchant.value.user_id) {
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchant.value.user_id}&merchantName=${encodeURIComponent(merchant.value.shop_name)}`
})
} else {
uni.showToast({ title: '无法联系商家', icon: 'none'})
}
}
const addToCart = async (product: ProductType) => {
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败,请重试',
icon: 'none'
})
}
}
const goToProduct = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}`
})
}
</script>
<style>
.shop-detail-page {
background-color: #f5f5f5;
flex: 1;
display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
}
.shop-header {
background-color: #fff;
padding-bottom: 20px;
margin-bottom: 10px;
}
.shop-banner {
width: 100%;
height: 150px;
background-color: #eee;
}
.shop-info-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 15px;
margin-top: -30px; /* Logo 向上重叠 banner */
position: relative;
z-index: 1;
}
.shop-logo {
width: 60px;
height: 60px;
border-radius: 8px;
border: 2px solid #fff;
background-color: #fff;
margin-right: 12px;
}
.shop-basic-info {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 30px; /* 给 logo 上浮留空间 */
}
.shop-name {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.shop-stats {
display: flex;
flex-direction: row;
}
.stat-item {
font-size: 12px;
color: #666;
margin-right: 12px;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
}
.shop-actions {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 30px;
}
.action-btn {
/* Common Button Styles */
border-radius: 20px;
margin-left: 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 6px 16px;
}
.action-text {
font-size: 14px;
}
.chat-btn {
background-color: #ffffff;
border: 1px solid #ddd;
}
.chat-btn .action-text {
color: #333;
}
.follow-btn {
background-color: #ff4444;
border: 1px solid #ff4444;
}
.follow-btn .action-text {
color: #ffffff;
}
.follow-btn .followed {
opacity: 0.9;
}
.shop-desc {
color: #666;
padding: 10px 15px 0;
line-height: 1.4;
}
/* Coupon Styles */
.shop-coupons {
margin-top: 15px;
padding: 0 15px;
}
.coupon-scroll {
width: 100%;
white-space: nowrap;
flex-direction: row; /* Ensure flex direction for scroll view */
}
.coupon-wrapper {
display: flex;
flex-direction: row;
flex-wrap: nowrap; /* Prevent wrapping */
align-items: center;
}
.coupon-card {
display: flex; /* Changed from inline-flex to flex */
flex-direction: row;
background-color: #fff5f5;
border: 1px solid #ffccc7;
border-radius: 4px;
margin-right: 10px;
width: 150px; /* Slight increase */
height: 64px;
overflow: hidden;
flex-shrink: 0; /* Critical for horizontal scroll */
}
.coupon-left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-right: 1px dashed #ffccc7;
padding: 0 5px;
}
.coupon-amount {
color: #ff4444;
font-weight: bold;
font-size: 18px;
}
.coupon-cond {
color: #999;
font-size: 10px;
}
.coupon-right {
width: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ff4444;
flex-direction: column;
}
.coupon-btn-label {
color: #fff;
font-size: 12px;
width: 14px; /* Force vertical flow by width constraint if needed, or just let it stack naturally if char by char */
text-align: center;
line-height: 1.2;
}
.product-section {
padding: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 4px solid #ff4444;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
.product-item {
width: 48%; /* Fallback for calc(50% - 5px) */
background-color: white;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
margin-bottom: 10px;
box-sizing: border-box;
}
.product-image {
width: 100%;
height: 170px;
background-color: #f5f5f5;
}
.product-info {
padding: 10px;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-overflow: ellipsis;
lines: 2;
overflow: hidden;
height: 40px;
line-height: 20px;
}
.price-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.price-left {
display: flex;
flex-direction: row;
align-items: flex-end;
}
.cart-btn {
width: 24px;
height: 24px;
background-color: #ff4444;
border-radius: 12px;
}
.cart-icon {
font-size: 14px;
color: white;
}
.product-price {
font-size: 16px;
color: #ff4444;
font-weight: bold;
}
.product-sales {
font-size: 12px;
color: #999;
}
/* PC/Tablet Responsive */
/* Note: UTS/uni-app x media queries support depends on platform.
On Web/H5 this works standard. On App, width is fixed based on screen.
Using standard CSS media queries for H5/PC adaptation.
*/
@media (min-width: 768px) {
.product-item {
width: 32% !important; /* Tablet: 3 items */
}
}
@media (min-width: 1024px) {
.product-item {
width: 16% !important; /* PC: 6 items */
}
.shop-info-card, .shop-header, .product-section {
/* Limit max width on PC to avoid overly stretched content */
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
}
</style>