consumer模块完成度95%,安卓端大部分页面能正常获取数据,页面样式显示基本正常,逐渐完善;消费者端的积分、余额、评价、优惠券等小模块正在完善
This commit is contained in:
@@ -364,12 +364,23 @@ export class AkSupaQueryBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (total == 0) {
|
if (total == 0) {
|
||||||
if (typeof res['count'] == 'number') {
|
// 使用 JSON 序列化访问 res 对象
|
||||||
total = res['count'] as number ?? 0;
|
const resStr = JSON.stringify(res)
|
||||||
|
const resParsed = JSON.parse(resStr)
|
||||||
|
if (resParsed != null) {
|
||||||
|
const resObj = resParsed as UTSJSONObject
|
||||||
|
const countVal = resObj.getNumber('count')
|
||||||
|
if (countVal != null) {
|
||||||
|
total = countVal
|
||||||
|
} else if (Array.isArray(resdata)) {
|
||||||
|
total = resdata.length
|
||||||
|
} else {
|
||||||
|
total = 0
|
||||||
|
}
|
||||||
} else if (Array.isArray(resdata)) {
|
} else if (Array.isArray(resdata)) {
|
||||||
total = resdata.length;
|
total = resdata.length
|
||||||
} else {
|
} else {
|
||||||
total = 0;
|
total = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasmore) hasmore = (page * limit) < total; // 如果是 head 模式,只返回 count 信息
|
if (!hasmore) hasmore = (page * limit) < total; // 如果是 head 模式,只返回 count 信息
|
||||||
|
|||||||
783
pages/mall/consumer/chat copy.uvue
Normal file
783
pages/mall/consumer/chat copy.uvue
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
<!-- pages/mall/consumer/chat.uvue -->
|
||||||
|
<template>
|
||||||
|
<view class="chat-page">
|
||||||
|
<!-- 聊天头部 -->
|
||||||
|
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
|
||||||
|
<view class="header-back" @click="goBack">
|
||||||
|
<text class="back-icon">‹</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-info">
|
||||||
|
<text class="chat-title">{{ headerTitle }}</text>
|
||||||
|
<text class="chat-status">在线</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-actions">
|
||||||
|
<text class="action-icon" @click="showMoreActions">⋮</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 聊天内容 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y="true"
|
||||||
|
class="chat-content"
|
||||||
|
:scroll-into-view="scrollToView"
|
||||||
|
:scroll-with-animation="false"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
upper-threshold="100"
|
||||||
|
@scrolltoupper="onScrollToUpper"
|
||||||
|
>
|
||||||
|
<!-- 聊天消息列表 -->
|
||||||
|
<view class="chat-messages">
|
||||||
|
<!-- 系统消息 -->
|
||||||
|
<view class="message-item system">
|
||||||
|
<text class="system-text">客服 小美 已接入,请描述您的问题</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 时间分割线 -->
|
||||||
|
<view class="time-divider">
|
||||||
|
<text class="time-text">今天 14:30</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 消息项 -->
|
||||||
|
<view
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
:class="['message-item', message.type]"
|
||||||
|
:id="message.viewId"
|
||||||
|
>
|
||||||
|
<!-- 对方消息 -->
|
||||||
|
<view v-if="message.type === 'received'" class="message-wrapper">
|
||||||
|
<image
|
||||||
|
class="avatar"
|
||||||
|
:src="merchantAvatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="message-content-wrapper">
|
||||||
|
<text class="sender-name">{{ headerTitle }}</text>
|
||||||
|
<view class="message-bubble">
|
||||||
|
<text class="message-text">{{ message.content }}</text>
|
||||||
|
<text class="message-time">{{ message.time }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 我的消息 -->
|
||||||
|
<view v-else class="message-wrapper me">
|
||||||
|
<view class="message-content-wrapper">
|
||||||
|
<view class="message-bubble me">
|
||||||
|
<text class="message-text">{{ message.content }}</text>
|
||||||
|
<text class="message-time">{{ message.time }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
class="avatar me"
|
||||||
|
src="/static/default-avatar.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 聊天输入区 -->
|
||||||
|
<view class="chat-input">
|
||||||
|
<view class="input-tools">
|
||||||
|
<text class="tool-icon" @click="showEmojiPicker">😊</text>
|
||||||
|
<text class="tool-icon" @click="showImagePicker">📷</text>
|
||||||
|
<text class="tool-icon" @click="showMoreTools">➕</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<input
|
||||||
|
class="message-input"
|
||||||
|
v-model="inputMessage"
|
||||||
|
placeholder="请输入消息..."
|
||||||
|
:focus="inputFocus"
|
||||||
|
@confirm="sendMessage"
|
||||||
|
confirm-type="send"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="send-button"
|
||||||
|
:class="{ active: inputMessage.trim() }"
|
||||||
|
@click="sendMessage"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表情选择器 -->
|
||||||
|
<scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
|
||||||
|
<view class="emoji-category">
|
||||||
|
<text
|
||||||
|
v-for="emoji in emojiList"
|
||||||
|
:key="emoji"
|
||||||
|
class="emoji-item"
|
||||||
|
@click="insertEmoji(emoji)"
|
||||||
|
>
|
||||||
|
{{ emoji }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
|
||||||
|
import supa from '@/components/supadb/aksupainstance.uts'
|
||||||
|
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
|
||||||
|
import { getCurrentUser } from '@/utils/store.uts'
|
||||||
|
|
||||||
|
type UiChatMessage = {
|
||||||
|
id: string
|
||||||
|
viewId: string
|
||||||
|
type: string
|
||||||
|
content: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const messages = ref<UiChatMessage[]>([])
|
||||||
|
const inputMessage = ref<string>('')
|
||||||
|
const inputFocus = ref<boolean>(false)
|
||||||
|
const showEmoji = ref<boolean>(false)
|
||||||
|
const scrollToView = ref<string>('')
|
||||||
|
const currentUserId = ref<string>('')
|
||||||
|
const merchantId = ref<string>('') // 商家ID
|
||||||
|
const headerTitle = ref<string>('在线客服')
|
||||||
|
const merchantAvatar = ref<string>('/static/default-shop.png') // 商家头像
|
||||||
|
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
|
||||||
|
const isInitialLoading = ref<boolean>(true)
|
||||||
|
let realtimeChannel: AkSupaRealtimeChannel | null = null
|
||||||
|
|
||||||
|
// 模拟表情列表
|
||||||
|
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
|
||||||
|
|
||||||
|
function scrollToBottom() : void {
|
||||||
|
if (messages.value.length === 0) return
|
||||||
|
|
||||||
|
// 获取最后一条消息的 ID
|
||||||
|
const lastMsg = messages.value[messages.value.length - 1]
|
||||||
|
const targetId = 'msg-' + lastMsg.id
|
||||||
|
|
||||||
|
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
|
||||||
|
// 我们先清除 ID,然后在下一帧赋值,确保 scroll-view 监听到变化。
|
||||||
|
scrollToView.value = ''
|
||||||
|
|
||||||
|
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = targetId
|
||||||
|
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
|
||||||
|
|
||||||
|
// 二次校准:针对长消息或图片导致的高度变化
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = ''
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = targetId
|
||||||
|
console.log('[scrollToBottom] 二次校准完成:', targetId)
|
||||||
|
}, 50)
|
||||||
|
}, 100)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const hours = now.getHours().toString().padStart(2, '0')
|
||||||
|
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRealtimeSubscription(): void {
|
||||||
|
console.log('开始建立聊天实时订阅...')
|
||||||
|
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
|
||||||
|
|
||||||
|
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
|
||||||
|
.on('postgres_changes', {
|
||||||
|
event: 'INSERT',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'ml_chat_messages'
|
||||||
|
}, (payload: any) => {
|
||||||
|
console.log('=== 收到实时订阅回调 ===')
|
||||||
|
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
|
||||||
|
const newMsgAny = payloadObj.get('new')
|
||||||
|
if (newMsgAny == null) {
|
||||||
|
console.log('newMsgAny 为空,跳过')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
|
||||||
|
console.log('收到新消息:', newMsg)
|
||||||
|
|
||||||
|
const senderId = newMsg.getString('sender_id') ?? ''
|
||||||
|
const receiverId = newMsg.getString('receiver_id') ?? ''
|
||||||
|
const msgId = newMsg.getString('id') ?? ''
|
||||||
|
const content = newMsg.getString('content') ?? ''
|
||||||
|
|
||||||
|
console.log('=== 消息详情 ===')
|
||||||
|
console.log('消息ID:', msgId)
|
||||||
|
console.log('发送者ID:', senderId)
|
||||||
|
console.log('接收者ID:', receiverId)
|
||||||
|
console.log('当前用户ID:', currentUserId.value)
|
||||||
|
console.log('商家ID:', merchantId.value)
|
||||||
|
console.log('消息内容:', content)
|
||||||
|
|
||||||
|
// 检查消息是否已经在列表中(避免重复)
|
||||||
|
for (let i = 0; i < messages.value.length; i++) {
|
||||||
|
if (messages.value[i].id == msgId) {
|
||||||
|
console.log('消息已存在,跳过')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断消息类型
|
||||||
|
const isMyMessage = (senderId == currentUserId.value)
|
||||||
|
const isForMe = (receiverId == currentUserId.value)
|
||||||
|
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
|
||||||
|
|
||||||
|
console.log('=== 条件判断 ===')
|
||||||
|
console.log('isMyMessage:', isMyMessage)
|
||||||
|
console.log('isForMe:', isForMe)
|
||||||
|
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
|
||||||
|
|
||||||
|
// 如果消息与当前聊天无关,跳过
|
||||||
|
if (!isRelatedToCurrentChat) {
|
||||||
|
console.log('消息与当前聊天无关,跳过')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是自己发送的消息,或者是发给自己的消息,都显示
|
||||||
|
if (isMyMessage || isForMe) {
|
||||||
|
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
|
||||||
|
const date = new Date(createdAt)
|
||||||
|
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
|
||||||
|
// 生成安全的 viewId
|
||||||
|
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||||
|
|
||||||
|
const incomingMsg: UiChatMessage = {
|
||||||
|
id: msgId,
|
||||||
|
viewId: safeViewId,
|
||||||
|
type: isMyMessage ? 'sent' : 'received',
|
||||||
|
content: content,
|
||||||
|
time: timeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== 添加新消息到列表 ===')
|
||||||
|
console.log('消息类型:', incomingMsg.type)
|
||||||
|
console.log('消息内容:', incomingMsg.content)
|
||||||
|
messages.value.push(incomingMsg)
|
||||||
|
scrollToBottom()
|
||||||
|
} else {
|
||||||
|
console.log('条件不满足,不添加消息')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribe((status: string, err: any | null) => {
|
||||||
|
console.log('订阅状态:', status)
|
||||||
|
if (err != null) {
|
||||||
|
console.log('订阅错误:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChatHistory(): Promise<void> {
|
||||||
|
let rawMsgs : ChatMessage[] = []
|
||||||
|
|
||||||
|
if (merchantId.value != '') {
|
||||||
|
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
||||||
|
} else {
|
||||||
|
console.warn("No merchant ID provided for chat")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保时间顺序是升序(旧的在前,新的在后)
|
||||||
|
// Supabase 返回的消息如果是降序,我们需要 reverse 过来显示
|
||||||
|
const sortedRawMsgs = rawMsgs.sort((a, b) => {
|
||||||
|
const timeA = new Date(a.created_at ?? '').getTime()
|
||||||
|
const timeB = new Date(b.created_at ?? '').getTime()
|
||||||
|
return timeA - timeB
|
||||||
|
})
|
||||||
|
|
||||||
|
const uiMessages : UiChatMessage[] = []
|
||||||
|
for (let i = 0; i < sortedRawMsgs.length; i++) {
|
||||||
|
const m = sortedRawMsgs[i]
|
||||||
|
const date = new Date(m.created_at ?? new Date().toISOString())
|
||||||
|
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
|
||||||
|
const sender = m.sender_id ?? ''
|
||||||
|
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
||||||
|
const rawId = (m.id ?? '').toString()
|
||||||
|
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
||||||
|
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||||
|
|
||||||
|
const uiMsg : UiChatMessage = {
|
||||||
|
id: msgId,
|
||||||
|
viewId: safeViewId,
|
||||||
|
type: msgType,
|
||||||
|
content: m.content ?? '',
|
||||||
|
time: timeStr
|
||||||
|
}
|
||||||
|
uiMessages.push(uiMsg)
|
||||||
|
}
|
||||||
|
messages.value = uiMessages
|
||||||
|
|
||||||
|
if (isInitialLoading.value) {
|
||||||
|
// 增加一点初始化延迟,等待 scroll-view 渲染就绪
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
isInitialLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScrollToUpper(e: any): void {
|
||||||
|
console.log('[onScrollToUpper] 触发加载历史记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMerchantInfo(): Promise<void> {
|
||||||
|
if (merchantId.value == '') return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('ml_shops')
|
||||||
|
.select('shop_logo, shop_name')
|
||||||
|
.eq('merchant_id', merchantId.value)
|
||||||
|
.limit(1)
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
if (response.error != null) {
|
||||||
|
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = response.data
|
||||||
|
if (rawData == null) return
|
||||||
|
|
||||||
|
const rawList = rawData as any[]
|
||||||
|
if (rawList.length == 0) return
|
||||||
|
|
||||||
|
const shopData = rawList[0]
|
||||||
|
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
|
||||||
|
|
||||||
|
const logo = shopObj.getString('shop_logo')
|
||||||
|
if (logo != null && logo != '') {
|
||||||
|
merchantAvatar.value = logo
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = shopObj.getString('shop_name')
|
||||||
|
if (name != null && name != '' && headerTitle.value == '在线客服') {
|
||||||
|
headerTitle.value = name
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onLoad((options: any) => {
|
||||||
|
// 动态获取状态栏高度
|
||||||
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
|
const statusBarH = sysInfo.statusBarHeight
|
||||||
|
// 状态栏高度 + 10px 原有顶部内边距
|
||||||
|
navPaddingTop.value = (statusBarH + 10) + 'px'
|
||||||
|
|
||||||
|
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
|
||||||
|
const mid = optObj.getString('merchantId') ?? ''
|
||||||
|
if (mid !== '') {
|
||||||
|
merchantId.value = mid
|
||||||
|
}
|
||||||
|
const mname = optObj.getString('merchantName') ?? ''
|
||||||
|
if (mname !== '') {
|
||||||
|
headerTitle.value = mname
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
supabaseService.ensureSession().then((uid) => {
|
||||||
|
if (uid != null) {
|
||||||
|
currentUserId.value = uid
|
||||||
|
} else {
|
||||||
|
getCurrentUser().then((user) => {
|
||||||
|
if (user != null) {
|
||||||
|
currentUserId.value = user.id ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMerchantInfo()
|
||||||
|
loadChatHistory()
|
||||||
|
setupRealtimeSubscription()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (realtimeChannel != null) {
|
||||||
|
supa.removeChannel(realtimeChannel!!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
const content = inputMessage.value.trim()
|
||||||
|
if (content == '') return
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
inputMessage.value = ''
|
||||||
|
|
||||||
|
// 发送到 Supabase
|
||||||
|
if (merchantId.value != '') {
|
||||||
|
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
|
||||||
|
const success = await supabaseService.sendMessage(merchantId.value, content)
|
||||||
|
console.log('[sendMessage] 发送结果:', success)
|
||||||
|
if (!success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '发送失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 不需要手动添加消息,等待实时订阅推送
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟客服回复 (已禁用,改用 Realtime)
|
||||||
|
/*
|
||||||
|
const simulateCustomerReply = async () => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */
|
||||||
|
|
||||||
|
// 插入表情
|
||||||
|
function insertEmoji(emoji: string): void {
|
||||||
|
inputMessage.value += emoji
|
||||||
|
inputFocus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示表情选择器
|
||||||
|
function showEmojiPicker(): void {
|
||||||
|
showEmoji.value = !showEmoji.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示图片选择器
|
||||||
|
function showImagePicker(): void {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
success: (res) => {
|
||||||
|
console.log('选择图片:', res.tempFilePaths)
|
||||||
|
// 这里可以处理图片上传
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示更多工具
|
||||||
|
function showMoreTools(): void {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['发送位置', '发送文件', '发送语音'],
|
||||||
|
success: (res) => {
|
||||||
|
console.log('选择工具:', res.tapIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示更多操作
|
||||||
|
function showMoreActions(): void {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['投诉客服', '结束对话', '清除记录'],
|
||||||
|
success: (res) => {
|
||||||
|
switch (res.tapIndex) {
|
||||||
|
case 0:
|
||||||
|
uni.navigateTo({ url: '/pages/mall/consumer/complaint' })
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认结束',
|
||||||
|
content: '确定要结束本次对话吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认清除',
|
||||||
|
content: '确定要清除聊天记录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天头部 */
|
||||||
|
.chat-header {
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-back {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #34c759;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .action-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天内容区 */
|
||||||
|
.chat-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统消息 */
|
||||||
|
.message-item.system {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 时间分割线 */
|
||||||
|
.time-divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息项 */
|
||||||
|
.message-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper.me {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.me {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
/* order: 2; removed for uni-app-x */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content-wrapper {
|
||||||
|
max-width: 70%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble {
|
||||||
|
background-color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
align-self: flex-start; /* 关键:根据内容宽度自适应,不撑满 */
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble.me {
|
||||||
|
background-color: #95ec69;
|
||||||
|
align-self: flex-end; /* 关键:靠右对齐且宽度自适应 */
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble:not(.me) {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天输入区 */
|
||||||
|
.chat-input {
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 10px 15px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-tools {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-right: 10px;
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 60px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button.active {
|
||||||
|
background-color: #ff5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表情选择器 */
|
||||||
|
.emoji-picker {
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 10px;
|
||||||
|
height: 200px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式适配 removed for strict uv-app-x compliance */
|
||||||
|
</style>
|
||||||
@@ -17,10 +17,13 @@
|
|||||||
|
|
||||||
<!-- 聊天内容 -->
|
<!-- 聊天内容 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
scroll-y
|
scroll-y="true"
|
||||||
class="chat-content"
|
class="chat-content"
|
||||||
:scroll-into-view="scrollToView"
|
:scroll-into-view="scrollToView"
|
||||||
scroll-with-animation
|
:scroll-with-animation="false"
|
||||||
|
:show-scrollbar="false"
|
||||||
|
upper-threshold="100"
|
||||||
|
@scrolltoupper="onScrollToUpper"
|
||||||
>
|
>
|
||||||
<!-- 聊天消息列表 -->
|
<!-- 聊天消息列表 -->
|
||||||
<view class="chat-messages">
|
<view class="chat-messages">
|
||||||
@@ -39,13 +42,13 @@
|
|||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
:class="['message-item', message.type]"
|
:class="['message-item', message.type]"
|
||||||
:id="'msg-' + message.id"
|
:id="message.viewId"
|
||||||
>
|
>
|
||||||
<!-- 对方消息 -->
|
<!-- 对方消息 -->
|
||||||
<view v-if="message.type === 'received'" class="message-wrapper">
|
<view v-if="message.type === 'received'" class="message-wrapper">
|
||||||
<image
|
<image
|
||||||
class="avatar"
|
class="avatar"
|
||||||
src="/static/icons/shop-default.png"
|
:src="merchantAvatar"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<view class="message-content-wrapper">
|
<view class="message-content-wrapper">
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<image
|
<image
|
||||||
class="avatar me"
|
class="avatar me"
|
||||||
src="/static/avatar-default.png"
|
src="/static/default-avatar.png"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -127,6 +130,7 @@ import { getCurrentUser } from '@/utils/store.uts'
|
|||||||
|
|
||||||
type UiChatMessage = {
|
type UiChatMessage = {
|
||||||
id: string
|
id: string
|
||||||
|
viewId: string
|
||||||
type: string
|
type: string
|
||||||
content: string
|
content: string
|
||||||
time: string
|
time: string
|
||||||
@@ -141,19 +145,39 @@ const scrollToView = ref<string>('')
|
|||||||
const currentUserId = ref<string>('')
|
const currentUserId = ref<string>('')
|
||||||
const merchantId = ref<string>('') // 商家ID
|
const merchantId = ref<string>('') // 商家ID
|
||||||
const headerTitle = ref<string>('在线客服')
|
const headerTitle = ref<string>('在线客服')
|
||||||
|
const merchantAvatar = ref<string>('/static/default-shop.png') // 商家头像
|
||||||
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
|
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
|
||||||
|
const isInitialLoading = ref<boolean>(true)
|
||||||
let realtimeChannel: AkSupaRealtimeChannel | null = null
|
let realtimeChannel: AkSupaRealtimeChannel | null = null
|
||||||
|
|
||||||
// 模拟表情列表
|
// 模拟表情列表
|
||||||
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
|
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
|
||||||
|
|
||||||
function scrollToBottom(): void {
|
function scrollToBottom() : void {
|
||||||
nextTick(() => {
|
if (messages.value.length === 0) return
|
||||||
if (messages.value.length > 0) {
|
|
||||||
const lastMsgId = messages.value[messages.value.length - 1].id
|
// 获取最后一条消息的 ID
|
||||||
scrollToView.value = 'msg-' + lastMsgId
|
const lastMsg = messages.value[messages.value.length - 1]
|
||||||
}
|
const targetId = 'msg-' + lastMsg.id
|
||||||
})
|
|
||||||
|
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
|
||||||
|
// 我们先清除 ID,然后在下一帧赋值,确保 scroll-view 监听到变化。
|
||||||
|
scrollToView.value = ''
|
||||||
|
|
||||||
|
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = targetId
|
||||||
|
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
|
||||||
|
|
||||||
|
// 二次校准:针对长消息或图片导致的高度变化
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = ''
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToView.value = targetId
|
||||||
|
console.log('[scrollToBottom] 二次校准完成:', targetId)
|
||||||
|
}, 50)
|
||||||
|
}, 100)
|
||||||
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentTime(): string {
|
function getCurrentTime(): string {
|
||||||
@@ -167,14 +191,12 @@ function setupRealtimeSubscription(): void {
|
|||||||
console.log('开始建立聊天实时订阅...')
|
console.log('开始建立聊天实时订阅...')
|
||||||
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
|
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
|
||||||
|
|
||||||
const filter = ({
|
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
|
||||||
event: 'INSERT',
|
.on('postgres_changes', {
|
||||||
schema: 'public',
|
event: 'INSERT',
|
||||||
table: 'ml_chat_messages'
|
schema: 'public',
|
||||||
} as UTSJSONObject)
|
table: 'ml_chat_messages'
|
||||||
|
}, (payload: any) => {
|
||||||
realtimeChannel = supa.channel('public:ml_chat_messages')
|
|
||||||
.on('postgres_changes', filter, (payload: any) => {
|
|
||||||
console.log('=== 收到实时订阅回调 ===')
|
console.log('=== 收到实时订阅回调 ===')
|
||||||
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
|
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
|
||||||
const newMsgAny = payloadObj.get('new')
|
const newMsgAny = payloadObj.get('new')
|
||||||
@@ -228,8 +250,12 @@ function setupRealtimeSubscription(): void {
|
|||||||
const date = new Date(createdAt)
|
const date = new Date(createdAt)
|
||||||
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
|
||||||
|
// 生成安全的 viewId
|
||||||
|
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||||
|
|
||||||
const incomingMsg: UiChatMessage = {
|
const incomingMsg: UiChatMessage = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
|
viewId: safeViewId,
|
||||||
type: isMyMessage ? 'sent' : 'received',
|
type: isMyMessage ? 'sent' : 'received',
|
||||||
content: content,
|
content: content,
|
||||||
time: timeStr
|
time: timeStr
|
||||||
@@ -253,40 +279,96 @@ function setupRealtimeSubscription(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadChatHistory(): Promise<void> {
|
async function loadChatHistory(): Promise<void> {
|
||||||
let rawMsgs: ChatMessage[] = []
|
let rawMsgs : ChatMessage[] = []
|
||||||
|
|
||||||
if (merchantId.value != '') {
|
if (merchantId.value != '') {
|
||||||
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
||||||
} else {
|
} else {
|
||||||
console.warn("No merchant ID provided for chat")
|
console.warn("No merchant ID provided for chat")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 for 循环替代 map
|
// 确保时间顺序是升序(旧的在前,新的在后)
|
||||||
const uiMessages: UiChatMessage[] = []
|
// Supabase 返回的消息如果是降序,我们需要 reverse 过来显示
|
||||||
for (let i = rawMsgs.length - 1; i >= 0; i--) {
|
const sortedRawMsgs = rawMsgs.sort((a, b) => {
|
||||||
const m = rawMsgs[i]
|
const timeA = new Date(a.created_at ?? '').getTime()
|
||||||
const date = new Date(m.created_at ?? new Date().toISOString())
|
const timeB = new Date(b.created_at ?? '').getTime()
|
||||||
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
return timeA - timeB
|
||||||
|
})
|
||||||
|
|
||||||
const sender = m.sender_id ?? ''
|
const uiMessages : UiChatMessage[] = []
|
||||||
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
for (let i = 0; i < sortedRawMsgs.length; i++) {
|
||||||
const rawId = (m.id ?? '').toString()
|
const m = sortedRawMsgs[i]
|
||||||
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
const date = new Date(m.created_at ?? new Date().toISOString())
|
||||||
|
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||||
|
|
||||||
|
const sender = m.sender_id ?? ''
|
||||||
|
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
||||||
|
const rawId = (m.id ?? '').toString()
|
||||||
|
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
||||||
|
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||||
|
|
||||||
|
const uiMsg : UiChatMessage = {
|
||||||
|
id: msgId,
|
||||||
|
viewId: safeViewId,
|
||||||
|
type: msgType,
|
||||||
|
content: m.content ?? '',
|
||||||
|
time: timeStr
|
||||||
|
}
|
||||||
|
uiMessages.push(uiMsg)
|
||||||
|
}
|
||||||
|
messages.value = uiMessages
|
||||||
|
|
||||||
|
if (isInitialLoading.value) {
|
||||||
|
// 增加一点初始化延迟,等待 scroll-view 渲染就绪
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
isInitialLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScrollToUpper(e: any): void {
|
||||||
|
console.log('[onScrollToUpper] 触发加载历史记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMerchantInfo(): Promise<void> {
|
||||||
|
if (merchantId.value == '') return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await supa
|
||||||
|
.from('ml_shops')
|
||||||
|
.select('shop_logo, shop_name')
|
||||||
|
.eq('merchant_id', merchantId.value)
|
||||||
|
.limit(1)
|
||||||
|
.execute()
|
||||||
|
|
||||||
const uiMsg: UiChatMessage = {
|
if (response.error != null) {
|
||||||
id: msgId,
|
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
|
||||||
type: msgType,
|
return
|
||||||
content: m.content ?? '',
|
|
||||||
time: timeStr
|
|
||||||
}
|
}
|
||||||
uiMessages.push(uiMsg)
|
|
||||||
|
const rawData = response.data
|
||||||
|
if (rawData == null) return
|
||||||
|
|
||||||
|
const rawList = rawData as any[]
|
||||||
|
if (rawList.length == 0) return
|
||||||
|
|
||||||
|
const shopData = rawList[0]
|
||||||
|
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
|
||||||
|
|
||||||
|
const logo = shopObj.getString('shop_logo')
|
||||||
|
if (logo != null && logo != '') {
|
||||||
|
merchantAvatar.value = logo
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = shopObj.getString('shop_name')
|
||||||
|
if (name != null && name != '' && headerTitle.value == '在线客服') {
|
||||||
|
headerTitle.value = name
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
|
||||||
}
|
}
|
||||||
messages.value = uiMessages
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
@@ -320,6 +402,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadMerchantInfo()
|
||||||
loadChatHistory()
|
loadChatHistory()
|
||||||
setupRealtimeSubscription()
|
setupRealtimeSubscription()
|
||||||
})
|
})
|
||||||
@@ -340,15 +423,16 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
// 发送到 Supabase
|
// 发送到 Supabase
|
||||||
if (merchantId.value != '') {
|
if (merchantId.value != '') {
|
||||||
// 不使用乐观更新,等待实时订阅推送
|
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
|
||||||
// 这样可以确保多端同步
|
|
||||||
const success = await supabaseService.sendMessage(merchantId.value, content)
|
const success = await supabaseService.sendMessage(merchantId.value, content)
|
||||||
|
console.log('[sendMessage] 发送结果:', success)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '发送失败',
|
title: '发送失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 不需要手动添加消息,等待实时订阅推送
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,22 +522,23 @@ const goBack = () => {
|
|||||||
<style>
|
<style>
|
||||||
.chat-page {
|
.chat-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 聊天头部 */
|
/* 聊天头部 */
|
||||||
.chat-header {
|
.chat-header {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
/* padding-top 由内联样式控制 */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-back {
|
.header-back {
|
||||||
@@ -492,8 +577,16 @@ const goBack = () => {
|
|||||||
/* 聊天内容区 */
|
/* 聊天内容区 */
|
||||||
.chat-content {
|
.chat-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 15px;
|
height: 0;
|
||||||
padding-bottom: 70px; /* 为输入区留出空间 */
|
padding: 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 系统消息 */
|
/* 系统消息 */
|
||||||
@@ -551,31 +644,37 @@ const goBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-content-wrapper {
|
.message-content-wrapper {
|
||||||
/* max-width removed */
|
max-width: 70%;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.sender-name {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message-bubble {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
border-radius: 18px;
|
border-radius: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
/* max-width wrap removed */
|
align-self: flex-start; /* 关键:根据内容宽度自适应,不撑满 */
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble.me {
|
.message-bubble.me {
|
||||||
background-color: #95ec69;
|
background-color: #95ec69;
|
||||||
border-bottom-right-radius: 4px;
|
align-self: flex-end; /* 关键:靠右对齐且宽度自适应 */
|
||||||
|
border-top-right-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble-not-me .message-content {
|
.message-bubble:not(.me) {
|
||||||
border-bottom-left-radius: 4px;
|
border-top-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
@@ -583,6 +682,9 @@ const goBack = () => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-time {
|
.message-time {
|
||||||
@@ -596,11 +698,12 @@ const goBack = () => {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
padding-bottom: 20px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 100;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-tools {
|
.input-tools {
|
||||||
@@ -653,9 +756,8 @@ const goBack = () => {
|
|||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
/* overflow-y removed */
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 60px;
|
bottom: 80px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -456,8 +456,14 @@ const getBrandIcon = (name: string): string => {
|
|||||||
return '🏢'
|
return '🏢'
|
||||||
}
|
}
|
||||||
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
||||||
const iconKeys = ['感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签']
|
const iconKeys = [
|
||||||
const iconValues = ['💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺']
|
'感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签',
|
||||||
|
'九芝堂', '同仁堂', '云南白药', '东阿阿胶', '太极', '江中', '三九', '华素制药', '汤臣倍健', '白云山', '修正', '葵花', '哈药', '扬子江', '恒瑞', '复星', '辉瑞', '阿斯利康', '罗氏', '默沙东', '赛诺菲', '诺华', '雅培', '雀巢', '蒙牛', '伊利', '海尔', '美的', '飞利浦', '西门子', '松下', '苏泊尔', '九阳', '华为', '小米', '苹果', '三星'
|
||||||
|
]
|
||||||
|
const iconValues = [
|
||||||
|
'💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺',
|
||||||
|
'📜', '🏛️', '⛰️', '🍯', '☯️', '🌿', '9️⃣', '💊', '💪', '⛰️', '🩹', '🌻', '🧪', '🚢', '🔬', '⭐', '🧬', '🧪', '🧬', '💊', '🧬', '🔬', '🏥', '🥣', '🐄', '🥛', '🏠', '❄️', '🪒', '⚡', '🔋', '🍳', '🥛', '📱', '🍚', '🍎', '📱'
|
||||||
|
]
|
||||||
|
|
||||||
// 尝试精确匹配
|
// 尝试精确匹配
|
||||||
for (let i = 0; i < iconKeys.length; i++) {
|
for (let i = 0; i < iconKeys.length; i++) {
|
||||||
|
|||||||
@@ -439,8 +439,14 @@ const getBrandIcon = (name: string): string => {
|
|||||||
return '🏢'
|
return '🏢'
|
||||||
}
|
}
|
||||||
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
||||||
const iconKeys = ['感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签']
|
const iconKeys = [
|
||||||
const iconValues = ['💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺']
|
'感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签',
|
||||||
|
'九芝堂', '同仁堂', '云南白药', '东阿阿胶', '太极', '江中', '三九', '华素制药', '汤臣倍健', '白云山', '修正', '葵花', '哈药', '扬子江', '恒瑞', '复星', '辉瑞', '阿斯利康', '罗氏', '默沙东', '赛诺菲', '诺华', '雅培', '雀巢', '蒙牛', '伊利', '海尔', '美的', '飞利浦', '西门子', '松下', '苏泊尔', '九阳', '华为', '小米', '苹果', '三星'
|
||||||
|
]
|
||||||
|
const iconValues = [
|
||||||
|
'💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺',
|
||||||
|
'📜', '🏛️', '⛰️', '🍯', '☯️', '🌿', '9️⃣', '💊', '💪', '⛰️', '🩹', '🌻', '🧪', '🚢', '🔬', '⭐', '🧬', '🧪', '🧬', '💊', '🧬', '🔬', '🏥', '🥣', '🐄', '🥛', '🏠', '❄️', '🪒', '⚡', '🔋', '🍳', '🥛', '📱', '🍚', '🍎', '📱'
|
||||||
|
]
|
||||||
|
|
||||||
// 尝试精确匹配
|
// 尝试精确匹配
|
||||||
for (let i = 0; i < iconKeys.length; i++) {
|
for (let i = 0; i < iconKeys.length; i++) {
|
||||||
@@ -1384,7 +1390,6 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-desc {
|
.card-desc {
|
||||||
@@ -1592,7 +1597,6 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
.keywords-list {
|
.keywords-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1602,7 +1606,6 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,50 +35,142 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
const orderId = ref('')
|
const orderId = ref('')
|
||||||
const productImage = ref('/static/product1.jpg')
|
const productImage = ref('/static/product1.jpg')
|
||||||
const logisticsStatus = ref('运输中')
|
const logisticsStatus = ref('暂无物流信息')
|
||||||
const courierName = ref('顺丰速运')
|
const courierName = ref('')
|
||||||
const courierPhone = ref('95338')
|
const courierPhone = ref('')
|
||||||
const trackingNo = ref('SF1234567890')
|
const trackingNo = ref('')
|
||||||
|
|
||||||
type TrackItem = {
|
type TrackItem = {
|
||||||
desc: string
|
desc: string,
|
||||||
time: string
|
time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackList = ref<TrackItem[]>([
|
const trackList = ref<TrackItem[]>([])
|
||||||
{
|
|
||||||
desc: '【深圳市】快件已到达 深圳南山集散中心',
|
// 加载物流信息函数 - 必须在 onLoad 之前定义
|
||||||
time: '2024-01-26 14:30:00'
|
const loadLogisticsInfo = async () => {
|
||||||
},
|
if (orderId.value == '') return
|
||||||
{
|
|
||||||
desc: '【广州市】快件已从 广州转运中心 发出,准备发往 深圳南山集散中心',
|
try {
|
||||||
time: '2024-01-26 09:20:00'
|
console.log('[logistics] 开始加载物流信息, orderId:', orderId.value)
|
||||||
},
|
const order = await supabaseService.getOrderDetail(orderId.value)
|
||||||
{
|
console.log('[logistics] 获取订单结果:', order != null ? '成功' : '失败')
|
||||||
desc: '【广州市】快件已到达 广州转运中心',
|
|
||||||
time: '2024-01-25 22:15:00'
|
if (order != null) {
|
||||||
},
|
const orderStr = JSON.stringify(order)
|
||||||
{
|
console.log('[logistics] 订单JSON:', orderStr)
|
||||||
desc: '【杭州市】商家已发货',
|
const orderParsed = JSON.parse(orderStr)
|
||||||
time: '2024-01-25 18:00:00'
|
if (orderParsed == null) {
|
||||||
}
|
console.error('[logistics] 解析订单数据失败')
|
||||||
])
|
return
|
||||||
|
}
|
||||||
|
const orderObj = orderParsed as UTSJSONObject
|
||||||
|
|
||||||
|
// 获取物流信息
|
||||||
|
const trackingNoVal = orderObj.getString('tracking_no')
|
||||||
|
const carrierNameVal = orderObj.getString('carrier_name')
|
||||||
|
const shippingStatus = orderObj.getNumber('shipping_status')
|
||||||
|
|
||||||
|
console.log('[logistics] tracking_no:', trackingNoVal)
|
||||||
|
console.log('[logistics] carrier_name:', carrierNameVal)
|
||||||
|
console.log('[logistics] shipping_status:', shippingStatus)
|
||||||
|
|
||||||
|
if (trackingNoVal != null && trackingNoVal != '') {
|
||||||
|
trackingNo.value = trackingNoVal
|
||||||
|
} else {
|
||||||
|
console.log('[logistics] 物流单号为空,订单可能未发货')
|
||||||
|
// 物流单号为空时显示提示
|
||||||
|
trackingNo.value = '暂无物流单号'
|
||||||
|
logisticsStatus.value = '商家未填写物流信息'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (carrierNameVal != null && carrierNameVal != '') {
|
||||||
|
courierName.value = carrierNameVal
|
||||||
|
// 根据快递公司设置电话
|
||||||
|
if (carrierNameVal.includes('顺丰')) {
|
||||||
|
courierPhone.value = '95338'
|
||||||
|
} else if (carrierNameVal.includes('中通')) {
|
||||||
|
courierPhone.value = '95311'
|
||||||
|
} else if (carrierNameVal.includes('圆通')) {
|
||||||
|
courierPhone.value = '95554'
|
||||||
|
} else if (carrierNameVal.includes('韵达')) {
|
||||||
|
courierPhone.value = '95546'
|
||||||
|
} else if (carrierNameVal.includes('申通')) {
|
||||||
|
courierPhone.value = '95543'
|
||||||
|
} else {
|
||||||
|
courierPhone.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据发货状态设置物流状态
|
||||||
|
if (shippingStatus == 2) {
|
||||||
|
logisticsStatus.value = '已签收'
|
||||||
|
} else if (shippingStatus == 1) {
|
||||||
|
logisticsStatus.value = '运输中'
|
||||||
|
} else {
|
||||||
|
logisticsStatus.value = '待发货'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取商品图片
|
||||||
|
const itemsRaw = orderObj.get('ml_order_items')
|
||||||
|
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||||||
|
const items = itemsRaw as any[]
|
||||||
|
if (items.length > 0) {
|
||||||
|
const firstItem = items[0]
|
||||||
|
const itemStr = JSON.stringify(firstItem)
|
||||||
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed != null) {
|
||||||
|
const itemObj = itemParsed as UTSJSONObject
|
||||||
|
const imgUrl = itemObj.getString('image_url')
|
||||||
|
if (imgUrl != null && imgUrl != '') {
|
||||||
|
productImage.value = imgUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建物流轨迹(如果有发货时间)
|
||||||
|
const shippedAt = orderObj.getString('shipped_at')
|
||||||
|
if (shippedAt != null && shippedAt != '') {
|
||||||
|
const trackItem: TrackItem = {
|
||||||
|
desc: '商家已发货,等待快递揽收',
|
||||||
|
time: shippedAt
|
||||||
|
}
|
||||||
|
trackList.value.push(trackItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已签收,添加签收信息
|
||||||
|
const deliveredAt = orderObj.getString('delivered_at')
|
||||||
|
if (deliveredAt != null && deliveredAt != '') {
|
||||||
|
const trackItem: TrackItem = {
|
||||||
|
desc: '快件已签收',
|
||||||
|
time: deliveredAt
|
||||||
|
}
|
||||||
|
trackList.value.unshift(trackItem)
|
||||||
|
logisticsStatus.value = '已签收'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载物流信息失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options == null) return
|
||||||
|
const orderIdValue = options['orderId']
|
||||||
|
if (orderIdValue != null) {
|
||||||
|
orderId.value = orderIdValue as string
|
||||||
|
loadLogisticsInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages()
|
// 逻辑已移到 onLoad
|
||||||
const currentPage = pages[pages.length - 1]
|
|
||||||
const options = currentPage.options
|
|
||||||
if (options != null) {
|
|
||||||
const optionsObj = options as UTSJSONObject
|
|
||||||
const orderIdValue = optionsObj.getString('orderId')
|
|
||||||
if (orderIdValue != null) {
|
|
||||||
orderId.value = orderIdValue
|
|
||||||
// 这里可以根据orderId去请求真实的物流信息
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1358
pages/mall/consumer/orders copy.uvue
Normal file
1358
pages/mall/consumer/orders copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,13 +18,20 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单状态筛选 -->
|
<!-- 订单状态筛选 -->
|
||||||
<view class="order-tabs">
|
<view class="order-tabs-fixed-container">
|
||||||
<scroll-view scroll-x class="tab-scroll" :show-scrollbar="false">
|
<view
|
||||||
<view class="tab-container">
|
:class="['tab-item-fixed', { active: activeTab === 'all' }]"
|
||||||
|
@click="switchTab('all')"
|
||||||
|
>
|
||||||
|
<text class="tab-name">全部</text>
|
||||||
|
<view v-if="activeTab === 'all'" class="active-indicator"></view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-x="true" class="tab-scroll-mobile" :show-scrollbar="false" :scroll-with-animation="true">
|
||||||
|
<view class="tab-container-mobile">
|
||||||
<view
|
<view
|
||||||
v-for="tab in orderTabs"
|
v-for="tab in orderTabsMobile"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:class="['tab-item', { active: activeTab === tab.id }]"
|
:class="['tab-item-mobile', { active: activeTab === tab.id }]"
|
||||||
@click="switchTab(tab.id)"
|
@click="switchTab(tab.id)"
|
||||||
>
|
>
|
||||||
<text class="tab-name">{{ tab.name }}</text>
|
<text class="tab-name">{{ tab.name }}</text>
|
||||||
@@ -58,10 +65,15 @@
|
|||||||
v-for="order in orders"
|
v-for="order in orders"
|
||||||
:key="order.id"
|
:key="order.id"
|
||||||
class="order-card"
|
class="order-card"
|
||||||
|
@click="viewOrderDetail(order.id)"
|
||||||
>
|
>
|
||||||
<!-- 订单头部 -->
|
<!-- 订单头部:显示店铺名称 -->
|
||||||
<view class="order-header">
|
<view class="order-card-header">
|
||||||
<text class="order-no">订单号:{{ order.order_no }}</text>
|
<view class="shop-info">
|
||||||
|
<text class="shop-icon">🏪</text>
|
||||||
|
<text class="shop-name">{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '自营店铺' }}</text>
|
||||||
|
<text class="arrow-right">›</text>
|
||||||
|
</view>
|
||||||
<text :class="['order-status', getStatusClass(order.status)]">
|
<text :class="['order-status', getStatusClass(order.status)]">
|
||||||
{{ getStatusText(order.status) }}
|
{{ getStatusText(order.status) }}
|
||||||
</text>
|
</text>
|
||||||
@@ -81,34 +93,29 @@
|
|||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<view class="product-info">
|
<view class="product-info">
|
||||||
<text class="product-name">{{ product.name }}</text>
|
<view class="product-top-info">
|
||||||
<text class="product-spec">{{ product.spec }}</text>
|
<text class="product-name">{{ product.name }}</text>
|
||||||
|
<text class="product-spec">{{ product.spec }}</text>
|
||||||
|
</view>
|
||||||
<view class="product-footer">
|
<view class="product-footer">
|
||||||
<text class="product-price">¥{{ product.price }}</text>
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
<text class="product-quantity">×{{ product.quantity }}</text>
|
<text class="product-quantity">x{{ product.quantity }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单信息 -->
|
<!-- 订单汇总信息 -->
|
||||||
<view class="order-info">
|
<view class="order-summary">
|
||||||
<view class="info-row">
|
<text class="order-time">{{ formatDate(order.create_time) }}</text>
|
||||||
<text class="info-label">商品合计</text>
|
<view class="summary-right">
|
||||||
<text class="info-value">¥{{ order.product_amount }}</text>
|
<text class="summary-label">共{{ order.products.length }}件商品 实付:</text>
|
||||||
</view>
|
<text class="summary-price">¥{{ order.total_amount }}</text>
|
||||||
<view class="info-row">
|
|
||||||
<text class="info-label">运费</text>
|
|
||||||
<text class="info-value">¥{{ order.shipping_fee }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-row total">
|
|
||||||
<text class="info-label">实付款</text>
|
|
||||||
<text class="info-value total-price">¥{{ order.total_amount }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单操作 -->
|
<!-- 订单操作 -->
|
||||||
<view class="order-actions">
|
<view class="order-actions" @click.stop="">
|
||||||
<view v-if="order.status === 1" class="action-buttons">
|
<view v-if="order.status === 1" class="action-buttons">
|
||||||
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
|
<button class="action-btn cancel" @click="cancelOrder(order.id)">取消订单</button>
|
||||||
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
|
<button class="action-btn pay" @click="payOrder(order.id)">立即支付</button>
|
||||||
@@ -162,24 +169,6 @@ import { ref, reactive, onMounted, computed } from 'vue'
|
|||||||
import { onShow, onLoad, onBackPress } from '@dcloudio/uni-app'
|
import { onShow, onLoad, onBackPress } from '@dcloudio/uni-app'
|
||||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
// 拦截返回事件,避免跳回登录页
|
|
||||||
onBackPress((options) => {
|
|
||||||
if (options.from === 'navigateBack') {
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
if (pages.length > 1) {
|
|
||||||
const prevPage = pages[pages.length - 2]
|
|
||||||
// 如果上一页是登录页,则重定向到个人中心
|
|
||||||
if (prevPage.route.includes('login')) {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: '/pages/mall/consumer/profile'
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义标签页类型
|
// 定义标签页类型
|
||||||
type OrderTabItem = {
|
type OrderTabItem = {
|
||||||
id: string,
|
id: string,
|
||||||
@@ -206,6 +195,8 @@ type OrderItem = {
|
|||||||
product_amount: number,
|
product_amount: number,
|
||||||
shipping_fee: number,
|
shipping_fee: number,
|
||||||
total_amount: number,
|
total_amount: number,
|
||||||
|
merchant_id: string,
|
||||||
|
shop_name: string,
|
||||||
products: OrderProduct[]
|
products: OrderProduct[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +221,10 @@ const orderTabs = ref<OrderTabItem[]>([
|
|||||||
{ id: 'cancelled', name: '已取消', count: 0 }
|
{ id: 'cancelled', name: '已取消', count: 0 }
|
||||||
])
|
])
|
||||||
|
|
||||||
// Removed Mock Data
|
// 模拟状态筛选(除去"全部"后的其余标签)
|
||||||
|
const orderTabsMobile = computed((): OrderTabItem[] => {
|
||||||
|
return orderTabs.value.filter((tab: OrderTabItem) => tab.id !== 'all')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// 辅助函数:获取状态码
|
// 辅助函数:获取状态码
|
||||||
@@ -243,59 +237,98 @@ const getStatusByTab = (tabId: string): number => {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:解析规格文本
|
// 格式化规格对象为友好的文本 - 必须在 parseSpecText 之前定义
|
||||||
const parseSpecText = (specs: any): string => {
|
function formatSpecObj(obj: any): string {
|
||||||
if (specs == null) return ''
|
if (obj == null) return ''
|
||||||
if (typeof specs === 'string') return specs
|
if (typeof obj !== 'object') {
|
||||||
// 对于对象类型,尝试转为JSON字符串或简单处理
|
// 非对象类型直接返回字符串形式
|
||||||
|
if (typeof obj === 'string') return obj
|
||||||
|
if (typeof obj === 'number') return obj.toString()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(specs)
|
const objStr = JSON.stringify(obj)
|
||||||
|
const objParsed = JSON.parse(objStr)
|
||||||
|
if (objParsed == null) return ''
|
||||||
|
|
||||||
|
const specObj = objParsed as UTSJSONObject
|
||||||
|
|
||||||
|
// 使用 JSON.stringify 获取所有键
|
||||||
|
const specObjStr = JSON.stringify(specObj)
|
||||||
|
const specObjForKeys = JSON.parse(specObjStr) as UTSJSONObject
|
||||||
|
|
||||||
|
// 手动提取键值对
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
// 尝试获取已知字段
|
||||||
|
const colorVal = specObjForKeys.getString('Color')
|
||||||
|
if (colorVal != null && colorVal != '') {
|
||||||
|
parts.push('Color: ' + colorVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeVal = specObjForKeys.getString('Size')
|
||||||
|
if (sizeVal != null && sizeVal != '') {
|
||||||
|
parts.push('Size: ' + sizeVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultVal = specObjForKeys.getString('默认')
|
||||||
|
if (defaultVal != null && defaultVal != '') {
|
||||||
|
parts.push('默认: ' + defaultVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配到已知字段,尝试直接显示 JSON
|
||||||
|
if (parts.length === 0) {
|
||||||
|
// 尝试遍历对象
|
||||||
|
const objAny = specObjForKeys as any
|
||||||
|
if (objAny != null) {
|
||||||
|
return specObjStr.replace(/[{}"]/g, '').replace(/:/g, ': ').replace(/,/g, ' | ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' | ')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:解析规格文本
|
||||||
|
function parseSpecText(specs: any): string {
|
||||||
|
if (specs == null) return ''
|
||||||
|
if (typeof specs === 'string') {
|
||||||
|
// 如果是 JSON 字符串,尝试解析
|
||||||
|
if (specs.startsWith('{') || specs.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(specs)
|
||||||
|
if (parsed == null) return specs
|
||||||
|
return formatSpecObj(parsed)
|
||||||
|
} catch (e) {
|
||||||
|
return specs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return specs
|
||||||
|
}
|
||||||
|
// 对于对象类型,格式化显示
|
||||||
|
return formatSpecObj(specs)
|
||||||
|
}
|
||||||
|
|
||||||
// 辅助函数:更新标签计数
|
// 辅助函数:更新标签计数
|
||||||
const updateTabsCounts = (allOrders: any[]) => {
|
const updateTabsCounts = (allOrders: OrderItem[]) => {
|
||||||
// 直接重新赋值整个数组
|
|
||||||
const tabsData = orderTabs.value
|
|
||||||
// 计算各状态数量
|
// 计算各状态数量
|
||||||
const countAll = allOrders.length
|
const countAll = allOrders.length
|
||||||
const countPending = allOrders.filter((o: any) => {
|
const countPending = allOrders.filter((o: OrderItem) => o.status === 1).length
|
||||||
const obj = o as Record<string, any>
|
const countShipping = allOrders.filter((o: OrderItem) => o.status === 2).length
|
||||||
return obj['status'] === 1
|
const countDelivering = allOrders.filter((o: OrderItem) => o.status === 3).length
|
||||||
}).length
|
const countCompleted = allOrders.filter((o: OrderItem) => o.status === 4).length
|
||||||
const countShipping = allOrders.filter((o: any) => {
|
const countCancelled = allOrders.filter((o: OrderItem) => o.status === 5).length
|
||||||
const obj = o as Record<string, any>
|
|
||||||
return obj['status'] === 2
|
|
||||||
}).length
|
|
||||||
const countDelivering = allOrders.filter((o: any) => {
|
|
||||||
const obj = o as Record<string, any>
|
|
||||||
return obj['status'] === 3
|
|
||||||
}).length
|
|
||||||
const countCompleted = allOrders.filter((o: any) => {
|
|
||||||
const obj = o as Record<string, any>
|
|
||||||
return obj['status'] === 4
|
|
||||||
}).length
|
|
||||||
const countCancelled = allOrders.filter((o: any) => {
|
|
||||||
const obj = o as Record<string, any>
|
|
||||||
return obj['status'] === 5
|
|
||||||
}).length
|
|
||||||
|
|
||||||
// 更新数组元素
|
// 更新数组元素
|
||||||
const tabsArr = tabsData as any[]
|
orderTabs.value[0].count = countAll
|
||||||
const tab0 = tabsArr[0] as Record<string, any>
|
orderTabs.value[1].count = countPending
|
||||||
tab0['count'] = countAll
|
orderTabs.value[2].count = countShipping
|
||||||
const tab1 = tabsArr[1] as Record<string, any>
|
orderTabs.value[3].count = countDelivering
|
||||||
tab1['count'] = countPending
|
orderTabs.value[4].count = countCompleted
|
||||||
const tab2 = tabsArr[2] as Record<string, any>
|
orderTabs.value[5].count = countCancelled
|
||||||
tab2['count'] = countShipping
|
|
||||||
const tab3 = tabsArr[3] as Record<string, any>
|
|
||||||
tab3['count'] = countDelivering
|
|
||||||
const tab4 = tabsArr[4] as Record<string, any>
|
|
||||||
tab4['count'] = countCompleted
|
|
||||||
const tab5 = tabsArr[5] as Record<string, any>
|
|
||||||
tab5['count'] = countCancelled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:按标签筛选订单
|
// 辅助函数:按标签筛选订单
|
||||||
@@ -317,69 +350,124 @@ const loadOrders = async () => {
|
|||||||
try {
|
try {
|
||||||
// Fetch all orders from Supabase (status=0)
|
// Fetch all orders from Supabase (status=0)
|
||||||
const fetchedOrders = await supabaseService.getOrders(0)
|
const fetchedOrders = await supabaseService.getOrders(0)
|
||||||
|
console.log('[loadOrders] 获取到订单数量:', fetchedOrders.length)
|
||||||
|
|
||||||
// Map to View Model
|
// Map to View Model
|
||||||
const mappedOrders: any[] = []
|
const mappedOrders: OrderItem[] = []
|
||||||
for (let i = 0; i < fetchedOrders.length; i++) {
|
for (let i = 0; i < fetchedOrders.length; i++) {
|
||||||
const order = fetchedOrders[i]
|
const order = fetchedOrders[i]
|
||||||
const orderObj = order as Record<string, any>
|
// 使用 JSON 序列化转换
|
||||||
const items = orderObj['ml_order_items'] as any[]
|
const orderStr = JSON.stringify(order)
|
||||||
const productsList: any[] = []
|
const orderParsed = JSON.parse(orderStr)
|
||||||
|
if (orderParsed == null) continue
|
||||||
|
const orderObj = orderParsed as UTSJSONObject
|
||||||
|
|
||||||
if (items != null) {
|
const itemsRaw = orderObj.get('ml_order_items')
|
||||||
for (let j = 0; j < items.length; j++) {
|
const productsList: OrderProduct[] = []
|
||||||
const item = items[j]
|
|
||||||
const itemObj = item as Record<string, any>
|
console.log('[loadOrders] 订单商品数据:', itemsRaw)
|
||||||
const specRaw = itemObj['specifications']
|
|
||||||
const specText = specRaw != null ? parseSpecText(specRaw) : ''
|
if (itemsRaw != null) {
|
||||||
productsList.push({
|
// 先检查是否为数组
|
||||||
id: itemObj['product_id'],
|
if (Array.isArray(itemsRaw)) {
|
||||||
name: itemObj['product_name'],
|
const items = itemsRaw as any[]
|
||||||
price: itemObj['price'],
|
console.log('[loadOrders] 商品数量:', items.length)
|
||||||
image: itemObj['image_url'] ?? '/static/default-product.png',
|
for (let j = 0; j < items.length; j++) {
|
||||||
spec: specText,
|
const item = items[j]
|
||||||
quantity: itemObj['quantity']
|
const itemStr = JSON.stringify(item)
|
||||||
})
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed == null) continue
|
||||||
|
const itemObj = itemParsed as UTSJSONObject
|
||||||
|
|
||||||
|
const specRaw = itemObj.get('specifications')
|
||||||
|
const specText = specRaw != null ? parseSpecText(specRaw) : ''
|
||||||
|
|
||||||
|
const productId = itemObj.getString('product_id')
|
||||||
|
const productName = itemObj.getString('product_name')
|
||||||
|
const price = itemObj.getNumber('price')
|
||||||
|
const imageUrl = itemObj.getString('image_url')
|
||||||
|
const quantity = itemObj.getNumber('quantity')
|
||||||
|
|
||||||
|
console.log('[loadOrders] 商品:', productName, '图片:', imageUrl, '规格:', specText)
|
||||||
|
|
||||||
|
const productItem: OrderProduct = {
|
||||||
|
id: productId ?? '',
|
||||||
|
name: productName ?? '未知商品',
|
||||||
|
price: price ?? 0,
|
||||||
|
image: imageUrl ?? '/static/default-product.png',
|
||||||
|
spec: specText,
|
||||||
|
quantity: quantity ?? 1
|
||||||
|
}
|
||||||
|
productsList.push(productItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedOrders.push({
|
const orderId = orderObj.getString('id')
|
||||||
id: orderObj['id'],
|
const orderNo = orderObj.getString('order_no')
|
||||||
order_no: orderObj['order_no'],
|
const orderStatus = orderObj.getNumber('order_status')
|
||||||
status: orderObj['order_status'],
|
const createdAt = orderObj.getString('created_at')
|
||||||
create_time: orderObj['created_at'],
|
const productAmount = orderObj.getNumber('product_amount')
|
||||||
product_amount: orderObj['product_amount'] ?? 0,
|
const shippingFee = orderObj.getNumber('shipping_fee')
|
||||||
shipping_fee: orderObj['shipping_fee'] ?? 0,
|
const totalAmount = orderObj.getNumber('total_amount')
|
||||||
total_amount: orderObj['total_amount'] ?? orderObj['paid_amount'] ?? 0,
|
const paidAmount = orderObj.getNumber('paid_amount')
|
||||||
|
const merchantId = orderObj.getString('merchant_id')
|
||||||
|
|
||||||
|
// 从关联查询的 ml_shops 表获取店铺名称
|
||||||
|
let shopName = '自营店铺'
|
||||||
|
const shopsRaw = orderObj.get('ml_shops')
|
||||||
|
if (shopsRaw != null) {
|
||||||
|
const shopStr = JSON.stringify(shopsRaw)
|
||||||
|
const shopParsed = JSON.parse(shopStr)
|
||||||
|
if (shopParsed != null) {
|
||||||
|
const shopObj = shopParsed as UTSJSONObject
|
||||||
|
const shopNameFromDb = shopObj.getString('shop_name')
|
||||||
|
if (shopNameFromDb != null && shopNameFromDb != '') {
|
||||||
|
shopName = shopNameFromDb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (merchantId != null && merchantId != '') {
|
||||||
|
shopName = '商家店铺'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[loadOrders] 订单号:', orderNo, '店铺:', shopName, '商品数:', productsList.length)
|
||||||
|
|
||||||
|
// 如果没有商品数据,添加一个占位商品
|
||||||
|
if (productsList.length === 0) {
|
||||||
|
const placeholderProduct: OrderProduct = {
|
||||||
|
id: 'placeholder',
|
||||||
|
name: '订单商品',
|
||||||
|
price: totalAmount ?? paidAmount ?? 0,
|
||||||
|
image: '/static/default-product.png',
|
||||||
|
spec: '',
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
productsList.push(placeholderProduct)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedOrder: OrderItem = {
|
||||||
|
id: orderId ?? '',
|
||||||
|
order_no: orderNo ?? '',
|
||||||
|
status: orderStatus ?? 1,
|
||||||
|
create_time: createdAt ?? '',
|
||||||
|
product_amount: productAmount ?? 0,
|
||||||
|
shipping_fee: shippingFee ?? 0,
|
||||||
|
total_amount: totalAmount ?? paidAmount ?? 0,
|
||||||
|
merchant_id: merchantId ?? '',
|
||||||
|
shop_name: shopName,
|
||||||
products: productsList
|
products: productsList
|
||||||
})
|
}
|
||||||
|
mappedOrders.push(mappedOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by created_at desc
|
// Sort by created_at desc - 直接使用 OrderItem 类型访问属性
|
||||||
mappedOrders.sort((a: any, b: any) => {
|
mappedOrders.sort((a: OrderItem, b: OrderItem) => {
|
||||||
const aObj = a as Record<string, any>
|
const timeA = new Date(a.create_time).getTime()
|
||||||
const bObj = b as Record<string, any>
|
const timeB = new Date(b.create_time).getTime()
|
||||||
const timeA = new Date(aObj['create_time'] as string).getTime()
|
|
||||||
const timeB = new Date(bObj['create_time'] as string).getTime()
|
|
||||||
return timeB - timeA
|
return timeB - timeA
|
||||||
})
|
})
|
||||||
|
|
||||||
// 将 mappedOrders 转换为 OrderItem[] 类型
|
allOrdersList.value = mappedOrders
|
||||||
const typedOrders: OrderItem[] = []
|
|
||||||
for (let i = 0; i < mappedOrders.length; i++) {
|
|
||||||
const mo = mappedOrders[i] as Record<string, any>
|
|
||||||
typedOrders.push({
|
|
||||||
id: mo['id'] as string,
|
|
||||||
order_no: mo['order_no'] as string,
|
|
||||||
status: mo['status'] as number,
|
|
||||||
create_time: mo['create_time'] as string,
|
|
||||||
product_amount: mo['product_amount'] as number,
|
|
||||||
shipping_fee: mo['shipping_fee'] as number,
|
|
||||||
total_amount: mo['total_amount'] as number,
|
|
||||||
products: mo['products'] as OrderProduct[]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
allOrdersList.value = typedOrders
|
|
||||||
|
|
||||||
// Update tab counts
|
// Update tab counts
|
||||||
updateTabsCounts(mappedOrders)
|
updateTabsCounts(mappedOrders)
|
||||||
@@ -397,14 +485,17 @@ const loadOrders = async () => {
|
|||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options['status'] != null) {
|
if (options == null) return
|
||||||
const status = options['status'] as string
|
const statusVal = options['status']
|
||||||
|
if (statusVal != null) {
|
||||||
|
const status = statusVal as string
|
||||||
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
|
if (['all', 'pending', 'shipping', 'delivering', 'completed', 'cancelled'].includes(status)) {
|
||||||
activeTab.value = status
|
activeTab.value = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options['type'] != null) {
|
const typeVal = options['type']
|
||||||
const type = options['type'] as string
|
if (typeVal != null) {
|
||||||
|
const type = typeVal as string
|
||||||
if (type === 'pending') activeTab.value = 'pending'
|
if (type === 'pending') activeTab.value = 'pending'
|
||||||
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
|
else if (type === 'shipped') activeTab.value = 'delivering' // 映射到待收货
|
||||||
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
|
else if (type === 'review') activeTab.value = 'completed' // 映射到已完成
|
||||||
@@ -500,6 +591,8 @@ const getStatusText = (status: number): string => {
|
|||||||
if (status == 3) return '待收货'
|
if (status == 3) return '待收货'
|
||||||
if (status == 4) return '已完成'
|
if (status == 4) return '已完成'
|
||||||
if (status == 5) return '已取消'
|
if (status == 5) return '已取消'
|
||||||
|
if (status == 6) return '退款中'
|
||||||
|
if (status == 7) return '已退款'
|
||||||
return '未知状态'
|
return '未知状态'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,6 +603,8 @@ const getStatusClass = (status: number): string => {
|
|||||||
if (status == 3) return 'status-delivering'
|
if (status == 3) return 'status-delivering'
|
||||||
if (status == 4) return 'status-completed'
|
if (status == 4) return 'status-completed'
|
||||||
if (status == 5) return 'status-cancelled'
|
if (status == 5) return 'status-cancelled'
|
||||||
|
if (status == 6) return 'status-refunding'
|
||||||
|
if (status == 7) return 'status-refunded'
|
||||||
return 'status-unknown'
|
return 'status-unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,10 +800,12 @@ const goShopping = () => {
|
|||||||
<style>
|
<style>
|
||||||
.orders-page {
|
.orders-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部 */
|
/* 头部 */
|
||||||
@@ -774,62 +871,73 @@ const goShopping = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 标签页 */
|
/* 标签页 */
|
||||||
.order-tabs {
|
.order-tabs-fixed-container {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
/* position: sticky; removed */
|
display: flex;
|
||||||
/* top: 50px; removed */
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-scroll {
|
.tab-item-fixed {
|
||||||
width: 100%;
|
padding: 15px 15px;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 0 10px;
|
|
||||||
/* width: max-content; removed */
|
|
||||||
/* min-width: 100%; removed */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-item {
|
|
||||||
/* 移除 flex: 1,改为自适应宽度或固定最小宽度 */
|
|
||||||
padding: 15px 15px; /* 增加水平内边距 */
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap; /* 防止文字换行 */
|
white-space: nowrap;
|
||||||
flex-shrink: 0; /* 防止被压缩 */
|
flex-shrink: 0;
|
||||||
|
min-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active {
|
.tab-item-fixed.active {
|
||||||
color: #ff5000;
|
color: #ff5000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active::after {
|
.tab-scroll-mobile {
|
||||||
/* content: ''; removed */
|
flex: 1;
|
||||||
/* content: '';
|
white-space: nowrap;
|
||||||
position: absolute;
|
/* 移除 width: 0,改用更稳健的安卓适配方式 */
|
||||||
bottom: 0;
|
display: flex;
|
||||||
left: 0;
|
flex-direction: row;
|
||||||
right: 0;
|
}
|
||||||
height: 2px;
|
|
||||||
background-color: #ff5000; */
|
.tab-container-mobile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
/* 加上这个确保容器能够横向撑开 */
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item-mobile {
|
||||||
|
padding: 15px 15px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式声明 */
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0; /* 绝对不能被压缩,否则无法滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item-mobile.active {
|
||||||
|
color: #ff5000;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-indicator {
|
.active-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0px;
|
||||||
left: 0;
|
left: 10px;
|
||||||
right: 0;
|
right: 10px;
|
||||||
height: 2px;
|
height: 3px;
|
||||||
background-color: #ff5000;
|
background-color: #ff5000;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-name {
|
.tab-name {
|
||||||
@@ -850,6 +958,8 @@ const goShopping = () => {
|
|||||||
/* 内容区 */
|
/* 内容区 */
|
||||||
.orders-content {
|
.orders-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
@@ -859,7 +969,6 @@ const goShopping = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 20px;
|
padding: 80px 20px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
@@ -941,6 +1050,14 @@ const goShopping = () => {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-refunding {
|
||||||
|
color: #ff5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-refunded {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
/* 订单商品 */
|
/* 订单商品 */
|
||||||
.order-products {
|
.order-products {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@@ -948,7 +1065,9 @@ const goShopping = () => {
|
|||||||
|
|
||||||
.order-product {
|
.order-product {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row; /* 显式声明横向排列 */
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-product:last-child {
|
.order-product:last-child {
|
||||||
@@ -959,30 +1078,46 @@ const goShopping = () => {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-right: 15px;
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0; /* 防止图片被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
.product-info {
|
||||||
flex: 1;
|
flex: 1; /* 占据右侧剩余所有空间 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 80px;
|
||||||
|
overflow: hidden; /* 防止文字溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-top-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 5px;
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
lines: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-spec {
|
.product-spec {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-bottom: 10px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-footer {
|
.product-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-price {
|
.product-price {
|
||||||
@@ -992,8 +1127,8 @@ const goShopping = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-quantity {
|
.product-quantity {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 订单信息 */
|
/* 订单信息 */
|
||||||
@@ -1128,6 +1263,114 @@ const goShopping = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式适配 */
|
/* 响应式适配 */
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.order-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
width: 48%;
|
||||||
|
margin: 0 1% 20px 1%;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1200px) {
|
||||||
|
.order-card {
|
||||||
|
width: 31%;
|
||||||
|
margin: 0 1% 20px 1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单卡片新样式 */
|
||||||
|
.order-card-header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-right {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title-row, .product-spec-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title-row {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec-row {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-price {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
.tab-item {
|
.tab-item {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
const orderId = ref('')
|
const orderId = ref('')
|
||||||
@@ -56,12 +57,8 @@ const loadOrderInfo = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onLoad((options) => {
|
||||||
const pages = getCurrentPages()
|
if (options == null) return
|
||||||
const currentPage = pages[pages.length - 1]
|
|
||||||
const options = currentPage.options as Record<string, any>
|
|
||||||
|
|
||||||
console.log('[payment-success] options:', JSON.stringify(options))
|
|
||||||
|
|
||||||
const orderIdValue = options['orderId']
|
const orderIdValue = options['orderId']
|
||||||
if (orderIdValue != null) {
|
if (orderIdValue != null) {
|
||||||
@@ -87,6 +84,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 逻辑已移到 onLoad
|
||||||
|
})
|
||||||
|
|
||||||
const viewOrder = () => {
|
const viewOrder = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/mall/consumer/orders'
|
url: '/pages/mall/consumer/orders'
|
||||||
@@ -182,9 +183,6 @@ const goHome = () => {
|
|||||||
border-radius: 22.5px;
|
border-radius: 22.5px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,11 +204,16 @@ const updateOrderInStorage = (targetOrderId: string, status: number) => {
|
|||||||
// 尝试从 'orders' 读取 (checkout页面写入的key)
|
// 尝试从 'orders' 读取 (checkout页面写入的key)
|
||||||
const ordersStr = uni.getStorageSync('orders')
|
const ordersStr = uni.getStorageSync('orders')
|
||||||
let orders: Record<string, any>[] = []
|
let orders: Record<string, any>[] = []
|
||||||
if (ordersStr != null) {
|
if (ordersStr != null && ordersStr !== '') {
|
||||||
const parsed = JSON.parse(ordersStr as string)
|
const parsed = JSON.parse(ordersStr as string)
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
for (let i = 0; i < parsed.length; i++) {
|
for (let i = 0; i < parsed.length; i++) {
|
||||||
orders.push(parsed[i] as Record<string, any>)
|
// 使用 JSON 序列化转换
|
||||||
|
const itemStr = JSON.stringify(parsed[i])
|
||||||
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed != null) {
|
||||||
|
orders.push(itemParsed as Record<string, any>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +235,8 @@ const updateOrderInStorage = (targetOrderId: string, status: number) => {
|
|||||||
uni.setStorageSync('orders', JSON.stringify(orders))
|
uni.setStorageSync('orders', JSON.stringify(orders))
|
||||||
console.log('订单状态已更新到Storage (orders):', targetOrderId, status)
|
console.log('订单状态已更新到Storage (orders):', targetOrderId, status)
|
||||||
} else {
|
} else {
|
||||||
console.warn('在Storage (orders)中未找到订单:', targetOrderId)
|
// 本地缓存中没有订单数据是正常的,数据在数据库中
|
||||||
|
console.log('本地缓存中无订单数据,已忽略:', targetOrderId)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('更新订单状态失败', e)
|
console.error('更新订单状态失败', e)
|
||||||
@@ -291,16 +297,26 @@ const loadOrderInfo = async () => {
|
|||||||
|
|
||||||
const order = await supabaseService.getOrderDetail(orderId.value)
|
const order = await supabaseService.getOrderDetail(orderId.value)
|
||||||
if (order != null) {
|
if (order != null) {
|
||||||
const orderObj = order as Record<string, any>
|
// 使用 JSON 序列化转换对象
|
||||||
orderNo.value = orderObj['order_no'] as string
|
const orderStr = JSON.stringify(order)
|
||||||
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
|
const orderParsed = JSON.parse(orderStr)
|
||||||
// But checking consistency is good
|
if (orderParsed == null) {
|
||||||
const totalAmount = orderObj['total_amount']
|
console.error('订单数据解析失败')
|
||||||
const dbAmount = totalAmount != null ? parseFloat(totalAmount.toString()) : 0
|
return
|
||||||
|
}
|
||||||
|
const orderObj = orderParsed as UTSJSONObject
|
||||||
|
|
||||||
|
const orderNoVal = orderObj.getString('order_no')
|
||||||
|
if (orderNoVal != null) {
|
||||||
|
orderNo.value = orderNoVal
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = orderObj.getNumber('total_amount')
|
||||||
|
const dbAmount = totalAmount ?? 0
|
||||||
if (dbAmount > 0) {
|
if (dbAmount > 0) {
|
||||||
amount.value = dbAmount
|
amount.value = dbAmount
|
||||||
}
|
}
|
||||||
const items = orderObj['items']
|
const items = orderObj.get('items')
|
||||||
if (items != null && Array.isArray(items) && items.length > 0) {
|
if (items != null && Array.isArray(items) && items.length > 0) {
|
||||||
// Could update product name etc if displayed
|
// Could update product name etc if displayed
|
||||||
}
|
}
|
||||||
@@ -315,43 +331,45 @@ const loadOrderInfo = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options != null) {
|
||||||
|
const orderIdValue = options['orderId']
|
||||||
|
if (orderIdValue != null) {
|
||||||
|
orderId.value = orderIdValue as string
|
||||||
|
loadOrderInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountValue = options['amount']
|
||||||
|
if (amountValue != null) {
|
||||||
|
amount.value = parseFloat(amountValue.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取传递的价格详情
|
||||||
|
const productAmountValue = options['productAmount']
|
||||||
|
if (productAmountValue != null) {
|
||||||
|
productAmount.value = parseFloat(productAmountValue.toString())
|
||||||
|
}
|
||||||
|
const deliveryFeeValue = options['deliveryFee']
|
||||||
|
if (deliveryFeeValue != null) {
|
||||||
|
deliveryFee.value = parseFloat(deliveryFeeValue.toString())
|
||||||
|
}
|
||||||
|
const discountAmountValue = options['discountAmount']
|
||||||
|
if (discountAmountValue != null) {
|
||||||
|
discountAmount.value = parseFloat(discountAmountValue.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
|
||||||
|
if (productAmountValue == null && amount.value > 0) {
|
||||||
|
calculatePriceDetails(amount.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPaymentMethods()
|
||||||
|
loadUserBalance()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages()
|
// onMounted 中的逻辑已移到 onLoad 中
|
||||||
const currentPage = pages[pages.length - 1]
|
|
||||||
const options = currentPage.options as Record<string, any>
|
|
||||||
|
|
||||||
const orderIdValue = options['orderId']
|
|
||||||
if (orderIdValue != null) {
|
|
||||||
orderId.value = orderIdValue as string
|
|
||||||
loadOrderInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountValue = options['amount']
|
|
||||||
if (amountValue != null) {
|
|
||||||
amount.value = parseFloat(amountValue.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取传递的价格详情
|
|
||||||
const productAmountValue = options['productAmount']
|
|
||||||
if (productAmountValue != null) {
|
|
||||||
productAmount.value = parseFloat(productAmountValue.toString())
|
|
||||||
}
|
|
||||||
const deliveryFeeValue = options['deliveryFee']
|
|
||||||
if (deliveryFeeValue != null) {
|
|
||||||
deliveryFee.value = parseFloat(deliveryFeeValue.toString())
|
|
||||||
}
|
|
||||||
const discountAmountValue = options['discountAmount']
|
|
||||||
if (discountAmountValue != null) {
|
|
||||||
discountAmount.value = parseFloat(discountAmountValue.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
|
|
||||||
if (productAmountValue == null && amount.value > 0) {
|
|
||||||
calculatePriceDetails(amount.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPaymentMethods()
|
|
||||||
loadUserBalance()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听返回操作(包含系统返回键和导航栏返回按钮)
|
// 监听返回操作(包含系统返回键和导航栏返回按钮)
|
||||||
@@ -370,11 +388,16 @@ onBackPress((options) => {
|
|||||||
const getCurrentUserId = (): string => {
|
const getCurrentUserId = (): string => {
|
||||||
const userStore = uni.getStorageSync('userInfo')
|
const userStore = uni.getStorageSync('userInfo')
|
||||||
if (userStore != null) {
|
if (userStore != null) {
|
||||||
const userObj = userStore as Record<string, any>
|
// 使用 JSON 序列化转换
|
||||||
const id = userObj['id']
|
const userStr = JSON.stringify(userStore)
|
||||||
if (id != null) {
|
const userParsed = JSON.parse(userStr)
|
||||||
return id as string
|
if (userParsed != null) {
|
||||||
}
|
const userObj = userParsed as UTSJSONObject
|
||||||
|
const id = userObj.getString('id')
|
||||||
|
if (id != null) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -649,9 +649,13 @@ export default {
|
|||||||
if (itemsRaw == null) return '/static/product1.jpg'
|
if (itemsRaw == null) return '/static/product1.jpg'
|
||||||
const items = itemsRaw as any[]
|
const items = itemsRaw as any[]
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
const firstItem = items[0] as Record<string, any>
|
const firstItem = items[0]
|
||||||
const imgUrl = firstItem['image_url'] as string
|
const itemStr = JSON.stringify(firstItem)
|
||||||
const prodImg = firstItem['product_image'] as string
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed == null) return '/static/product1.jpg'
|
||||||
|
const itemObj = itemParsed as UTSJSONObject
|
||||||
|
const imgUrl = itemObj.getString('image_url')
|
||||||
|
const prodImg = itemObj.getString('product_image')
|
||||||
const img = (imgUrl != null && imgUrl !== '') ? imgUrl : prodImg
|
const img = (imgUrl != null && imgUrl !== '') ? imgUrl : prodImg
|
||||||
if (img != null && img !== '') return img
|
if (img != null && img !== '') return img
|
||||||
}
|
}
|
||||||
@@ -663,12 +667,16 @@ export default {
|
|||||||
if (itemsRaw == null) return '精选商品'
|
if (itemsRaw == null) return '精选商品'
|
||||||
const items = itemsRaw as any[]
|
const items = itemsRaw as any[]
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
const firstItem = items[0] as Record<string, any>
|
const firstItem = items[0]
|
||||||
const pName = firstItem['product_name'] as string
|
const itemStr = JSON.stringify(firstItem)
|
||||||
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed == null) return '精选商品'
|
||||||
|
const itemObj = itemParsed as UTSJSONObject
|
||||||
|
const pName = itemObj.getString('product_name')
|
||||||
const name = (pName != null && pName !== '') ? pName : '商品'
|
const name = (pName != null && pName !== '') ? pName : '商品'
|
||||||
|
|
||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
return `${name} 等${items.length}件商品`
|
return name + ' 等' + items.length + '件商品'
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ const handleLogin = async () => {
|
|||||||
|
|
||||||
uni.showToast({ title: '管理员登录成功', icon: 'success' })
|
uni.showToast({ title: '管理员登录成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.reLaunch({ url: '/pages/mall/consumer/category' })
|
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||||
}, 500)
|
}, 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,7 @@ const handleLogin = async () => {
|
|||||||
|
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.reLaunch({ url: '/pages/mall/consumer/category' })
|
uni.reLaunch({ url: '/pages/mall/consumer/index' })
|
||||||
}, 500)
|
}, 500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('登录错误:', err)
|
console.error('登录错误:', err)
|
||||||
|
|||||||
@@ -1314,7 +1314,7 @@ class SupabaseService {
|
|||||||
const mappedData: Product[] = []
|
const mappedData: Product[] = []
|
||||||
const rawData = res2.data as any[]
|
const rawData = res2.data as any[]
|
||||||
for(let i = 0; i < rawData.length; i++) {
|
for(let i = 0; i < rawData.length; i++) {
|
||||||
const item = rawData[i] as UTSJSONObject
|
const item = JSON.parse(JSON.stringify(rawData[i])) as UTSJSONObject
|
||||||
const images: string[] = []
|
const images: string[] = []
|
||||||
|
|
||||||
const mainImageUrl = item.getString('main_image_url')
|
const mainImageUrl = item.getString('main_image_url')
|
||||||
@@ -1479,7 +1479,7 @@ class SupabaseService {
|
|||||||
const mappedData: Product[] = []
|
const mappedData: Product[] = []
|
||||||
const rawData = res2.data as any[]
|
const rawData = res2.data as any[]
|
||||||
for(let i = 0; i < rawData.length; i++) {
|
for(let i = 0; i < rawData.length; i++) {
|
||||||
const item = rawData[i] as UTSJSONObject
|
const item = JSON.parse(JSON.stringify(rawData[i])) as UTSJSONObject
|
||||||
const images: string[] = []
|
const images: string[] = []
|
||||||
|
|
||||||
const mainImageUrl = item.getString('main_image_url')
|
const mainImageUrl = item.getString('main_image_url')
|
||||||
@@ -2745,12 +2745,14 @@ class SupabaseService {
|
|||||||
// 批量删除购物车商品
|
// 批量删除购物车商品
|
||||||
async batchDeleteCartItems(cartItemIds: string[]): Promise<boolean> {
|
async batchDeleteCartItems(cartItemIds: string[]): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
console.log('[batchDeleteCartItems] 开始删除, ids:', cartItemIds.length)
|
||||||
const userId = this.getCurrentUserId()
|
const userId = this.getCurrentUserId()
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
console.error('用户未登录,无法删除购物车商品')
|
console.error('用户未登录,无法删除购物车商品')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[batchDeleteCartItems] userId:', userId)
|
||||||
const response = await supa
|
const response = await supa
|
||||||
.from('ml_shopping_cart')
|
.from('ml_shopping_cart')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
@@ -2758,11 +2760,13 @@ class SupabaseService {
|
|||||||
.delete()
|
.delete()
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
console.log('[batchDeleteCartItems] response.error:', response.error)
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
console.error('批量删除购物车商品失败:', response.error)
|
console.error('批量删除购物车商品失败:', response.error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[batchDeleteCartItems] 删除成功')
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量删除购物车商品异常:', error)
|
console.error('批量删除购物车商品异常:', error)
|
||||||
@@ -2840,17 +2844,21 @@ class SupabaseService {
|
|||||||
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
const addrObj = new UTSJSONObject()
|
const addr: UserAddress = {
|
||||||
addrObj.set('id', itemObj.getString('id') ?? '')
|
id: itemObj.getString('id') ?? '',
|
||||||
addrObj.set('user_id', itemObj.getString('user_id') ?? '')
|
user_id: itemObj.getString('user_id') ?? '',
|
||||||
addrObj.set('recipient_name', itemObj.getString('receiver_name') ?? itemObj.getString('recipient_name') ?? '')
|
recipient_name: itemObj.getString('receiver_name') ?? itemObj.getString('recipient_name') ?? '',
|
||||||
addrObj.set('phone', itemObj.getString('receiver_phone') ?? itemObj.getString('phone') ?? '')
|
phone: itemObj.getString('receiver_phone') ?? itemObj.getString('phone') ?? '',
|
||||||
addrObj.set('province', itemObj.getString('province') ?? '')
|
province: itemObj.getString('province') ?? '',
|
||||||
addrObj.set('city', itemObj.getString('city') ?? '')
|
city: itemObj.getString('city') ?? '',
|
||||||
addrObj.set('district', itemObj.getString('district') ?? '')
|
district: itemObj.getString('district') ?? '',
|
||||||
addrObj.set('detail_address', itemObj.getString('address_detail') ?? itemObj.getString('detail_address') ?? '')
|
detail_address: itemObj.getString('address_detail') ?? itemObj.getString('detail_address') ?? '',
|
||||||
addrObj.set('is_default', itemObj.getBoolean('is_default') ?? false)
|
is_default: itemObj.getBoolean('is_default') ?? false,
|
||||||
result.push(addrObj as UserAddress)
|
label: itemObj.getString('label') ?? '',
|
||||||
|
created_at: itemObj.getString('created_at') ?? '',
|
||||||
|
updated_at: itemObj.getString('updated_at') ?? ''
|
||||||
|
}
|
||||||
|
result.push(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[getAddresses] 返回地址数量:', result.length)
|
console.log('[getAddresses] 返回地址数量:', result.length)
|
||||||
@@ -3168,9 +3176,22 @@ class SupabaseService {
|
|||||||
const queryData = queryResponse.data as any
|
const queryData = queryResponse.data as any
|
||||||
let orderId = ''
|
let orderId = ''
|
||||||
|
|
||||||
|
console.log('[CreateOrder] queryData 类型:', typeof queryData, '是否数组:', Array.isArray(queryData))
|
||||||
|
|
||||||
if (Array.isArray(queryData) && queryData.length > 0) {
|
if (Array.isArray(queryData) && queryData.length > 0) {
|
||||||
const firstItem = queryData[0] as Record<string, any>
|
console.log('[CreateOrder] queryData 长度:', queryData.length)
|
||||||
orderId = firstItem['id'] as string
|
const firstItemRaw = queryData[0]
|
||||||
|
console.log('[CreateOrder] firstItemRaw 类型:', typeof firstItemRaw)
|
||||||
|
|
||||||
|
// 将 firstItemRaw 转换为可访问的对象
|
||||||
|
const firstItemStr = JSON.stringify(firstItemRaw)
|
||||||
|
const firstItemParsed = JSON.parse(firstItemStr)
|
||||||
|
if (firstItemParsed == null) {
|
||||||
|
console.error('[CreateOrder] 解析订单数据失败')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const firstItem = firstItemParsed as UTSJSONObject
|
||||||
|
orderId = (firstItem.getString('id') ?? '') as string
|
||||||
console.log('[CreateOrder] 找到新创建的订单, id:', orderId)
|
console.log('[CreateOrder] 找到新创建的订单, id:', orderId)
|
||||||
} else {
|
} else {
|
||||||
console.error('[CreateOrder] 未找到新创建的订单,插入可能失败')
|
console.error('[CreateOrder] 未找到新创建的订单,插入可能失败')
|
||||||
@@ -3180,12 +3201,21 @@ class SupabaseService {
|
|||||||
console.log('[CreateOrder] 订单创建成功, orderId:', orderId)
|
console.log('[CreateOrder] 订单创建成功, orderId:', orderId)
|
||||||
|
|
||||||
const orderItems: UTSJSONObject[] = []
|
const orderItems: UTSJSONObject[] = []
|
||||||
|
console.log('[CreateOrder] orderData.items 类型:', typeof orderData.items, '是否数组:', Array.isArray(orderData.items))
|
||||||
const rawItems = orderData.items as any[]
|
const rawItems = orderData.items as any[]
|
||||||
|
console.log('[CreateOrder] rawItems 长度:', rawItems.length)
|
||||||
|
|
||||||
for(let i = 0; i < rawItems.length; i++) {
|
for(let i = 0; i < rawItems.length; i++) {
|
||||||
let item: UTSJSONObject
|
console.log('[CreateOrder] 处理商品项', i, '类型:', typeof rawItems[i])
|
||||||
const rawItem = rawItems[i]
|
const rawItem = rawItems[i]
|
||||||
item = rawItem as UTSJSONObject
|
const itemStr = JSON.stringify(rawItem)
|
||||||
|
console.log('[CreateOrder] 商品项 JSON:', itemStr)
|
||||||
|
const itemParsed = JSON.parse(itemStr)
|
||||||
|
if (itemParsed == null) {
|
||||||
|
console.error('[CreateOrder] 商品项解析失败')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = itemParsed as UTSJSONObject
|
||||||
|
|
||||||
const itemJson = new UTSJSONObject()
|
const itemJson = new UTSJSONObject()
|
||||||
|
|
||||||
@@ -3193,19 +3223,21 @@ class SupabaseService {
|
|||||||
if (pId == null) {
|
if (pId == null) {
|
||||||
pId = item.get('id')
|
pId = item.get('id')
|
||||||
}
|
}
|
||||||
|
const productId = (pId ?? '') as string
|
||||||
|
|
||||||
itemJson.set('order_id', orderId)
|
itemJson.set('order_id', orderId)
|
||||||
itemJson.set('product_id', pId)
|
itemJson.set('product_id', productId)
|
||||||
|
|
||||||
const skuIdVal = item.get('sku_id')
|
const skuIdVal = item.get('sku_id')
|
||||||
if (skuIdVal != null && skuIdVal !== '') {
|
if (skuIdVal != null && skuIdVal !== '') {
|
||||||
itemJson.set('sku_id', skuIdVal)
|
itemJson.set('sku_id', skuIdVal as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
itemJson.set('product_name', item.get('product_name') ?? '')
|
const productName = (item.get('product_name') ?? '') as string
|
||||||
|
itemJson.set('product_name', productName)
|
||||||
|
|
||||||
const sName = item.get('sku_name')
|
const sName = item.get('sku_name')
|
||||||
itemJson.set('sku_name', sName ?? '')
|
itemJson.set('sku_name', (sName ?? '') as string)
|
||||||
|
|
||||||
const specVal = item.get('specifications')
|
const specVal = item.get('specifications')
|
||||||
let skuSnapshot = '{}'
|
let skuSnapshot = '{}'
|
||||||
@@ -3221,7 +3253,7 @@ class SupabaseService {
|
|||||||
|
|
||||||
const img1 = item.get('product_image')
|
const img1 = item.get('product_image')
|
||||||
const img2 = item.get('image_url')
|
const img2 = item.get('image_url')
|
||||||
let imgUrl = (img1 ?? img2 ?? '') as string
|
let imgUrl = ((img1 ?? img2 ?? '') as string)
|
||||||
while (imgUrl.indexOf('`') >= 0) {
|
while (imgUrl.indexOf('`') >= 0) {
|
||||||
imgUrl = imgUrl.replace('`', '')
|
imgUrl = imgUrl.replace('`', '')
|
||||||
}
|
}
|
||||||
@@ -3238,15 +3270,31 @@ class SupabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[CreateOrder] 插入订单项数量:', orderItems.length)
|
console.log('[CreateOrder] 插入订单项数量:', orderItems.length)
|
||||||
console.log('[CreateOrder] 订单项数据:', JSON.stringify(orderItems))
|
|
||||||
|
|
||||||
for (let j: number = 0; j < orderItems.length; j++) {
|
for (let j: number = 0; j < orderItems.length; j++) {
|
||||||
|
console.log('[CreateOrder] 开始插入订单项', j)
|
||||||
const itemJson = orderItems[j]
|
const itemJson = orderItems[j]
|
||||||
|
// 将 UTSJSONObject 转换为普通对象
|
||||||
|
console.log('[CreateOrder] 序列化订单项...')
|
||||||
|
const itemObjStr = JSON.stringify(itemJson)
|
||||||
|
console.log('[CreateOrder] 订单项 JSON:', itemObjStr)
|
||||||
|
const itemObjParsed = JSON.parse(itemObjStr)
|
||||||
|
console.log('[CreateOrder] itemObjParsed 类型:', typeof itemObjParsed)
|
||||||
|
if (itemObjParsed == null) {
|
||||||
|
console.error('[CreateOrder] 订单项转换失败')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 UTSJSONObject 而不是 Record<string, any>
|
||||||
|
const itemObj = itemObjParsed as UTSJSONObject
|
||||||
|
console.log('[CreateOrder] 执行 insert...')
|
||||||
|
|
||||||
const itemsResponse = await supa
|
const itemsResponse = await supa
|
||||||
.from('ml_order_items')
|
.from('ml_order_items')
|
||||||
.insert(itemJson)
|
.insert(itemObj)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
console.log('[CreateOrder] insert 完成, error:', itemsResponse.error)
|
||||||
if (itemsResponse.error != null) {
|
if (itemsResponse.error != null) {
|
||||||
console.error('[CreateOrder] 创建订单项失败:', itemsResponse.error)
|
console.error('[CreateOrder] 创建订单项失败:', itemsResponse.error)
|
||||||
}
|
}
|
||||||
@@ -3256,7 +3304,10 @@ class SupabaseService {
|
|||||||
|
|
||||||
const cartItemIds: string[] = []
|
const cartItemIds: string[] = []
|
||||||
for(let i = 0; i < rawItems.length; i++) {
|
for(let i = 0; i < rawItems.length; i++) {
|
||||||
const item = rawItems[i] as UTSJSONObject
|
const rawItem = rawItems[i]
|
||||||
|
const itemParsed = JSON.parse(JSON.stringify(rawItem))
|
||||||
|
if (itemParsed == null) continue
|
||||||
|
const item = itemParsed as UTSJSONObject
|
||||||
const iid = item.getString('id')
|
const iid = item.getString('id')
|
||||||
if (iid != null && iid.length > 10) {
|
if (iid != null && iid.length > 10) {
|
||||||
cartItemIds.push(iid)
|
cartItemIds.push(iid)
|
||||||
@@ -3282,14 +3333,14 @@ class SupabaseService {
|
|||||||
|
|
||||||
let grandTotal = 0.0
|
let grandTotal = 0.0
|
||||||
for(let k = 0; k < groups.length; k++) {
|
for(let k = 0; k < groups.length; k++) {
|
||||||
const g = groups[k] as UTSJSONObject
|
const g = JSON.parse(JSON.stringify(groups[k])) as UTSJSONObject
|
||||||
// 安全获取 items 数组
|
// 安全获取 items 数组
|
||||||
const gItemsRaw = g.get('items')
|
const gItemsRaw = g.get('items')
|
||||||
if (gItemsRaw == null) continue
|
if (gItemsRaw == null) continue
|
||||||
const gItems = gItemsRaw as any[]
|
const gItems = gItemsRaw as any[]
|
||||||
|
|
||||||
for(let gi = 0; gi < gItems.length; gi++) {
|
for(let gi = 0; gi < gItems.length; gi++) {
|
||||||
const it = gItems[gi] as UTSJSONObject
|
const it = JSON.parse(JSON.stringify(gItems[gi])) as UTSJSONObject
|
||||||
const itPrice = it.getNumber('price') ?? 0
|
const itPrice = it.getNumber('price') ?? 0
|
||||||
const itQty = it.getNumber('quantity') ?? 1
|
const itQty = it.getNumber('quantity') ?? 1
|
||||||
grandTotal += itPrice * itQty
|
grandTotal += itPrice * itQty
|
||||||
@@ -3298,14 +3349,14 @@ class SupabaseService {
|
|||||||
|
|
||||||
// 为每个店铺创建一个订单
|
// 为每个店铺创建一个订单
|
||||||
for (let i = 0; i < groups.length; i++) {
|
for (let i = 0; i < groups.length; i++) {
|
||||||
const group = groups[i] as UTSJSONObject
|
const group = JSON.parse(JSON.stringify(groups[i])) as UTSJSONObject
|
||||||
const shopItemsRaw = group.get('items')
|
const shopItemsRaw = group.get('items')
|
||||||
if (shopItemsRaw == null) continue
|
if (shopItemsRaw == null) continue
|
||||||
const shopItems = shopItemsRaw as any[]
|
const shopItems = shopItemsRaw as any[]
|
||||||
|
|
||||||
let productAmount = 0.0
|
let productAmount = 0.0
|
||||||
for(let j = 0; j < shopItems.length; j++) {
|
for(let j = 0; j < shopItems.length; j++) {
|
||||||
const sItem = shopItems[j] as UTSJSONObject
|
const sItem = JSON.parse(JSON.stringify(shopItems[j])) as UTSJSONObject
|
||||||
const siPrice = sItem.getNumber('price') ?? 0
|
const siPrice = sItem.getNumber('price') ?? 0
|
||||||
const siQty = sItem.getNumber('quantity') ?? 1
|
const siQty = sItem.getNumber('quantity') ?? 1
|
||||||
productAmount += siPrice * siQty
|
productAmount += siPrice * siQty
|
||||||
@@ -3329,6 +3380,16 @@ class SupabaseService {
|
|||||||
|
|
||||||
const finalMerchantId = (mId != null && mId != '') ? mId : (sId ?? '')
|
const finalMerchantId = (mId != null && mId != '') ? mId : (sId ?? '')
|
||||||
console.log('[createOrdersByShop] 最终使用的 merchant_id:', finalMerchantId)
|
console.log('[createOrdersByShop] 最终使用的 merchant_id:', finalMerchantId)
|
||||||
|
|
||||||
|
// 将 shopItems 转换为普通对象数组
|
||||||
|
const plainItems: any[] = []
|
||||||
|
for(let k = 0; k < shopItems.length; k++) {
|
||||||
|
const plainItemRaw = JSON.parse(JSON.stringify(shopItems[k]))
|
||||||
|
if (plainItemRaw != null) {
|
||||||
|
plainItems.push(plainItemRaw as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('[createOrdersByShop] plainItems 数量:', plainItems.length)
|
||||||
|
|
||||||
const orderId = await this.createOrder({
|
const orderId = await this.createOrder({
|
||||||
merchant_id: finalMerchantId,
|
merchant_id: finalMerchantId,
|
||||||
@@ -3336,7 +3397,7 @@ class SupabaseService {
|
|||||||
shipping_fee: shopShippingFee,
|
shipping_fee: shopShippingFee,
|
||||||
total_amount: shopTotal,
|
total_amount: shopTotal,
|
||||||
shipping_address: params.shipping_address,
|
shipping_address: params.shipping_address,
|
||||||
items: shopItems
|
items: plainItems
|
||||||
})
|
})
|
||||||
|
|
||||||
if (orderId != null) {
|
if (orderId != null) {
|
||||||
@@ -3362,12 +3423,10 @@ class SupabaseService {
|
|||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关联查询店铺表获取店铺名称
|
||||||
let query = supa
|
let query = supa
|
||||||
.from('ml_orders')
|
.from('ml_orders')
|
||||||
.select(`
|
.select('*, ml_order_items(*), ml_shops(shop_name)')
|
||||||
*,
|
|
||||||
ml_order_items (*)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
|
|
||||||
@@ -3377,6 +3436,11 @@ class SupabaseService {
|
|||||||
|
|
||||||
const response = await query.execute()
|
const response = await query.execute()
|
||||||
|
|
||||||
|
console.log('[getOrders] response.error:', response.error)
|
||||||
|
if (response.data != null && Array.isArray(response.data)) {
|
||||||
|
console.log('[getOrders] 订单数量:', response.data.length)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
console.error('获取订单列表失败:', response.error)
|
console.error('获取订单列表失败:', response.error)
|
||||||
const empty: any[] = []
|
const empty: any[] = []
|
||||||
@@ -3405,14 +3469,14 @@ class SupabaseService {
|
|||||||
|
|
||||||
const response = await supa
|
const response = await supa
|
||||||
.from('ml_orders')
|
.from('ml_orders')
|
||||||
.select(`
|
.select('*, ml_order_items(*)')
|
||||||
*,
|
|
||||||
ml_order_items (*)
|
|
||||||
`)
|
|
||||||
.eq('id', orderId)
|
.eq('id', orderId)
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId!)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
console.log('[getOrderDetail] response.error:', response.error)
|
||||||
|
console.log('[getOrderDetail] response.data:', JSON.stringify(response.data))
|
||||||
|
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
console.error('[getOrderDetail] 获取订单详情失败:', response.error)
|
console.error('[getOrderDetail] 获取订单详情失败:', response.error)
|
||||||
@@ -3433,7 +3497,35 @@ class SupabaseService {
|
|||||||
|
|
||||||
const orderData = rawList[0]
|
const orderData = rawList[0]
|
||||||
console.log('[getOrderDetail] 成功获取订单')
|
console.log('[getOrderDetail] 成功获取订单')
|
||||||
return orderData
|
|
||||||
|
const orderObj = JSON.parse(JSON.stringify(orderData)) as UTSJSONObject
|
||||||
|
|
||||||
|
const result = new UTSJSONObject()
|
||||||
|
result.set('id', orderObj.get('id') ?? '')
|
||||||
|
result.set('order_no', orderObj.get('order_no') ?? '')
|
||||||
|
result.set('order_status', orderObj.get('order_status') ?? 1)
|
||||||
|
result.set('user_id', orderObj.get('user_id') ?? '')
|
||||||
|
result.set('merchant_id', orderObj.get('merchant_id') ?? '')
|
||||||
|
result.set('product_amount', orderObj.get('product_amount') ?? 0)
|
||||||
|
result.set('shipping_fee', orderObj.get('shipping_fee') ?? 0)
|
||||||
|
result.set('total_amount', orderObj.get('total_amount') ?? 0)
|
||||||
|
result.set('paid_amount', orderObj.get('paid_amount') ?? 0)
|
||||||
|
result.set('discount_amount', orderObj.get('discount_amount') ?? 0)
|
||||||
|
result.set('payment_method', orderObj.get('payment_method') ?? '')
|
||||||
|
result.set('payment_status', orderObj.get('payment_status') ?? 1)
|
||||||
|
result.set('shipping_status', orderObj.get('shipping_status') ?? 1)
|
||||||
|
result.set('created_at', orderObj.get('created_at') ?? '')
|
||||||
|
result.set('paid_at', orderObj.get('paid_at') ?? '')
|
||||||
|
result.set('shipped_at', orderObj.get('shipped_at') ?? '')
|
||||||
|
result.set('completed_at', orderObj.get('completed_at') ?? '')
|
||||||
|
result.set('shipping_address', orderObj.get('shipping_address'))
|
||||||
|
result.set('ml_order_items', orderObj.get('ml_order_items'))
|
||||||
|
// 添加物流信息
|
||||||
|
result.set('tracking_no', orderObj.get('tracking_no') ?? '')
|
||||||
|
result.set('carrier_name', orderObj.get('carrier_name') ?? '')
|
||||||
|
result.set('delivered_at', orderObj.get('delivered_at') ?? '')
|
||||||
|
|
||||||
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[getOrderDetail] 获取订单详情异常:', e)
|
console.error('[getOrderDetail] 获取订单详情异常:', e)
|
||||||
return null
|
return null
|
||||||
@@ -3537,7 +3629,7 @@ class SupabaseService {
|
|||||||
const userId = this.getCurrentUserId()
|
const userId = this.getCurrentUserId()
|
||||||
if (userId == null) return { success: false, message: '请先登录' }
|
if (userId == null) return { success: false, message: '请先登录' }
|
||||||
|
|
||||||
const d = data as UTSJSONObject
|
const d = JSON.parse(JSON.stringify(data)) as UTSJSONObject
|
||||||
const orderId = d.getString('order_id')
|
const orderId = d.getString('order_id')
|
||||||
const refundType = d.getNumber('refund_type')
|
const refundType = d.getNumber('refund_type')
|
||||||
const refundReason = d.getString('refund_reason')
|
const refundReason = d.getString('refund_reason')
|
||||||
@@ -3578,7 +3670,7 @@ class SupabaseService {
|
|||||||
async rePurchase(order: any): Promise<boolean> {
|
async rePurchase(order: any): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// 将 order 转换为 UTSJSONObject 以安全访问属性
|
// 将 order 转换为 UTSJSONObject 以安全访问属性
|
||||||
const orderObj = order as UTSJSONObject
|
const orderObj = JSON.parse(JSON.stringify(order)) as UTSJSONObject
|
||||||
// 尝试获取 ml_order_items 或 items
|
// 尝试获取 ml_order_items 或 items
|
||||||
let itemsKey = 'ml_order_items'
|
let itemsKey = 'ml_order_items'
|
||||||
let itemsRaw = orderObj.get(itemsKey)
|
let itemsRaw = orderObj.get(itemsKey)
|
||||||
@@ -3597,7 +3689,7 @@ class SupabaseService {
|
|||||||
// 简单的循环添加,实际项目中可以优化为批量插入
|
// 简单的循环添加,实际项目中可以优化为批量插入
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
// 同样,item 也是 UTSJSONObject 或支持访问的对象
|
// 同样,item 也是 UTSJSONObject 或支持访问的对象
|
||||||
const item = items[i] as UTSJSONObject
|
const item = JSON.parse(JSON.stringify(items[i])) as UTSJSONObject
|
||||||
const productId = item.getString('product_id')
|
const productId = item.getString('product_id')
|
||||||
const skuId = item.getString('sku_id')
|
const skuId = item.getString('sku_id')
|
||||||
// 数量可能是数字或字符串
|
// 数量可能是数字或字符串
|
||||||
@@ -4157,12 +4249,11 @@ class SupabaseService {
|
|||||||
console.log(`[ToggleFav] Current status: ${exists}`)
|
console.log(`[ToggleFav] Current status: ${exists}`)
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
// Delete
|
|
||||||
const response = await supa
|
const response = await supa
|
||||||
.from('ml_user_favorites')
|
.from('ml_user_favorites')
|
||||||
.eq('user_id', userId!)
|
.eq('user_id', userId!)
|
||||||
.eq('target_id', productId)
|
.eq('target_id', productId)
|
||||||
.eq('target_type', 1)
|
.eq('target_type', '1')
|
||||||
.delete()
|
.delete()
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
@@ -4172,13 +4263,12 @@ class SupabaseService {
|
|||||||
}
|
}
|
||||||
return false // 已取消收藏
|
return false // 已取消收藏
|
||||||
} else {
|
} else {
|
||||||
// Add
|
|
||||||
const response = await supa
|
const response = await supa
|
||||||
.from('ml_user_favorites')
|
.from('ml_user_favorites')
|
||||||
.insert({
|
.insert({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
target_id: productId,
|
target_id: productId,
|
||||||
target_type: 1,
|
target_type: '1',
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
@@ -4209,7 +4299,7 @@ class SupabaseService {
|
|||||||
.from('ml_user_favorites')
|
.from('ml_user_favorites')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('user_id', userId!)
|
.eq('user_id', userId!)
|
||||||
.eq('target_type', 1)
|
.eq('target_type', '1')
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
@@ -4227,12 +4317,22 @@ class SupabaseService {
|
|||||||
const productIds: string[] = []
|
const productIds: string[] = []
|
||||||
for (let i = 0; i < favorites.length; i++) {
|
for (let i = 0; i < favorites.length; i++) {
|
||||||
let item: any = favorites[i]
|
let item: any = favorites[i]
|
||||||
let pid = ''
|
let itemObj: UTSJSONObject
|
||||||
if (item instanceof UTSJSONObject) {
|
if (item instanceof UTSJSONObject) {
|
||||||
pid = item.getString('target_id') ?? ''
|
itemObj = item as UTSJSONObject
|
||||||
} else {
|
} else {
|
||||||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
pid = itemObj.getString('target_id') ?? ''
|
}
|
||||||
|
|
||||||
|
// target_id 可能是 Integer 或 String 类型,需要安全转换
|
||||||
|
const targetIdRaw = itemObj.get('target_id')
|
||||||
|
let pid = ''
|
||||||
|
if (targetIdRaw != null) {
|
||||||
|
if (typeof targetIdRaw === 'string') {
|
||||||
|
pid = targetIdRaw as string
|
||||||
|
} else if (typeof targetIdRaw === 'number') {
|
||||||
|
pid = (targetIdRaw as number).toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pid !== '') productIds.push(pid)
|
if (pid !== '') productIds.push(pid)
|
||||||
}
|
}
|
||||||
@@ -4274,16 +4374,24 @@ class SupabaseService {
|
|||||||
let newItem: UTSJSONObject
|
let newItem: UTSJSONObject
|
||||||
|
|
||||||
if (item instanceof UTSJSONObject) {
|
if (item instanceof UTSJSONObject) {
|
||||||
// Deep copy to ensure we have a fresh object to modify
|
|
||||||
newItem = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
newItem = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
} else {
|
} else {
|
||||||
newItem = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
newItem = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetId = newItem.getString('target_id')
|
// target_id 可能是 Integer 或 String 类型,需要安全转换
|
||||||
// Careful with null targetId
|
const targetIdRaw = newItem.get('target_id')
|
||||||
if (targetId != null) {
|
let targetId = ''
|
||||||
const product = productMap.get(targetId as string)
|
if (targetIdRaw != null) {
|
||||||
|
if (typeof targetIdRaw === 'string') {
|
||||||
|
targetId = targetIdRaw as string
|
||||||
|
} else if (typeof targetIdRaw === 'number') {
|
||||||
|
targetId = (targetIdRaw as number).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId !== '') {
|
||||||
|
const product = productMap.get(targetId)
|
||||||
if (product != null) {
|
if (product != null) {
|
||||||
newItem.set('ml_products', product)
|
newItem.set('ml_products', product)
|
||||||
result.push(newItem)
|
result.push(newItem)
|
||||||
@@ -4689,12 +4797,9 @@ class SupabaseService {
|
|||||||
// 如果没有 view,可能需要改为两个查询或者使用 left join
|
// 如果没有 view,可能需要改为两个查询或者使用 left join
|
||||||
const response = await supa
|
const response = await supa
|
||||||
.from('ml_user_coupons')
|
.from('ml_user_coupons')
|
||||||
.select(`
|
.select('*, template:ml_coupon_templates(name, amount, min_spend)')
|
||||||
*,
|
|
||||||
template:ml_coupon_templates(name, amount, min_spend)
|
|
||||||
`)
|
|
||||||
.eq('user_id', userId!)
|
.eq('user_id', userId!)
|
||||||
.eq('status', status)
|
.eq('status', status.toString())
|
||||||
.order('expire_at', { ascending: true })
|
.order('expire_at', { ascending: true })
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
@@ -4789,9 +4894,9 @@ class SupabaseService {
|
|||||||
.from('ml_user_coupons')
|
.from('ml_user_coupons')
|
||||||
.select('id', { count: 'exact' })
|
.select('id', { count: 'exact' })
|
||||||
.eq('user_id', userId!)
|
.eq('user_id', userId!)
|
||||||
.eq('status', 1) // 1: unused
|
.eq('status', '1')
|
||||||
.gt('expire_at', new Date().toISOString()) // 未过期
|
.gt('expire_at', new Date().toISOString())
|
||||||
.limit(1) // Limit to 1 to reduce data transfer, we only want the count
|
.limit(1)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user