搜索
{{p.image_url ? '' : '📦'}}
{{p.name || p.name_zh || p.title}}
¥{{(p.vip_price && user.is_vip ? p.vip_price : p.price) || 0}}
¥{{p.price}}
已售 {{p.sales_count||0}}
{page=p;loadProducts()}" :current-page="page"/>
{{detail.image_url?detail.image_url:'📦'}}
{{detail.name_zh}}
{{detail.name_en}}
¥{{(detail.vip_price && user.is_vip ? detail.vip_price : detail.price) || 0}}
¥{{detail.price}}
VIP 优惠价
📄 {{detail.file_name}}
📏 {{(detail.file_size/1024/1024).toFixed(1)}} MB
👥 已售 {{detail.sales_count}}
{{detail.description_zh || detail.description || '暂无描述'}}
{{user.authenticated ? '立即购买' : '请先登录'}}
加入购物车
✅ 您已购买此商品
{{downloadInfo.product_name}}
💬 咨询卖家
我的订单
{{o.status==='paid'?'已支付':'待支付'}}
下载
模拟支付
加载中…
🌟 VIP会员计划
解锁更多特权,提升模具开发效率
{{ p.level==='elite' ? '💎' : p.level==='basic' ? '⭐' : '🎓' }}
{{ p.level==='elite' ? '精英' : p.level==='basic' ? '认证' : '学生' }}{{ p.duration >= 365 ? '·年卡' : '·月卡' }}
{{ p.name }}
{{ p.desc }}
¥{{ (p.price/100).toFixed(2) }} / {{ p.duration }}天
我的商品({{ sellerProducts.length }})
{{ showNewProduct ? '取消' : '+ 新增商品' }}
| 商品名称 | 价格 | 状态 | 下载 | 操作 |
| {{ p.name }} |
¥{{ (p.price/100).toFixed(2) }} |
{{ p.is_active ? '已上架' : '待审核' }} |
{{ p.download_count || 0 }}次 |
编辑 |
*
*
* 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:#409eff;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:#409eff}
/* ---- 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:#409eff;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:#409eff;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);