170 lines
7.0 KiB
JavaScript
170 lines
7.0 KiB
JavaScript
const apiFetch = async (url, options = {}) => {
|
|
const opts = {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options,
|
|
};
|
|
if (opts.body && typeof opts.body !== 'string') {
|
|
opts.body = JSON.stringify(opts.body);
|
|
}
|
|
const res = await fetch(url, opts);
|
|
if (!res.ok) {
|
|
throw new Error(`API error: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
};
|
|
|
|
const initFrontend = () => {
|
|
const contactsList = document.querySelector('[data-contacts]');
|
|
const chatForm = document.querySelector('[data-chat-form]');
|
|
const chatInput = document.querySelector('[data-chat-input]');
|
|
const chatBody = document.querySelector('[data-chat-body]');
|
|
const chatTitle = document.querySelector('[data-chat-title]');
|
|
const countryModal = new bootstrap.Modal(document.getElementById('countryModal'));
|
|
|
|
// Emoji Picker
|
|
const emojiPicker = document.querySelector('emoji-picker');
|
|
const emojiTrigger = document.querySelector('[data-emoji-trigger]');
|
|
|
|
if (emojiPicker && emojiTrigger) {
|
|
emojiPicker.addEventListener('emoji-click', event => {
|
|
chatInput.value += event.detail.unicode;
|
|
emojiPicker.classList.add('d-none');
|
|
});
|
|
|
|
emojiTrigger.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
emojiPicker.classList.toggle('d-none');
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!emojiTrigger.contains(e.target) && !emojiPicker.contains(e.target)) {
|
|
emojiPicker.classList.add('d-none');
|
|
}
|
|
});
|
|
}
|
|
|
|
let state = JSON.parse(localStorage.getItem('sms_state') || '{"contacts": {}, "messages": {}}');
|
|
let currentChatPhone = null;
|
|
|
|
const saveState = () => localStorage.setItem('sms_state', JSON.stringify(state));
|
|
|
|
const formatTime = (ts) => new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
|
const renderContacts = () => {
|
|
contactsList.innerHTML = Object.keys(state.contacts).sort((a,b) => (state.messages[b]?.slice(-1)[0]?.time || 0) - (state.messages[a]?.slice(-1)[0]?.time || 0)).map(phone => `
|
|
<div class="contact-item ${currentChatPhone === phone ? 'active' : ''}" data-contact="${phone}">
|
|
<div class="contact-avatar">${phone.substring(phone.length - 2)}</div>
|
|
<div class="contact-info flex-grow-1" onclick="switchChat('${phone}')">
|
|
<div class="fw-bold">${phone}</div>
|
|
<div class="small text-muted text-truncate">${state.messages[phone]?.slice(-1)[0]?.text || ''}</div>
|
|
</div>
|
|
<div class="contact-actions dropstart">
|
|
<a class="text-secondary" data-bs-toggle="dropdown" href="#">▼</a>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="deleteContact('${phone}')">删除</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="alert('拉黑')">拉黑</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
};
|
|
|
|
window.switchChat = (phone) => {
|
|
currentChatPhone = phone;
|
|
chatTitle.textContent = phone;
|
|
chatBody.innerHTML = (state.messages[phone] || []).map((m, idx) => `
|
|
<div class="mb-2 ${m.type === 'out' ? 'text-end' : 'text-start'}">
|
|
<div class="message-bubble d-inline-block ${m.type === 'out' ? 'bg-success text-white' : 'bg-white'} p-2 rounded"
|
|
oncontextmenu="event.preventDefault(); showMsgActions(event, '${phone}', ${idx})">
|
|
${m.text}
|
|
<div class="msg-time">${formatTime(m.time)}</div>
|
|
<div class="msg-actions dropdown">
|
|
<a href="#" class="text-secondary" data-bs-toggle="dropdown">▼</a>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="msgAction('edit', '${phone}', ${idx})">编辑</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="msgAction('delete', '${phone}', ${idx})">删除</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="msgAction('recall', '${phone}', ${idx})">撤回</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
renderContacts();
|
|
};
|
|
|
|
window.showMsgActions = (e, phone, idx) => {
|
|
const bubble = e.currentTarget;
|
|
const dropdown = bubble.querySelector('.dropdown-toggle') || bubble.querySelector('[data-bs-toggle="dropdown"]');
|
|
if (dropdown) {
|
|
const bsDropdown = new bootstrap.Dropdown(dropdown);
|
|
bsDropdown.show();
|
|
}
|
|
};
|
|
|
|
window.msgAction = (action, phone, idx) => {
|
|
if (action === 'delete' || action === 'recall') {
|
|
state.messages[phone].splice(idx, 1);
|
|
} else if (action === 'edit') {
|
|
const newText = prompt('编辑消息:', state.messages[phone][idx].text);
|
|
if (newText) state.messages[phone][idx].text = newText;
|
|
}
|
|
saveState();
|
|
switchChat(phone);
|
|
};
|
|
|
|
window.deleteContact = (phone) => {
|
|
delete state.contacts[phone];
|
|
delete state.messages[phone];
|
|
if (currentChatPhone === phone) {
|
|
currentChatPhone = null;
|
|
chatTitle.textContent = '请选择联系人';
|
|
chatBody.innerHTML = '';
|
|
}
|
|
saveState();
|
|
renderContacts();
|
|
};
|
|
|
|
const selectedCode = document.getElementById('selectedCode');
|
|
const phoneNumber = document.getElementById('phoneNumber');
|
|
|
|
document.getElementById('startChat').addEventListener('click', () => {
|
|
if (!phoneNumber.value) return;
|
|
const phone = selectedCode.textContent + phoneNumber.value;
|
|
if (!state.contacts[phone]) {
|
|
state.contacts[phone] = { created: Date.now() };
|
|
state.messages[phone] = [];
|
|
}
|
|
currentChatPhone = phone;
|
|
saveState();
|
|
renderContacts();
|
|
switchChat(phone);
|
|
countryModal.hide();
|
|
});
|
|
|
|
chatForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
if(!currentChatPhone || !chatInput.value) return;
|
|
const msg = { type: 'out', text: chatInput.value, time: Date.now() };
|
|
state.messages[currentChatPhone].push(msg);
|
|
saveState();
|
|
chatInput.value = '';
|
|
switchChat(currentChatPhone);
|
|
|
|
apiFetch('/api/messages.php', { method: 'POST', body: { phone: currentChatPhone, body: msg.text, direction: 'out' } }).catch(() => {});
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
currentChatPhone = null;
|
|
chatTitle.textContent = '请选择联系人';
|
|
chatBody.innerHTML = '';
|
|
renderContacts();
|
|
}
|
|
});
|
|
|
|
renderContacts();
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (document.body.dataset.page === 'agent') initFrontend();
|
|
}); |