🏭 模具行业一站式服务平台 — 找图纸·发任务·学技术·招人才
个人中心免费注册
📢 公告
- 🔥 新用户注册即送100积分,商城图纸免费下载
- 📋 最新商品:模具设计图纸全集 · VIP会员专享优惠
- 🏪 卖家入驻开放,免费开店上传模具资源
- 🌟 开通VIP会员即享全站图纸8折优惠
- 🔥 新用户注册即送100积分,商城图纸免费下载
- 📋 最新商品:模具设计图纸全集 · VIP会员专享优惠
- 🏪 卖家入驻开放,免费开店上传模具资源
- 🌟 开通VIP会员即享全站图纸8折优惠
📦
📦 模极社商城
模具技术资源中心
模具图纸·设计模型·教程文档·专业工具 — 专业资源一站购齐
搜索
共 {{total}} 件商品
{{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.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}}
🛒 确认订单
{{item.name}}
¥{{item.price}} × {{item.quantity}}
¥{{(item.price * item.quantity).toFixed(2)}}
合计
¥{{checkoutTotal.toFixed(2)}}
请先登录
返回购物车
{{checkoutLoading ? '创建订单…' : '提交订单 ¥'+checkoutTotal.toFixed(2)}}
✅
支付成功!
{{payResult.message}}
查看订单
继续购物
❌
支付失败
{{payResult.message || '请稍后重试'}}
重新下单
我的订单
🌟 VIP 会员计划
解锁更多特权,提升模具行业竞争力
加载套餐中…
{{vipError}}
{{
p.level==='student' ? '🎓' :
p.level==='basic' && p.duration===30 ? '🥇' :
p.level==='basic' ? '🌟' :
p.level==='elite' && p.duration===30 ? '💎' :
'👑'
}}
{{p.name}}
{{p.desc}}
{{{
p.level==='student' ? '学生专属' :
p.level==='basic' ? '认证会员' :
'精英会员'
}} · {{p.duration}}天
¥{{(p.price/100).toFixed(2)}} / {{p.duration}}天
相当于¥{{(p.price/100/p.duration).toFixed(2)}}/天
{{
vipStatus.level==='student' ? '🎓' :
vipStatus.level==='basic' ? '🌟' :
vipStatus.level==='elite' ? '💎' : '⭐'
}}
{{
vipStatus.level==='student' ? '学生会员' :
vipStatus.level==='basic' ? '认证会员' :
vipStatus.level==='elite' ? '精英会员' :
'普通用户'
}}
会员生效:{{vipStatus.vip_since}}
到期时间:{{vipStatus.vip_until}}
确认订单
套餐{{vipOrderProduct?.name}}
时长{{vipOrderProduct?.duration}}天
等级{{vipOrderProduct?.level}}
合计¥{{(vipOrderProduct?.price/100).toFixed(2)}}
{{vipOrderResult.success ? '✅' : '❌'}}
{{vipOrderResult.message}}
{{vipOrderResult.detail||''}}
💬 咨询卖家
我的订单
{{o.status==='paid'?'已支付':'待支付'}}
下载
模拟支付
加载中…
加载中…
暂无对话
{{ c.type === 'order' ? '📦' : c.type === 'course' ? '📚' : '💬' }}
{{ c.title || '对话' }}
{{ c.last_message?.content || '暂无消息' }}
{{ c.unread_count }}
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);
![]()