consumerm模块完成度90%,完善消费者和商家端数据库表,商品、聊天、订单数据对接好了supabase,和商家端对接了聊天功能,安卓端编译通过了css样式,剩余几个页面在处理函数规范问题

This commit is contained in:
cyh666666
2026-02-24 17:17:49 +08:00
parent e2f1dfb097
commit e606c597ca
174 changed files with 37917 additions and 4444 deletions

View File

@@ -86,7 +86,7 @@
mode="aspectFill"
/>
<view v-else class="message-icon-default" :style="{ backgroundColor: message.color }">
<text>{{ message.icon }}</text>
<text class="message-icon-text">{{ message.icon }}</text>
</view>
<view v-if="message.online" class="online-dot"></view>
</view>
@@ -202,20 +202,53 @@
<view class="safe-area"></view>
</scroll-view>
<!-- 底部固定按钮 -->
<view class="floating-action">
<!-- 底部固定按钮 (Hidden) -->
<!-- <view class="floating-action">
<button class="action-button" @click="startNewChat">
<text class="button-icon">✏️</text>
<text class="button-text">新建聊天</text>
</button>
</view>
</view> -->
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type Notification, type ChatMessage } from '@/utils/supabaseService.uts'
import { supabaseService, type Notification, type ChatMessage, type ChatRoom } from '@/utils/supabaseService.uts'
// 定义消息项类型
type MessageItem = {
id: string,
title: string,
content: string,
time: string,
read: boolean,
type: string,
avatar: string | null,
important: boolean,
coupon: string,
expiry: string,
claimed: boolean,
order_no: string,
status: string,
statusText: string,
role: string,
lastMessage: string,
online: boolean,
unreadCount: number,
tags: string[],
icon: string,
color: string,
active: boolean
}
// 定义标签类型
type MessageTab = {
id: string,
name: string,
unread: number
}
// 响应式数据
const activeTab = ref<string>('service')
@@ -226,33 +259,22 @@ const statusBarHeight = ref(0)
const scrollTop = ref(0)
const scrollHeight = ref(0)
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 计算滚动区域高度:屏幕高度 - 状态栏 - 导航栏(44) - 标签栏(42)
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 消息分类标签
const messageTabs = reactive([
const messageTabs = reactive<MessageTab[]>([
{ id: 'service', name: '客服消息', unread: 5 },
{ id: 'system', name: '系统通知', unread: 3 },
{ id: 'order', name: '订单消息', unread: 2 },
{ id: 'promo', name: '优惠活动', unread: 2 }
])
// Mock 客服消息数据
const serviceMessages = reactive<any[]>([])
const systemMessages = reactive<any[]>([])
const orderMessages = reactive<any[]>([])
// Mock 优惠活动数据
const promoMessages = reactive<any[]>([])
// 消息数据
const serviceMessages = reactive<MessageItem[]>([])
const systemMessages = reactive<MessageItem[]>([])
const orderMessages = reactive<MessageItem[]>([])
const promoMessages = reactive<MessageItem[]>([])
// 计算当前显示的消息
const currentMessages = computed(() => {
const currentMessages = computed<MessageItem[]>(() => {
switch (activeTab.value) {
case 'system': return systemMessages
case 'order': return orderMessages
@@ -262,21 +284,9 @@ const currentMessages = computed(() => {
}
})
// 生命周期
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
// loadMessages() // 移至 onShow 调用
})
onShow(() => {
console.log('Messages Page Show')
loadMessages()
})
// 简单的日期格式化
const formatTime = (isoString: string): string => {
if (!isoString) return ''
if (isoString == '') return ''
try {
return isoString.split('T')[0]
} catch(e) {
@@ -284,12 +294,56 @@ const formatTime = (isoString: string): string => {
}
}
// 加载消息
// 更新未读数量 - 必须在 loadMessages 之前定义
const updateUnreadCount = () => {
let totalUnread = 0
let serviceUnread = 0
serviceMessages.forEach((msg: MessageItem) => {
if (!msg.read) serviceUnread++
})
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread
let systemUnread = 0
systemMessages.forEach((msg: MessageItem) => {
if (!msg.read) systemUnread++
})
messageTabs[1].unread = systemUnread
totalUnread += systemUnread
let orderUnread = 0
orderMessages.forEach((msg: MessageItem) => {
if (!msg.read) orderUnread++
})
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
let promoUnread = 0
promoMessages.forEach((msg: MessageItem) => {
if (!msg.read) promoUnread++
})
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 初始化页面布局数据
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - statusBarHeight.value - 44 - 42
}
// 加载消息函数 - 必须在 updateUnreadCount 之后定义
const loadMessages = async () => {
loading.value = true
try {
// 清空现有Mock数据
// 清空现有数据
serviceMessages.length = 0
systemMessages.length = 0
orderMessages.length = 0
@@ -299,17 +353,15 @@ const loadMessages = async () => {
const notes = await supabaseService.getUserNotifications()
notes.forEach((note: Notification) => {
// 这里使用 any 类型构建对象,以匹配 reactive 数组的结构
const item = {
const item: MessageItem = {
id: note.id,
title: note.title,
content: note.content,
time: formatTime(note.created_at || ''),
time: formatTime(note.created_at ?? ''),
read: note.is_read,
type: note.type, // 'system', 'order', 'promotion' => 'promo'
// 默认填充字段以避免渲染报错
type: note.type,
avatar: note.icon_url,
important: note.type === 'system', // 简单逻辑
important: note.type === 'system',
coupon: '点击查看',
expiry: '',
claimed: false,
@@ -320,9 +372,10 @@ const loadMessages = async () => {
lastMessage: '',
online: false,
unreadCount: 0,
tags: [],
tags: [] as string[],
icon: '',
color: ''
color: '',
active: false
}
if (note.type === 'system') {
@@ -330,85 +383,44 @@ const loadMessages = async () => {
} else if (note.type === 'order') {
orderMessages.push(item)
} else if (note.type === 'promotion') {
// map type 'promotion' to 'promo' for tab
item.type = 'promo'
promoMessages.push(item)
}
})
// 2. 获取客服消息 (Chat)
const chats = await supabaseService.getUserChatMessages()
// console.log('Raw chats:', chats)
if (chats.length > 0) {
const currentUserId = supabaseService.getCurrentUserId()
const conversations = new Map<string, any>()
// 1. Group by conversation partner
for (const msg of chats) {
const partnerId = (msg.sender_id == currentUserId) ? msg.receiver_id : msg.sender_id
// Skip if partner is null/invalid
if (!partnerId) continue;
if (!conversations.has(partnerId)) {
conversations.set(partnerId, {
partnerId: partnerId,
lastMessage: msg,
unreadCount: 0
})
}
const conv = conversations.get(partnerId)
// Since chats are likely sorted desc, the first one seen is the latest.
// Just count unread: if I am the receiver and it's not read
if (msg.receiver_id == currentUserId && !msg.is_read) {
conv.unreadCount++
}
}
console.log('Conversations found:', conversations.size)
// 2. Fetch shop details for each conversation
const convList = Array.from(conversations.values())
const promises = convList.map(async (conv) => {
const shop = await supabaseService.getShopByMerchantId(conv.partnerId)
const shopName = shop ? shop.shop_name : '未知商家'
const shopAvatar = (shop && shop.logo_url) ? shop.logo_url : '/static/icons/shop-default.png'
return {
id: conv.partnerId, // Use partnerId as the ID for navigation
title: shopName,
role: '商家客服',
content: conv.lastMessage.content,
lastMessage: conv.lastMessage.content,
time: formatTime(conv.lastMessage.created_at || ''),
read: conv.unreadCount === 0,
type: 'service',
avatar: shopAvatar,
online: true,
unreadCount: conv.unreadCount,
tags: shop ? ['官方认证'] : [],
icon: '🏪',
color: '#FF9800',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: ''
}
})
const renderedMessages = await Promise.all(promises)
serviceMessages.push(...renderedMessages)
}
const rooms = await supabaseService.getChatRooms()
rooms.forEach((room: ChatRoom) => {
const msgItem: MessageItem = {
id: room.merchant_id,
title: room.shop_name,
role: '商家客服',
content: room.last_message ?? '暂无消息',
lastMessage: room.last_message ?? '暂无消息',
time: formatTime(room.last_message_at ?? ''),
read: room.unread_count === 0,
type: 'service',
avatar: room.shop_logo ?? '/static/icons/shop-default.png',
online: true,
unreadCount: room.unread_count,
tags: [] as string[],
icon: '🏪',
color: '#FF9800',
important: false,
coupon: '',
expiry: '',
claimed: false,
order_no: '',
status: '',
statusText: '',
active: false
}
serviceMessages.push(msgItem)
})
// 如果没有消息,为了演示效果(或者真的需要),可以保留一个默认的系统客服
// 如果没有消息,添加默认客服
if (serviceMessages.length === 0) {
serviceMessages.push({
const defaultService: MessageItem = {
id: 'default_service',
title: '平台客服',
role: '智能助手',
@@ -429,8 +441,10 @@ const loadMessages = async () => {
claimed: false,
order_no: '',
status: '',
statusText: ''
})
statusText: '',
active: false
}
serviceMessages.push(defaultService)
}
} catch (e) {
@@ -441,28 +455,16 @@ const loadMessages = async () => {
}
}
// 更新未读数量
const updateUnreadCount = () => {
let totalUnread = 0
const serviceUnread = serviceMessages.filter(msg => !msg.read).length
messageTabs[0].unread = serviceUnread
totalUnread += serviceUnread
const systemUnread = systemMessages.filter(msg => !msg.read).length
messageTabs[1].unread = systemUnread
totalUnread += systemUnread
const orderUnread = orderMessages.filter(msg => !msg.read).length
messageTabs[2].unread = orderUnread
totalUnread += orderUnread
const promoUnread = promoMessages.filter(msg => !msg.read).length
messageTabs[3].unread = promoUnread
totalUnread += promoUnread
unreadCount.value = totalUnread
}
// 生命周期钩子
onMounted(() => {
console.log('Messages Page Mounted')
initPage()
})
onShow(() => {
console.log('Messages Page Show')
loadMessages()
})
// 切换标签
const switchTab = (tabId: string) => {
@@ -472,7 +474,7 @@ const switchTab = (tabId: string) => {
}
// 开始与客服聊天
const startChatWithService = (message: any) => {
const startChatWithService = (message: MessageItem) => {
message.read = true
message.unreadCount = 0
updateUnreadCount()
@@ -506,7 +508,7 @@ const startNewChat = () => {
}
// 查看系统消息
const viewSystemMessage = (message: any) => {
const viewSystemMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -515,7 +517,7 @@ const viewSystemMessage = (message: any) => {
}
// 查看订单消息
const viewOrderMessage = (message: any) => {
const viewOrderMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -524,7 +526,7 @@ const viewOrderMessage = (message: any) => {
}
// 查看优惠活动
const viewPromoMessage = (message: any) => {
const viewPromoMessage = (message: MessageItem) => {
message.read = true
updateUnreadCount()
uni.navigateTo({
@@ -533,7 +535,7 @@ const viewPromoMessage = (message: any) => {
}
// 领取优惠券
const claimCoupon = (message: any) => {
const claimCoupon = (message: MessageItem) => {
if (message.claimed) {
uni.showToast({
title: '您已领取该优惠券',
@@ -544,13 +546,14 @@ const claimCoupon = (message: any) => {
message.claimed = true
// 保存领取状态到本地存储,供个人页读取
const claimedCouponsCount = uni.getStorageSync('claimedCoupons') || 0
uni.setStorageSync('claimedCoupons', (claimedCouponsCount as number) + 1)
const claimedCouponsCount = uni.getStorageSync('claimedCoupons')
const count = (claimedCouponsCount != null) ? (claimedCouponsCount as number) : 0
uni.setStorageSync('claimedCoupons', count + 1)
// 保存详细的优惠券信息到 myCoupons 列表
const myCoupons = uni.getStorageSync('myCoupons')
let couponsList: any[] = []
if (myCoupons) {
if (myCoupons != null) {
try {
couponsList = JSON.parse(myCoupons as string) as any[]
} catch (e) {
@@ -579,15 +582,23 @@ const clearAllUnread = () => {
content: '确定要标记所有消息为已读吗?',
success: (res) => {
if (res.confirm) {
serviceMessages.forEach(msg => {
serviceMessages.forEach((msg: MessageItem) => {
msg.read = true
msg.unreadCount = 0
})
systemMessages.forEach(msg => msg.read = true)
orderMessages.forEach(msg => msg.read = true)
promoMessages.forEach(msg => msg.read = true)
systemMessages.forEach((msg: MessageItem) => {
msg.read = true
})
orderMessages.forEach((msg: MessageItem) => {
msg.read = true
})
promoMessages.forEach((msg: MessageItem) => {
msg.read = true
})
messageTabs.forEach(tab => tab.unread = 0)
messageTabs.forEach((tab: MessageTab) => {
tab.unread = 0
})
unreadCount.value = 0
uni.showToast({
@@ -617,7 +628,7 @@ const onRefresh = () => {
/* 页面结构优化 - 避免双滚动条 */
.messages-page {
width: 100%;
height: 100vh;
height: 100%;
background-color: #f8fafc;
display: flex;
flex-direction: column;
@@ -673,7 +684,7 @@ const onRefresh = () => {
background: rgba(255, 255, 255, 0.2);
padding: 4px 12px;
border-radius: 20px;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.2s ease;
}
@@ -689,7 +700,7 @@ const onRefresh = () => {
.action-text {
font-size: 12px;
color: white;
font-weight: 500;
font-weight: bold;
}
/* 导航栏占位符 */
@@ -716,7 +727,7 @@ const onRefresh = () => {
/* overflow-x: auto; 移除自动滚动,改为自适应宽度 */
max-width: 1400px;
margin: 0 auto;
gap: 4px; /* 减小间距 */
/* gap: 4px; removed for uniapp-x support */
justify-content: space-between; /* 均匀分布 */
}
@@ -726,6 +737,7 @@ const onRefresh = () => {
.tab-item {
padding: 0 4px;
margin: 0 2px; /* replaced gap */
display: flex; /* 改为 flex 布局 */
flex-direction: row; /* 关键:横向排列 文字和数字 */
align-items: center; /* 垂直居中 */
@@ -804,7 +816,7 @@ const onRefresh = () => {
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
font-weight: bold;
}
.service-status.online {
@@ -820,9 +832,12 @@ const onRefresh = () => {
}
.service-categories {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
display: flex;
flex-direction: row;
flex-wrap: wrap; /* allow wrapping to simulate grid */
/* grid-template-columns: repeat(2, 1fr); REMOVED */
/* gap: 12px; removed for uniapp-x support */
padding: 6px; /* compensated padding */
}
.category-item {
@@ -832,8 +847,10 @@ const onRefresh = () => {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
transition: all 0.3s ease;
margin: 1%; /* replaced gap */
width: 48%; /* 2 columns */
}
.category-item:hover {
@@ -850,7 +867,7 @@ const onRefresh = () => {
.category-name {
font-size: 13px;
color: #333;
font-weight: 500;
font-weight: bold;
}
/* 消息项 */
@@ -867,7 +884,7 @@ const onRefresh = () => {
align-items: flex-start;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
cursor: pointer;
/* cursor: pointer; removed for uniapp-x support */
}
.message-item:hover {
@@ -906,7 +923,7 @@ const onRefresh = () => {
justify-content: center;
}
.message-icon-default text {
.message-icon-text {
font-size: 24px;
color: white;
}
@@ -950,7 +967,7 @@ const onRefresh = () => {
font-size: 16px;
color: #333;
font-weight: bold;
display: block;
/* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
@@ -963,20 +980,21 @@ const onRefresh = () => {
background: #E8F5E9;
padding: 2px 8px;
border-radius: 10px;
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
}
.message-header-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
/* gap: 4px; removed for uniapp-x support */
}
.message-time {
font-size: 12px;
color: #999;
white-space: nowrap;
margin-bottom: 4px; /* replaced gap */
}
.message-unread-count {
@@ -999,21 +1017,23 @@ const onRefresh = () => {
color: #666;
line-height: 1.4;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* display: -webkit-box; REMOVED for uniapp-x support */
/* -webkit-line-clamp: 2; REMOVED for uniapp-x support */
/* -webkit-box-orient: vertical; REMOVED for uniapp-x support */
lines: 2; /* UTS text truncation */
overflow: hidden;
text-overflow: ellipsis; /* Ensure standard CSS property is present */
}
.last-message {
font-size: 13px;
color: #999;
display: block;
/* display: block; REMOVED for uniapp-x support */
}
.message-tags {
display: flex;
gap: 6px;
/* gap: 6px; removed for uniapp-x support */
flex-wrap: wrap;
}
@@ -1023,6 +1043,8 @@ const onRefresh = () => {
background: #f0f0f0;
padding: 3px 8px;
border-radius: 10px;
margin-right: 6px; /* replaced gap */
margin-bottom: 4px; /* for wrapping */
}
.order-info {
@@ -1031,17 +1053,19 @@ const onRefresh = () => {
background-color: #E8F5E9;
padding: 4px 10px;
border-radius: 4px;
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.order-status {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
font-size: 12px;
padding: 4px 10px;
border-radius: 12px;
margin-top: 8px;
margin-left: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.order-status.shipping {
@@ -1060,13 +1084,14 @@ const onRefresh = () => {
}
.important-tag {
display: inline-block;
/* display: inline-block; REMOVED for uniapp-x support */
background-color: #FF5722;
color: white;
font-size: 11px;
padding: 3px 8px;
border-radius: 10px;
margin-top: 8px;
align-self: flex-start; /* Ensure it doesn't stretch */
}
.coupon-info {
@@ -1080,7 +1105,7 @@ const onRefresh = () => {
.coupon-text {
font-size: 14px;
font-weight: bold;
display: block;
/* display: block; REMOVED for uniapp-x support */
margin-bottom: 4px;
}
@@ -1107,7 +1132,7 @@ const onRefresh = () => {
margin: 15px;
display: flex;
align-items: flex-start;
gap: 10px;
/* gap: 10px; removed for uniapp-x support */
}
.tip-icon {
@@ -1115,6 +1140,7 @@ const onRefresh = () => {
color: #FF9800;
flex-shrink: 0;
margin-top: 2px;
margin-right: 10px; /* replaced gap */
}
.tip-text {
@@ -1167,7 +1193,7 @@ const onRefresh = () => {
display: flex;
align-items: center;
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
font-weight: 500;
font-weight: bold;
}
.button-icon {
@@ -1191,8 +1217,12 @@ const onRefresh = () => {
}
.service-categories {
grid-template-columns: 1fr;
/* grid-template-columns: 1fr; REMOVED */
}
.category-item {
width: 100%; /* 1 column */
}
}
@media screen and (min-width: 415px) {
@@ -1206,7 +1236,7 @@ const onRefresh = () => {
border-radius: 30px;
}
.message-icon-default text {
.message-icon-text {
font-size: 28px;
}
@@ -1215,9 +1245,16 @@ const onRefresh = () => {
}
.service-categories {
grid-template-columns: repeat(4, 1fr);
/* grid-template-columns: repeat(4, 1fr); REMOVED */
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.service-categories .category-item {
width: 25%;
}
/* 平板和桌面端优化标签栏显示 */
.message-tabs {
justify-content: flex-start; /* 左对齐 */