🏭 模具行业一站式服务平台 — 找图纸·发任务·学技术·招人才 个人中心免费注册
🏭 模具行业一站式服务平台 个人中心 免费注册
📢 公告
  • 🔥 新用户注册即送100积分,商城图纸免费下载
  • 📋 最新商品:模具设计图纸全集 · VIP会员专享优惠
  • 🏪 卖家入驻开放,免费开店上传模具资源
  • 🌟 开通VIP会员即享全站图纸8折优惠
  • 🔥 新用户注册即送100积分,商城图纸免费下载
  • 📋 最新商品:模具设计图纸全集 · VIP会员专享优惠
  • 🏪 卖家入驻开放,免费开店上传模具资源
  • 🌟 开通VIP会员即享全站图纸8折优惠
加载中…
🛒 购物车
{{item.name}}
¥{{item.price}} x {{item.quantity}}
合计 ¥{{cartTotal.toFixed(2)}}
去结算

购物车是空的

加载对话中…
const tab = ref('list'); const user = ref({authenticated: false, is_vip: false, username: '', user_id: ''}); const products = ref([]); const categories = ref([]); const detail = ref({}); const orders = ref([]); const cartItems = ref([]); const checkoutItems = ref([]); const checkoutTotal = ref(0); const checkoutLoading = ref(false); const payResult = ref({success: false, message: '', downloadUrl: ''}); const downloadInfo = ref(null); // TradeChat 对话 const chatDialog = ref({visible: false, loading: false, convId: null, messages: [], text: '', loadingMsgs: false}); // KeenChat const keenDialog = ref({visible: false, loading: false, convs: [], search: ''}); const keenUnread = ref(0); const filteredConvs = computed(() => { const q = keenDialog.value.search.toLowerCase().trim(); if (!q) return keenDialog.value.convs; return keenDialog.value.convs.filter(c => (c.title||'').toLowerCase().includes(q) || (c.last_message?.content||'').toLowerCase().includes(q)); }); const page = ref(1); const pageSize = ref(20); const total = ref(0); const filters = ref({category: '', keyword: ''}); const loading = ref(false); const loadingDetail = ref(false); // VIP state const vipProducts = ref([]); const vipSelectedId = ref(null); const vipLoading = ref(false); const vipError = ref(''); const vipBuying = ref(false); const vipOrderDialog = ref(false); const vipOrderProduct = ref(null); const vipOrderProcessing = ref(false); const vipOrderResult = ref(null); const vipStatus = ref(null); const vipStatusLoading = ref(false); const totalPages = computed(() => Math.ceil(total.value / pageSize.value)); const cartCount = computed(() => cartItems.value.reduce((s, i) => s + i.quantity, 0)); const cartTotal = computed(() => cartItems.value.reduce((s, i) => s + i.price * i.quantity, 0)); async function checkAuth() { try { const r = await fetch(API + '/auth/me', {credentials: 'include'}); const d = await r.json(); if (d.authenticated) user.value = {...user.value, ...d}; } catch(e) {} } // 商品API使用独立域名(auth等其他API保持原路径) async function loadProducts() { loading.value = true; try { const params = new URLSearchParams({page: page.value, page_size: pageSize.value}); if (filters.value.category) params.set('category_id', filters.value.category); if (filters.value.keyword) params.set('keyword', filters.value.keyword); const r = await fetch(PAPI + '/products/list?' + params); const d = await r.json(); products.value = d.items || []; total.value = d.total || 0; } catch(e) { products.value = []; total.value = 0; } finally { loading.value = false; } } async function loadCategories() { try { const r = await fetch(PAPI + '/products/categories/list'); const d = await r.json(); categories.value = Array.isArray(d) ? d : d.items || []; } catch(e) { categories.value = []; } } async function showDetail(id) { tab.value = 'detail'; detail.value = {}; downloadInfo.value = null; loadingDetail.value = true; try { const r = await fetch(PAPI + '/products/detail/' + id); detail.value = await r.json(); } catch(e) {} finally { loadingDetail.value = false; // 挂载TradeChat setTimeout(() => { }, 200); } } // KeenChat: 打开消息中心 async function openKeenChat() { keenDialog.value.visible = true; keenDialog.value.loading = true; try { const MAPI = '/messaging/api'; const uid = parseInt(user.value.user_id); if (!uid) return; const r = await fetch(MAPI + '/conversations?user_id=' + uid); const d = await r.json(); keenDialog.value.convs = d.data || []; // 更新未读计数 const unreadR = await fetch(MAPI + '/overview?user_id=' + uid); const unreadD = await unreadR.json(); if (unreadD.code === 0) keenUnread.value = unreadD.data.total_unread || 0; } catch(e) {} keenDialog.value.loading = false; } function openKeenConv(c) { // 打开订单对话(如果有商品ID就打开商品详情) if (c.type === 'order' && c.order_id) { keenDialog.value.visible = false; showDetail(c.order_id); } } // 轮询未读 let keenPollTimer = null; onMounted(() => { // 已有onMounted,不重复注册 }); // TradeChat: 打开订单对话弹窗 async function openOrderChat(productId, sellerId) { const uid = parseInt(user.value.user_id); if (!uid || !sellerId || uid === sellerId) return; chatDialog.value.visible = true; chatDialog.value.loading = true; chatDialog.value.messages = []; chatDialog.value.convId = null; chatDialog.value.text = ''; chatDialog.value._scrollTimer = null; try { // 查找现有对话 const MAPI = '/messaging/api'; const res = await fetch(MAPI + '/conversations?user_id=' + uid); const data = await res.json(); const convs = data.data || []; const existing = convs.find(c => c.type === 'order' && String(c.order_id) === String(productId)); let convId = existing ? existing.conversation_id : null; // 如果没有则创建 if (!convId) { const cr = await fetch(MAPI + '/conversations/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ user_id: uid, type: 'order', participant_ids: [sellerId], order_id: productId, title: '订单 #' + productId + ' 咨询' }) }); const cd = await cr.json(); convId = cd.conversation_id || cd.data?.conversation_id; } chatDialog.value.convId = convId; if (convId) { chatDialog.value.loadingMsgs = true; const mr = await fetch(MAPI + '/messages?user_id=' + uid + '&conversation_id=' + convId); const md = await mr.json(); chatDialog.value.messages = md.data || md.messages || md || []; chatDialog.value.loadingMsgs = false; nextTick(() => { const el = document.querySelector('.el-dialog__body [style*="overflow-y"]'); if (el) el.scrollTop = el.scrollHeight; }); } } catch(e) { ElMessage.error('加载对话失败'); } chatDialog.value.loading = false; } // 发送对话消息 async function sendChatMsg() { const text = chatDialog.value.text.trim(); if (!text || !chatDialog.value.convId) return; const msg = { msg_id: 'tmp_' + Date.now(), sender_id: parseInt(user.value.user_id), content: text, created_at: new Date().toISOString() }; chatDialog.value.messages.push(msg); chatDialog.value.text = ''; try { await fetch(MAPI + '/messages/send', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ user_id: parseInt(user.value.user_id), conversation_id: chatDialog.value.convId, content: text }) }); } catch(e) { ElMessage.error('发送失败'); } } function doLogin() { window.location.href = 'https://mj7.cn/login?redirect=' + encodeURIComponent('https://mj7.cn/store/'); } async function buyNow(productId) { if (!user.value.authenticated) { doLogin(); return; } try { const r = await fetch(API + '/orders/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: user.value.user_id, product_id: productId}) }); const d = await r.json(); if (d.order_id || d.id) { const orderId = d.order_id || d.id; // 跳转支付页面 fetch(API + '/orders/' + orderId + '/pay', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'}); window.open('/api/orders/' + orderId, '_blank'); ElMessage.success('订单已创建'); setTimeout(() => { tab.value = 'orders'; loadOrders(); }, 3000); } } catch(e) { ElMessage.error('下单失败,请重试'); } } async function addToCart(productId) { if (!user.value.authenticated) { doLogin(); return; } try { const r = await fetch(API + '/cart/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user_id: user.value.user_id, product_id: productId}) }); const d = await r.json(); if (d.success) { ElMessage.success('已加入购物车'); loadCart(); } else { ElMessage.warning(d.message || '加入购物车失败'); } } catch(e) { ElMessage.error('网络错误'); } } async function loadCart() { if (!user.value.authenticated) return; try { const r = await fetch(API + '/cart/?user_id=' + user.value.user_id); const d = await r.json(); cartItems.value = d.items || []; } catch(e) { cartItems.value = []; } } async function loadOrders() { if (!user.value.authenticated) return; loading.value = true; try { const r = await fetch(API + '/orders/?user_id=' + user.value.user_id); const d = await r.json(); orders.value = d.items || d || []; } catch(e) { orders.value = []; } finally { loading.value = false; } } function simulatePay(o) { const orderId = o.id || o.order_id; fetch(API + '/orders/' + orderId + '/pay', {method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'}); window.open('/api/orders/' + orderId, '_blank'); ElMessage.success('支付中…'); setTimeout(loadOrders, 3000); } async function checkDownload(o) { try { const r = await fetch(API + '/orders/' + o.id + '/download'); const d = await r.json(); if (d.file_url) window.open(d.file_url); else ElMessage.warning('下载链接暂不可用'); } catch(e) { ElMessage.error('下载出错'); } } async function removeFromCart(item) { try { const url = API + '/cart/' + item.id + '/'; const r = await fetch(url, {method: 'DELETE', headers:{'Content-Type':'application/json'}}); const d = await r.json(); if (d.success || d.message) { ElMessage.success('已移除'); loadCart(); } else { ElMessage.warning('移除失败'); } } catch(e) { // fallback: remove locally cartItems.value = cartItems.value.filter(i => i.id !== item.id); ElMessage.success('已移除'); } } function goCheckout() { if (!user.value.authenticated) { doLogin(); return; } if (!cartItems.value.length) { ElMessage.warning('购物车为空'); return; } checkoutItems.value = cartItems.value.map(i => ({...i})); checkoutTotal.value = checkoutItems.value.reduce((s, i) => s + i.price * i.quantity, 0); tab.value = 'checkout'; } async function checkoutCreateOrder() { if (!user.value.authenticated) { doLogin(); return; } checkoutLoading.value = true; try { const r = await fetch(API + '/orders/create', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ user_id: user.value.user_id, items: checkoutItems.value.map(i => ({product_id: i.product_id || i.id, quantity: i.quantity})) }) }); const d = await r.json(); const orderId = d.order_id || d.id; if (!orderId) { payResult.value = {success: false, message: '下单失败', downloadUrl: ''}; tab.value = 'pay-result'; checkoutLoading.value = false; return; } // Mock Pay try { const payResp = await fetch(API + '/orders/' + orderId + '/pay', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}' }); const payData = await payResp.json(); if (payData.success || payData.code === 0) { // Get download URL let downloadUrl = ''; try { const dlResp = await fetch(API + '/orders/' + orderId + '/download'); const dlData = await dlResp.json(); downloadUrl = dlData.file_url || ''; } catch(e) {} payResult.value = { success: true, message: '订单 #' + orderId + ' 已完成支付', downloadUrl: downloadUrl }; // Clear cart cartItems.value = []; await loadOrders(); } else { payResult.value = {success: false, message: payData.message || '支付失败', downloadUrl: ''}; } } catch(e) { payResult.value = {success: false, message: '支付请求失败', downloadUrl: ''}; } tab.value = 'pay-result'; } catch(e) { payResult.value = {success: false, message: '网络错误,请重试', downloadUrl: ''}; tab.value = 'pay-result'; } checkoutLoading.value = false; } function doDownload() { if (downloadInfo.value?.file_url) window.open(downloadInfo.value.file_url); } // ========== VIP Functions ========== async function loadVipProducts() { vipLoading.value = true; vipError.value = ''; try { const r = await fetch('https://mj7.cn/api/v1/vip/products'); const d = await r.json(); if (d.code === 0 && d.data) { vipProducts.value = d.data; } else { vipError.value = '加载套餐失败'; } } catch(e) { vipError.value = '网络错误,请刷新重试'; } vipLoading.value = false; } function vipBuy(product) { vipSelectedId.value = product.id; vipOrderProduct.value = product; vipOrderResult.value = null; vipOrderDialog.value = true; } async function vipConfirmOrder() { if (!vipOrderProduct.value || !user.value.authenticated) return; vipOrderProcessing.value = true; try { // Step 1: Create order const createResp = await fetch('https://mj7.cn/api/v1/vip/order/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: parseInt(user.value.user_id), product_id: vipOrderProduct.value.id }) }); const createData = await createResp.json(); if (createData.code !== 0 || !createData.data) { vipOrderResult.value = { success: false, message: '下单失败', detail: createData.message || '' }; vipOrderProcessing.value = false; return; } const orderId = createData.data.order_id || createData.data.id; // Step 2: Mock Pay const payResp = await fetch('https://mj7.cn/api/v1/vip/mock-pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: orderId }) }); const payData = await payResp.json(); if (payData.code === 0) { vipOrderResult.value = { success: true, message: '🎉 支付成功!', detail: '您已成功开通 ' + vipOrderProduct.value.name + ',部分功能可能需要刷新页面生效。' }; user.value.is_vip = true; } else { vipOrderResult.value = { success: false, message: '支付失败', detail: payData.message || '请稍后重试' }; } } catch(e) { vipOrderResult.value = { success: false, message: '网络错误', detail: '请检查网络连接后重试' }; } vipOrderProcessing.value = false; } async function loadVipStatus() { if (!user.value.authenticated || !user.value.user_id) return; vipStatusLoading.value = true; try { const r = await fetch('https://mj7.cn/api/v1/vip/status?user_id=' + parseInt(user.value.user_id)); const d = await r.json(); vipStatus.value = d.data || d; } catch(e) { vipStatus.value = { is_vip: false }; } vipStatusLoading.value = false; } onMounted(async () => { // Hash routing: support /store/#vip or /store/#vip-status const hash = window.location.hash.slice(1); if (hash === 'vip' || hash === 'vip-status') { tab.value = hash; } await checkAuth(); await loadCategories(); await loadProducts(); await loadVipProducts(); if (user.value.authenticated) { await loadCart(); await loadOrders(); // KeenChat 轮询未读 setInterval(async () => { try { const uid = parseInt(user.value.user_id); if (!uid) return; const r = await fetch('/messaging/api/overview?user_id=' + uid); const d = await r.json(); if (d.code === 0) keenUnread.value = d.data.total_unread || 0; } catch(e) {} }, 30000); } }); return { tab, user, products, categories, detail, orders, cartItems, downloadInfo, page, pageSize, total, filters, totalPages, cartCount, loading, loadingDetail, chatDialog, keenDialog, keenUnread, filteredConvs, loadProducts, showDetail, doLogin, buyNow, addToCart, loadOrders, simulatePay, checkDownload, doDownload, openOrderChat, sendChatMsg, openKeenChat, openKeenConv, vipProducts, vipSelectedId, vipLoading, vipError, vipBuying, vipOrderDialog, vipOrderProduct, vipOrderProcessing, vipOrderResult, vipStatus, vipStatusLoading, loadVipProducts, vipBuy, vipConfirmOrder, loadVipStatus, cartTotal, checkoutItems, checkoutTotal, checkoutLoading, payResult, removeFromCart, goCheckout, checkoutCreateOrder }; } }); app.use(ElementPlus); app.mount('#app'); * *
* TradeChat.mount('#trade-chat-root') */ (function (global) { 'use strict'; // ============================================================ // 样式 // ============================================================ const STYLE_ID = 'mj-trade-chat-styles'; if (!document.getElementById(STYLE_ID)) { const css = document.createElement('style'); css.id = STYLE_ID; css.textContent = ` /* ---- TradeChat Container ---- */ .trade-chat-wrap{background:#fff;border-radius:8px;border:1px solid #e4e7ed;overflow:hidden;margin-top:16px} .trade-chat-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid #e4e7ed;background:#f8f9fa} .trade-chat-header h3{margin:0;font-size:14px;font-weight:600;color:#303133} .trade-chat-header .hint{font-size:12px;color:#909399} .trade-chat-header .toggle-btn{background:none;border:none;color:#1D4ED8;font-size:13px;cursor:pointer;padding:4px 8px;border-radius:4px} .trade-chat-header .toggle-btn:hover{background:#ecf5ff} /* ---- Messages ---- */ .trade-chat-body{max-height:360px;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:8px;scroll-behavior:smooth} .trade-chat-body .loading{text-align:center;padding:12px;color:#c0c4cc;font-size:12px} .trade-chat-body .load-more{text-align:center;padding:6px;color:#909399;font-size:12px;cursor:pointer} .trade-chat-body .load-more:hover{color:#1D4ED8} /* ---- Bubbles ---- */ .trade-msg{max-width:80%;padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.6;word-break:break-word;position:relative;animation:fadeInChat .2s} @keyframes fadeInChat{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}} .trade-msg.buyer{align-self:flex-start;background:#f0f2f5;color:#303133;border-bottom-left-radius:4px} .trade-msg.seller{align-self:flex-end;background:#1D4ED8;color:#fff;border-bottom-right-radius:4px} .trade-msg.system{align-self:center;font-size:12px;color:#909399;background:transparent;padding:4px 8px;max-width:95%;text-align:center} .trade-msg .meta{font-size:10px;margin-top:4px;opacity:.6} .trade-msg.seller .meta{color:rgba(255,255,255,.7)} .trade-msg.buyer .meta{color:#c0c4cc} .trade-msg .sender-name{font-weight:600;font-size:11px;margin-bottom:2px;display:block} .trade-msg.seller .sender-name{color:rgba(255,255,255,.8)} .trade-msg.buyer .sender-name{color:#606266} /* ---- Input ---- */ .trade-chat-input{display:flex;align-items:center;padding:10px 16px;border-top:1px solid #e4e7ed;gap:8px} .trade-chat-input textarea{flex:1;border:none;outline:none;resize:none;font-size:13px;font-family:inherit;padding:8px 0;max-height:60px;min-height:18px;line-height:1.5} .trade-chat-input .send-btn{width:34px;height:34px;border-radius:50%;background:#1D4ED8;color:#fff;border:none;cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0} .trade-chat-input .send-btn:hover{background:#66b1ff} .trade-chat-input .send-btn:disabled{background:#c0c4cc;cursor:not-allowed} /* ---- Error / Empty ---- */ .trade-chat-error{padding:10px 20px;background:#fef0f0;color:#f56c6c;font-size:12px;display:flex;align-items:center;gap:6px} .trade-chat-empty{padding:30px 20px;text-align:center;color:#c0c4cc;font-size:13px} .trade-chat-empty .icon{font-size:32px;display:block;margin-bottom:8px} /* ---- Unread Banner ---- */ .trade-chat-unread{padding:8px 20px;background:#fdf6ec;color:#e6a23c;font-size:12px;display:flex;align-items:center;gap:6px;cursor:pointer;border-bottom:1px solid #faecd8} .trade-chat-unread:hover{background:#fef0db} /* ---- Collapsed ---- */ .trade-chat-wrap.collapsed .trade-chat-body, .trade-chat-wrap.collapsed .trade-chat-input{display:none} .trade-chat-wrap.collapsed .trade-chat-unread{display:flex !important} `; document.head.appendChild(css); } // ============================================================ // TradeChat 组件 // ============================================================ class TradeChat { constructor(options = {}) { this.orderId = options.orderId; this.userId = options.userId; this.partnerId = options.partnerId; // 对方user_id(卖家或买家) this.isSeller = options.isSeller || false; // 当前用户是卖家? this.sdk = options.sdk; this.container = options.container || document.body; this.conversationId = null; this.messages = []; this._collapsed = false; this._build(); this._init(); } _build() { this._el = document.createElement('div'); this._el.className = 'trade-chat-wrap'; this._el.innerHTML = `

💬 订单沟通

咨询卖家
加载中…
`; this._body = this._el.querySelector('.trade-chat-body'); this._input = this._el.querySelector('[data-input]'); this._sendBtn = this._el.querySelector('[data-action="send"]'); this._headerToggle = this._el.querySelector('[data-action="toggle"]'); this._unreadBanner = this._el.querySelector('[data-action="expand"]'); this.container.appendChild(this._el); } async _init() { if (!this.sdk) { if (global.MessagingSDK && this.userId) { this.sdk = new MessagingSDK({ userId: this.userId }); try { await this.sdk.init(); } catch(e) {} } else { return; } } // 1. 先找有没有现存的订单对话 await this._findOrCreateConversation(); // 2. 绑定事件 this._bindEvents(); // 3. 加载消息 if (this.conversationId) { await this._loadMessages(); } } async _findOrCreateConversation() { try { // 查看现有对话 const convs = await this.sdk.getConversations(); const existing = convs.find(c => c.type === 'order' && String(c.order_id) === String(this.orderId) ); if (existing) { this.conversationId = existing.conversation_id; return; } // 如果没有则创建新对话 const result = await this.sdk.createConversation({ type: 'order', orderId: this.orderId, participantIds: this.partnerId ? [this.partnerId] : [], title: `订单 #${this.orderId} 咨询`, }); if (result.conversation_id) { this.conversationId = result.conversation_id; } } catch (e) { this._showError('对话创建失败'); } } _bindEvents() { // 发送 this._sendBtn.addEventListener('click', () => this._send()); this._input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this._send(); } }); this._input.addEventListener('input', () => { this._sendBtn.disabled = !this._input.value.trim(); // auto-resize this._input.style.height = 'auto'; this._input.style.height = Math.min(this._input.scrollHeight, 60) + 'px'; // 发送typing if (this.sdk && this.conversationId) { this.sdk.sendTyping(this.conversationId); } }); // 折叠/展开 this._headerToggle.addEventListener('click', () => this._toggle()); this._unreadBanner.addEventListener('click', () => this._expand()); // SDK 事件 if (this.sdk) { this.sdk.on('new_message', (msg) => { if (msg.conversation_id === this.conversationId) { this.messages.push(msg.message); this._renderMessages(); this._scrollBottom(); } }); this.sdk.on('unread_update', (data) => { if (this._collapsed && data.conversations) { const conv = data.conversations.find(c => c.conversation_id === this.conversationId); if (conv && conv.unread_count > 0) { this._unreadBanner.style.display = 'flex'; } } }); } } async _loadMessages() { if (!this.sdk || !this.conversationId) return; try { const msgs = await this.sdk.getMessages(this.conversationId, { limit: 30 }); this.messages = msgs || []; this._renderMessages(); this._scrollBottom(); } catch (e) { this._showError('消息加载失败'); } } _renderMessages() { if (!this.messages.length) { this._body.innerHTML = '
💬暂无对话消息
发送第一条消息开始咨询
'; return; } this._body.innerHTML = ''; if (this.messages.length > 20) { const loadMore = document.createElement('div'); loadMore.className = 'load-more'; loadMore.textContent = '加载更早消息…'; loadMore.addEventListener('click', async () => { const oldest = this.messages[0]; if (!oldest) return; try { const older = await this.sdk.getMessages(this.conversationId, { before: oldest.msg_id, limit: 20 }); if (older && older.length) { this.messages = [...older, ...this.messages]; this._renderMessages(); } else { loadMore.textContent = '没有更多消息'; } } catch(e) { loadMore.textContent = '加载失败'; } }); this._body.appendChild(loadMore); } this.messages.forEach(msg => { this._body.appendChild(this._createBubble(msg)); }); } _createBubble(msg) { const isSeller = msg.sender_id !== this.userId; const isSystem = msg.msg_type === 'system'; const bubble = document.createElement('div'); bubble.className = `trade-msg ${isSystem ? 'system' : isSeller ? 'seller' : 'buyer'}`; bubble.dataset.msgId = msg.msg_id; let html = ''; if (isSeller && !isSystem) { html += '卖家'; } else if (msg.sender_name) { html += `${this.escapeHtml(msg.sender_name)}`; } html += this.escapeHtml(msg.content || ''); if (msg.created_at) { const time = new Date(msg.created_at).toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit' }); html += `
${time}
`; } bubble.innerHTML = html; return bubble; } async _send() { const text = this._input.value.trim(); if (!text || !this.sdk || !this.conversationId) return; this._input.value = ''; this._input.style.height = 'auto'; this._sendBtn.disabled = true; // 乐观更新 const optimistic = { msg_id: `temp_${Date.now()}`, sender_id: this.userId, content: text, msg_type: 'text', created_at: new Date().toISOString(), }; this.messages.push(optimistic); this._renderMessages(); this._scrollBottom(); try { await this.sdk.sendWithAck(this.conversationId, text); } catch (e) { this._showError('发送失败'); } } _toggle() { this._collapsed = !this._collapsed; this._el.classList.toggle('collapsed', this._collapsed); this._headerToggle.textContent = this._collapsed ? '展开' : '收起'; } _expand() { this._collapsed = false; this._el.classList.remove('collapsed'); this._headerToggle.textContent = '收起'; this._unreadBanner.style.display = 'none'; // 标记已读 if (this.sdk && this.conversationId && this.messages.length) { const last = this.messages[this.messages.length - 1]; if (last.msg_id && typeof last.msg_id === 'number') { this.sdk.markReadWS(this.conversationId, last.msg_id); } } } _scrollBottom() { requestAnimationFrame(() => { this._body.scrollTop = this._body.scrollHeight; }); } _showError(msg) { const errEl = document.createElement('div'); errEl.className = 'trade-chat-error'; errEl.innerHTML = `⚠️ ${this.escapeHtml(msg)}`; this._el.insertBefore(errEl, this._body); setTimeout(() => errEl.remove(), 4000); } escapeHtml(text) { if (!text) return ''; const d = document.createElement('div'); d.textContent = text; return d.innerHTML; } } global.TradeChat = TradeChat; })(typeof window !== 'undefined' ? window : this);