// pages/support/support.js const app = getApp() const api = require('../../utils/api') const util = require('../../utils/util') const imageUrl = require('../../utils/imageUrl') // ๅธธ็”จ่กจๆƒ… const EMOJIS = [ "๐Ÿ˜Š", "๐Ÿ˜€", "๐Ÿ˜", "๐Ÿ˜ƒ", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜…", "๐Ÿ˜†", "๐Ÿ˜‰", "๐Ÿ˜‹", "๐Ÿ˜Ž", "๐Ÿ˜", "๐Ÿ˜˜", "๐Ÿฅฐ", "๐Ÿ˜—", "๐Ÿ˜™", "๐Ÿ™‚", "๐Ÿค—", "๐Ÿคฉ", "๐Ÿค”", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿ™„", "๐Ÿ˜", "๐Ÿ˜ฃ", "๐Ÿ˜ฅ", "๐Ÿ˜ฎ", "๐Ÿ˜ฏ", "๐Ÿ˜ช", "๐Ÿ˜ซ", "๐Ÿ˜ด", "๐Ÿ˜Œ", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿ˜", "๐Ÿ˜’", "๐Ÿ˜“", "๐Ÿ˜”", "๐Ÿ˜•", "๐Ÿ™ƒ", "๐Ÿ˜ฒ", "๐Ÿ˜–", "๐Ÿ˜ž", "๐Ÿ˜Ÿ", "๐Ÿ˜ค", "๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜จ", "๐Ÿ˜ฉ", "๐Ÿ˜ฌ", "๐Ÿ˜ฐ", "๐Ÿ˜ฑ", "๐Ÿ˜ณ", "๐Ÿ˜ต", "๐Ÿ˜ก", "๐Ÿ˜ ", "๐Ÿ˜ท", "๐Ÿค’", "๐Ÿค•", "๐Ÿ˜‡", "๐Ÿฅณ", "๐Ÿฅบ", "๐Ÿ‘‹", "๐Ÿ‘Œ", "โœŒ๏ธ", "๐Ÿคž", "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿค", "๐Ÿ™", "๐Ÿ’ช", "โค๏ธ", "๐Ÿงก", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐Ÿ–ค", "๐Ÿ’”", "๐Ÿ’•", "๐Ÿ’–", "๐Ÿ’—", "๐Ÿ’˜", "๐Ÿ’", "๐ŸŒน", "๐ŸŒบ", "๐ŸŒป", "๐ŸŒผ", "๐ŸŒท", "๐ŸŽ‰", "๐ŸŽŠ", "๐ŸŽ" ] Page({ data: { statusBarHeight: 44, navHeight: 96, myAvatar: '/images/default-avatar.svg', messages: [], inputText: '', inputFocus: false, isTyping: false, ticketId: '', scrollIntoView: '', scrollTop: 0, pollingTimer: null, // ๆ–ฐๅขž็Šถๆ€ isVoiceMode: false, isRecording: false, showEmoji: false, showMorePanel: false, voiceCancelHint: false, recordingDuration: 0, emojis: EMOJIS, playingVoiceId: null }, onLoad() { const { statusBarHeight, navHeight, userInfo } = app.globalData const myAvatar = imageUrl.getAvatarUrl(userInfo?.avatar) this.setData({ statusBarHeight, navHeight, myAvatar }) this.initSupport() }, onAvatarError() { this.setData({ myAvatar: '/images/default-avatar.svg' }) }, onUnload() { this.stopPolling() }, onHide() { this.stopPolling() }, onShow() { if (this.data.ticketId) { this.startPolling() } }, /** * ๅˆๅง‹ๅŒ–ๅฎขๆœไผš่ฏ */ async initSupport() { wx.showLoading({ title: 'ๅŠ ่ฝฝไธญ...' }) try { const guestId = wx.getStorageSync('guestId') || util.generateId() if (!wx.getStorageSync('guestId')) { wx.setStorageSync('guestId', guestId) } // ่Žทๅ–ๅทฒๆœ‰ๅ’จ่ฏขๅˆ—่กจ const res = await api.customerService.getList(guestId) const data = res.data || {} const tickets = data.tickets || [] if (tickets.length > 0) { // ไฝฟ็”จๆœ€่ฟ‘็š„ไธ€ไธชๅทฅๅ• const latestTicket = tickets[0] this.setData({ ticketId: latestTicket.id }) await this.loadMessages(latestTicket.id) } else { console.log('[support] No existing tickets found.') } } catch (err) { console.error('[support] initSupport error:', err) } finally { wx.hideLoading() this.startPolling() } }, /** * ๅŠ ่ฝฝๆถˆๆฏๅކๅฒ */ async loadMessages(ticketId) { try { const res = await api.customerService.getDetail(ticketId) if (res.success && res.data) { const messages = res.data.messages.map(msg => ({ id: msg.id, isMe: msg.senderType === 'user', text: msg.type === 'text' ? msg.content : (msg.type === 'image' ? '[ๅ›พ็‰‡]' : (msg.type === 'voice' ? '[่ฏญ้Ÿณ]' : msg.content)), type: msg.type || 'text', imageUrl: msg.type === 'image' ? imageUrl.getFullImageUrl(msg.content) : '', audioUrl: msg.type === 'voice' ? msg.content : '', duration: msg.duration || 0, time: util.formatTime(new Date(msg.createdAt), 'HH:mm'), senderName: msg.senderName })) // ๅฆ‚ๆžœๆœ‰ๆ–ฐๆถˆๆฏๆ‰ๆ›ดๆ–ฐ๏ผŒ้ฟๅ…้—ช็ƒ if (JSON.stringify(messages) !== JSON.stringify(this.data.messages)) { this.setData({ messages }, () => { this.scrollToBottom() }) } } } catch (err) { console.error('[support] loadMessages error:', err) } }, /** * ๅ‘้€ๆถˆๆฏ */ async onSend() { const content = this.data.inputText.trim() if (!content || this.isSending) return // ๆ”ถ่ตท้”ฎ็›˜ๅ’Œ้ขๆฟ this.setData({ showEmoji: false, showMorePanel: false }) await this.sendMessage(content, 'text') }, /** * ็ปŸไธ€ๅ‘้€ๆถˆๆฏๆ–นๆณ• * @param {string} content - ๆถˆๆฏๅ†…ๅฎน๏ผˆๆ–‡ๆœฌๆˆ–URL๏ผ‰ * @param {string} type - ๆถˆๆฏ็ฑปๅž‹ (text/image/voice) * @param {number} duration - ่ฏญ้Ÿณๆ—ถ้•ฟ */ async sendMessage(content, type = 'text', duration = 0) { if (this.isSending) return this.isSending = true const tempId = util.generateId() const now = new Date() // ๅ…ˆๅœจๆœฌๅœฐๆ˜พ็คบ const userMsg = { id: tempId, isMe: true, text: type === 'text' ? content : (type === 'image' ? '[ๅ›พ็‰‡]' : '[่ฏญ้Ÿณ]'), type: type, imageUrl: type === 'image' ? content : '', audioUrl: type === 'voice' ? content : '', duration: duration, time: util.formatTime(now, 'HH:mm'), uploading: true } this.setData({ messages: [...this.data.messages, userMsg], inputText: '', inputFocus: type === 'text' // ไป…ๆ–‡ๆœฌๅ‘้€ๅŽ่š็„ฆ }, () => { this.scrollToBottom() }) try { const payload = { content: content, type: type, duration: duration, userName: app.globalData.userInfo?.nickname || '่ฎฟๅฎข' } if (this.data.ticketId) { // ๅ›žๅคๅทฒๆœ‰ๅทฅๅ• await api.customerService.reply({ ticketId: this.data.ticketId, ...payload }) } else { // ๅˆ›ๅปบๆ–ฐๅทฅๅ• const guestId = wx.getStorageSync('guestId') const res = await api.customerService.create({ category: 'other', guestId: guestId, ...payload }) if (res.success && res.data) { this.setData({ ticketId: res.data.ticketId }) } } // ๆ›ดๆ–ฐๆœฌๅœฐๆถˆๆฏ็Šถๆ€ const messages = this.data.messages.map(msg => { if (msg.id === tempId) { return { ...msg, uploading: false } } return msg }) this.setData({ messages }) // ๅ‘้€ๅŽ็ซ‹ๅณๆ‹‰ๅ–ไธ€ๆฌก if (this.data.ticketId) { await this.loadMessages(this.data.ticketId) } } catch (err) { console.error('[support] send message error:', err) wx.showToast({ title: 'ๅ‘้€ๅคฑ่ดฅ', icon: 'none' }) // ๆ›ดๆ–ฐๅคฑ่ดฅ็Šถๆ€ const messages = this.data.messages.map(msg => { if (msg.id === tempId) { return { ...msg, uploading: false, error: true } } return msg }) this.setData({ messages }) } finally { this.isSending = false } }, onInput(e) { this.setData({ inputText: e.detail.value }) }, /** * ๅผ€ๅง‹่ฝฎ่ฏข */ startPolling() { this.stopPolling() this.data.pollingTimer = setInterval(() => { if (this.data.ticketId) { this.loadMessages(this.data.ticketId) } }, 4000) // ๆฏ4็ง’่ฝฎ่ฏขไธ€ๆฌก }, /** * ๅœๆญข่ฝฎ่ฏข */ stopPolling() { if (this.data.pollingTimer) { clearInterval(this.data.pollingTimer) this.data.pollingTimer = null } }, onBack() { wx.navigateBack() }, onTapChatArea() { this.setData({ inputFocus: false, showEmoji: false, showMorePanel: false }) }, scrollToBottom() { this.setData({ scrollIntoView: 'chat-bottom-anchor' }) }, // ==================== ๅบ•้ƒจๅŠŸ่ƒฝๅŒบ้€ป่พ‘ ==================== onVoiceMode() { this.setData({ isVoiceMode: !this.data.isVoiceMode, showEmoji: false, showMorePanel: false, inputFocus: false }) }, onEmojiToggle() { this.setData({ inputFocus: false }) setTimeout(() => { this.setData({ showEmoji: !this.data.showEmoji, showMorePanel: false, isVoiceMode: false }) }, 50) }, onAddMore() { this.setData({ inputFocus: false }) setTimeout(() => { this.setData({ showMorePanel: !this.data.showMorePanel, showEmoji: false, isVoiceMode: false }) }, 50) }, onClosePanels() { this.setData({ showEmoji: false, showMorePanel: false }) }, onEmojiSelect(e) { const emoji = e.currentTarget.dataset.emoji this.setData({ inputText: this.data.inputText + emoji }) }, // ==================== ่ฏญ้Ÿณๅฝ•ๅˆถ ==================== onVoiceTouchStart(e) { this.touchStartY = e.touches[0].clientY this.setData({ isRecording: true, voiceCancelHint: false, recordingDuration: 0 }) const recorderManager = wx.getRecorderManager() recorderManager.start({ duration: 60000, format: 'mp3' }) this.recorderManager = recorderManager this.recordingTimer = setInterval(() => { this.setData({ recordingDuration: this.data.recordingDuration + 1 }) }, 1000) }, onVoiceTouchMove(e) { const moveY = e.touches[0].clientY const diff = this.touchStartY - moveY this.setData({ voiceCancelHint: diff > 50 }) }, onVoiceTouchEnd() { clearInterval(this.recordingTimer) const { voiceCancelHint, recordingDuration } = this.data this.setData({ isRecording: false }) if (this.recorderManager) { this.recorderManager.stop() } if (voiceCancelHint) { util.showToast('ๅทฒๅ–ๆถˆ') return } if (recordingDuration < 1) { util.showError('ๅฝ•้Ÿณๆ—ถ้—ดๅคช็Ÿญ') return } this.recorderManager.onStop(async (res) => { const tempFilePath = res.tempFilePath // ไธŠไผ ่ฏญ้Ÿณ try { const uploadRes = await api.uploadFile(tempFilePath, 'audio') if (uploadRes.success && uploadRes.data && uploadRes.data.url) { await this.sendMessage(uploadRes.data.url, 'voice', recordingDuration) } else { throw new Error('Upload failed') } } catch (err) { console.error('่ฏญ้ŸณไธŠไผ ๅคฑ่ดฅ', err) util.showError('่ฏญ้Ÿณๅ‘้€ๅคฑ่ดฅ') } }) }, onVoiceTouchCancel() { clearInterval(this.recordingTimer) this.setData({ isRecording: false }) if (this.recorderManager) { this.recorderManager.stop() } }, // ==================== ๅ›พ็‰‡/ๆ‹็…ง ==================== onChooseImage() { this.setData({ showMorePanel: false }) wx.chooseMedia({ count: 9, mediaType: ['image'], sourceType: ['album'], success: (res) => { res.tempFiles.forEach(file => { this.uploadAndSendImage(file.tempFilePath) }) } }) }, onTakePhoto() { this.setData({ showMorePanel: false }) wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['camera'], camera: 'back', success: (res) => { this.uploadAndSendImage(res.tempFiles[0].tempFilePath) } }) }, async uploadAndSendImage(filePath) { try { const uploadRes = await api.uploadFile(filePath, 'uploads') if (uploadRes.success && uploadRes.data && uploadRes.data.url) { const fullUrl = imageUrl.getFullImageUrl(uploadRes.data.url) await this.sendMessage(fullUrl, 'image') } else { throw new Error('Upload failed') } } catch (err) { console.error('ๅ›พ็‰‡ไธŠไผ ๅคฑ่ดฅ', err) util.showError('ๅ›พ็‰‡ๅ‘้€ๅคฑ่ดฅ') } }, // ==================== ้ข„่งˆไธŽๆ’ญๆ”พ ==================== onPreviewImage(e) { const url = e.currentTarget.dataset.url wx.previewImage({ current: url, urls: [url] }) }, onPlayVoice(e) { const { id, url } = e.currentTarget.dataset if (!url) return const innerAudioContext = wx.createInnerAudioContext() innerAudioContext.src = url innerAudioContext.play() this.setData({ playingVoiceId: id }) innerAudioContext.onEnded(() => { this.setData({ playingVoiceId: null }) innerAudioContext.destroy() }) innerAudioContext.onError((res) => { console.error(res.errMsg) this.setData({ playingVoiceId: null }) }) } })