consumer模块完成95%,在和商家端对接聊天购物闭环

This commit is contained in:
2026-02-06 17:10:31 +08:00
parent 06b7369494
commit e2f1dfb097
1454 changed files with 5425 additions and 210555 deletions

View File

@@ -12,9 +12,34 @@
<text class="stat-item">销量 {{ merchant.total_sales }}</text>
</view>
</view>
<button class="follow-btn" @click="toggleFollow">{{ isFollowed ? '已关注' : '+ 关注' }}</button>
<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 || '这家店很懒,什么都没写~' }}</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>
<!-- 商品列表 -->
@@ -43,9 +68,17 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
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: '',
@@ -63,21 +96,72 @@ const merchant = ref<MerchantType>({
const products = ref<ProductType[]>([])
const isFollowed = ref(false)
const coupons = ref<any[]>([]) // 新增优惠券
onMounted(() => {
const pages = getCurrentPages()
const options = pages[pages.length - 1].options as any
const merchantId = options['merchantId'] as string
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
const paramId = (options['merchantId'] || options['id']) as string
if (merchantId) {
loadShopData(merchantId)
loadShopProducts(merchantId)
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'})
}
})
onPullDownRefresh(() => {
// 下拉刷新
currentPage.value = 1
hasMore.value = true
isLoading.value = false
if (currentMerchantId.value != '') {
const id = currentMerchantId.value
// 重新加载所有数据
loadShopData(id)
loadCoupons(id)
loadShopProducts(id)
} else {
setTimeout(() => {
uni.stopPullDownRefresh()
}, 500)
}
})
onReachBottom(() => {
// 触底加载更多
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
console.log('Reach bottom, loading more...')
loadShopProducts(currentMerchantId.value)
}
})
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, // 映射关系
@@ -92,41 +176,132 @@ const loadShopData = async (id: string) => {
total_sales: shop.total_sales || 0,
created_at: 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) {
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) => {
const res = await supabaseService.getProductsByMerchantId(id)
if (res.data.length > 0) {
products.value = res.data.map((item): ProductType => {
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[] = []
if (item.image_urls) {
// 注意类型转换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)) {
// 已经是数组
images = rawUrl as string[]
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('[')) {
images = JSON.parse(rawUrl) as string[]
const parsed = JSON.parse(rawUrl)
if (Array.isArray(parsed)) {
const arr = parsed as string[]
if (images.length == 0) images.push(...arr)
}
} else {
// 单个图片路径字符串
images = [rawUrl]
// 单个图片路径字符串,如果跟 main_image 不一样才加
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
}
}
} catch(e) {
console.error('解析图片数组失败:', e)
// 降级处理:尝试直接作为单个图片
if (typeof item.image_urls === 'string') {
images = [item.image_urls!]
}
}
}
if (images.length === 0 && item.main_image_url) {
images.push(item.main_image_url!)
// 没有任何图片则使用默认
if (images.length === 0) {
images.push('/static/default-product.png')
}
// 安全获取属性的方式,处理字段名称不一样的问题
const safeItem = item as any
const safePrice = (safeItem['base_price'] || safeItem['price'] || 0) as number
const safeMarketPrice = (safeItem['market_price'] || safeItem['original_price'] || safePrice) as number
const safeStock = (safeItem['total_stock'] || safeItem['available_stock'] || safeItem['stock'] || 0) as number
const safeSales = (safeItem['sale_count'] || safeItem['sales'] || 0) as number
return {
id: item.id,
merchant_id: item.merchant_id,
@@ -134,24 +309,102 @@ const loadShopProducts = async (id: string) => {
name: item.name,
description: item.description || '',
images: images,
price: item.base_price,
original_price: item.market_price || item.base_price,
stock: item.total_stock || 0,
sales: item.sale_count || 0,
price: safePrice,
original_price: safeMarketPrice,
stock: safeStock,
sales: safeSales,
status: 1,
created_at: item.created_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 toggleFollow = () => {
// TODO: Implement actual follow logic with Supabase
isFollowed.value = !isFollowed.value
uni.showToast({
title: isFollowed.value ? '关注成功' : '已取消关注',
icon: 'none'
})
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) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
// 这里的 merchant.value.id 假如是 ML_SHOPS.id
const shopId = merchant.value.id
if (!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) {
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) => {
@@ -246,14 +499,48 @@ const goToProduct = (id: string) => {
border-radius: 4px;
}
.follow-btn {
font-size: 14px;
background-color: #ff4444;
color: white;
padding: 6px 16px;
.shop-actions {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 30px;
}
.action-btn {
/* Common Button Styles */
border-radius: 20px;
margin-top: 30px; /* 对齐 */
line-height: 1.5;
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 {
@@ -264,6 +551,66 @@ const goToProduct = (id: string) => {
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;
writing-mode: vertical-rl; /* Note: writing-mode may not work in all environments, used flex direction in product detail instead, but let's try or use flex col */
}
.coupon-btn-label {
color: #fff;
font-size: 12px;
writing-mode: vertical-rl;
}
.product-section {
padding: 15px;
}
@@ -279,8 +626,10 @@ const goToProduct = (id: string) => {
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.product-item {
@@ -291,6 +640,7 @@ const goToProduct = (id: string) => {
display: flex;
flex-direction: column;
margin-bottom: 10px;
box-sizing: border-box;
}
.product-image {
@@ -355,4 +705,28 @@ const goToProduct = (id: string) => {
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: calc(33.33% - 7px) !important; /* Tablet: 3 items */
}
}
@media (min-width: 1024px) {
.product-item {
width: calc(16.66% - 9px) !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>