consumer模块完成95%,在和商家端对接聊天购物闭环
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user