253 lines
9.0 KiB
JavaScript
253 lines
9.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 showToast = (message, type = 'success') => {
|
|
const container = document.querySelector('.toast-container');
|
|
if (!container) return;
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast align-items-center text-bg-${type} border-0 show`;
|
|
toast.setAttribute('role', 'alert');
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">${message}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>`;
|
|
container.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3200);
|
|
};
|
|
|
|
const formatTime = (value) => {
|
|
if (!value) return '';
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return value;
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
};
|
|
|
|
const initAgent = () => {
|
|
const listEl = document.querySelector('[data-contacts]');
|
|
const chatTitle = document.querySelector('[data-chat-title]');
|
|
const chatMeta = document.querySelector('[data-chat-meta]');
|
|
const chatBody = document.querySelector('[data-chat-body]');
|
|
const form = document.querySelector('[data-chat-form]');
|
|
const input = document.querySelector('[data-chat-input]');
|
|
const emojiTrigger = document.querySelector('[data-emoji-trigger]');
|
|
const emojiPicker = document.querySelector('[data-emoji-picker]');
|
|
const shortcutContainer = document.querySelector('[data-shortcuts]');
|
|
const shortcutList = document.getElementById('shortcutList');
|
|
const newShortcut = document.getElementById('newShortcut');
|
|
const addShortcutBtn = document.getElementById('addShortcut');
|
|
|
|
let contacts = [];
|
|
let activeId = null;
|
|
let shortcuts = JSON.parse(localStorage.getItem('sms_shortcuts') || '[]');
|
|
|
|
// Quick reply logic
|
|
const renderShortcuts = () => {
|
|
shortcutList.innerHTML = '';
|
|
shortcutContainer.innerHTML = '';
|
|
shortcuts.forEach((s, i) => {
|
|
const li = document.createElement('li');
|
|
li.className = 'list-group-item d-flex justify-content-between';
|
|
li.innerHTML = `<span>${s}</span><button class="btn btn-sm btn-danger" onclick="removeShortcut(${i})">删除</button>`;
|
|
shortcutList.appendChild(li);
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'btn btn-sm btn-outline-primary me-1 mb-1';
|
|
btn.textContent = s;
|
|
btn.onclick = () => { input.value = s; };
|
|
shortcutContainer.appendChild(btn);
|
|
});
|
|
};
|
|
|
|
window.removeShortcut = (index) => {
|
|
shortcuts.splice(index, 1);
|
|
localStorage.setItem('sms_shortcuts', JSON.stringify(shortcuts));
|
|
renderShortcuts();
|
|
};
|
|
|
|
addShortcutBtn.onclick = () => {
|
|
const val = newShortcut.value.trim();
|
|
if (!val) return;
|
|
shortcuts.push(val);
|
|
localStorage.setItem('sms_shortcuts', JSON.stringify(shortcuts));
|
|
newShortcut.value = '';
|
|
renderShortcuts();
|
|
};
|
|
renderShortcuts();
|
|
|
|
// Emoji picker
|
|
emojiTrigger.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
emojiPicker.classList.toggle("d-none");
|
|
});
|
|
|
|
emojiPicker.addEventListener("emoji-click", e => {
|
|
input.value += e.detail.unicode;
|
|
emojiPicker.classList.add('d-none');
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!emojiTrigger.contains(e.target) && !emojiPicker.contains(e.target)) {
|
|
emojiPicker.classList.add('d-none');
|
|
}
|
|
});
|
|
|
|
// Country selector
|
|
const countryListEl = document.getElementById('countryList');
|
|
const countrySearch = document.getElementById('countrySearch');
|
|
const selectedCode = document.getElementById('selectedCode');
|
|
const phoneInput = document.getElementById('phoneNumber');
|
|
const startChatBtn = document.getElementById('startChat');
|
|
|
|
const renderCountries = (filter = "") => {
|
|
countryListEl.innerHTML = '';
|
|
countries.filter(c => c.name.includes(filter)).forEach(c => {
|
|
const item = document.createElement('div');
|
|
item.className = 'country-item';
|
|
item.innerHTML = `${c.name} <small class="text-muted">(${c.code})</small>`;
|
|
item.onclick = () => {
|
|
selectedCode.textContent = c.code;
|
|
countrySearch.value = "";
|
|
renderCountries();
|
|
};
|
|
countryListEl.appendChild(item);
|
|
});
|
|
};
|
|
countrySearch.addEventListener('input', (e) => renderCountries(e.target.value));
|
|
renderCountries();
|
|
|
|
startChatBtn.addEventListener('click', async () => {
|
|
const fullPhone = selectedCode.textContent + phoneInput.value;
|
|
if (!phoneInput.value) return showToast('请输入手机号', 'danger');
|
|
try {
|
|
const res = await apiFetch('/api/contacts.php', { method: 'POST', body: { phone: fullPhone } });
|
|
bootstrap.Modal.getInstance(document.getElementById('countryModal')).hide();
|
|
phoneInput.value = '';
|
|
await loadContacts();
|
|
await setActive(res.id);
|
|
} catch (e) { showToast('无法启动聊天', 'danger'); }
|
|
});
|
|
|
|
// ESC to exit
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
activeId = null;
|
|
chatTitle.textContent = '请选择联系人';
|
|
chatMeta.textContent = '状态';
|
|
chatBody.innerHTML = '';
|
|
renderContacts();
|
|
}
|
|
});
|
|
|
|
const renderContacts = () => {
|
|
listEl.innerHTML = '';
|
|
contacts.forEach((contact) => {
|
|
const item = document.createElement('div');
|
|
item.className = `contact-item ${contact.id === activeId ? 'active' : ''}`;
|
|
|
|
item.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-center w-100">
|
|
<div style="flex-grow: 1;" onclick="setActive('${contact.id}')">
|
|
<div class="fw-semibold">${contact.phone}</div>
|
|
<div class="meta text-truncate" style="font-size: 0.8em; color: #666;">${contact.last_message || '暂无消息'}</div>
|
|
</div>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-link text-muted" data-bs-toggle="dropdown" aria-expanded="false">▼</button>
|
|
<ul class="dropdown-menu">
|
|
<li><button class="dropdown-item text-danger" onclick="deleteContact('${contact.id}')">删除</button></li>
|
|
<li><button class="dropdown-item text-dark" onclick="blockContact('${contact.id}')">拉黑</button></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
listEl.appendChild(item);
|
|
});
|
|
};
|
|
|
|
window.setActive = async (id) => {
|
|
activeId = id;
|
|
const contact = contacts.find(c => c.id === id);
|
|
chatTitle.textContent = contact ? contact.phone : '请选择联系人';
|
|
chatMeta.textContent = contact ? "手机号: " + contact.phone : "状态";
|
|
renderContacts();
|
|
await loadMessages();
|
|
};
|
|
|
|
window.deleteContact = async (id) => {
|
|
if (!confirm('确定删除此联系人?')) return;
|
|
try {
|
|
await apiFetch(`/api/contacts.php?action=delete&id=${id}`, { method: 'POST' });
|
|
await loadContacts();
|
|
showToast('已删除联系人');
|
|
if (activeId === id) {
|
|
activeId = null;
|
|
chatTitle.textContent = '请选择联系人';
|
|
chatMeta.textContent = '状态';
|
|
chatBody.innerHTML = '';
|
|
}
|
|
} catch (e) { showToast('删除失败', 'danger'); }
|
|
};
|
|
|
|
window.blockContact = async (id) => {
|
|
if (!confirm('确定拉黑此联系人?')) return;
|
|
try {
|
|
await apiFetch(`/api/contacts.php?action=block&id=${id}`, { method: 'POST' });
|
|
await loadContacts();
|
|
showToast('已拉黑联系人');
|
|
if (activeId === id) {
|
|
activeId = null;
|
|
chatTitle.textContent = '请选择联系人';
|
|
chatMeta.textContent = '状态';
|
|
chatBody.innerHTML = '';
|
|
}
|
|
} catch (e) { showToast('操作失败', 'danger'); }
|
|
};
|
|
|
|
const loadContacts = async () => {
|
|
const data = await apiFetch('/api/contacts.php');
|
|
contacts = data.contacts || [];
|
|
renderContacts();
|
|
};
|
|
|
|
const loadMessages = async () => {
|
|
if (!activeId) return;
|
|
const data = await apiFetch(`/api/messages.php?contact_id=${activeId}`);
|
|
chatBody.innerHTML = (data.messages || []).map(msg => `
|
|
<div class="message ${msg.direction === 'out' ? 'out' : 'in'}">
|
|
<div>${msg.body}</div>
|
|
<div class="time">${formatTime(msg.created_at)}</div>
|
|
</div>
|
|
`).join('');
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
};
|
|
|
|
form?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
if (!activeId) return;
|
|
const body = input.value.trim();
|
|
if (!body) return;
|
|
await apiFetch('/api/messages.php', { method: 'POST', body: { contact_id: activeId, body } });
|
|
input.value = '';
|
|
await loadMessages();
|
|
});
|
|
|
|
loadContacts();
|
|
setInterval(loadContacts, 3000);
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (document.body.dataset.page === 'agent') initAgent();
|
|
});
|