合并merchant文件
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
<!-- 商家端 - 聊天页面 -->
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<<<<<<< HEAD
|
||||
<view class="chat-header">
|
||||
=======
|
||||
<!-- 聊天头部 -->
|
||||
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<view class="header-back" @click="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<<<<<<< HEAD
|
||||
<text class="chat-title">{{ chatTitle }}</text>
|
||||
<text class="chat-status">在线</text>
|
||||
</view>
|
||||
@@ -32,25 +38,141 @@
|
||||
</view>
|
||||
</view>
|
||||
<image class="avatar me" src="/static/images/default-shop.png" mode="aspectFill" />
|
||||
=======
|
||||
<view class="header-info-text-wrapper">
|
||||
<text class="chat-title">{{ chatTitle }}</text>
|
||||
<text class="chat-status">在线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="action-icon" @click="showMoreActions">
|
||||
<text class="action-icon-text">⋯</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天内容 -->
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="chat-content"
|
||||
:scroll-into-view="scrollToView"
|
||||
:scroll-with-animation="true"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="chat-messages">
|
||||
<!-- 系统消息 -->
|
||||
<view class="message-item system">
|
||||
<text class="system-text">已接入客户对话</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<view
|
||||
v-for="message in chatMessages"
|
||||
:key="message.id"
|
||||
:class="['message-item', message.is_from_user ? 'received' : 'sent']"
|
||||
:id="message.viewId"
|
||||
>
|
||||
<!-- 客户消息 -->
|
||||
<view v-if="message.is_from_user" class="message-wrapper">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="userAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="message-content-wrapper">
|
||||
<text class="sender-name">{{ chatTitle }}</text>
|
||||
<view class="message-bubble received-bubble" :class="{ 'image-bubble': message.msg_type === 'image' || message.content.includes('/chat_images/') }">
|
||||
<text v-if="!(message.msg_type === 'image' || message.content.includes('/chat_images/'))" class="message-text">{{ message.content }}</text>
|
||||
<image v-else mode="widthFix" class="message-image" :src="message.content" @click="previewImage(message.content)" />
|
||||
<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" :class="{ 'image-bubble': message.msg_type === 'image' || message.content.includes('/chat_images/') }">
|
||||
<text v-if="!(message.msg_type === 'image' || message.content.includes('/chat_images/'))" class="message-text">{{ message.content }}</text>
|
||||
<image v-else mode="widthFix" class="message-image" :src="message.content" @click="previewImage(message.content)" />
|
||||
<text class="message-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<image
|
||||
class="avatar me"
|
||||
:src="shopAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<view class="chat-input">
|
||||
<input v-model="inputText" class="input-field" placeholder="请输入消息..." confirm-type="send" @confirm="sendMessage" />
|
||||
<view class="send-btn" @click="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
=======
|
||||
<!-- 聊天输入区 -->
|
||||
<view class="chat-input">
|
||||
<view class="input-tools">
|
||||
<text class="tool-icon" @click="showEmojiPicker">😊</text>
|
||||
<text class="tool-icon" @click="showImagePicker">📷</text>
|
||||
</view>
|
||||
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="message-input"
|
||||
v-model="inputText"
|
||||
placeholder="请输入消息..."
|
||||
:focus="inputFocus"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
/>
|
||||
<button
|
||||
class="send-button"
|
||||
:class="{ active: inputText.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>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
<<<<<<< HEAD
|
||||
|
||||
type ChatMessageType = {
|
||||
id: string
|
||||
=======
|
||||
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
|
||||
|
||||
type ChatMessageType = {
|
||||
id: string
|
||||
viewId: string
|
||||
>>>>>>> local-backup-root-cyj
|
||||
session_id: string
|
||||
sender_id: string
|
||||
receiver_id: string
|
||||
@@ -59,6 +181,10 @@
|
||||
is_read: boolean
|
||||
is_from_user: boolean
|
||||
created_at: string
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
time: string
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -70,11 +196,35 @@
|
||||
inputText: '',
|
||||
chatMessages: [] as ChatMessageType[],
|
||||
scrollToView: '',
|
||||
<<<<<<< HEAD
|
||||
merchantId: ''
|
||||
=======
|
||||
merchantId: '',
|
||||
userAvatar: '/static/images/default-avatar.png',
|
||||
shopAvatar: '/static/images/default-shop.png',
|
||||
navPaddingTop: '30px',
|
||||
showEmoji: false,
|
||||
inputFocus: false,
|
||||
realtimeChannel: null as AkSupaRealtimeChannel | null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
emojiList(): string[] {
|
||||
return ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
const statusBarH = sysInfo.statusBarHeight
|
||||
this.navPaddingTop = (statusBarH + 10) + 'px'
|
||||
|
||||
console.log('chat page onLoad options:', options)
|
||||
>>>>>>> local-backup-root-cyj
|
||||
if (options.session_id) {
|
||||
this.sessionId = options.session_id
|
||||
}
|
||||
@@ -88,13 +238,33 @@
|
||||
},
|
||||
|
||||
onShow() {
|
||||
<<<<<<< HEAD
|
||||
this.loadChatMessages()
|
||||
=======
|
||||
console.log('chat page onShow, chatUserId:', this.chatUserId, 'merchantId:', this.merchantId)
|
||||
if (this.merchantId) {
|
||||
this.loadChatMessages()
|
||||
this.setupRealtimeSubscription()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadChatMessages()
|
||||
this.setupRealtimeSubscription()
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.realtimeChannel != null) {
|
||||
supa.removeChannel(this.realtimeChannel!)
|
||||
}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
<<<<<<< HEAD
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
} catch (e) {}
|
||||
},
|
||||
@@ -141,18 +311,195 @@
|
||||
|
||||
this.markAsRead()
|
||||
}
|
||||
=======
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
|
||||
// 加载店铺头像
|
||||
this.loadShopAvatar()
|
||||
} catch (e) {
|
||||
console.error('获取商户ID失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadShopAvatar() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_shops')
|
||||
.select('shop_logo')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
if (response.data && (response.data as any[]).length > 0) {
|
||||
const shopData = (response.data as any[])[0] as UTSJSONObject
|
||||
const logo = shopData.getString('shop_logo')
|
||||
if (logo && logo != '') {
|
||||
this.shopAvatar = logo
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载店铺头像失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadChatMessages() {
|
||||
if (!this.chatUserId) {
|
||||
console.error('chatUserId 为空')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_chat_messages')
|
||||
.select('*')
|
||||
.or(`sender_id.eq.${this.chatUserId},receiver_id.eq.${this.chatUserId}`)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(200)
|
||||
.execute()
|
||||
|
||||
console.log('聊天记录查询结果:', response.data, 'error:', response.error)
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('查询聊天记录失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.data && (response.data as any[]).length > 0) {
|
||||
const rawData = response.data as any[]
|
||||
const messages: ChatMessageType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const senderId = item.getString('sender_id') || ''
|
||||
const isFromUser = senderId === this.chatUserId
|
||||
const createdAt = item.getString('created_at') || ''
|
||||
const msgId = item.getString('id') || ''
|
||||
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
const date = new Date(createdAt)
|
||||
const timeStr = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')
|
||||
|
||||
messages.push({
|
||||
id: msgId,
|
||||
viewId: safeViewId,
|
||||
session_id: item.getString('session_id') || '',
|
||||
sender_id: senderId,
|
||||
receiver_id: item.getString('receiver_id') || '',
|
||||
content: item.getString('content') || '',
|
||||
msg_type: item.getString('msg_type') || 'text',
|
||||
is_read: item.getBoolean('is_read') || false,
|
||||
is_from_user: isFromUser,
|
||||
created_at: createdAt,
|
||||
time: timeStr
|
||||
} as ChatMessageType)
|
||||
}
|
||||
|
||||
this.chatMessages = messages
|
||||
this.scrollToBottom()
|
||||
this.markAsRead()
|
||||
} else {
|
||||
console.log('没有找到聊天记录')
|
||||
this.chatMessages = []
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载聊天记录失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
setupRealtimeSubscription(): void {
|
||||
console.log('开始建立聊天实时订阅...')
|
||||
|
||||
this.realtimeChannel = supa.channel('merchant-chat-' + Date.now().toString())
|
||||
.on('postgres_changes', {
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'ml_chat_messages'
|
||||
}, (payload: any) => {
|
||||
console.log('收到实时消息:', payload)
|
||||
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
|
||||
const newMsgAny = payloadObj.get('new')
|
||||
if (newMsgAny == null) return
|
||||
|
||||
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
|
||||
|
||||
const senderId = newMsg.getString('sender_id') ?? ''
|
||||
const receiverId = newMsg.getString('receiver_id') ?? ''
|
||||
const msgId = newMsg.getString('id') ?? ''
|
||||
const content = newMsg.getString('content') ?? ''
|
||||
|
||||
// 检查是否与当前聊天相关
|
||||
if (senderId != this.chatUserId && receiverId != this.chatUserId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查消息是否已存在
|
||||
for (let i = 0; i < this.chatMessages.length; i++) {
|
||||
if (this.chatMessages[i].id == msgId) return
|
||||
}
|
||||
|
||||
const isFromUser = senderId === this.chatUserId
|
||||
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')
|
||||
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
this.chatMessages.push({
|
||||
id: msgId,
|
||||
viewId: safeViewId,
|
||||
session_id: newMsg.getString('session_id') || '',
|
||||
sender_id: senderId,
|
||||
receiver_id: receiverId,
|
||||
content: content,
|
||||
msg_type: newMsg.getString('msg_type') || 'text',
|
||||
is_read: false,
|
||||
is_from_user: isFromUser,
|
||||
created_at: createdAt,
|
||||
time: timeStr
|
||||
} as ChatMessageType)
|
||||
|
||||
this.scrollToBottom()
|
||||
|
||||
if (isFromUser) {
|
||||
this.markAsRead()
|
||||
}
|
||||
})
|
||||
.subscribe((status: string, err: any | null) => {
|
||||
console.log('订阅状态:', status)
|
||||
if (err != null) {
|
||||
console.log('订阅错误:', err)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
scrollToBottom(): void {
|
||||
if (this.chatMessages.length === 0) return
|
||||
|
||||
const lastMsg = this.chatMessages[this.chatMessages.length - 1]
|
||||
const targetId = lastMsg.viewId
|
||||
|
||||
this.scrollToView = ''
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollToView = targetId
|
||||
}, 100)
|
||||
},
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
async markAsRead() {
|
||||
try {
|
||||
await supa
|
||||
.from('ml_chat_messages')
|
||||
.update({ is_read: true })
|
||||
.eq('receiver_id', this.merchantId)
|
||||
<<<<<<< HEAD
|
||||
.eq('is_read', false)
|
||||
.execute()
|
||||
} catch (e) {}
|
||||
@@ -163,6 +510,22 @@
|
||||
|
||||
const content = this.inputText.trim()
|
||||
this.inputText = ''
|
||||
=======
|
||||
.eq('sender_id', this.chatUserId)
|
||||
.eq('is_read', false)
|
||||
.execute()
|
||||
} catch (e) {
|
||||
console.error('标记已读失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async sendMessage() {
|
||||
const content = this.inputText.trim()
|
||||
if (content == '') return
|
||||
|
||||
this.inputText = ''
|
||||
this.showEmoji = false
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
try {
|
||||
const newMessage = {
|
||||
@@ -180,6 +543,7 @@
|
||||
.insert([newMessage])
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (!response.error) {
|
||||
this.loadChatMessages()
|
||||
}
|
||||
@@ -198,12 +562,73 @@
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
=======
|
||||
if (response.error != null) {
|
||||
console.error('发送消息失败:', response.error)
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('发送消息异常:', e)
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
showEmojiPicker(): void {
|
||||
this.showEmoji = !this.showEmoji
|
||||
if (this.showEmoji) {
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
},
|
||||
|
||||
insertEmoji(emoji: string): void {
|
||||
this.inputText += emoji
|
||||
this.showEmoji = false
|
||||
this.inputFocus = true
|
||||
},
|
||||
|
||||
showImagePicker(): void {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
console.log('选择图片:', res.tempFilePaths)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
showMoreActions(): void {
|
||||
uni.showActionSheet({
|
||||
itemList: ['结束对话', '清除记录'],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
uni.navigateBack()
|
||||
break
|
||||
case 1:
|
||||
this.chatMessages = []
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
previewImage(url : string) {
|
||||
if (url == '') return
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: 0
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<<<<<<< HEAD
|
||||
.chat-page { display: flex; flex-direction: column; height: 100vh; background-color: #f5f5f5; }
|
||||
.chat-header { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-bottom: 1rpx solid #eee; }
|
||||
.header-back { padding: 10rpx 20rpx 10rpx 0; }
|
||||
@@ -229,4 +654,51 @@
|
||||
.input-field { flex: 1; height: 72rpx; background-color: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 28rpx; }
|
||||
.send-btn { margin-left: 20rpx; width: 72rpx; height: 72rpx; background-color: #007AFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.send-icon { font-size: 32rpx; color: #fff; }
|
||||
=======
|
||||
.chat-page { width: 100%; flex: 1; background-color: #f5f5f5; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
.chat-header { background-color: white; padding-left: 15px; padding-right: 15px; padding-bottom: 10px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #eee; flex-shrink: 0; }
|
||||
.header-back { width: 40px; }
|
||||
.back-icon { font-size: 24px; color: #333; }
|
||||
.header-info { flex: 1; }
|
||||
.header-info-text-wrapper { display: flex; flex-direction: column; align-items: 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; }
|
||||
.action-icon-text { text-align: right; width: 100%; }
|
||||
|
||||
.chat-content { flex: 1; height: 0; padding: 10px; padding-bottom: 20px; }
|
||||
.chat-messages { display: flex; flex-direction: column; padding-bottom: 80px; }
|
||||
|
||||
.message-item.system { display: flex; flex-direction: row; justify-content: center; margin-bottom: 20px; }
|
||||
.system-text { font-size: 12px; color: #999; background-color: #f0f0f0; padding-top: 5px; padding-bottom: 5px; padding-left: 15px; padding-right: 15px; border-radius: 15px; text-align: center; }
|
||||
|
||||
.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; background-color: #e0e0e0; }
|
||||
.avatar.me { margin-right: 0; margin-left: 10px; }
|
||||
.message-content-wrapper { width: 260px; display: flex; flex-direction: column; }
|
||||
.message-bubble { background-color: white; padding-top: 10px; padding-bottom: 10px; padding-left: 15px; padding-right: 15px; border-radius: 12px; }
|
||||
.received-bubble { align-self: flex-start; border-top-left-radius: 2px; }
|
||||
.message-bubble.me { background-color: #95ec69; align-self: flex-end; border-top-right-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; }
|
||||
.message-time { font-size: 11px; color: #999; text-align: right; }
|
||||
|
||||
.chat-input { background-color: white; border-top-width: 1px; border-top-style: solid; border-top-color: #eee; padding-top: 10px; padding-bottom: 20px; padding-left: 15px; padding-right: 15px; 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-top: 10px; padding-bottom: 10px; padding-left: 15px; padding-right: 15px; font-size: 15px; margin-right: 10px; min-height: 40px; max-height: 100px; }
|
||||
.send-button { background-color: #ccc; color: white; border-radius: 20px; padding-top: 8px; padding-bottom: 8px; padding-left: 20px; padding-right: 20px; font-size: 14px; min-width: 60px; }
|
||||
.send-button.active { background-color: #ff5000; }
|
||||
|
||||
.emoji-picker { background-color: white; border-top-width: 1px; border-top-style: solid; border-top-color: #eee; padding: 10px; height: 200px; position: fixed; bottom: 80px; left: 0; right: 0; }
|
||||
.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; }
|
||||
|
||||
.image-bubble { padding: 0 !important; background-color: transparent !important; }
|
||||
.image-bubble .message-time { margin-top: 5px; text-align: right; }
|
||||
.message-image { width: 150px; border-radius: 8px; }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</style>
|
||||
|
||||
544
pages/mall/merchant/exclusive-discounts.uvue
Normal file
544
pages/mall/merchant/exclusive-discounts.uvue
Normal file
@@ -0,0 +1,544 @@
|
||||
<template>
|
||||
<view class="exclusive-discounts-page">
|
||||
<view class="header">
|
||||
<text class="title">由于您的需求,为 {{ userName }} 配置专属商品折扣</text>
|
||||
<text class="subtitle">专属打折商品的折扣不受全场默认 VIP 折扣影响。</text>
|
||||
</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="add-btn" @click="openProductSelect">+ 添加更多覆盖商品</button>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="list-container">
|
||||
<view v-if="discounts.length === 0" class="empty-tip">该客户暂无专属折扣商品</view>
|
||||
|
||||
<view v-for="item in discounts" :key="item.id" class="discount-item">
|
||||
<view class="product-info">
|
||||
<image :src="item.main_image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFill" />
|
||||
<view class="product-details">
|
||||
<text class="product-name">{{ item.product_name }}</text>
|
||||
<view class="price-row">
|
||||
<text class="original-price">原价 ¥{{ item.base_price }}</text>
|
||||
<text class="current-discount">当前设置: {{ parseFloat(item.discount_rate) * 10 }} 折</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="edit-btn" @click="editDiscount(item)">改价</button>
|
||||
<button class="del-btn" @click="removeDiscount(item.id)">移除</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 修改折扣弹窗 -->
|
||||
<view class="modal" v-if="showEditModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">设置该商品的折扣倍数</view>
|
||||
<view class="modal-body">
|
||||
<input type="digit" class="input" v-model="editForm.rate" placeholder="示例:填写 0.85 代表 85折" />
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn cancel" @click="showEditModal = false">取消</button>
|
||||
<button class="btn confirm" @click="saveDiscount">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择商品弹窗 -->
|
||||
<view class="modal" v-if="showProductSelect">
|
||||
<view class="modal-content product-modal-content">
|
||||
<view class="modal-header">选择要设置折扣的商品</view>
|
||||
<scroll-view scroll-y class="product-scroll">
|
||||
<view class="product-p-item" v-for="p in allProducts" :key="p.id" @click="selectProductForDiscount(p)">
|
||||
<image :src="p.main_image_url || '/static/images/default-product.png'" class="p-img" mode="aspectFill" />
|
||||
<view class="p-info">
|
||||
<text class="p-name">{{p.name}}</text>
|
||||
<text class="p-price">¥{{p.base_price}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="allProducts.length === 0" class="empty-tip">暂无可选商品</view>
|
||||
</scroll-view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn cancel" @click="showProductSelect = false">关闭</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type DiscountDoc = {
|
||||
id: string
|
||||
user_id: string
|
||||
product_id: string
|
||||
discount_rate: string
|
||||
product_name: string
|
||||
main_image_url: string
|
||||
base_price: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userId: '',
|
||||
userName: '',
|
||||
discounts: [] as DiscountDoc[],
|
||||
allProducts: [] as any[], // 所有商品列表
|
||||
|
||||
showProductSelect: false,
|
||||
showEditModal: false,
|
||||
editForm: {
|
||||
id: '',
|
||||
product_id: '',
|
||||
rate: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
if (options['user_id']) {
|
||||
this.userId = String(options['user_id'])
|
||||
} else if (options.user_id) {
|
||||
this.userId = String(options.user_id)
|
||||
}
|
||||
|
||||
if (options['user_name']) {
|
||||
this.userName = decodeURIComponent(String(options['user_name']))
|
||||
} else if (options.user_name) {
|
||||
this.userName = decodeURIComponent(String(options.user_name))
|
||||
}
|
||||
|
||||
if (this.userId !== '') {
|
||||
this.loadDiscounts()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async openProductSelect() {
|
||||
this.showProductSelect = true
|
||||
if (this.allProducts.length === 0) {
|
||||
uni.showLoading({ title: '获取商品中' })
|
||||
try {
|
||||
let merchantId = ''
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (merchantId === '') {
|
||||
const storageId = uni.getStorageSync('user_id')
|
||||
if (storageId != null) {
|
||||
merchantId = String(storageId)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await supa.from('ml_products')
|
||||
.select('id, name, main_image_url, base_price')
|
||||
.eq('status', 1)
|
||||
.eq('merchant_id', merchantId)
|
||||
.execute()
|
||||
uni.hideLoading()
|
||||
if (res.data) {
|
||||
this.allProducts = res.data as any[]
|
||||
}
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectProductForDiscount(p: any) {
|
||||
this.showProductSelect = false
|
||||
|
||||
// Reset form and ID when selecting a new product
|
||||
this.editForm.id = ''
|
||||
this.editForm.product_id = String(p['id'])
|
||||
this.editForm.rate = '1.0'
|
||||
|
||||
this.showEditModal = true
|
||||
},
|
||||
|
||||
async loadDiscounts() {
|
||||
uni.showLoading({ title: '加载中' })
|
||||
try {
|
||||
// 1. 获取折扣记录(不依赖数据库外键,避免报错)
|
||||
const response = await supa
|
||||
.from('ml_user_product_discounts')
|
||||
.select('*')
|
||||
.eq('user_id', this.userId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
uni.hideLoading()
|
||||
const errMsg = response.error['message'] != null ? String(response.error['message']) : '加载失败'
|
||||
uni.showToast({ title: '加载失败: ' + errMsg, icon: 'none', duration: 3000 })
|
||||
console.error('加载折扣异常', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null || rawData.length === 0) {
|
||||
uni.hideLoading()
|
||||
this.discounts = []
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 收集所有相关的 product_id
|
||||
const productIds: string[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const pid = rawData[i]['product_id']
|
||||
if (pid != null) {
|
||||
productIds.push(String(pid))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 查出对应商品的详情信息
|
||||
let productsMap = {} as UTSJSONObject
|
||||
if (productIds.length > 0) {
|
||||
const prodRes = await supa
|
||||
.from('ml_products')
|
||||
.select('id, name, main_image_url, base_price')
|
||||
.in('id', productIds)
|
||||
.execute()
|
||||
if (prodRes.data != null) {
|
||||
const pData = prodRes.data as any[]
|
||||
for (let j = 0; j < pData.length; j++) {
|
||||
const p = pData[j] as any
|
||||
if (p['id'] != null) {
|
||||
productsMap[String(p['id'])] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 组装数据
|
||||
this.discounts = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as any
|
||||
const pid = item['product_id'] != null ? String(item['product_id']) : ''
|
||||
const prod = productsMap[pid] as any
|
||||
|
||||
this.discounts.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
user_id: item['user_id'] != null ? String(item['user_id']) : '',
|
||||
product_id: pid,
|
||||
discount_rate: item['discount_rate'] != null ? String(item['discount_rate']) : '1.0',
|
||||
product_name: prod != null && prod['name'] != null ? String(prod['name']) : '未知商品',
|
||||
main_image_url: prod != null && prod['main_image_url'] != null ? String(prod['main_image_url']) : '',
|
||||
base_price: prod != null && prod['base_price'] != null ? String(prod['base_price']) : '0'
|
||||
} as DiscountDoc)
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
editDiscount(item: DiscountDoc) {
|
||||
this.editForm.id = item.id
|
||||
this.editForm.product_id = item.product_id
|
||||
this.editForm.rate = item.discount_rate
|
||||
this.showEditModal = true
|
||||
},
|
||||
|
||||
async saveDiscount() {
|
||||
if (!this.editForm.rate) {
|
||||
uni.showToast({ title: '请输入折扣', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const rate = parseFloat(this.editForm.rate)
|
||||
if (isNaN(rate) || rate <= 0 || rate > 1) {
|
||||
uni.showToast({ title: '折扣应当在0~1之间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中' })
|
||||
try {
|
||||
const payload = {
|
||||
user_id: this.userId,
|
||||
product_id: this.editForm.product_id,
|
||||
discount_rate: rate,
|
||||
updated_at: new Date().toISOString()
|
||||
} as UTSJSONObject
|
||||
|
||||
let res: any = null
|
||||
if (this.editForm.id !== '') {
|
||||
res = await supa.from('ml_user_product_discounts').update(payload).eq('id', this.editForm.id).execute()
|
||||
} else {
|
||||
// Check if actually modifying an existing one they just didn't click "edit" on
|
||||
const existing = this.discounts.find(d => d.product_id === this.editForm.product_id)
|
||||
if (existing != null && existing.id !== '') {
|
||||
res = await supa.from('ml_user_product_discounts').update(payload).eq('id', existing.id).execute()
|
||||
} else {
|
||||
res = await supa.from('ml_user_product_discounts').insert([payload]).execute()
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
if (res.error != null) {
|
||||
const errMsg = res.error['message'] != null ? String(res.error['message']) : '未知错误'
|
||||
uni.showToast({ title: '保存失败: ' + errMsg, icon: 'none', duration: 3000 })
|
||||
console.error('保存折扣失败:', res.error)
|
||||
return
|
||||
}
|
||||
|
||||
this.showEditModal = false
|
||||
uni.showToast({ title: '保存成功', icon: 'success', duration: 1500 })
|
||||
|
||||
setTimeout(() => {
|
||||
this.loadDiscounts()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '网络异常或请求中断', icon: 'none' })
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
|
||||
async removeDiscount(id: string) {
|
||||
const that = this
|
||||
uni.showModal({
|
||||
title: '提醒',
|
||||
content: '确定移除此商品的打折?移除后将恢复通常价格',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '移除中' })
|
||||
try {
|
||||
const s_res = await supa.from('ml_user_product_discounts').eq('id', id).delete().execute()
|
||||
uni.hideLoading()
|
||||
if (s_res.error == null) {
|
||||
uni.showToast({ title: '移除成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
that.loadDiscounts()
|
||||
}, 1500)
|
||||
} else {
|
||||
const errMsg = s_res.error['message'] != null ? String(s_res.error['message']) : '未知错误'
|
||||
uni.showToast({ title: '移除失败: ' + errMsg, icon: 'none' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
uni.hideLoading()
|
||||
const errMsg = e instanceof Error ? e.message : String(e)
|
||||
uni.showToast({ title: '移除异常: ' + errMsg, icon: 'none', duration: 4000 })
|
||||
console.error('移除折扣报错:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.exclusive-discounts-page {
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
.action-bar {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.add-btn {
|
||||
background: #2196F3;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.list-container {
|
||||
height: calc(100vh - 260rpx);
|
||||
}
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 60rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.discount-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
.product-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.product-details {
|
||||
flex: 1;
|
||||
}
|
||||
.product-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.price-row {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.original-price {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.current-discount {
|
||||
font-size: 24rpx;
|
||||
color: #F44336;
|
||||
font-weight: bold;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.edit-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
background: #4CAF50;
|
||||
color: #fff;
|
||||
line-height: normal;
|
||||
}
|
||||
.del-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
background: #f44336;
|
||||
color: #fff;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
width: 600rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-header {
|
||||
padding: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 40rpx;
|
||||
}
|
||||
.input {
|
||||
background: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
.modal-footer .btn {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 32rpx;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
.btn.cancel {
|
||||
color: #999;
|
||||
border-right: 1rpx solid #eee;
|
||||
}
|
||||
.btn.confirm {
|
||||
color: #2196F3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Product Selection Modal Styles */
|
||||
.product-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
.product-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20rpx;
|
||||
min-height: 400rpx;
|
||||
}
|
||||
.product-p-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
.product-p-item:active {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.p-img {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.p-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.p-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.p-price {
|
||||
font-size: 26rpx;
|
||||
color: #F44336;
|
||||
font-weight: bold;
|
||||
}
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端首页 - UTS Android 兼容 -->
|
||||
<template>
|
||||
<view class="merchant-container">
|
||||
@@ -134,6 +135,245 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
=======
|
||||
<!-- 商家端首页 -->
|
||||
<template>
|
||||
<view class="merchant-container">
|
||||
<scroll-view scroll-y class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header">
|
||||
<view class="header-bg"></view>
|
||||
<view class="header-content">
|
||||
<view class="shop-info">
|
||||
<image :src="shopInfo.shop_logo || '/static/images/default-shop.png'" class="shop-logo" mode="aspectFill" @click="goToSettings" />
|
||||
<view class="shop-details">
|
||||
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
|
||||
<view class="shop-meta">
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">⭐</text>
|
||||
<text class="meta-value">{{ shopInfo.rating_avg || 5.0 }}</text>
|
||||
</view>
|
||||
<view class="meta-divider"></view>
|
||||
<view class="meta-item">
|
||||
<text class="meta-icon">📦</text>
|
||||
<text class="meta-value">{{ shopInfo.total_sales || 0 }}销量</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" @click="goToMessages">
|
||||
<text class="action-icon">🔔</text>
|
||||
<view v-if="unreadCount > 0" class="action-badge"><text>{{ unreadCount > 99 ? '99+' : unreadCount }}</text></view>
|
||||
</view>
|
||||
<view class="action-btn" @click="goToSettings">
|
||||
<text class="action-icon">⚙️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-area">
|
||||
<!-- 今日数据卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-header">
|
||||
<view class="stats-title-row">
|
||||
<text class="stats-title">📊 今日数据</text>
|
||||
<text class="stats-date">{{ currentDate }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap blue">
|
||||
<text class="stats-icon">📋</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.orders || 0 }}</text>
|
||||
<text class="stats-label">订单数</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap green">
|
||||
<text class="stats-icon">💰</text>
|
||||
</view>
|
||||
<text class="stats-value">¥{{ formatNumber(todayStats.sales) }}</text>
|
||||
<text class="stats-label">销售额</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap orange">
|
||||
<text class="stats-icon">👥</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.visitors || 0 }}</text>
|
||||
<text class="stats-label">访客数</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-icon-wrap purple">
|
||||
<text class="stats-icon">📈</text>
|
||||
</view>
|
||||
<text class="stats-value">{{ todayStats.conversion || 0 }}%</text>
|
||||
<text class="stats-label">转化率</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理事项 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🔔 待处理事项</text>
|
||||
</view>
|
||||
<view class="pending-grid">
|
||||
<view class="pending-item" @click="goToOrders('pending')">
|
||||
<view class="pending-icon-wrap orange">
|
||||
<text class="pending-icon">📦</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.pending_shipment > 0">{{ pendingCounts.pending_shipment }}</text>
|
||||
<text class="pending-text">待发货</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToOrders('refund')">
|
||||
<view class="pending-icon-wrap red">
|
||||
<text class="pending-icon">↩️</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.refund_requests > 0">{{ pendingCounts.refund_requests }}</text>
|
||||
<text class="pending-text">退款</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToInventory">
|
||||
<view class="pending-icon-wrap yellow">
|
||||
<text class="pending-icon">⚠️</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.low_stock > 0">{{ pendingCounts.low_stock }}</text>
|
||||
<text class="pending-text">库存预警</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pending-item" @click="goToReviews">
|
||||
<view class="pending-icon-wrap blue">
|
||||
<text class="pending-icon">💬</text>
|
||||
</view>
|
||||
<view class="pending-info">
|
||||
<text class="pending-count" v-if="pendingCounts.pending_reviews > 0">{{ pendingCounts.pending_reviews }}</text>
|
||||
<text class="pending-text">待回复</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用功能 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🚀 常用功能</text>
|
||||
</view>
|
||||
<view class="shortcuts-grid">
|
||||
<view class="shortcut-item" @click="goToProducts('add')">
|
||||
<view class="shortcut-icon-wrap gradient-blue">
|
||||
<text class="shortcut-icon">➕</text>
|
||||
</view>
|
||||
<text class="shortcut-text">发布商品</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToOrders('all')">
|
||||
<view class="shortcut-icon-wrap gradient-orange">
|
||||
<text class="shortcut-icon">📋</text>
|
||||
</view>
|
||||
<text class="shortcut-text">订单管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToProducts('manage')">
|
||||
<view class="shortcut-icon-wrap gradient-green">
|
||||
<text class="shortcut-icon">📦</text>
|
||||
</view>
|
||||
<text class="shortcut-text">商品管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToInventory">
|
||||
<view class="shortcut-icon-wrap gradient-purple">
|
||||
<text class="shortcut-icon">📊</text>
|
||||
</view>
|
||||
<text class="shortcut-text">库存管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToPromotions">
|
||||
<view class="shortcut-icon-wrap gradient-red">
|
||||
<text class="shortcut-icon">🎯</text>
|
||||
</view>
|
||||
<text class="shortcut-text">营销活动</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToStatistics">
|
||||
<view class="shortcut-icon-wrap gradient-cyan">
|
||||
<text class="shortcut-icon">📈</text>
|
||||
</view>
|
||||
<text class="shortcut-text">数据统计</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToFinance">
|
||||
<view class="shortcut-icon-wrap gradient-yellow">
|
||||
<text class="shortcut-icon">💰</text>
|
||||
</view>
|
||||
<text class="shortcut-text">财务结算</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToMembers">
|
||||
<view class="shortcut-icon-wrap gradient-pink">
|
||||
<text class="shortcut-icon">VIP</text>
|
||||
</view>
|
||||
<text class="shortcut-text">会员管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToSettings">
|
||||
<view class="shortcut-icon-wrap gradient-pink">
|
||||
<text class="shortcut-icon">🏪</text>
|
||||
</view>
|
||||
<text class="shortcut-text">店铺设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新订单 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">🛒 最新订单</text>
|
||||
<text class="section-more" @click="goToOrders('all')">查看全部 ›</text>
|
||||
</view>
|
||||
<view v-if="recentOrders.length === 0" class="empty-orders">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
<text class="empty-hint">有新订单时会在这里显示</text>
|
||||
</view>
|
||||
<view v-else class="orders-list">
|
||||
<view v-for="order in recentOrders" :key="order.id" class="order-card" @click="goToOrderDetail(order.id)">
|
||||
<view class="order-header">
|
||||
<view class="order-no-wrap">
|
||||
<text class="order-label">订单号</text>
|
||||
<text class="order-no">{{ order.order_no }}</text>
|
||||
</view>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
|
||||
</view>
|
||||
<view class="order-goods">
|
||||
<view v-for="item in order.items.slice(0, 3)" :key="item.id" class="goods-item">
|
||||
<image :src="item.image_url || '/static/images/default-product.png'" class="goods-image" mode="aspectFill" />
|
||||
<view class="goods-info">
|
||||
<text class="goods-name">{{ item.product_name }}</text>
|
||||
<view class="goods-bottom">
|
||||
<text class="goods-spec" v-if="item.sku_name">{{ item.sku_name }}</text>
|
||||
<text class="goods-qty">×{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="goods-price">¥{{ item.price }}</text>
|
||||
</view>
|
||||
<view v-if="order.items.length > 3" class="goods-more">
|
||||
<text>还有{{ order.items.length - 3 }}件商品</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-footer">
|
||||
<text class="order-time">{{ formatTime(order.created_at) }}</text>
|
||||
<view class="order-amount-wrap">
|
||||
<text class="amount-label">合计</text>
|
||||
<text class="amount-value">¥{{ order.total_amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -219,7 +459,20 @@
|
||||
low_stock: 0,
|
||||
pending_reviews: 0
|
||||
} as PendingCountsType,
|
||||
<<<<<<< HEAD
|
||||
recentOrders: [] as OrderType[]
|
||||
=======
|
||||
recentOrders: [] as OrderType[],
|
||||
unreadCount: 0,
|
||||
refreshing: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentDate(): string {
|
||||
const now = new Date()
|
||||
return `${now.getMonth() + 1}月${now.getDate()}日`
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
},
|
||||
|
||||
@@ -229,6 +482,7 @@
|
||||
|
||||
onShow() {
|
||||
if (this.merchantId) {
|
||||
<<<<<<< HEAD
|
||||
this.loadMerchantData()
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
@@ -239,16 +493,36 @@
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentOrders()
|
||||
=======
|
||||
this.loadAllData()
|
||||
this.startRealtimeSubscription()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadAllData()
|
||||
this.startRealtimeSubscription()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
methods: {
|
||||
formatNumber(value: number | null): string {
|
||||
if (value == null) return '0.00'
|
||||
return value.toFixed(2)
|
||||
},
|
||||
|
||||
=======
|
||||
onHide() {
|
||||
this.stopRealtimeSubscription()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.stopRealtimeSubscription()
|
||||
},
|
||||
|
||||
methods: {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
@@ -263,6 +537,56 @@
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
startRealtimeSubscription() {
|
||||
if (!this.merchantId) return
|
||||
|
||||
// 监听订单表的变化
|
||||
try {
|
||||
supa.channel('ml_orders_realtime')
|
||||
.on('postgres_changes', {
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'ml_orders',
|
||||
filter: `merchant_id=eq.${this.merchantId}`
|
||||
}, (payload) => {
|
||||
console.log('收到订单实时更新:', payload)
|
||||
// 延迟一下再刷新,避免连续变动导致频繁请求
|
||||
setTimeout(() => {
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentOrders()
|
||||
}, 500)
|
||||
})
|
||||
.subscribe()
|
||||
} catch (e) {
|
||||
console.error('订阅实时更新失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
stopRealtimeSubscription() {
|
||||
try {
|
||||
supa.channel('ml_orders_realtime').unsubscribe()
|
||||
} catch (e) {
|
||||
console.error('取消订阅失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadAllData() {
|
||||
await this.loadMerchantData()
|
||||
await this.loadTodayStats()
|
||||
await this.loadPendingCounts()
|
||||
await this.loadRecentOrders()
|
||||
await this.loadUnreadCount()
|
||||
},
|
||||
|
||||
formatNumber(value: number | null): string {
|
||||
if (value == null) return '0.00'
|
||||
return value.toFixed(2)
|
||||
},
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
async loadMerchantData() {
|
||||
try {
|
||||
const response = await supa
|
||||
@@ -272,7 +596,12 @@
|
||||
.limit(1)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
|
||||
=======
|
||||
if (response.error != null) { console.error('ml_shops请求500报错', response.error) }
|
||||
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.shopInfo = {
|
||||
id: null,
|
||||
merchant_id: this.merchantId,
|
||||
@@ -291,7 +620,11 @@
|
||||
|
||||
const rawData = (response.data as any[])[0] as UTSJSONObject
|
||||
this.shopInfo = {
|
||||
<<<<<<< HEAD
|
||||
id: rawData.getString('id') || null,
|
||||
=======
|
||||
id: rawData.getString('id') || null,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
merchant_id: rawData.getString('merchant_id') || null,
|
||||
shop_name: rawData.getString('shop_name') || '我的店铺',
|
||||
shop_logo: rawData.getString('shop_logo') || null,
|
||||
@@ -303,6 +636,43 @@
|
||||
total_sales: rawData.getNumber('total_sales') || 0,
|
||||
status: rawData.getNumber('status') || 1
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
// 重新动态查询并计算该店铺下所有商品的真实销量总和
|
||||
try {
|
||||
const salesRes = await supa
|
||||
.from('ml_products')
|
||||
.select('sale_count')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.execute()
|
||||
|
||||
if (salesRes.error != null) { console.error('ml_products sale_count报错', salesRes.error) }
|
||||
if (salesRes.data != null) {
|
||||
let calcTotalSales: number = 0
|
||||
const salesData = salesRes.data as any[]
|
||||
for (let i = 0; i < salesData.length; i++) {
|
||||
const productInfo = salesData[i] as UTSJSONObject
|
||||
const currentSale = productInfo.getNumber('sale_count')
|
||||
if (currentSale != null) {
|
||||
calcTotalSales += currentSale
|
||||
}
|
||||
}
|
||||
|
||||
let baseSales: number = 0
|
||||
if (this.shopInfo.total_sales != null) {
|
||||
baseSales = Number(this.shopInfo.total_sales)
|
||||
}
|
||||
|
||||
if (calcTotalSales > baseSales) {
|
||||
this.shopInfo.total_sales = calcTotalSales
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取店铺真实销量失败:', e)
|
||||
}
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} catch (e) {
|
||||
console.error('加载店铺信息失败:', e)
|
||||
}
|
||||
@@ -310,6 +680,7 @@
|
||||
|
||||
async loadTodayStats() {
|
||||
try {
|
||||
<<<<<<< HEAD
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select('total_amount, order_status', { count: 'exact' })
|
||||
@@ -323,24 +694,89 @@
|
||||
|
||||
let totalOrders = 0
|
||||
let totalSales = 0
|
||||
=======
|
||||
// 1. 获取所有订单
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
total_amount,
|
||||
order_status,
|
||||
created_at,
|
||||
order_items (quantity)
|
||||
`)
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) { console.error('ml_orders stats报错', response.error); return }
|
||||
|
||||
let todayOrders = 0
|
||||
let todaySales = 0
|
||||
let allTimeSalesVolume = 0 // 总销量(件数)
|
||||
|
||||
const now = new Date()
|
||||
// 获取今日0点的毫秒数 (本地时间)
|
||||
const todayStartMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData != null) {
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const status = item.getNumber('order_status')
|
||||
<<<<<<< HEAD
|
||||
if (status >= 2) {
|
||||
totalOrders++
|
||||
totalSales += item.getNumber('total_amount') || 0
|
||||
=======
|
||||
|
||||
// 有效订单(已支付、已发货、已完成) >= 2
|
||||
// 如果是退款(0)或取消(5),可能不计入今日销售额,这里按需调整
|
||||
if (status != null && status >= 2 && status < 5) {
|
||||
// 计算总销量(即售出的商品总件数)
|
||||
const itemsObj = item.get('order_items')
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArr = itemsObj as any[]
|
||||
for (let j = 0; j < itemsArr.length; j++) {
|
||||
const orderItem = itemsArr[j] as UTSJSONObject
|
||||
allTimeSalesVolume += Math.floor(orderItem.getNumber('quantity') || 1)
|
||||
}
|
||||
} else {
|
||||
allTimeSalesVolume += 1
|
||||
}
|
||||
|
||||
// 判断是否是今日数据
|
||||
const createdAtStr = item.getString('created_at') || ''
|
||||
if (createdAtStr.length > 0) {
|
||||
const orderDateMs = new Date(createdAtStr).getTime()
|
||||
if (orderDateMs >= todayStartMs) {
|
||||
todayOrders++
|
||||
todaySales += item.getNumber('total_amount') || 0
|
||||
}
|
||||
}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
this.todayStats = {
|
||||
orders: totalOrders,
|
||||
sales: totalSales,
|
||||
visitors: Math.floor(totalOrders * 3),
|
||||
conversion: totalOrders > 0 ? 15 : 0
|
||||
=======
|
||||
// 更新店铺总销量显示
|
||||
let currentShopSales = Number(this.shopInfo.total_sales || 0)
|
||||
if (allTimeSalesVolume > currentShopSales) {
|
||||
this.shopInfo.total_sales = allTimeSalesVolume
|
||||
}
|
||||
|
||||
this.todayStats = {
|
||||
orders: todayOrders,
|
||||
sales: todaySales,
|
||||
visitors: Math.floor(todayOrders * (2.5 + Math.random())) + 5, // 模拟访客数
|
||||
conversion: todayOrders > 0 ? (12 + Math.floor(Math.random() * 8)) : 0 // 模拟转化率
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取今日统计异常:', e)
|
||||
@@ -356,22 +792,39 @@
|
||||
.eq('order_status', 2)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
const refundRes = await supa
|
||||
=======
|
||||
if (pendingShipmentRes.error != null) { console.error('pendingShipment报错', pendingShipmentRes.error) }
|
||||
const refundRes = await supa
|
||||
>>>>>>> local-backup-root-cyj
|
||||
.from('ml_orders')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.eq('order_status', 0)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
const lowStockRes = await supa
|
||||
=======
|
||||
if (refundRes.error != null) { console.error('refundRes报错', refundRes.error) }
|
||||
const lowStockRes = await supa
|
||||
>>>>>>> local-backup-root-cyj
|
||||
.from('ml_products')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.lte('total_stock', 10)
|
||||
<<<<<<< HEAD
|
||||
.gte('total_stock', 0)
|
||||
.execute()
|
||||
|
||||
this.pendingCounts = {
|
||||
=======
|
||||
.execute()
|
||||
|
||||
if (lowStockRes.error != null) { console.error('lowStockRes报错', lowStockRes.error) }
|
||||
this.pendingCounts = {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
pending_shipment: pendingShipmentRes.total || 0,
|
||||
refund_requests: refundRes.total || 0,
|
||||
low_stock: lowStockRes.total || 0,
|
||||
@@ -388,7 +841,11 @@
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
<<<<<<< HEAD
|
||||
order_items!inner (
|
||||
=======
|
||||
order_items (
|
||||
>>>>>>> local-backup-root-cyj
|
||||
id,
|
||||
product_id,
|
||||
product_name,
|
||||
@@ -403,10 +860,15 @@
|
||||
.limit(5)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null || !response.data) {
|
||||
this.recentOrders = []
|
||||
return
|
||||
}
|
||||
=======
|
||||
if (response.error != null) { console.error('recentOrders报错', response.error) }
|
||||
if (response.error != null || !response.data) { this.recentOrders = []; return; }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
const rawData = response.data as any[]
|
||||
const ordersData: OrderType[] = []
|
||||
@@ -447,6 +909,7 @@
|
||||
|
||||
this.recentOrders = ordersData
|
||||
} catch (e) {
|
||||
<<<<<<< HEAD
|
||||
console.error('加载最新订单异常:', e)
|
||||
}
|
||||
},
|
||||
@@ -472,6 +935,51 @@
|
||||
case 0: return '退款中'
|
||||
default: return '未知状态'
|
||||
}
|
||||
=======
|
||||
console.error('加载最新订单异常:', e); uni.showModal({title: '最新订单报错', content: e.toString()})
|
||||
}
|
||||
},
|
||||
|
||||
async loadUnreadCount() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_chat_messages')
|
||||
.select('id', { count: 'exact' })
|
||||
.eq('receiver_id', this.merchantId)
|
||||
.eq('is_read', false)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) { uni.showModal({title: 'ml_chat_messages报错', content: JSON.stringify(response.error)}) }
|
||||
this.unreadCount = response.total || 0
|
||||
} catch (e) {
|
||||
console.error('获取未读消息数失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadAllData().then(() => {
|
||||
this.refreshing = false
|
||||
})
|
||||
},
|
||||
|
||||
getOrderStatusClass(status: number): string {
|
||||
if (status === 1) return 'status-pending'
|
||||
if (status === 2) return 'status-paid'
|
||||
if (status === 3) return 'status-shipped'
|
||||
if (status === 4) return 'status-completed'
|
||||
if (status === 0) return 'status-refund'
|
||||
return 'status-default'
|
||||
},
|
||||
|
||||
getOrderStatusText(status: number): string {
|
||||
if (status === 1) return '待付款'
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '已发货'
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 0) return '退款中'
|
||||
return '未知'
|
||||
>>>>>>> local-backup-root-cyj
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
@@ -480,6 +988,7 @@
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
<<<<<<< HEAD
|
||||
|
||||
if (minutes < 60) {
|
||||
return `${minutes}分钟前`
|
||||
@@ -542,12 +1051,67 @@
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/merchant/order-detail?id=${orderId}`
|
||||
})
|
||||
=======
|
||||
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
},
|
||||
|
||||
goToMessages() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/messages' })
|
||||
},
|
||||
|
||||
goToSettings() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/shop-edit' })
|
||||
},
|
||||
|
||||
goToOrders(type: string) {
|
||||
uni.navigateTo({ url: `/pages/mall/merchant/orders?type=${type}` })
|
||||
},
|
||||
|
||||
goToProducts(type: string) {
|
||||
if (type === 'add') {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/product-edit' })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/products' })
|
||||
}
|
||||
},
|
||||
|
||||
goToPromotions() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/promotions' })
|
||||
},
|
||||
|
||||
goToStatistics() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/statistics' })
|
||||
},
|
||||
|
||||
goToFinance() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/finance' })
|
||||
},
|
||||
|
||||
goToReviews() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/reviews' })
|
||||
},
|
||||
|
||||
goToInventory() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/inventory' })
|
||||
},
|
||||
|
||||
goToMembers() {
|
||||
uni.navigateTo({ url: '/pages/mall/merchant/members' })
|
||||
},
|
||||
|
||||
goToOrderDetail(orderId: string) {
|
||||
uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` })
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<<<<<<< HEAD
|
||||
.merchant-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
@@ -872,3 +1436,115 @@
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
=======
|
||||
.merchant-container { background-color: #f5f7fa; min-height: 100vh; }
|
||||
.main-scroll { height: 100vh; }
|
||||
|
||||
.header { position: relative; padding-bottom: 30rpx; }
|
||||
.header-bg { position: absolute; top: 0; left: 0; right: 0; height: 300rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 0 0 40rpx 40rpx; }
|
||||
.header-content { position: relative; padding: 40rpx 30rpx 0; }
|
||||
.shop-info { display: flex; flex-direction: row; align-items: center; }
|
||||
.shop-logo { width: 110rpx; height: 110rpx; border-radius: 20rpx; border-width: 4rpx; border-style: solid; border-color: rgba(255,255,255,0.8); margin-right: 24rpx; background-color: #fff; }
|
||||
.shop-details { flex: 1; }
|
||||
.shop-name { font-size: 40rpx; font-weight: bold; color: #fff; margin-bottom: 12rpx; }
|
||||
.shop-meta { display: flex; flex-direction: row; align-items: center; }
|
||||
.meta-item { display: flex; flex-direction: row; align-items: center; }
|
||||
.meta-icon { font-size: 24rpx; margin-right: 6rpx; }
|
||||
.meta-value { font-size: 26rpx; color: rgba(255,255,255,0.9); }
|
||||
.meta-divider { width: 2rpx; height: 24rpx; background-color: rgba(255,255,255,0.3); margin-left: 20rpx; margin-right: 20rpx; }
|
||||
.header-actions { display: flex; flex-direction: row; }
|
||||
.action-btn { position: relative; margin-left: 24rpx; width: 72rpx; height: 72rpx; background-color: rgba(255,255,255,0.2); border-radius: 36rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.action-icon { font-size: 36rpx; }
|
||||
.action-badge { position: absolute; top: -4rpx; right: -4rpx; min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding-left: 8rpx; padding-right: 8rpx; border-width: 2rpx; border-style: solid; border-color: #fff; }
|
||||
.action-badge-text { font-size: 20rpx; color: #fff; font-weight: bold; }
|
||||
|
||||
.content-area { padding-left: 24rpx; padding-right: 24rpx; padding-bottom: 30rpx; margin-top: 10rpx; }
|
||||
|
||||
.stats-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.stats-header { margin-bottom: 24rpx; }
|
||||
.stats-title-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.stats-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.stats-date { font-size: 24rpx; color: #999; background-color: #f5f7fa; padding-top: 6rpx; padding-bottom: 6rpx; padding-left: 16rpx; padding-right: 16rpx; border-radius: 12rpx; }
|
||||
.stats-grid { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.stats-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; }
|
||||
.stats-icon-wrap { width: 64rpx; height: 64rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.stats-icon-wrap.blue { background-color: #E3F2FD; }
|
||||
.stats-icon-wrap.green { background-color: #E8F5E9; }
|
||||
.stats-icon-wrap.orange { background-color: #FFF3E0; }
|
||||
.stats-icon-wrap.purple { background-color: #F3E5F5; }
|
||||
.stats-icon { font-size: 28rpx; }
|
||||
.stats-value { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 4rpx; }
|
||||
.stats-label { font-size: 24rpx; color: #999; }
|
||||
|
||||
.section-card { background-color: #fff; border-radius: 24rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
||||
.section-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.section-more { font-size: 26rpx; color: #007AFF; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 16rpx; padding-right: 16rpx; background-color: #E3F2FD; border-radius: 12rpx; }
|
||||
|
||||
.pending-grid { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.pending-item { width: 160rpx; display: flex; flex-direction: column; align-items: center; padding-top: 16rpx; padding-bottom: 16rpx; }
|
||||
.pending-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.pending-icon-wrap.orange { background-color: #FFF3E0; }
|
||||
.pending-icon-wrap.red { background-color: #FFEBEE; }
|
||||
.pending-icon-wrap.yellow { background-color: #FFFDE7; }
|
||||
.pending-icon-wrap.blue { background-color: #E3F2FD; }
|
||||
.pending-icon { font-size: 40rpx; }
|
||||
.pending-info { display: flex; flex-direction: column; align-items: center; }
|
||||
.pending-count { font-size: 32rpx; font-weight: bold; color: #FF6B35; margin-bottom: 4rpx; }
|
||||
.pending-text { font-size: 24rpx; color: #666; }
|
||||
|
||||
.shortcuts-grid { display: flex; flex-direction: row; flex-wrap: wrap; }
|
||||
.shortcut-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding-top: 20rpx; padding-bottom: 20rpx; }
|
||||
.shortcut-icon-wrap { width: 88rpx; height: 88rpx; border-radius: 24rpx; display: flex; align-items: center; justify-content: center; margin-bottom: 12rpx; }
|
||||
.shortcut-icon-wrap.gradient-blue { background-color: #667eea; }
|
||||
.shortcut-icon-wrap.gradient-orange { background-color: #f093fb; }
|
||||
.shortcut-icon-wrap.gradient-green { background-color: #4facfe; }
|
||||
.shortcut-icon-wrap.gradient-purple { background-color: #a18cd1; }
|
||||
.shortcut-icon-wrap.gradient-red { background-color: #ff9a9e; }
|
||||
.shortcut-icon-wrap.gradient-cyan { background-color: #a1c4fd; }
|
||||
.shortcut-icon-wrap.gradient-yellow { background-color: #f6d365; }
|
||||
.shortcut-icon-wrap.gradient-pink { background-color: #ffecd2; }
|
||||
.shortcut-icon { font-size: 40rpx; }
|
||||
.shortcut-text { font-size: 24rpx; color: #666; }
|
||||
|
||||
.empty-orders { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 80rpx; padding-bottom: 80rpx; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text { font-size: 30rpx; color: #666; margin-bottom: 8rpx; }
|
||||
.empty-hint { font-size: 24rpx; color: #999; }
|
||||
.orders-list { display: flex; flex-direction: column; }
|
||||
.order-card { background-color: #f9fafb; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; border-width: 1rpx; border-style: solid; border-color: #eee; }
|
||||
.order-header { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
|
||||
.order-no-wrap { display: flex; flex-direction: row; align-items: center; }
|
||||
.order-label { font-size: 22rpx; color: #999; background-color: #eee; padding-top: 4rpx; padding-bottom: 4rpx; padding-left: 12rpx; padding-right: 12rpx; border-radius: 8rpx; margin-right: 12rpx; }
|
||||
.order-no { font-size: 26rpx; color: #333; font-weight: 500; }
|
||||
.order-status { font-size: 24rpx; padding-top: 8rpx; padding-bottom: 8rpx; padding-left: 20rpx; padding-right: 20rpx; border-radius: 20rpx; font-weight: 500; }
|
||||
.status-pending { background-color: #FFF3E0; color: #FF9800; }
|
||||
.status-paid { background-color: #E3F2FD; color: #2196F3; }
|
||||
.status-shipped { background-color: #E8F5E9; color: #4CAF50; }
|
||||
.status-completed { background-color: #F3E5F5; color: #9C27B0; }
|
||||
.status-refund { background-color: #FFEBEE; color: #F44336; }
|
||||
.order-goods { margin-bottom: 16rpx; }
|
||||
.goods-item { display: flex; flex-direction: row; align-items: center; margin-bottom: 16rpx; background-color: #fff; padding: 16rpx; border-radius: 12rpx; }
|
||||
.goods-image { width: 100rpx; height: 100rpx; border-radius: 12rpx; margin-right: 16rpx; background-color: #f5f5f5; }
|
||||
.goods-info { flex: 1; }
|
||||
.goods-name { font-size: 28rpx; color: #333; margin-bottom: 8rpx; font-weight: 500; }
|
||||
.goods-bottom { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.goods-spec { font-size: 22rpx; color: #999; }
|
||||
.goods-qty { font-size: 24rpx; color: #999; }
|
||||
.goods-price { font-size: 28rpx; color: #FF6B35; font-weight: bold; }
|
||||
.goods-more { text-align: center; padding-top: 12rpx; padding-bottom: 12rpx; font-size: 24rpx; color: #999; background-color: #fff; border-radius: 12rpx; }
|
||||
.order-footer { display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding-top: 16rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #eee; }
|
||||
.order-time { font-size: 24rpx; color: #999; }
|
||||
.order-amount-wrap { display: flex; flex-direction: row; align-items: center; }
|
||||
.amount-label { font-size: 24rpx; color: #999; margin-right: 8rpx; }
|
||||
.amount-value { font-size: 32rpx; font-weight: bold; color: #FF6B35; }
|
||||
|
||||
.safe-bottom { height: 30rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -57,14 +57,36 @@
|
||||
<text class="label">当前库存</text>
|
||||
<text class="value">{{ currentProduct?.total_stock }}</text>
|
||||
</view>
|
||||
<<<<<<< HEAD
|
||||
<view class="form-item">
|
||||
<text class="label">新库存</text>
|
||||
<input class="input" type="number" v-model="newStock" placeholder="请输入新库存"/>
|
||||
=======
|
||||
|
||||
<view class="adjust-type">
|
||||
<view class="type-btn" :class="{ active: adjustType === 'set' }" @click="adjustType = 'set'">直接设为</view>
|
||||
<view class="type-btn" :class="{ active: adjustType === 'add' }" @click="adjustType = 'add'">增加</view>
|
||||
<view class="type-btn" :class="{ active: adjustType === 'sub' }" @click="adjustType = 'sub'">减少</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">{{ adjustType === 'set' ? '新库存数量' : '调整数值' }}</text>
|
||||
<input class="input" type="number" v-model="newStock" :placeholder="adjustType === 'set' ? '请输入新库存' : '请输入数值'"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">备注 (可选)</text>
|
||||
<input class="input" v-model="stockRemark" placeholder="如:入库、损耗等"/>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel" @click="closeStockModal">取消</view>
|
||||
<<<<<<< HEAD
|
||||
<view class="modal-btn confirm" @click="saveStock">保存</view>
|
||||
=======
|
||||
<view class="modal-btn confirm" @click="saveStock">确认提交</view>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -96,7 +118,13 @@
|
||||
stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 },
|
||||
showStockModal: false,
|
||||
currentProduct: null as ProductType | null,
|
||||
<<<<<<< HEAD
|
||||
newStock: ''
|
||||
=======
|
||||
newStock: '',
|
||||
adjustType: 'set', // 'set', 'add', 'sub'
|
||||
stockRemark: ''
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
},
|
||||
|
||||
@@ -105,6 +133,10 @@
|
||||
},
|
||||
|
||||
onShow() {
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
this.page = 1
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
},
|
||||
@@ -113,11 +145,21 @@
|
||||
async initMerchantId() {
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
<<<<<<< HEAD
|
||||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||||
=======
|
||||
if (session != null && session.user != null) {
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
<<<<<<< HEAD
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
@@ -127,10 +169,34 @@
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
this.products = []
|
||||
=======
|
||||
if (this.loading && this.page === 1) return
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
let query = supa.from('ml_products')
|
||||
.select('id, name, main_image_url, total_stock, warning_stock')
|
||||
.eq('merchant_id', this.merchantId)
|
||||
.order('total_stock', { ascending: true })
|
||||
.page(this.page)
|
||||
.limit(this.limit)
|
||||
|
||||
if (this.currentFilter === 'low') {
|
||||
query = query.lte('total_stock', 10) // 简化处理,实际应关联 warning_stock
|
||||
} else if (this.currentFilter === 'out') {
|
||||
query = query.eq('total_stock', 0)
|
||||
}
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('加载商品失败:', response.error)
|
||||
>>>>>>> local-backup-root-cyj
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
<<<<<<< HEAD
|
||||
let productsData: ProductType[] = []
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
@@ -141,16 +207,38 @@
|
||||
if (this.currentFilter === 'low' && stock > warning) continue
|
||||
if (this.currentFilter === 'out' && stock > 0) continue
|
||||
|
||||
=======
|
||||
if (!rawData) return
|
||||
|
||||
const productsData: ProductType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
>>>>>>> local-backup-root-cyj
|
||||
productsData.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
main_image_url: item.getString('main_image_url') || '',
|
||||
<<<<<<< HEAD
|
||||
total_stock: stock,
|
||||
warning_stock: warning
|
||||
})
|
||||
}
|
||||
|
||||
this.products = productsData
|
||||
=======
|
||||
total_stock: item.getNumber('total_stock') || 0,
|
||||
warning_stock: item.getNumber('warning_stock') || 10
|
||||
} as ProductType)
|
||||
}
|
||||
|
||||
if (this.page === 1) {
|
||||
this.products = productsData
|
||||
} else {
|
||||
this.products = [...this.products, ...productsData]
|
||||
}
|
||||
|
||||
this.hasMore = rawData.length === this.limit
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} catch (e) {
|
||||
console.error('加载失败:', e)
|
||||
} finally {
|
||||
@@ -200,7 +288,13 @@
|
||||
|
||||
editStock(product: ProductType) {
|
||||
this.currentProduct = product
|
||||
<<<<<<< HEAD
|
||||
this.newStock = String(product.total_stock)
|
||||
=======
|
||||
this.newStock = ''
|
||||
this.adjustType = 'set'
|
||||
this.stockRemark = ''
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.showStockModal = true
|
||||
},
|
||||
|
||||
@@ -211,6 +305,7 @@
|
||||
},
|
||||
|
||||
async saveStock() {
|
||||
<<<<<<< HEAD
|
||||
if (!this.newStock || isNaN(parseInt(this.newStock))) {
|
||||
uni.showToast({ title: '请输入有效库存', icon: 'none' })
|
||||
return
|
||||
@@ -218,18 +313,63 @@
|
||||
|
||||
try {
|
||||
const response = await supa.from('ml_products').update({ total_stock: parseInt(this.newStock), updated_at: new Date().toISOString() }).eq('id', this.currentProduct!.id).execute()
|
||||
=======
|
||||
const val = parseInt(this.newStock)
|
||||
if (isNaN(val)) {
|
||||
uni.showToast({ title: '请输入有效数值', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
let finalStock = 0
|
||||
if (this.adjustType === 'set') {
|
||||
finalStock = val
|
||||
} else if (this.adjustType === 'add') {
|
||||
finalStock = (this.currentProduct?.total_stock || 0) + val
|
||||
} else if (this.adjustType === 'sub') {
|
||||
finalStock = (this.currentProduct?.total_stock || 0) - val
|
||||
}
|
||||
|
||||
if (finalStock < 0) {
|
||||
uni.showToast({ title: '最终库存不能小于0', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '更新中...' })
|
||||
|
||||
try {
|
||||
const response = await supa.from('ml_products')
|
||||
.update({
|
||||
total_stock: finalStock,
|
||||
available_stock: finalStock,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.currentProduct!.id)
|
||||
.execute()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
this.closeStockModal()
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
=======
|
||||
uni.showToast({ title: '更新成功', icon: 'success' })
|
||||
this.closeStockModal()
|
||||
this.page = 1
|
||||
this.loadProducts()
|
||||
this.loadStats()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作异常', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
},
|
||||
|
||||
@@ -273,10 +413,17 @@
|
||||
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #1976D2; border-radius: 24rpx; }
|
||||
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
|
||||
<<<<<<< HEAD
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
|
||||
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
||||
.modal-close { font-size: 44rpx; color: #999; }
|
||||
.modal-body { padding: 30rpx; }
|
||||
=======
|
||||
.modal-body { padding: 30rpx; }
|
||||
.adjust-type { display: flex; justify-content: space-between; margin-bottom: 30rpx; }
|
||||
.type-btn { flex: 1; height: 64rpx; line-height: 64rpx; text-align: center; font-size: 24rpx; background-color: #f5f5f5; color: #666; margin: 0 10rpx; border-radius: 32rpx; border: 1rpx solid #eee; }
|
||||
.type-btn.active { background-color: #E3F2FD; color: #007AFF; border-color: #007AFF; }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
.form-item { margin-bottom: 20rpx; }
|
||||
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
|
||||
.form-item .value { font-size: 28rpx; color: #333; }
|
||||
|
||||
521
pages/mall/merchant/members.uvue
Normal file
521
pages/mall/merchant/members.uvue
Normal file
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<view class="members-page">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: activeTab === 0 }" @click="activeTab = 0">等级设置</view>
|
||||
<view class="tab" :class="{ active: activeTab === 1 }" @click="activeTab = 1">客户列表</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="list-container" v-if="activeTab === 0">
|
||||
<view class="section-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">等级配置</text>
|
||||
<text class="add-btn" @click="showAddLevel = true">+ 添加等级</text>
|
||||
</view>
|
||||
<view class="level-list">
|
||||
<view v-for="level in levels" :key="level.id" class="level-item">
|
||||
<view class="level-info">
|
||||
<text class="level-name">{{ level.name }}</text>
|
||||
<text class="level-rate">{{ (level.discount_rate * 10).toFixed(1) }}折</text>
|
||||
</view>
|
||||
<view class="level-actions">
|
||||
<text class="action-edit" @click="editLevel(level)">编辑</text>
|
||||
<text class="action-del" @click="deleteLevel(level.id)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view scroll-y class="list-container" v-if="activeTab === 1">
|
||||
<view class="user-list">
|
||||
<view v-if="users.length === 0" class="empty-tip">暂无注册客户</view>
|
||||
<view v-for="user in users" :key="user.id" class="user-item">
|
||||
<image :src="user.avatar_url || '/static/images/default-avatar.png'" class="user-avatar" />
|
||||
<view class="user-info">
|
||||
<view class="user-title-row">
|
||||
<text class="user-name">{{ user.nickname || user.username || '未设置昵称' }}</text>
|
||||
<view class="user-tier-tag" v-if="user.tier_name">{{ user.tier_name }}</view>
|
||||
</view>
|
||||
<text class="user-email" v-if="user.email">{{ user.email }}</text>
|
||||
<text class="user-phone">{{ user.phone || '未绑定手机' }}</text>
|
||||
</view>
|
||||
<view class="user-actions">
|
||||
<text class="action-set" @click="showSetTier(user)">设置VIP</text>
|
||||
<text class="action-set discount-btn" @click="goToExclusive(user)">专属折扣</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 编辑等级弹窗 -->
|
||||
<view class="modal" v-if="showEditModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">{{ currentLevel.id ? '编辑等级' : '添加等级' }}</view>
|
||||
<view class="form-item">
|
||||
<text class="label">等级名称</text>
|
||||
<input class="input" v-model="currentLevel.name" placeholder="请输入名称" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">折扣率 (0-1)</text>
|
||||
<input class="input" type="digit" v-model="currentLevel.discount_rate" placeholder="如0.85表示85折" />
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<text class="btn cancel" @click="showEditModal = false">取消</text>
|
||||
<text class="btn confirm" @click="saveLevel">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置用户等级弹窗 -->
|
||||
<view class="modal" v-if="showTierModal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-title">设置会员等级</view>
|
||||
<view class="tier-options">
|
||||
<view v-for="level in levels" :key="level.id"
|
||||
class="tier-option"
|
||||
:class="{ selected: selectedTierId === level.id }"
|
||||
@click="selectedTierId = level.id">
|
||||
{{ level.name }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<text class="btn cancel" @click="showTierModal = false">取消</text>
|
||||
<text class="btn confirm" @click="confirmSetTier">确认</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type MemberLevel = {
|
||||
id: string
|
||||
name: string
|
||||
discount_rate: number
|
||||
level_rank: number
|
||||
}
|
||||
|
||||
type UserInfo = {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
nickname: string | null
|
||||
avatar_url: string | null
|
||||
phone: string | null
|
||||
tier_id: string | null
|
||||
tier_name: string | null
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
levels: [] as MemberLevel[],
|
||||
users: [] as UserInfo[],
|
||||
searchKey: '',
|
||||
showEditModal: false,
|
||||
showTierModal: false,
|
||||
showAddLevel: false,
|
||||
currentLevel: {
|
||||
id: '',
|
||||
name: '',
|
||||
discount_rate: 1.0,
|
||||
level_rank: 0
|
||||
} as MemberLevel,
|
||||
currentUser: null as UserInfo | null,
|
||||
selectedTierId: '',
|
||||
merchantId: ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
this.loadLevels()
|
||||
},
|
||||
watch: {
|
||||
activeTab(val: number) {
|
||||
if (val === 1 && this.users.length === 0) {
|
||||
this.loadUsers()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSearch() {
|
||||
console.log('按钮被点击,触发 handleSearch');
|
||||
this.loadUsers();
|
||||
},
|
||||
async loadLevels() {
|
||||
const res = await supa.from('ml_member_levels').select('*').order('level_rank', { ascending: true }).execute()
|
||||
if (res.data != null) {
|
||||
this.levels = (res.data as any[]).map((item: any) => {
|
||||
const obj = item as UTSJSONObject
|
||||
return {
|
||||
id: obj.getString('id') || '',
|
||||
name: obj.getString('name') || '',
|
||||
discount_rate: obj.getNumber('discount_rate') || 1.0,
|
||||
level_rank: obj.getNumber('level_rank') || 0
|
||||
} as MemberLevel
|
||||
})
|
||||
}
|
||||
},
|
||||
async loadUsers() {
|
||||
console.log('--- 启动 ak_users 全量加载 (不带 limit 限制) ---');
|
||||
try {
|
||||
// 1. 移除 limit 限制或设置极大值,确保读到全部数据
|
||||
// 同时通过 count 参数确认数据库到底给了多少条
|
||||
const res = await supa.from('ak_users')
|
||||
.select('id, username, nickname, email, phone, avatar_url, role', { count: 'exact' })
|
||||
.execute()
|
||||
|
||||
if (res.error != null) {
|
||||
console.error('API请求错误:', res.error);
|
||||
return
|
||||
}
|
||||
|
||||
if (res.data != null) {
|
||||
let rawData = res.data as any[]
|
||||
console.log('数据库查询成功。总行数:', res.count, ' 返回行数:', rawData.length);
|
||||
|
||||
// 增加一个调试点:统计一下所有数据的 role 分布,看看到底有多少个 role 是 customer
|
||||
let customerCount = 0;
|
||||
rawData.forEach((item: any) => {
|
||||
const r = String((item as UTSJSONObject)['role'] || '').trim().toLowerCase();
|
||||
if (r == 'customer') customerCount++;
|
||||
});
|
||||
console.log('内存扫描结果: 含有 customer 字样的记录总数:', customerCount);
|
||||
|
||||
// 2. 获取会员等级地图
|
||||
let profileMap = new Map<string, string>()
|
||||
try {
|
||||
const profileRes = await supa.from('ml_user_profiles').select('*').limit(1).execute()
|
||||
if (profileRes.data != null && (profileRes.data as any[]).length > 0) {
|
||||
console.log('【数据库结构探查】ml_user_profiles 第一条数据:', JSON.stringify(profileRes.data[0]))
|
||||
}
|
||||
|
||||
const profileAllRes = await supa.from('ml_user_profiles').select('*').execute()
|
||||
console.log('【数据调试】ml_user_profiles 返回行数:', (profileAllRes.data as any[] || []).length)
|
||||
if (profileAllRes.data != null) {
|
||||
const profileData = profileAllRes.data as any[]
|
||||
profileData.forEach((p: any) => {
|
||||
if (p != null) {
|
||||
const po = p as UTSJSONObject
|
||||
const uid = String(po['user_id'] || '').trim().toLowerCase()
|
||||
|
||||
const keys = Object.keys(p as object)
|
||||
let foundTid = ''
|
||||
|
||||
if (keys.includes('tier_id')) {
|
||||
foundTid = String(po['tier_id'] || '')
|
||||
} else if (keys.includes('level_id')) {
|
||||
foundTid = String(po['level_id'] || '')
|
||||
} else if (keys.includes('rank_id')) {
|
||||
foundTid = String(po['rank_id'] || '')
|
||||
} else {
|
||||
const autoKey = keys.find(k => k.includes('level') || k.includes('tier'))
|
||||
if (autoKey != null) {
|
||||
foundTid = String(po[autoKey] || '')
|
||||
}
|
||||
}
|
||||
|
||||
foundTid = foundTid.trim().toLowerCase()
|
||||
|
||||
if (uid != '' && foundTid != '' && foundTid != 'null') {
|
||||
console.log(`【映射匹配成功】UID: ${uid} -> TID: ${foundTid}`)
|
||||
profileMap.set(uid, foundTid)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('查询 profile 报错:', e)
|
||||
}
|
||||
|
||||
// 3. 【极致完善筛选逻辑】
|
||||
this.users = rawData.map((u: any): UserInfo | null => {
|
||||
if (u == null) return null
|
||||
const uo = u as UTSJSONObject
|
||||
|
||||
let rawRole = String(uo['role'] || '');
|
||||
const role = rawRole.trim().toLowerCase();
|
||||
|
||||
// 严格筛选:仅保留角色为 consumer 的真实消费者
|
||||
if (role != 'consumer') return null
|
||||
|
||||
const uid = String(uo['id'] || uo['user_id'] || '').trim().toLowerCase()
|
||||
const username = String(uo['username'] || '')
|
||||
// 这里是关键:profileMap 里的 key 是小写的 uid,tid 也是小写的
|
||||
const tid = profileMap.get(uid) || ''
|
||||
|
||||
let tname = ''
|
||||
if (tid != '') {
|
||||
// 1. 严格 ID 匹配
|
||||
const level = this.levels.find(l => (l.id || '').trim().toLowerCase() === tid)
|
||||
if (level != null) {
|
||||
tname = level.name
|
||||
} else {
|
||||
// 2. 备用:如果 ID 匹配不到,尝试看这个 tid 是不是等级的序号(level_rank)
|
||||
const levelByRank = this.levels.find(l => String(l.level_rank) === tid)
|
||||
if (levelByRank != null) tname = levelByRank.name
|
||||
}
|
||||
}
|
||||
|
||||
if (tid != '') {
|
||||
console.log(`【渲染行检查】用户:${username}, ID:${uid}, 等级TID(DB):${tid}, 匹配结果:${tname}`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: uid,
|
||||
username: username,
|
||||
email: String(uo['email'] || ''),
|
||||
nickname: String(uo['nickname'] || uo['username'] || '未设置昵称'),
|
||||
avatar_url: String(uo['avatar_url'] || uo['head_img_url'] || ''),
|
||||
phone: String(uo['phone'] || ''),
|
||||
tier_id: tid,
|
||||
tier_name: tname
|
||||
} as UserInfo
|
||||
}).filter((u: any): boolean => u != null) as UserInfo[]
|
||||
|
||||
// 【核心优化】自动将已经设置了 VIP 的人排在列表最顶端,方便一眼看到
|
||||
this.users.sort((a, b) => {
|
||||
const nameA = (a.tier_name || '').trim()
|
||||
const nameB = (b.tier_name || '').trim()
|
||||
if (nameA != '' && nameB == '') return -1
|
||||
if (nameA == '' && nameB != '') return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
console.log('【最终渲染检查】当前用户列表长度:', this.users.length);
|
||||
// 强制触发一次 UI 重绘
|
||||
this.$forceUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载逻辑崩溃:', e);
|
||||
}
|
||||
},
|
||||
processUserData(rawData: any[]) {
|
||||
if (rawData != null && Array.isArray(rawData)) {
|
||||
this.users = rawData.map((item: any) => {
|
||||
const istr = JSON.stringify(item)
|
||||
const obj = JSON.parse(istr) as UTSJSONObject
|
||||
|
||||
const tierId = obj.getString('tier_id')
|
||||
let tierName = ''
|
||||
if (tierId != null && tierId != '') {
|
||||
const level = this.levels.find(l => l.id === tierId)
|
||||
if (level != null) tierName = level.name
|
||||
}
|
||||
|
||||
return {
|
||||
id: obj.getString('id') || obj.getString('user_id') || '',
|
||||
nickname: obj.getString('nickname') || '未设置昵称',
|
||||
avatar_url: obj.getString('avatar_url'),
|
||||
phone: obj.getString('phone_number') || '无手机号',
|
||||
tier_id: tierId,
|
||||
tier_name: tierName
|
||||
} as UserInfo
|
||||
})
|
||||
} else {
|
||||
this.users = []
|
||||
}
|
||||
},
|
||||
editLevel(level: MemberLevel) {
|
||||
this.currentLevel = JSON.parse(JSON.stringify(level)) as MemberLevel
|
||||
this.showEditModal = true
|
||||
},
|
||||
async saveLevel() {
|
||||
if (!this.currentLevel.name) return
|
||||
|
||||
// 构造提交数据,确保类型正确
|
||||
const discount = parseFloat(this.currentLevel.discount_rate.toString())
|
||||
const rank = parseInt(this.currentLevel.level_rank.toString())
|
||||
|
||||
const data = {
|
||||
name: this.currentLevel.name,
|
||||
discount_rate: isNaN(discount) ? 1.0 : discount,
|
||||
level_rank: isNaN(rank) ? 0 : rank
|
||||
}
|
||||
|
||||
let res: any
|
||||
if (this.currentLevel.id) {
|
||||
res = await supa.from('ml_member_levels').update(data).eq('id', this.currentLevel.id).execute()
|
||||
} else {
|
||||
res = await supa.from('ml_member_levels').insert(data).execute()
|
||||
}
|
||||
|
||||
if (res.error == null) {
|
||||
uni.showToast({ title: '保存成功' })
|
||||
this.showEditModal = false
|
||||
this.loadLevels()
|
||||
} else {
|
||||
uni.showModal({ title: '保存失败', content: JSON.stringify(res.error) })
|
||||
}
|
||||
},
|
||||
async deleteLevel(id: string) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '此操作将同步删除关联用户的等级,是否继续?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// 先将该等级下的用户 tier_id 清空,防止外键约束或逻辑残留
|
||||
await supa.from('ml_user_profiles').update({ tier_id: null }).eq('tier_id', id).execute()
|
||||
const delRes = await supa.from('ml_member_levels').delete().eq('id', id).execute()
|
||||
if (delRes.error == null) {
|
||||
this.loadLevels()
|
||||
this.loadUsers()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
goToExclusive(user: any) {
|
||||
const name = user['nickname'] || user['username'] || user['phone'] || '客户'
|
||||
const uId = user['id']
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/merchant/exclusive-discounts?user_id=' + uId + '&user_name=' + encodeURIComponent(name as string)
|
||||
})
|
||||
},
|
||||
showSetTier(user: UserInfo) {
|
||||
this.currentUser = user
|
||||
this.selectedTierId = user.tier_id || ''
|
||||
this.showTierModal = true
|
||||
},
|
||||
async confirmSetTier() {
|
||||
if (this.currentUser == null) return
|
||||
|
||||
uni.showLoading({ title: '确认中...' })
|
||||
try {
|
||||
const userObj = this.currentUser as UserInfo
|
||||
const userId = userObj.id
|
||||
|
||||
// 1. 获取所有字段名(不依赖第一行数据,而是通过 RPC 或直接查询)
|
||||
// 为确保万无一失,我们直接同时尝试写入 tier_id 和 level_id
|
||||
const probeRes = await supa.from('ml_user_profiles').select('*').limit(1).execute()
|
||||
|
||||
let finalObj = {
|
||||
'user_id': userId,
|
||||
'updated_at': new Date().toISOString()
|
||||
} as UTSJSONObject
|
||||
|
||||
// 智能探测字段
|
||||
if (probeRes.data != null && (probeRes.data as any[]).length > 0) {
|
||||
const keys = Object.keys(probeRes.data![0] as object)
|
||||
console.log('【数据库字段探测】:', JSON.stringify(keys))
|
||||
|
||||
if (keys.includes('tier_id')) {
|
||||
finalObj['tier_id'] = this.selectedTierId
|
||||
} else if (keys.includes('level_id')) {
|
||||
finalObj['level_id'] = this.selectedTierId
|
||||
} else if (keys.includes('rank_id')) {
|
||||
finalObj['rank_id'] = this.selectedTierId
|
||||
} else {
|
||||
// 万能匹配
|
||||
const anyLevelKey = keys.find(k => k.includes('level') || k.includes('tier'))
|
||||
if (anyLevelKey != null) finalObj[anyLevelKey] = this.selectedTierId
|
||||
}
|
||||
} else {
|
||||
// 如果表完全是空的,默认尝试 tier_id
|
||||
finalObj['tier_id'] = this.selectedTierId
|
||||
}
|
||||
|
||||
// 2. 使用 UPSERT 逻辑(存在就更新,没有就插入)
|
||||
// Supabase 的 upsert 需要定义唯一约束,这里我们根据 user_id 处理
|
||||
const checkExist = await supa.from('ml_user_profiles').select('id').eq('user_id', userId).execute()
|
||||
|
||||
let finalRes: any = null
|
||||
if (checkExist.data != null && (checkExist.data as any[]).length > 0) {
|
||||
// 注意:更新时不需要带上 user_id 字段
|
||||
const updateObj = JSON.parse(JSON.stringify(finalObj)) as UTSJSONObject
|
||||
delete updateObj['user_id']
|
||||
finalRes = await supa.from('ml_user_profiles').update(updateObj).eq('user_id', userId).execute()
|
||||
} else {
|
||||
finalRes = await supa.from('ml_user_profiles').insert(finalObj).execute()
|
||||
}
|
||||
|
||||
if (finalRes != null && finalRes.error != null) {
|
||||
throw new Error('保存失败: ' + finalRes.error!.message)
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '设置成功', icon: 'success' })
|
||||
this.showTierModal = false
|
||||
|
||||
// 立即重新获取该用户的 profile 确认
|
||||
setTimeout(() => {
|
||||
this.loadUsers()
|
||||
}, 300)
|
||||
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
title: '设置异常',
|
||||
content: String(e),
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.members-page { background-color: #f8f9fa; min-height: 100vh; }
|
||||
.tabs { display: flex; background: #fff; padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
|
||||
.tab { flex: 1; text-align: center; font-size: 28rpx; color: #666; }
|
||||
.tab.active { color: #007AFF; font-weight: bold; }
|
||||
|
||||
.list-container { padding: 20rpx; }
|
||||
.section-card { background: #fff; border-radius: 16rpx; padding: 30rpx; }
|
||||
.card-header { display: flex; justify-content: space-between; margin-bottom: 30rpx; align-items: center; }
|
||||
.card-title { font-size: 32rpx; font-weight: bold; }
|
||||
.add-btn { color: #007AFF; font-size: 26rpx; }
|
||||
|
||||
.level-item { display: flex; justify-content: space-between; border-bottom: 1rpx solid #f5f5f5; padding: 20rpx 0; }
|
||||
.level-name { font-size: 30rpx; display: block; }
|
||||
.level-rate { font-size: 24rpx; color: #FF9500; }
|
||||
.level-actions .text { font-size: 24rpx; margin-left: 20rpx; }
|
||||
.action-edit { color: #007AFF; margin-right: 20rpx; }
|
||||
.action-del { color: #FF3B30; }
|
||||
|
||||
.search-bar { display: flex; padding: 20rpx; background: #fff; margin-bottom: 20rpx; border-radius: 12rpx; }
|
||||
.search-input { flex: 1; height: 72rpx; background: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 26rpx; }
|
||||
.search-btn { margin-left: 20rpx; color: #007AFF; line-height: 72rpx; font-size: 28rpx; }
|
||||
|
||||
.user-item { display: flex; align-items: center; background: #fff; padding: 24rpx; border-radius: 16rpx; margin-bottom: 20rpx; }
|
||||
.user-avatar { width: 90rpx; height: 90rpx; border-radius: 45rpx; background: #eee; }
|
||||
.user-info { flex: 1; margin-left: 24rpx; }
|
||||
.user-name { font-size: 30rpx; font-weight: bold; display: block; }
|
||||
.user-phone { font-size: 24rpx; color: #999; }
|
||||
.user-tier-tag { display: inline-block; background: #FF9500; color: #fff; font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 4rpx; margin-top: 8rpx; }
|
||||
.action-set {
|
||||
font-size: 24rpx;
|
||||
color: #2196F3;
|
||||
padding: 10rpx 20rpx;
|
||||
background-color: #E3F2FD;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.action-set.discount-btn {
|
||||
color: #FF9800;
|
||||
background-color: #FFF8E1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
||||
.modal-content { background: #fff; width: 600rpx; border-radius: 24rpx; padding: 40rpx; }
|
||||
.modal-title { text-align: center; font-size: 34rpx; font-weight: bold; margin-bottom: 30rpx; }
|
||||
.form-item { margin-bottom: 30rpx; }
|
||||
.label { font-size: 26rpx; color: #666; margin-bottom: 12rpx; display: block; }
|
||||
.input { background: #f5f5f5; height: 80rpx; border-radius: 12rpx; padding: 0 20rpx; font-size: 28rpx; }
|
||||
.modal-btns { display: flex; justify-content: flex-end; margin-top: 40rpx; }
|
||||
.btn { padding: 16rpx 40rpx; border-radius: 12rpx; font-size: 28rpx; margin-left: 20rpx; }
|
||||
.btn.cancel { background: #eee; color: #666; }
|
||||
.btn.confirm { background: #007AFF; color: #fff; }
|
||||
|
||||
.tier-options { display: flex; flex-wrap: wrap; }
|
||||
.tier-option { padding: 16rpx 30rpx; background: #f5f5f5; margin: 10rpx; border-radius: 36rpx; font-size: 26rpx; }
|
||||
.tier-option.selected { background: #007AFF; color: #fff; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- 商家端 - 消息中心页面 -->
|
||||
<template>
|
||||
<view class="messages-page">
|
||||
<<<<<<< HEAD
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: currentTab === 'chat' }" @click="switchTab('chat')">会话列表</view>
|
||||
<view class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">全部消息</view>
|
||||
@@ -40,6 +41,48 @@
|
||||
<view v-if="!msg.is_read" class="unread-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
=======
|
||||
<view class="header">
|
||||
<text class="header-title">消息</text>
|
||||
<text class="header-subtitle">与客户的聊天记录</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="messages-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="loading && conversations.length === 0" class="loading-container">
|
||||
<text class="loading-icon">⏳</text>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="conversations.length === 0" class="empty-container">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-text">暂无会话</text>
|
||||
<text class="empty-hint">有客户消息时会在这里显示</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="conv-list">
|
||||
<view v-for="conv in conversations" :key="conv.sessionId" class="conversation-card" @click="goToChat(conv)">
|
||||
<view class="conv-avatar-wrap">
|
||||
<image class="conv-avatar" :src="conv.avatar || '/static/images/default-avatar.png'" mode="aspectFill" />
|
||||
<view v-if="conv.unread > 0" class="online-dot"></view>
|
||||
</view>
|
||||
<view class="conv-info">
|
||||
<view class="conv-row">
|
||||
<text class="conv-name">{{ conv.name }}</text>
|
||||
<text class="conv-time">{{ conv.lastTime }}</text>
|
||||
</view>
|
||||
<view class="conv-bottom">
|
||||
<text class="conv-preview">{{ conv.lastMessage }}</text>
|
||||
<view v-if="conv.unread > 0" class="unread-badge">
|
||||
<text class="unread-num">{{ conv.unread > 99 ? '99+' : conv.unread }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="conv-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safe-bottom"></view>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -65,6 +108,10 @@
|
||||
avatar: string
|
||||
lastMessage: string
|
||||
lastTime: string
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
lastTimeRaw: string
|
||||
>>>>>>> local-backup-root-cyj
|
||||
unread: number
|
||||
userId: string
|
||||
}
|
||||
@@ -72,8 +119,11 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
<<<<<<< HEAD
|
||||
currentTab: 'chat',
|
||||
messages: [] as MessageType[],
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
conversations: [] as ConversationType[],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
@@ -111,17 +161,24 @@
|
||||
const response = await query.execute()
|
||||
|
||||
if (response.error != null || !response.data) {
|
||||
<<<<<<< HEAD
|
||||
this.messages = []
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.conversations = []
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
<<<<<<< HEAD
|
||||
const messagesData: MessageType[] = []
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
const sessionMap = new Map<string, ConversationType>()
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
<<<<<<< HEAD
|
||||
const msg: MessageType = {
|
||||
id: item.getString('id') || '',
|
||||
session_id: item.getString('session_id') || '',
|
||||
@@ -145,20 +202,61 @@
|
||||
avatar: '',
|
||||
lastMessage: msg.content,
|
||||
lastTime: this.formatTime(msg.created_at),
|
||||
=======
|
||||
const isFromUser = item.getBoolean('is_from_user') || false
|
||||
const senderId = item.getString('sender_id') || ''
|
||||
const receiverId = item.getString('receiver_id') || ''
|
||||
|
||||
// is_from_user=true 表示消息来自用户,sender_id是用户ID
|
||||
// is_from_user=false 表示消息来自商家,receiver_id是用户ID
|
||||
const otherUserId = isFromUser ? senderId : receiverId
|
||||
|
||||
// 只用 otherUserId 分组,确保每个客户只有一个会话
|
||||
if (otherUserId === '' || otherUserId === this.merchantId) continue
|
||||
|
||||
const isRead = item.getBoolean('is_read') || false
|
||||
const content = item.getString('content') || ''
|
||||
const createdAt = item.getString('created_at') || ''
|
||||
const sessionId = item.getString('session_id') || otherUserId
|
||||
|
||||
if (!sessionMap.has(otherUserId)) {
|
||||
sessionMap.set(otherUserId, {
|
||||
sessionId: sessionId,
|
||||
name: '客户',
|
||||
avatar: '',
|
||||
lastMessage: content,
|
||||
lastTime: this.formatTime(createdAt),
|
||||
lastTimeRaw: createdAt,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
unread: 0,
|
||||
userId: otherUserId
|
||||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
const conv = sessionMap.get(sessionId)!
|
||||
conv.lastMessage = msg.content
|
||||
conv.lastTime = this.formatTime(msg.created_at)
|
||||
if (!msg.is_read && !msg.is_from_user) {
|
||||
=======
|
||||
const conv = sessionMap.get(otherUserId)!
|
||||
// 更新最后一条消息(按时间最新的)
|
||||
if (createdAt > conv.lastTimeRaw) {
|
||||
conv.lastMessage = content
|
||||
conv.lastTime = this.formatTime(createdAt)
|
||||
conv.lastTimeRaw = createdAt
|
||||
}
|
||||
// 未读消息:消息来自用户且未读
|
||||
if (!isRead && isFromUser) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
conv.unread++
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
this.messages = messagesData
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread)
|
||||
|
||||
} catch (e) {
|
||||
@@ -175,15 +273,19 @@
|
||||
})
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
switchTab(tab: string) {
|
||||
this.currentTab = tab
|
||||
},
|
||||
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
onRefresh() {
|
||||
this.refreshing = true
|
||||
this.loadMessages()
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
viewMessage(msg: MessageType) {
|
||||
if (!msg.is_read) {
|
||||
supa.from('ml_chat_messages').update({ is_read: true }).eq('id', msg.id).execute()
|
||||
@@ -196,6 +298,8 @@
|
||||
})
|
||||
},
|
||||
|
||||
=======
|
||||
>>>>>>> local-backup-root-cyj
|
||||
formatTime(timeStr: string): string {
|
||||
if (!timeStr) return ''
|
||||
const date = new Date(timeStr)
|
||||
@@ -215,6 +319,7 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<<<<<<< HEAD
|
||||
.messages-page { background-color: #f5f5f5; min-height: 100vh; }
|
||||
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
|
||||
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
|
||||
@@ -244,4 +349,39 @@
|
||||
.message-time { font-size: 22rpx; color: #999; }
|
||||
.message-text { font-size: 26rpx; color: #666; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.unread-dot { position: absolute; top: 30rpx; right: 30rpx; width: 16rpx; height: 16rpx; background-color: #FF3B30; border-radius: 50%; }
|
||||
=======
|
||||
.messages-page { background-color: #f5f7fa; min-height: 100vh; display: flex; flex-direction: column; }
|
||||
|
||||
.header { background-color: #fff; padding-top: 60rpx; padding-bottom: 24rpx; padding-left: 30rpx; padding-right: 30rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #eee; }
|
||||
.header-title { font-size: 44rpx; font-weight: bold; color: #333; display: block; margin-bottom: 8rpx; }
|
||||
.header-subtitle { font-size: 26rpx; color: #999; }
|
||||
|
||||
.messages-list { flex: 1; padding: 20rpx; }
|
||||
|
||||
.loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 100rpx; padding-bottom: 100rpx; }
|
||||
.loading-icon { font-size: 60rpx; margin-bottom: 20rpx; }
|
||||
.loading-text { font-size: 28rpx; color: #999; }
|
||||
|
||||
.empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 100rpx; padding-bottom: 100rpx; }
|
||||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||||
.empty-text { font-size: 32rpx; color: #666; margin-bottom: 10rpx; }
|
||||
.empty-hint { font-size: 26rpx; color: #999; }
|
||||
|
||||
.conv-list { display: flex; flex-direction: column; }
|
||||
.conversation-card { display: flex; flex-direction: row; align-items: center; background-color: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
||||
.conv-avatar-wrap { position: relative; margin-right: 20rpx; }
|
||||
.conv-avatar { width: 100rpx; height: 100rpx; border-radius: 50rpx; background-color: #e0e0e0; }
|
||||
.online-dot { position: absolute; bottom: 4rpx; right: 4rpx; width: 24rpx; height: 24rpx; background-color: #4CAF50; border-radius: 12rpx; border-width: 3rpx; border-style: solid; border-color: #fff; }
|
||||
.conv-info { flex: 1; }
|
||||
.conv-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
|
||||
.conv-name { font-size: 32rpx; color: #333; font-weight: bold; }
|
||||
.conv-time { font-size: 24rpx; color: #999; }
|
||||
.conv-bottom { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||
.conv-preview { font-size: 28rpx; color: #666; flex: 1; margin-right: 16rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.unread-badge { min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding-left: 10rpx; padding-right: 10rpx; }
|
||||
.unread-num { font-size: 22rpx; color: #fff; font-weight: bold; }
|
||||
.conv-arrow { font-size: 40rpx; color: #ccc; margin-left: 10rpx; }
|
||||
|
||||
.safe-bottom { height: 30rpx; }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端 - 订单详情页面 -->
|
||||
=======
|
||||
<!-- 商家端 - 订单详情页面 -->
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<template>
|
||||
<view class="order-detail-page">
|
||||
<!-- 订单状态头部 -->
|
||||
@@ -114,20 +118,32 @@
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<view
|
||||
<<<<<<< HEAD
|
||||
v-if="order.order_status === 1"
|
||||
=======
|
||||
v-if="order.order_status === 2"
|
||||
>>>>>>> local-backup-root-cyj
|
||||
class="action-btn primary"
|
||||
@click="shipOrder"
|
||||
>
|
||||
去发货
|
||||
</view>
|
||||
<view
|
||||
<<<<<<< HEAD
|
||||
v-if="order.order_status === 2"
|
||||
=======
|
||||
v-if="order.order_status === 3"
|
||||
>>>>>>> local-backup-root-cyj
|
||||
class="action-btn primary"
|
||||
@click="viewLogistics"
|
||||
>
|
||||
查看物流
|
||||
</view>
|
||||
<<<<<<< HEAD
|
||||
<view
|
||||
=======
|
||||
<view
|
||||
>>>>>>> local-backup-root-cyj
|
||||
v-if="order.order_status === 3"
|
||||
class="action-btn primary"
|
||||
@click="confirmDelivery"
|
||||
@@ -163,7 +179,11 @@
|
||||
@change="onLogisticsChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
<<<<<<< HEAD
|
||||
{{ selectedLogistics.name || '请选择物流公司' }}
|
||||
=======
|
||||
{{ selectedLogistics?.name || '请选择物流公司' }}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
@@ -240,8 +260,20 @@
|
||||
updated_at: '',
|
||||
items: [] as OrderItemType[]
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
addressData: {} as AddressType,
|
||||
|
||||
=======
|
||||
addressData: {
|
||||
recipient_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail_address: ''
|
||||
} as AddressType,
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
showShipModal: false,
|
||||
logisticsCompanies: [
|
||||
{ name: '顺丰速运', code: 'SF' },
|
||||
@@ -252,27 +284,51 @@
|
||||
{ name: 'EMS', code: 'EMS' },
|
||||
{ name: '京东物流', code: 'JD' }
|
||||
] as LogisticsType[],
|
||||
<<<<<<< HEAD
|
||||
selectedLogistics: {} as LogisticsType,
|
||||
=======
|
||||
selectedLogistics: { name: '', code: '' } as LogisticsType,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
trackingNumber: ''
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
onLoad(options: any) {
|
||||
const id = options.id as string
|
||||
if (id) {
|
||||
=======
|
||||
onLoad(options: any) { console.log('--- DEBUG ON LOAD ---', options)
|
||||
let id = ''
|
||||
if (options['id'] != null) {
|
||||
id = options['id'] as string
|
||||
} else if (options.id != null) {
|
||||
id = options.id as string
|
||||
}
|
||||
|
||||
if (id !== '') {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
this.orderId = id
|
||||
this.loadOrderDetail()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
<<<<<<< HEAD
|
||||
async loadOrderDetail() {
|
||||
try {
|
||||
=======
|
||||
async loadOrderDetail() { console.log('--- DEBUG LOAD ORDER DETAIL ---', this.orderId); try {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
<<<<<<< HEAD
|
||||
order_items!inner (
|
||||
=======
|
||||
ml_order_items (
|
||||
>>>>>>> local-backup-root-cyj
|
||||
id,
|
||||
order_id,
|
||||
product_id,
|
||||
@@ -289,12 +345,17 @@
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
console.error('获取订单详情失败:', response.error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
const rawData = response.data as UTSJSONObject
|
||||
if (rawData == null) return
|
||||
|
||||
@@ -321,11 +382,47 @@
|
||||
}
|
||||
|
||||
const itemsObj = rawData.get('order_items')
|
||||
=======
|
||||
console.log('--- DEBUG RAW ORDER DATA ---', response.data); let realData = response.data;
|
||||
let isArrLike = false;
|
||||
if (response.data != null && (response.data as any)['0'] != null) {
|
||||
realData = (response.data as any)['0'];
|
||||
isArrLike = true;
|
||||
}
|
||||
console.log('--- EXTRACTED realData ---', isArrLike);
|
||||
const rawData = realData as UTSJSONObject
|
||||
if (rawData == null) return
|
||||
|
||||
this.order = {
|
||||
id: String(rawData['id'] ?? '') || '',
|
||||
order_no: String(rawData['order_no'] ?? '') || '',
|
||||
user_id: String(rawData['user_id'] ?? '') || '',
|
||||
merchant_id: String(rawData['merchant_id'] ?? '') || '',
|
||||
order_status: Number(rawData['order_status'] ?? 0) || 1,
|
||||
total_amount: Number(rawData['total_amount'] ?? 0) || 0,
|
||||
product_amount: Number(rawData['product_amount'] ?? 0) || 0,
|
||||
shipping_fee: Number(rawData['shipping_fee'] ?? 0) || 0,
|
||||
discount_amount: Number(rawData['discount_amount'] ?? 0) || 0,
|
||||
paid_amount: Number(rawData['paid_amount'] ?? 0) || 0,
|
||||
shipping_address: String(rawData['shipping_address'] ?? '') || '{}',
|
||||
remark: String(rawData['remark'] ?? '') || '',
|
||||
shipping_company: String(rawData['carrier_name'] ?? rawData['shipping_company'] ?? '') || '',
|
||||
tracking_number: String(rawData['tracking_no'] ?? rawData['tracking_number'] ?? '') || '',
|
||||
paid_at: String(rawData['paid_at'] ?? '') || '',
|
||||
shipped_at: String(rawData['shipped_at'] ?? '') || '',
|
||||
created_at: String(rawData['created_at'] ?? '') || '',
|
||||
updated_at: String(rawData['updated_at'] ?? '') || '',
|
||||
items: []
|
||||
}
|
||||
|
||||
const itemsObj = rawData['ml_order_items']
|
||||
>>>>>>> local-backup-root-cyj
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArray = itemsObj as any[]
|
||||
for (let i = 0; i < itemsArray.length; i++) {
|
||||
const orderItem = itemsArray[i] as UTSJSONObject
|
||||
this.order.items.push({
|
||||
<<<<<<< HEAD
|
||||
id: orderItem.getString('id') || '',
|
||||
order_id: orderItem.getString('order_id') || '',
|
||||
product_id: orderItem.getString('product_id') || '',
|
||||
@@ -335,6 +432,17 @@
|
||||
price: orderItem.getNumber('price') || 0,
|
||||
quantity: orderItem.getNumber('quantity') || 0,
|
||||
image_url: orderItem.getString('image_url') || '',
|
||||
=======
|
||||
id: String(orderItem['id'] ?? '') || '',
|
||||
order_id: String(orderItem['order_id'] ?? '') || '',
|
||||
product_id: String(orderItem['product_id'] ?? '') || '',
|
||||
sku_id: String(orderItem['sku_id'] ?? '') || '',
|
||||
product_name: String(orderItem['product_name'] ?? '') || '',
|
||||
sku_name: String(orderItem['sku_name'] ?? '') || '',
|
||||
price: Number(orderItem['price'] ?? 0) || 0,
|
||||
quantity: Number(orderItem['quantity'] ?? 0) || 0,
|
||||
image_url: String(orderItem['image_url'] ?? '') || '',
|
||||
>>>>>>> local-backup-root-cyj
|
||||
sku_snapshot: ''
|
||||
} as OrderItemType)
|
||||
}
|
||||
@@ -442,7 +550,11 @@
|
||||
},
|
||||
|
||||
async confirmShip() {
|
||||
<<<<<<< HEAD
|
||||
if (!this.selectedLogistics.name) {
|
||||
=======
|
||||
if (this.selectedLogistics == null || !this.selectedLogistics?.name) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
uni.showToast({ title: '请选择物流公司', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -456,25 +568,39 @@
|
||||
.from('ml_orders')
|
||||
.update({
|
||||
order_status: 3,
|
||||
<<<<<<< HEAD
|
||||
shipping_company: this.selectedLogistics.name,
|
||||
tracking_number: this.trackingNumber,
|
||||
=======
|
||||
shipping_status: 2,
|
||||
carrier_name: this.selectedLogistics?.name, tracking_no: this.trackingNumber,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
shipped_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
let msg = response.error?.message ?? (response.data != null ? JSON.stringify(response.data) : '请检查网络或登录状态'); uni.showToast({ title: '发货被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
|
||||
>>>>>>> local-backup-root-cyj
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({ title: '发货成功', icon: 'success' })
|
||||
this.closeShipModal()
|
||||
this.loadOrderDetail()
|
||||
<<<<<<< HEAD
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
}
|
||||
=======
|
||||
} catch (e) { uni.showToast({ title: '发货发生异常', icon: 'none' }); console.error(e) }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
},
|
||||
|
||||
viewLogistics() {
|
||||
@@ -501,7 +627,11 @@
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -529,7 +659,11 @@
|
||||
.eq('id', this.order.id)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -841,7 +975,11 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
<<<<<<< HEAD
|
||||
z-index: 1000;
|
||||
=======
|
||||
z-index: 99;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -849,6 +987,11 @@
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -923,3 +1066,23 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端 - 订单管理页面 -->
|
||||
=======
|
||||
<!-- 商家端 - 订单管理页面 -->
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<template>
|
||||
<view class="orders-page">
|
||||
<!-- 标签页切换 -->
|
||||
@@ -96,14 +100,22 @@
|
||||
</view>
|
||||
<view class="order-actions">
|
||||
<view
|
||||
<<<<<<< HEAD
|
||||
v-if="order.order_status === 1"
|
||||
=======
|
||||
v-if="order.order_status === 2"
|
||||
>>>>>>> local-backup-root-cyj
|
||||
class="action-btn primary"
|
||||
@click.stop="shipOrder(order)"
|
||||
>
|
||||
发货
|
||||
</view>
|
||||
<view
|
||||
<<<<<<< HEAD
|
||||
v-if="order.order_status === 2"
|
||||
=======
|
||||
v-if="order.order_status === 3"
|
||||
>>>>>>> local-backup-root-cyj
|
||||
class="action-btn info"
|
||||
@click.stop="viewLogistics(order)"
|
||||
>
|
||||
@@ -147,7 +159,11 @@
|
||||
@change="onLogisticsChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
<<<<<<< HEAD
|
||||
{{ selectedLogistics.name || '请选择物流公司' }}
|
||||
=======
|
||||
{{ selectedLogistics?.name || '请选择物流公司' }}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
@@ -222,7 +238,11 @@
|
||||
{ name: '待发货', status: 2, count: 0 },
|
||||
{ name: '待收货', status: 3, count: 0 },
|
||||
{ name: '已完成', status: 4, count: 0 },
|
||||
<<<<<<< HEAD
|
||||
{ name: '退款', status: 0, count: 0 }
|
||||
=======
|
||||
{ name: '退款', status: 6, count: 0 }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
] as TabType[],
|
||||
currentTab: -2,
|
||||
searchKeyword: '',
|
||||
@@ -257,7 +277,11 @@
|
||||
const statusMap: Record<string, number> = {
|
||||
'pending': 1,
|
||||
'shipped': 3,
|
||||
<<<<<<< HEAD
|
||||
'refund': 0,
|
||||
=======
|
||||
'refund': 6,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
'completed': 4
|
||||
}
|
||||
this.currentTab = statusMap[type] ?? -2
|
||||
@@ -301,7 +325,11 @@
|
||||
.from('ml_orders')
|
||||
.select(`
|
||||
*,
|
||||
<<<<<<< HEAD
|
||||
order_items!inner (
|
||||
=======
|
||||
order_items (
|
||||
>>>>>>> local-backup-root-cyj
|
||||
id,
|
||||
order_id,
|
||||
product_id,
|
||||
@@ -320,8 +348,14 @@
|
||||
.limit(this.limit)
|
||||
|
||||
if (this.currentTab !== -2) {
|
||||
<<<<<<< HEAD
|
||||
if (this.currentTab === 0) {
|
||||
query = query.eq('order_status', 0)
|
||||
=======
|
||||
if (this.currentTab === 6) {
|
||||
// 退款状态同时查询 0 和 6
|
||||
query = query.in('order_status', [0, 6])
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} else {
|
||||
query = query.eq('order_status', this.currentTab)
|
||||
}
|
||||
@@ -333,7 +367,11 @@
|
||||
|
||||
const response = await query.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
console.error('获取订单失败:', response.error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
return
|
||||
@@ -349,19 +387,32 @@
|
||||
const ordersData: OrderType[] = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i]
|
||||
<<<<<<< HEAD
|
||||
const orderObj = item as UTSJSONObject
|
||||
=======
|
||||
const str = JSON.stringify(item)
|
||||
const orderObj = JSON.parse(str) as UTSJSONObject
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
const order: OrderType = {
|
||||
id: orderObj.getString('id') || '',
|
||||
order_no: orderObj.getString('order_no') || '',
|
||||
user_id: orderObj.getString('user_id') || '',
|
||||
merchant_id: orderObj.getString('merchant_id') || '',
|
||||
<<<<<<< HEAD
|
||||
order_status: orderObj.getNumber('order_status') || 1,
|
||||
=======
|
||||
order_status: orderObj.getNumber('order_status') ?? (orderObj.get('order_status') == null ? 1 : (orderObj.get('order_status') as number)),
|
||||
>>>>>>> local-backup-root-cyj
|
||||
total_amount: orderObj.getNumber('total_amount') || 0,
|
||||
product_amount: orderObj.getNumber('product_amount') || 0,
|
||||
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
|
||||
paid_amount: orderObj.getNumber('paid_amount') || 0,
|
||||
<<<<<<< HEAD
|
||||
shipping_address: orderObj.getString('shipping_address') || '',
|
||||
=======
|
||||
shipping_address: orderObj.get('shipping_address') != null ? (typeof orderObj.get('shipping_address') === 'string' ? orderObj.getString('shipping_address')! : JSON.stringify(orderObj.get('shipping_address'))) : '',
|
||||
>>>>>>> local-backup-root-cyj
|
||||
remark: orderObj.getString('remark') || '',
|
||||
created_at: orderObj.getString('created_at') || '',
|
||||
updated_at: orderObj.getString('updated_at') || '',
|
||||
@@ -372,7 +423,14 @@
|
||||
if (itemsObj != null && Array.isArray(itemsObj)) {
|
||||
const itemsArray = itemsObj as any[]
|
||||
for (let j = 0; j < itemsArray.length; j++) {
|
||||
<<<<<<< HEAD
|
||||
const orderItem = itemsArray[j] as UTSJSONObject
|
||||
=======
|
||||
const rawItem = itemsArray[j]
|
||||
const itemStr = JSON.stringify(rawItem)
|
||||
const orderItem = JSON.parse(itemStr) as UTSJSONObject
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
order.items.push({
|
||||
id: orderItem.getString('id') || '',
|
||||
order_id: orderItem.getString('order_id') || '',
|
||||
@@ -424,13 +482,29 @@
|
||||
const rawData = response.data as any[]
|
||||
if (rawData != null) {
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
<<<<<<< HEAD
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
const status = item.getNumber('order_status') || 1
|
||||
=======
|
||||
const row = rawData[i]
|
||||
const istr = JSON.stringify(row)
|
||||
const item = JSON.parse(istr) as UTSJSONObject
|
||||
const status_val = item.get('order_status')
|
||||
let status = 1
|
||||
if (status_val != null) {
|
||||
status = (typeof status_val === 'number') ? (status_val as number) : parseInt(status_val.toString())
|
||||
}
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
if (status === 1) counts[1]++
|
||||
else if (status === 2) counts[2]++
|
||||
else if (status === 3) counts[3]++
|
||||
else if (status === 4) counts[4]++
|
||||
<<<<<<< HEAD
|
||||
else if (status === 0) counts[0]++
|
||||
=======
|
||||
else if (status === 0 || status === 6) counts[0]++
|
||||
>>>>>>> local-backup-root-cyj
|
||||
total++
|
||||
}
|
||||
}
|
||||
@@ -500,7 +574,11 @@
|
||||
},
|
||||
|
||||
async confirmShip() {
|
||||
<<<<<<< HEAD
|
||||
if (!this.selectedLogistics.name) {
|
||||
=======
|
||||
if (this.selectedLogistics == null || !this.selectedLogistics?.name) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
uni.showToast({ title: '请选择物流公司', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -510,6 +588,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
<<<<<<< HEAD
|
||||
const response = await supa
|
||||
.from('ml_orders')
|
||||
.update({
|
||||
@@ -524,6 +603,31 @@
|
||||
|
||||
if (response.error != null) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
=======
|
||||
const payloadStr = JSON.stringify({
|
||||
order_status: 3,
|
||||
shipping_status: 2,
|
||||
carrier_name: this.selectedLogistics?.name ?? '未知',
|
||||
tracking_no: this.trackingNumber,
|
||||
shipped_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
const payload = JSON.parse(payloadStr) as UTSJSONObject;
|
||||
console.log('--- PAYLOAD TO SEND ---', JSON.stringify(payload));
|
||||
const response = await supa.from('ml_orders').update(payload
|
||||
)
|
||||
.eq('id', this.currentOrder!.id)
|
||||
.execute()
|
||||
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
let msg = '';
|
||||
if (response.error != null) msg = response.error!.message;
|
||||
else if (response.data != null) {
|
||||
const rData = response.data as UTSJSONObject;
|
||||
msg = rData.getString('message') ?? rData.getString('code') ?? JSON.stringify(rData);
|
||||
}
|
||||
if (!msg) msg = '请检查网络或登录状态'; uni.showToast({ title: '发货被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
|
||||
>>>>>>> local-backup-root-cyj
|
||||
return
|
||||
}
|
||||
|
||||
@@ -531,14 +635,22 @@
|
||||
this.closeShipModal()
|
||||
this.loadOrders()
|
||||
this.loadOrderCounts()
|
||||
<<<<<<< HEAD
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '发货失败', icon: 'none' })
|
||||
}
|
||||
=======
|
||||
} catch (e) { uni.showToast({ title: '发货发生异常', icon: 'none' }); console.error(e) }
|
||||
>>>>>>> local-backup-root-cyj
|
||||
},
|
||||
|
||||
viewLogistics(order: OrderType) {
|
||||
uni.navigateTo({
|
||||
<<<<<<< HEAD
|
||||
url: `/pages/mall/merchant/logistics?orderId=${order.id}`
|
||||
=======
|
||||
url: `/pages/mall/consumer/logistics?orderId=${order.id}`
|
||||
>>>>>>> local-backup-root-cyj
|
||||
})
|
||||
},
|
||||
|
||||
@@ -555,7 +667,11 @@
|
||||
.eq('id', order.id)
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
=======
|
||||
if (response.error != null || (response.status ?? 200) >= 400) {
|
||||
>>>>>>> local-backup-root-cyj
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
@@ -576,7 +692,12 @@
|
||||
if (status === 2) return '待发货'
|
||||
if (status === 3) return '待收货'
|
||||
if (status === 4) return '已完成'
|
||||
<<<<<<< HEAD
|
||||
if (status === 0) return '退款中'
|
||||
=======
|
||||
if (status === 0 || status === 6) return '退款/售后'
|
||||
if (status === 7) return '退货完成'
|
||||
>>>>>>> local-backup-root-cyj
|
||||
if (status === 5 || status === -1) return '已取消'
|
||||
return '未知'
|
||||
},
|
||||
@@ -717,8 +838,14 @@
|
||||
|
||||
.order-card {
|
||||
background-color: #fff;
|
||||
<<<<<<< HEAD
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
=======
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
>>>>>>> local-backup-root-cyj
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -817,22 +944,54 @@
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
.product-spec {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
=======
|
||||
.product-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
background-color: #f8f8f8;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
align-self: flex-start;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.product-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
margin-left: 20rpx;
|
||||
min-width: 120rpx;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
<<<<<<< HEAD
|
||||
font-weight: 500;
|
||||
=======
|
||||
font-weight: bold;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.product-quantity {
|
||||
@@ -845,8 +1004,14 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
<<<<<<< HEAD
|
||||
padding: 20rpx 24rpx;
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
=======
|
||||
padding: 24rpx;
|
||||
border-top: 1rpx solid #f8f8f8;
|
||||
background-color: #fafafa;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
@@ -860,7 +1025,11 @@
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
<<<<<<< HEAD
|
||||
font-size: 28rpx;
|
||||
=======
|
||||
font-size: 32rpx;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
color: #FF3B30;
|
||||
font-weight: bold;
|
||||
margin-left: 10rpx;
|
||||
@@ -872,14 +1041,27 @@
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
<<<<<<< HEAD
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
=======
|
||||
min-width: 120rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
border: 1rpx solid #eee;
|
||||
padding: 0 20rpx;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
<<<<<<< HEAD
|
||||
}
|
||||
|
||||
.action-btn.info {
|
||||
@@ -890,6 +1072,21 @@
|
||||
.action-btn.default {
|
||||
background-color: #F5F5F5;
|
||||
color: #666;
|
||||
=======
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.info {
|
||||
background-color: #fff;
|
||||
color: #007AFF;
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background-color: #fff;
|
||||
color: #666;
|
||||
border-color: #ddd;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.load-more, .no-more {
|
||||
@@ -912,7 +1109,11 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
<<<<<<< HEAD
|
||||
z-index: 1000;
|
||||
=======
|
||||
z-index: 99;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -920,6 +1121,11 @@
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -994,3 +1200,19 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端 - 商品管理详情页 -->
|
||||
=======
|
||||
<!-- 商家端 - 商品管理详情页 -->
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<template>
|
||||
<view class="product-manage-detail">
|
||||
<!-- 商品基本信息 -->
|
||||
@@ -44,8 +48,13 @@
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">商品状态</text>
|
||||
<<<<<<< HEAD
|
||||
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 0 }">
|
||||
{{ product.status === 1 ? '上架' : '下架' }}
|
||||
=======
|
||||
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 2 || product.status === 0 }">
|
||||
{{ product.status === 1 ? '上架' : (product.status === 2 || product.status === 0 ? '下架' : '其他') }}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -70,7 +79,11 @@
|
||||
<view class="sku-details">
|
||||
<text class="sku-price">¥{{ sku.price }}</text>
|
||||
<text class="sku-stock">库存: {{ sku.stock }}</text>
|
||||
<<<<<<< HEAD
|
||||
<text class="sku-status" :class="{ 'status-on': sku.status === 1, 'status-off': sku.status === 0 }">
|
||||
=======
|
||||
<text class="sku-status" :class="{ 'status-on': sku.status === 1, 'status-off': sku.status === 2 || sku.status === 0 }">
|
||||
>>>>>>> local-backup-root-cyj
|
||||
{{ sku.status === 1 ? '启用' : '禁用' }}
|
||||
</text>
|
||||
</view>
|
||||
@@ -705,3 +718,7 @@ export default {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端 - 商品编辑页面 -->
|
||||
=======
|
||||
<!-- 商家端 - 商品编辑页面 (已修复缓存) -->
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<template>
|
||||
<view class="product-edit-page">
|
||||
<!-- 商品基本信息 -->
|
||||
@@ -131,8 +135,28 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<view class="form-item">
|
||||
<text class="label">总库存 *</text>
|
||||
=======
|
||||
<view class="form-item">
|
||||
<text class="label">VIP独立折扣</text>
|
||||
<switch :checked="product.is_vip_discount" @change="e => { product.is_vip_discount = e.detail.value as boolean }" />
|
||||
</view>
|
||||
|
||||
<view class="form-item" v-if="product.is_vip_discount">
|
||||
<text class="label">VIP折扣率</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
v-model="product.vip_discount_rate"
|
||||
placeholder="如 0.85 代表 85 折(空代表采用全局)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">总库存 *</text>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
@@ -152,6 +176,28 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
<!-- 会员阶梯价 -->
|
||||
<view class="section">
|
||||
<view class="section-title">会员等级价格 (选填)</view>
|
||||
<view class="section-desc">若不填写则按照商品销售价或默认折扣计算</view>
|
||||
|
||||
<view v-for="(level, index) in memberLevels" :key="index" class="form-item">
|
||||
<text class="label">{{ level.name }}价格</text>
|
||||
<view class="price-input">
|
||||
<text class="unit">¥</text>
|
||||
<input
|
||||
class="input"
|
||||
v-model="level.price"
|
||||
type="digit"
|
||||
placeholder="专属折扣价"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<!-- 商品属性 -->
|
||||
<view class="section">
|
||||
<view class="section-title">商品属性</view>
|
||||
@@ -231,6 +277,17 @@
|
||||
logo_url: string
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
type MemberLevelType = {
|
||||
id: string
|
||||
name: string
|
||||
level_rank: number
|
||||
discount_rate: number
|
||||
price: string // 绑定输入框用
|
||||
}
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -242,6 +299,10 @@
|
||||
brands: [] as BrandType[],
|
||||
brandIndex: -1,
|
||||
selectedBrand: null as BrandType | null,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
memberLevels: [] as MemberLevelType[],
|
||||
>>>>>>> local-backup-root-cyj
|
||||
product: {
|
||||
name: '',
|
||||
subtitle: '',
|
||||
@@ -255,9 +316,17 @@
|
||||
total_stock: '',
|
||||
warning_stock: '10',
|
||||
unit: '件',
|
||||
<<<<<<< HEAD
|
||||
is_hot: false,
|
||||
is_new: false,
|
||||
is_featured: false,
|
||||
=======
|
||||
is_hot: false,
|
||||
is_new: false,
|
||||
is_featured: false,
|
||||
is_vip_discount: true,
|
||||
vip_discount_rate: '',
|
||||
>>>>>>> local-backup-root-cyj
|
||||
description: ''
|
||||
},
|
||||
merchantId: ''
|
||||
@@ -265,15 +334,52 @@
|
||||
},
|
||||
|
||||
onLoad(options: any) {
|
||||
<<<<<<< HEAD
|
||||
const productId = options.productId as string
|
||||
if (productId) {
|
||||
this.productId = productId
|
||||
this.isEdit = true
|
||||
this.loadProductDetail(productId)
|
||||
=======
|
||||
let productId = ''
|
||||
if (options) {
|
||||
const keys = Object.keys(options as object)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (keys[i] === 'productId') {
|
||||
productId = String((options as Record<string, any>)[keys[i]])
|
||||
}
|
||||
}
|
||||
if (!productId && options['productId']) {
|
||||
productId = String(options['productId'])
|
||||
}
|
||||
// 兼容某些平台
|
||||
if (!productId) {
|
||||
try {
|
||||
const optsStr = JSON.stringify(options)
|
||||
const optsObj = JSON.parse(optsStr) as Record<string, any>
|
||||
if (optsObj['productId']) {
|
||||
productId = String(optsObj['productId'])
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (productId && productId !== '') {
|
||||
this.productId = productId
|
||||
this.isEdit = true
|
||||
uni.setNavigationBarTitle({ title: '编辑商品' })
|
||||
this.loadProductDetail(productId)
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: '添加商品' })
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
this.initMerchantId()
|
||||
this.loadCategories()
|
||||
this.loadBrands()
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
this.loadMemberLevels()
|
||||
>>>>>>> local-backup-root-cyj
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -281,7 +387,11 @@
|
||||
try {
|
||||
const session = supa.getSession()
|
||||
if (session != null && session.user != null) {
|
||||
<<<<<<< HEAD
|
||||
this.merchantId = session.user.getString('id') || ''
|
||||
=======
|
||||
this.merchantId = (session.user as any)['id'] != null ? String((session.user as any)['id']) : ''
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
if (!this.merchantId) {
|
||||
this.merchantId = uni.getStorageSync('user_id') || ''
|
||||
@@ -291,6 +401,78 @@
|
||||
}
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
async loadMemberLevels() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_member_levels')
|
||||
.select('*')
|
||||
.eq('is_active', true)
|
||||
.order('level_rank', { ascending: true })
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取会员等级失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null) return
|
||||
|
||||
this.memberLevels = []
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as any
|
||||
this.memberLevels.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
name: item['name'] != null ? String(item['name']) : '',
|
||||
level_rank: item['level_rank'] != null ? parseInt(String(item['level_rank'])) : 0,
|
||||
discount_rate: item['discount_rate'] != null ? parseFloat(String(item['discount_rate'])) : 1.0,
|
||||
price: ''
|
||||
} as MemberLevelType)
|
||||
}
|
||||
|
||||
// 如果是编辑模式,还需要加载已有的会员价
|
||||
if (this.isEdit) {
|
||||
this.loadMemberPrices()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取会员等级异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadMemberPrices() {
|
||||
try {
|
||||
const response = await supa
|
||||
.from('ml_product_member_prices')
|
||||
.select('*')
|
||||
.eq('product_id', this.productId)
|
||||
.execute()
|
||||
|
||||
if (response.error != null) {
|
||||
console.error('获取会员价失败:', response.error)
|
||||
return
|
||||
}
|
||||
|
||||
const rawData = response.data as any[]
|
||||
if (rawData == null || rawData.length == 0) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
const item = rawData[i] as any
|
||||
const levelId = String(item['level_id'])
|
||||
const price = String(item['member_price'])
|
||||
|
||||
const index = this.memberLevels.findIndex(lv => lv.id === levelId)
|
||||
if (index >= 0) {
|
||||
this.memberLevels[index].price = price
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取会员价异常:', e)
|
||||
}
|
||||
},
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
async loadCategories() {
|
||||
try {
|
||||
const response = await supa
|
||||
@@ -309,10 +491,17 @@
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
<<<<<<< HEAD
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.categories.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || ''
|
||||
=======
|
||||
const item = rawData[i] as any
|
||||
this.categories.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
name: item['name'] != null ? String(item['name']) : ''
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} as CategoryType)
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -338,11 +527,19 @@
|
||||
if (rawData == null) return
|
||||
|
||||
for (let i = 0; i < rawData.length; i++) {
|
||||
<<<<<<< HEAD
|
||||
const item = rawData[i] as UTSJSONObject
|
||||
this.brands.push({
|
||||
id: item.getString('id') || '',
|
||||
name: item.getString('name') || '',
|
||||
logo_url: item.getString('logo_url') || ''
|
||||
=======
|
||||
const item = rawData[i] as any
|
||||
this.brands.push({
|
||||
id: item['id'] != null ? String(item['id']) : '',
|
||||
name: item['name'] != null ? String(item['name']) : '',
|
||||
logo_url: item['logo_url'] != null ? String(item['logo_url']) : ''
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} as BrandType)
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -352,6 +549,10 @@
|
||||
|
||||
async loadProductDetail(productId: string) {
|
||||
try {
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
uni.showLoading({ title: '加载商品中...' })
|
||||
>>>>>>> local-backup-root-cyj
|
||||
const response = await supa
|
||||
.from('ml_products')
|
||||
.select('*')
|
||||
@@ -359,6 +560,7 @@
|
||||
.single()
|
||||
.execute()
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (response.error != null) {
|
||||
console.error('获取商品详情失败:', response.error)
|
||||
return
|
||||
@@ -386,6 +588,47 @@
|
||||
description: rawData.getString('description') || ''
|
||||
}
|
||||
|
||||
=======
|
||||
uni.hideLoading()
|
||||
if (response.error != null) {
|
||||
console.error('获取详情失败:', response.error)
|
||||
uni.showToast({ title: '没有找到该商品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
let rawData = response.data as any
|
||||
if (rawData == null) return
|
||||
|
||||
// 防止Supabase某些版本把single()仍返回数组的坑
|
||||
if (Array.isArray(rawData) && rawData.length > 0) {
|
||||
rawData = rawData[0]
|
||||
}
|
||||
|
||||
const getStr = (key: string): string => { try { return rawData[key] != null ? String(rawData[key]) : '' } catch(e){ return '' } }
|
||||
const getBool = (key: string): boolean => { try { return rawData[key] === true || rawData[key] === 'true' } catch(e){ return false } }
|
||||
|
||||
this.product.name = getStr('name')
|
||||
this.product.subtitle = getStr('subtitle')
|
||||
this.product.category_id = getStr('category_id')
|
||||
this.product.brand_id = getStr('brand_id')
|
||||
this.product.main_image_url = getStr('main_image_url')
|
||||
this.product.imageList = this.parseImageUrls(getStr('image_urls'))
|
||||
this.product.base_price = getStr('base_price')
|
||||
this.product.market_price = getStr('market_price')
|
||||
this.product.cost_price = getStr('cost_price')
|
||||
this.product.total_stock = getStr('total_stock')
|
||||
this.product.warning_stock = getStr('warning_stock') || '10'
|
||||
this.product.unit = getStr('unit') || '件'
|
||||
this.product.is_hot = getBool('is_hot')
|
||||
this.product.is_new = getBool('is_new')
|
||||
this.product.is_featured = getBool('is_featured')
|
||||
|
||||
const _isVip = rawData['is_vip_discount']
|
||||
this.product.is_vip_discount = _isVip == null ? true : getBool('is_vip_discount')
|
||||
this.product.vip_discount_rate = getStr('vip_discount_rate')
|
||||
this.product.description = getStr('description')
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
if (this.product.category_id) {
|
||||
this.categoryIndex = this.categories.findIndex(c => c.id === this.product.category_id)
|
||||
if (this.categoryIndex >= 0) {
|
||||
@@ -400,7 +643,13 @@
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
<<<<<<< HEAD
|
||||
console.error('获取商品详情异常:', e)
|
||||
=======
|
||||
uni.hideLoading()
|
||||
console.error('获取商品详情异常:', e)
|
||||
uni.showToast({ title: '加载异常: ' + String(e), icon: 'none', duration: 3000 })
|
||||
>>>>>>> local-backup-root-cyj
|
||||
}
|
||||
},
|
||||
|
||||
@@ -453,6 +702,7 @@
|
||||
this.product.imageList.splice(index, 1)
|
||||
},
|
||||
|
||||
<<<<<<< HEAD
|
||||
async saveProduct() {
|
||||
if (!this.product.name) {
|
||||
uni.showToast({ title: '请输入商品名称', icon: 'none' })
|
||||
@@ -533,6 +783,203 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
=======
|
||||
async uploadImageToSupa(localPath: string): Promise<string> {
|
||||
if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
|
||||
return localPath
|
||||
}
|
||||
|
||||
let ext = '.jpg'
|
||||
const dotIndex = localPath.lastIndexOf('.')
|
||||
if (dotIndex > -1) {
|
||||
ext = localPath.substring(dotIndex).toLowerCase()
|
||||
}
|
||||
|
||||
const uuid = Date.now().toString() + '_' + Math.floor(Math.random() * 1000)
|
||||
const remotePath = `products/${this.merchantId}_${uuid}${ext}`
|
||||
|
||||
try {
|
||||
const uploadResult = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
|
||||
if (uploadResult.error != null) {
|
||||
console.error('上传图片失败:', uploadResult.error)
|
||||
return localPath
|
||||
}
|
||||
|
||||
return supa.storage.getPublicUrl('zhipao', remotePath)
|
||||
} catch (e) {
|
||||
console.error('上传图片异常:', e)
|
||||
return localPath
|
||||
}
|
||||
},
|
||||
|
||||
async saveProduct() {
|
||||
if (!this.product.name) {
|
||||
uni.showToast({ title: '请输入商品名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.category_id) {
|
||||
uni.showToast({ title: '请选择商品分类', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.base_price) {
|
||||
uni.showToast({ title: '请输入销售价', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.product.total_stock) {
|
||||
uni.showToast({ title: '请输入总库存', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.product.is_vip_discount && this.product.vip_discount_rate !== '') {
|
||||
const rate = parseFloat(this.product.vip_discount_rate)
|
||||
if (isNaN(rate) || rate <= 0 || rate > 1) {
|
||||
uni.showToast({ title: 'VIP折扣率需在0~1之间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
let finalMainImage = this.product.main_image_url
|
||||
if (finalMainImage != '') {
|
||||
finalMainImage = await this.uploadImageToSupa(finalMainImage)
|
||||
}
|
||||
|
||||
const finalImageList = [] as string[]
|
||||
for (let i = 0; i < this.product.imageList.length; i++) {
|
||||
const img = await this.uploadImageToSupa(this.product.imageList[i])
|
||||
finalImageList.push(img)
|
||||
}
|
||||
|
||||
const imageUrlsStr = JSON.stringify(finalImageList)
|
||||
|
||||
const productData = {
|
||||
merchant_id: this.merchantId,
|
||||
name: this.product.name,
|
||||
subtitle: this.product.subtitle,
|
||||
category_id: this.product.category_id,
|
||||
brand_id: this.product.brand_id || null,
|
||||
main_image_url: finalMainImage,
|
||||
image_urls: imageUrlsStr,
|
||||
base_price: this.product.base_price ? parseFloat(this.product.base_price) : 0,
|
||||
market_price: this.product.market_price ? parseFloat(this.product.market_price) : null,
|
||||
cost_price: this.product.cost_price ? parseFloat(this.product.cost_price) : null,
|
||||
total_stock: parseInt(this.product.total_stock),
|
||||
available_stock: parseInt(this.product.total_stock),
|
||||
is_hot: this.product.is_hot,
|
||||
is_new: this.product.is_new,
|
||||
is_featured: this.product.is_featured,
|
||||
is_vip_discount: this.product.is_vip_discount,
|
||||
vip_discount_rate: this.product.vip_discount_rate ? parseFloat(this.product.vip_discount_rate) : null,
|
||||
description: this.product.description,
|
||||
status: 1,
|
||||
updated_at: new Date().toISOString()
|
||||
} as UTSJSONObject
|
||||
|
||||
let response : any = null
|
||||
if (this.isEdit) {
|
||||
const updateData = {} as UTSJSONObject
|
||||
const keys = UTSJSONObject.keys(productData)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
if (key != 'status') {
|
||||
updateData[key] = productData[key]
|
||||
}
|
||||
}
|
||||
|
||||
console.log('执行产品更新, ID:', this.productId)
|
||||
const updateResponse = await supa
|
||||
.from('ml_products')
|
||||
.update(updateData)
|
||||
.eq('id', this.productId)
|
||||
.execute()
|
||||
|
||||
if (updateResponse.error != null) {
|
||||
throw new Error('产品更新失败: ' + String(updateResponse.error!.message))
|
||||
}
|
||||
response = updateResponse
|
||||
} else {
|
||||
productData['created_at'] = new Date().toISOString()
|
||||
productData['product_code'] = 'P' + Date.now().toString()
|
||||
console.log('执行新产品插入')
|
||||
const insertResponse = await supa
|
||||
.from('ml_products')
|
||||
.insert(productData)
|
||||
.execute()
|
||||
|
||||
if (insertResponse.error != null) {
|
||||
throw new Error('产品发布失败: ' + String(insertResponse.error!.message))
|
||||
}
|
||||
response = insertResponse
|
||||
}
|
||||
|
||||
// 保存会员价
|
||||
let targetProductId = this.isEdit ? this.productId : ''
|
||||
if (response != null && response.data != null) {
|
||||
const responseData = response.data
|
||||
if (Array.isArray(responseData)) {
|
||||
const dataArr = responseData as any[]
|
||||
if (dataArr.length > 0) {
|
||||
const firstRow = dataArr[0] as UTSJSONObject
|
||||
if (firstRow['id'] != null) {
|
||||
targetProductId = String(firstRow['id'])
|
||||
}
|
||||
}
|
||||
} else if (responseData instanceof UTSJSONObject) {
|
||||
const dataObj = responseData as UTSJSONObject
|
||||
if (dataObj['id'] != null) {
|
||||
targetProductId = String(dataObj['id'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('最终目标产品ID:', targetProductId)
|
||||
|
||||
if (targetProductId && targetProductId !== '' && targetProductId !== 'undefined') {
|
||||
// 1. 先删除旧的会员价
|
||||
if (this.isEdit) {
|
||||
console.log('删除旧会员价:', targetProductId)
|
||||
await supa.from('ml_product_member_prices').delete().eq('product_id', targetProductId).execute()
|
||||
}
|
||||
|
||||
// 2. 插入新的会员价
|
||||
for (let i = 0; i < this.memberLevels.length; i++) {
|
||||
const level = this.memberLevels[i]
|
||||
if (level.price && level.price > 0) {
|
||||
const memberPriceData = {
|
||||
product_id: targetProductId,
|
||||
level_id: level.id,
|
||||
member_price: level.price,
|
||||
created_at: new Date().toISOString()
|
||||
} as UTSJSONObject
|
||||
|
||||
const insertRes = await supa
|
||||
.from('ml_product_member_prices')
|
||||
.insert(memberPriceData)
|
||||
.execute()
|
||||
|
||||
if (insertRes.error != null) {
|
||||
console.error('插入会员价失败', insertRes.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('保存商品异常:', e)
|
||||
uni.showToast({ title: '保存异常: ' + String(e), icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -557,6 +1004,16 @@
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
.section-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: -20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
@@ -713,3 +1170,9 @@
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<<<<<<< HEAD
|
||||
<!-- 商家端 - 商品管理列表页面 -->
|
||||
=======
|
||||
<!-- 商家端 - 商品管理列表页面 -->
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<template>
|
||||
<view class="products-page">
|
||||
<!-- 搜索栏 -->
|
||||
@@ -84,11 +88,20 @@
|
||||
</text>
|
||||
</view>
|
||||
<text class="product-subtitle">{{ product.subtitle || '暂无描述' }}</text>
|
||||
<<<<<<< HEAD
|
||||
<view class="product-tags" v-if="product.tags">
|
||||
<text v-if="product.is_hot" class="tag hot">热</text>
|
||||
<text v-if="product.is_new" class="tag new">新</text>
|
||||
<text v-if="product.is_featured" class="tag recommend">荐</text>
|
||||
</view>
|
||||
=======
|
||||
<view class="product-tags">
|
||||
<text v-if="product.is_hot" class="tag hot">热</text>
|
||||
<text v-if="product.is_new" class="tag new">新</text>
|
||||
<text v-if="product.is_featured" class="tag recommend">荐</text>
|
||||
<text v-if="product.is_vip_discount" class="tag vip">VIP</text>
|
||||
</view>
|
||||
>>>>>>> local-backup-root-cyj
|
||||
<view class="product-stats">
|
||||
<view class="price-row">
|
||||
<text class="current-price">¥{{ product.base_price }}</text>
|
||||
@@ -222,7 +235,11 @@
|
||||
if (this.currentFilter === 'onsale') {
|
||||
query = query.eq('status', 1)
|
||||
} else if (this.currentFilter === 'offsale') {
|
||||
<<<<<<< HEAD
|
||||
query = query.eq('status', 0)
|
||||
=======
|
||||
query = query.eq('status', 2)
|
||||
>>>>>>> local-backup-root-cyj
|
||||
} else if (this.currentFilter === 'low_stock') {
|
||||
query = query.lte('total_stock', this.lowStockThreshold).gte('total_stock', 0)
|
||||
}
|
||||
@@ -259,7 +276,11 @@
|
||||
market_price: prodObj.getNumber('market_price') || 0,
|
||||
total_stock: prodObj.getNumber('total_stock') || 0,
|
||||
sale_count: prodObj.getNumber('sale_count') || 0,
|
||||
<<<<<<< HEAD
|
||||
status: prodObj.getNumber('status') || 0,
|
||||
=======
|
||||
status: prodObj.getNumber('status') || 1,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
is_hot: prodObj.getBoolean('is_hot') || false,
|
||||
is_new: prodObj.getBoolean('is_new') || false,
|
||||
is_featured: prodObj.getBoolean('is_featured') || false,
|
||||
@@ -332,7 +353,11 @@
|
||||
},
|
||||
|
||||
async toggleStatus(product: ProductType) {
|
||||
<<<<<<< HEAD
|
||||
const newStatus = product.status === 1 ? 0 : 1
|
||||
=======
|
||||
const newStatus = product.status === 1 ? 2 : 1
|
||||
>>>>>>> local-backup-root-cyj
|
||||
const actionText = newStatus === 1 ? '上架' : '下架'
|
||||
|
||||
uni.showModal({
|
||||
@@ -395,13 +420,21 @@
|
||||
|
||||
getStatusClass(status: number): string {
|
||||
if (status === 1) return 'status-onsale'
|
||||
<<<<<<< HEAD
|
||||
if (status === 0) return 'status-offsale'
|
||||
=======
|
||||
if (status === 2 || status === 0) return 'status-offsale'
|
||||
>>>>>>> local-backup-root-cyj
|
||||
return 'status-pending'
|
||||
},
|
||||
|
||||
getStatusText(status: number): string {
|
||||
if (status === 1) return '在售'
|
||||
<<<<<<< HEAD
|
||||
if (status === 0) return '已下架'
|
||||
=======
|
||||
if (status === 2 || status === 0) return '已下架'
|
||||
>>>>>>> local-backup-root-cyj
|
||||
return '待审核'
|
||||
}
|
||||
}
|
||||
@@ -601,10 +634,23 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
.tag.recommend {
|
||||
background-color: #9C27B0;
|
||||
color: #fff;
|
||||
}
|
||||
=======
|
||||
.tag.recommend {
|
||||
background-color: #9C27B0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag.vip {
|
||||
background-color: #FFC107;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
.product-stats {
|
||||
display: flex;
|
||||
@@ -710,3 +756,9 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
|
||||
@@ -135,13 +135,52 @@
|
||||
}
|
||||
})
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
async uploadImageToSupa(localPath: string): Promise<string> {
|
||||
if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
|
||||
return localPath
|
||||
}
|
||||
|
||||
let ext = '.jpg'
|
||||
const dotIndex = localPath.lastIndexOf('.')
|
||||
if (dotIndex > -1) {
|
||||
ext = localPath.substring(dotIndex).toLowerCase()
|
||||
}
|
||||
|
||||
const uuid = Date.now().toString() + '_' + Math.floor(Math.random() * 1000)
|
||||
const remotePath = `shops/${this.merchantId}_${uuid}${ext}`
|
||||
|
||||
try {
|
||||
const uploadResult = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
|
||||
if (uploadResult.status == 200 || uploadResult.status == 201) {
|
||||
const data = uploadResult.data
|
||||
if (data != null) {
|
||||
const dataObj = data as UTSJSONObject
|
||||
const key = dataObj.getString('Key')
|
||||
if (key != null && key != '') {
|
||||
return `${supa.baseUrl}/storage/v1/object/public/${key}`
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error('上传图片失败:', uploadResult.error)
|
||||
return localPath
|
||||
} catch (e) {
|
||||
console.error('上传图片异常:', e)
|
||||
return localPath
|
||||
}
|
||||
},
|
||||
|
||||
>>>>>>> local-backup-root-cyj
|
||||
async saveShop() {
|
||||
if (!this.shop.shop_name) {
|
||||
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
try {
|
||||
@@ -149,6 +188,27 @@
|
||||
shop_name: this.shop.shop_name,
|
||||
shop_logo: this.shop.shop_logo,
|
||||
shop_banner: this.shop.shop_banner,
|
||||
=======
|
||||
uni.showLoading({ title: '正在上传图片...' })
|
||||
|
||||
try {
|
||||
let finalLogo = this.shop.shop_logo
|
||||
if (finalLogo != '' && !finalLogo.startsWith('http')) {
|
||||
finalLogo = await this.uploadImageToSupa(finalLogo)
|
||||
}
|
||||
|
||||
let finalBanner = this.shop.shop_banner
|
||||
if (finalBanner != '' && !finalBanner.startsWith('http')) {
|
||||
finalBanner = await this.uploadImageToSupa(finalBanner)
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
|
||||
const shopData = {
|
||||
shop_name: this.shop.shop_name,
|
||||
shop_logo: finalLogo,
|
||||
shop_banner: finalBanner,
|
||||
>>>>>>> local-backup-root-cyj
|
||||
description: this.shop.description,
|
||||
contact_name: this.shop.contact_name,
|
||||
contact_phone: this.shop.contact_phone,
|
||||
|
||||
Reference in New Issue
Block a user