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

786 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="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" />
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.price }}</text>
<view class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</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'
// 优惠券类型定义
type CouponType = {
id: string
discount_value: number
min_order_amount: number
name: string
start_time: string
end_time: string
status: number
}
// 分页相关状态
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<CouponType[]>([]) // 新增优惠券
const isRefresherTriggered = ref(false)
// 函数定义必须在 onMounted 之前
// checkFollowStatus 必须在 loadShopData 之前定义
const checkFollowStatus = async (shopId: string) => {
const userId = supabaseService.getCurrentUserId()
if (userId != null && userId != '') {
try {
// @ts-ignore
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
} catch(e) {
console.warn('isShopFollowed method not found')
}
}
}
const loadShopData = async (id: string) => {
console.log('Loading shop data for:', id)
const shop = await supabaseService.getShopByMerchantId(id)
if (shop != null) {
console.log('Shop loaded successfully:', shop.shop_name)
// 使用显式类型转换
const merchantData: MerchantType = {
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 : ''
}
merchant.value = merchantData
// 检查关注状态
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) => {
try {
// @ts-ignore
const res = await supabaseService.fetchShopCoupons(id)
if (res != null && Array.isArray(res)) {
const couponList: CouponType[] = []
for (let i = 0; i < res.length; i++) {
const item = res[i]
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
couponList.push({
id: itemObj.getString('id') ?? '',
discount_value: itemObj.getNumber('discount_value') ?? 0,
min_order_amount: itemObj.getNumber('min_order_amount') ?? 0,
name: itemObj.getString('name') ?? '',
start_time: itemObj.getString('start_time') ?? '',
end_time: itemObj.getString('end_time') ?? '',
status: itemObj.getNumber('status') ?? 1
} as CouponType)
}
coupons.value = couponList
}
} catch(e1) {
console.warn('SupabaseService.fetchShopCoupons method missing. Please rebuild project.')
}
}
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}`)
let res: any = {}
try {
// @ts-ignore
res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
} catch(e) {
console.error('getProductsByMerchantId missing or error', e)
isLoading.value = false
uni.stopPullDownRefresh()
return
}
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: any): ProductType => {
// 解析图片数组
let images: string[] = []
// 转换为 UTSJSONObject 安全访问属性
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
// 1. 尝试 main_image_url
const mainImageUrl = itemObj.getString('main_image_url')
if (mainImageUrl != null && mainImageUrl != '') {
images.push(mainImageUrl)
}
// 2. 尝试 image_urls (如果 main 为空,或者需要展示多图)
const imageUrls = itemObj.get('image_urls')
if (imageUrls != null) {
try {
if (Array.isArray(imageUrls)) {
const arr = imageUrls as string[]
if (arr.length > 0) {
if (images.length == 0) images.push(...arr)
}
} else if (typeof imageUrls === 'string') {
const rawUrl = imageUrls as 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 {
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
}
}
} catch(e) {
console.error('解析图片数组失败:', e)
}
}
// 没有任何图片则使用默认
if (images.length === 0) {
images.push('/static/default-product.png')
}
return {
id: itemObj.getString('id') ?? '',
merchant_id: itemObj.getString('merchant_id') ?? '',
category_id: itemObj.getString('category_id') ?? '',
name: itemObj.getString('name') ?? '未知商品',
description: itemObj.getString('description') ?? '',
images: images,
price: itemObj.getNumber('base_price') ?? 0,
original_price: itemObj.getNumber('market_price') ?? 0,
stock: itemObj.getNumber('total_stock') ?? 0,
sales: itemObj.getNumber('sale_count') ?? 0,
status: 1,
created_at: itemObj.getString('created_at') ?? ''
} as ProductType
})
if (currentPage.value === 1) {
products.value = list
} else {
products.value.push(...list)
}
currentPage.value++
hasMore.value = list.length >= pageSize.value
} else {
hasMore.value = false
}
isLoading.value = false
uni.stopPullDownRefresh()
}
onMounted(() => {
const pages = getCurrentPages()
const options = pages[pages.length - 1].options as UTSJSONObject
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
const mId = options.get('merchantId')
const pId = options.get('id')
const paramId = (mId != null ? mId : pId) as string
if (paramId != null && 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 != null && 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 claimCoupon = async (coupon: any) => {
const userId = supabaseService.getCurrentUserId()
if (userId == null) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
uni.showLoading({ title: '领取中' })
// 转换为 UTSJSONObject 安全访问属性
const couponObj = JSON.parse(JSON.stringify(coupon)) as UTSJSONObject
const couponId = couponObj.getString('id') ?? ''
let success = false
try {
// @ts-ignore
success = await supabaseService.claimShopCoupon(couponId, userId)
} catch(e1) {
try {
// @ts-ignore
success = await supabaseService.claimCoupon(couponId, userId)
} catch(e2) {
console.warn('claimCoupon not found')
}
}
uni.hideLoading()
if (success) {
uni.showToast({ title: '领取成功', icon: 'success' })
} else {
uni.showToast({ title: '领取失败', icon: 'none' })
}
}
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 != null && 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: '检查商品...' })
try {
// 使用店铺的 merchant_id
const merchantId = merchant.value.user_id ?? ''
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(product.id)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
uni.showToast({
title: '请选择规格',
icon: 'none'
})
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + product.id
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
uni.showToast({
title: '添加失败,请重试',
icon: 'none'
})
}
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
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: #ff5000;
border: 1px solid #ff5000;
}
.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: #ff5000;
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: #ff5000;
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 #ff5000;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
.product-item {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%;
margin-bottom: 12px;
}
.product-image {
width: 100%;
height: 170px;
border-radius: 8px;
margin-bottom: 8px;
background: #f5f5f5;
}
.product-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
.product-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
}
.product-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.product-add-btn {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.add-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* PC/Tablet Responsive */
@media (min-width: 768px) {
.product-item {
width: 32% !important;
}
}
@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>