diff --git a/assets/css/custom.css b/assets/css/custom.css index 09f7e31..21a7225 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,62 +1,90 @@ /* Wrapper for Frontend */ .app-frontend { - height: 100vh; + height: 94vh; + margin: 3vh; display: flex; flex-direction: column; + border: 1px solid #d1d7db; + border-radius: 12px; + overflow: hidden; + background: #fff; + box-shadow: 0 10px 25px rgba(0,0,0,0.1); } .app-frontend .frontend-topbar { display: flex; justify-content: space-between; align-items: center; - padding: 0.8rem 2rem; - background: #fff; - border-bottom: 1px solid #e5e7eb; + padding: 0.8rem 1.5rem; + background: #f0f2f5; + border-bottom: 1px solid #d1d7db; color: #333; height: 60px; } -.app-frontend .brand { display: flex; align-items: center; gap: 15px; font-weight: bold; } +.app-frontend .brand { display: flex; align-items: center; gap: 10px; font-weight: bold; } .app-frontend .frontend-app-shell { display: flex; flex: 1; overflow: hidden; + background: #fff; } -.app-frontend .contacts-panel { width: 300px; border-right: 1px solid #e5e7eb; padding: 1rem; background: #fff;} -.app-frontend .chat-panel { flex: 1; background: #f8f9fa; display: flex; flex-direction: column;} -.app-frontend .frontend-chat-header { padding: 1rem; border-bottom: 1px solid #e5e7eb; background: #fff; } -.app-frontend .frontend-chat-body { flex: 1; padding: 1rem; overflow-y: auto; } -.app-frontend .frontend-chat-input-wrapper { padding: 1rem; border-top: 1px solid #e5e7eb; background: #fff; } - -/* Wrapper for Admin */ -.app-admin { - height: 100vh; - display: flex; +.app-frontend .contacts-panel { + width: 350px; + border-right: 1px solid #d1d7db; + display: flex; flex-direction: column; + overflow-y: auto; } -.app-admin .topbar { - height: 60px; - background: #25D366; - display: flex; - justify-content: space-between; +.app-frontend .contacts-search-box { padding: 10px; border-bottom: 1px solid #f0f2f5; } +.app-frontend .contact-item { + display: flex; + align-items: center; + padding: 12px 15px; + border-bottom: 1px solid #f2f2f2; + cursor: pointer; + transition: background 0.2s; +} +.app-frontend .contact-item:hover, .app-frontend .contact-item.active { background: #f5f6f6; } +.app-frontend .contact-avatar { + width: 45px; + height: 45px; + border-radius: 50%; + background: #00a884; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + margin-right: 15px; +} +.app-frontend .contact-info { flex: 1; overflow: hidden; } +.app-frontend .contact-actions { cursor: pointer; color: #54656f; padding: 5px; } +.app-frontend .chat-panel { flex: 1; background: #e5ddd5; display: flex; flex-direction: column;} +.app-frontend .frontend-chat-header { + padding: 10px 15px; + background: #f0f2f5; + display: flex; align-items: center; - padding: 0 20px; + border-bottom: 1px solid #d1d7db; } -.app-admin .brand { display: flex; align-items: center; gap: 15px; } -.app-admin .admin-shell { display: flex; flex: 1; background: #f3f4f6; } -.app-admin .admin-sidebar { width: 260px; background: white; padding: 20px; border-right: 1px solid #e5e7eb; } -.app-admin .admin-sidebar .nav-link { - color: #374151; - padding: 12px 16px; - border-radius: 8px; - transition: all 0.2s; +.app-frontend .frontend-chat-body { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column;} +.app-frontend .frontend-chat-input-wrapper { padding: 10px; background: #f0f2f5; } + +/* Message Bubble */ +.message-bubble { + position: relative; + padding: 8px 12px; + border-radius: 8px; + margin-bottom: 4px; + max-width: 70%; + cursor: context-menu; } -.app-admin .admin-sidebar .nav-link:hover, .app-admin .admin-sidebar .nav-link.active { - background: #ecfdf5; - color: #059669; - font-weight: 600; +.message-bubble .msg-actions { + display: none; + position: absolute; + top: 5px; + right: 5px; + z-index: 10; } -.app-admin .admin-content { flex: 1; padding: 20px; overflow-y: auto; } -.app-admin .section-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); } -.app-admin .stat-card { padding: 20px; border-radius: 12px; background: #f9fafb; border: 1px solid #e5e7eb; } -.app-admin .btn-success { background-color: #25D366; border-color: #25D366; color: white; } -.app-admin .btn-success:hover { background-color: #128C7E; border-color: #128C7E; } -.app-admin .toast-container { position: fixed; bottom: 20px; right: 20px; } \ No newline at end of file +.message-bubble:hover .msg-actions { display: block; } +.msg-time { font-size: 0.7rem; color: #999; margin-top: 4px; text-align: right; } +.dropdown-menu { font-size: 0.9rem; } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 619ea6b..cbbcbbd 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -13,237 +13,158 @@ const apiFetch = async (url, options = {}) => { 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 = ` -
-
${message}
- -
`; - container.appendChild(toast); - setTimeout(() => toast.remove(), 3200); -}; - const initFrontend = () => { - const countrySearch = document.getElementById('countrySearch'); - const countryList = document.getElementById('countryList'); + 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 => ` +
+
${phone.substring(phone.length - 2)}
+
+
${phone}
+
${state.messages[phone]?.slice(-1)[0]?.text || ''}
+
+
+ + +
+
+ `).join(''); + }; + + window.switchChat = (phone) => { + currentChatPhone = phone; + chatTitle.textContent = phone; + chatBody.innerHTML = (state.messages[phone] || []).map((m, idx) => ` +
+
+ ${m.text} +
${formatTime(m.time)}
+ +
+
+ `).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'); - const startChat = document.getElementById('startChat'); - const shortcutListUI = document.getElementById('shortcutList'); - const newShortcut = document.getElementById('newShortcut'); - const addShortcut = document.getElementById('addShortcut'); - const chatInput = document.querySelector('[data-chat-input]'); - const shortcutsDisplay = document.querySelector('[data-shortcuts]'); - const emojiTrigger = document.querySelector('[data-emoji-trigger]'); - const emojiPicker = document.querySelector('[data-emoji-picker]'); - - // Country Search & Selection - const renderCountries = (filter = '') => { - countryList.innerHTML = countries - .filter(c => c.name.includes(filter) || c.code.includes(filter)) - .map(c => ` -
- ${c.name} ${c.code} -
- `).join(''); - }; - countrySearch.addEventListener('input', (e) => renderCountries(e.target.value)); - - window.selectCountry = (name, code) => { - selectedCode.textContent = code; - document.getElementById('searchTrigger').textContent = name + ' (' + code + ')'; - bootstrap.Modal.getInstance(document.getElementById('countryModal')).hide(); - }; - - renderCountries(); - - // Shortcuts Management - let shortcuts = JSON.parse(localStorage.getItem('shortcuts') || '[]'); - - const renderShortcuts = () => { - shortcutListUI.innerHTML = shortcuts.map((s, i) => ` -
  • ${s} - -
  • - `).join(''); - - shortcutsDisplay.innerHTML = shortcuts.map(s => ` - - `).join(''); - }; - - window.insertShortcut = (text) => chatInput.value += text; - window.deleteShortcut = (index) => { - shortcuts.splice(index, 1); - localStorage.setItem('shortcuts', JSON.stringify(shortcuts)); - renderShortcuts(); - }; - - addShortcut.addEventListener('click', () => { - if (!newShortcut.value) return; - shortcuts.push(newShortcut.value); - localStorage.setItem('shortcuts', JSON.stringify(shortcuts)); - newShortcut.value = ''; - renderShortcuts(); - }); - - renderShortcuts(); - - // Emoji Picker - emojiTrigger.addEventListener('click', () => emojiPicker.toggleAttribute('hidden')); - emojiPicker.addEventListener('emoji-click', (e) => { - chatInput.value += e.detail.unicode; - emojiPicker.setAttribute('hidden', ''); - }); - - // Start Chat - startChat.addEventListener('click', () => { + document.getElementById('startChat').addEventListener('click', () => { if (!phoneNumber.value) return; const phone = selectedCode.textContent + phoneNumber.value; - document.querySelector('[data-chat-title]').textContent = phone; - bootstrap.Modal.getInstance(document.getElementById('countryModal')).hide(); - showToast('已开始与 ' + phone + ' 的聊天'); - }); -}; - -const initAdmin = () => { - const navLinks = document.querySelectorAll('[data-section-link]'); - const sections = document.querySelectorAll('[data-section]'); - - navLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const target = link.dataset.sectionLink; - navLinks.forEach(l => l.classList.remove('active')); - link.classList.add('active'); - sections.forEach(s => s.classList.add('d-none')); - document.querySelector(`[data-section="${target}"]`).classList.remove('d-none'); - - if (target === 'dashboard') loadDashboard(); - if (target === 'contacts') loadAdminContacts(); - if (target === 'messages') loadAdminMessages(); - if (target === 'auto') loadAutoReplies(); - if (target === 'settings') loadSettings(); - }); + if (!state.contacts[phone]) { + state.contacts[phone] = { created: Date.now() }; + state.messages[phone] = []; + } + currentChatPhone = phone; + saveState(); + renderContacts(); + switchChat(phone); + countryModal.hide(); }); - // Forms - document.querySelector('[data-send-form]')?.addEventListener('submit', async (e) => { + chatForm.addEventListener('submit', (e) => { e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target)); - try { - await apiFetch('/api/messages.php', { method: 'POST', body: { contact_phone: data.phone, body: data.body, direction: 'out' } }); - showToast('发送成功'); - e.target.reset(); - } catch (e) { showToast('发送失败', 'danger'); } + 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.querySelector('[data-reply-form]')?.addEventListener('submit', async (e) => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target)); - try { - await apiFetch('/api/auto_reply.php', { method: 'POST', body: data }); - showToast('已添加规则'); - e.target.reset(); - loadAutoReplies(); - } catch (e) { showToast('添加失败', 'danger'); } + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + currentChatPhone = null; + chatTitle.textContent = '请选择联系人'; + chatBody.innerHTML = ''; + renderContacts(); + } }); - document.querySelector('[data-settings-form]')?.addEventListener('submit', async (e) => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.target)); - try { - await apiFetch('/api/settings.php', { method: 'POST', body: data }); - showToast('配置已保存'); - } catch (e) { showToast('保存失败', 'danger'); } - }); - - const loadDashboard = async () => { - try { - const data = await apiFetch('/api/stats.php'); - document.querySelector('[data-stat="sent"]').textContent = data.sent || 0; - document.querySelector('[data-stat="received"]').textContent = data.received || 0; - document.querySelector('[data-stat="active"]').textContent = data.active || 0; - } catch (e) { console.error(e); } - }; - - const loadAdminContacts = async () => { - const data = await apiFetch('/api/contacts.php'); - const list = document.querySelector('[data-admin-contacts]'); - list.innerHTML = (data.contacts || []).map(c => ` - - ${c.phone} - ${c.tags || '-'} - ${c.status} - - - - - - `).join(''); - }; - - window.toggleBlock = async (id, currentStatus) => { - const newStatus = currentStatus === 'blocked' ? 'normal' : 'blocked'; - await apiFetch(`/api/contacts.php?action=update&id=${id}`, { method: 'POST', body: { status: newStatus } }); - loadAdminContacts(); - }; - - window.deleteContact = async (id) => { - if (!confirm('确定删除?')) return; - await apiFetch(`/api/contacts.php?action=delete&id=${id}`, { method: 'POST' }); - loadAdminContacts(); - }; - - const loadAdminMessages = async () => { - const data = await apiFetch('/api/messages.php'); - const list = document.querySelector('[data-admin-messages]'); - list.innerHTML = (data.messages || []).map(m => ` - - ${m.phone} - ${m.direction} - ${m.body} - ${m.created_at} - - `).join(''); - }; - - const loadAutoReplies = async () => { - const data = await apiFetch('/api/auto_reply.php'); - const list = document.querySelector('[data-reply-list]'); - list.innerHTML = (data.rules || []).map(r => ` -
  • -
    ${r.keyword}: ${r.reply}
    - -
  • - `).join(''); - }; - - window.deleteRule = async (id) => { - await apiFetch(`/api/auto_reply.php?action=delete&id=${id}`, { method: 'POST' }); - loadAutoReplies(); - }; - - const loadSettings = async () => { - const data = await apiFetch('/api/settings.php'); - if (!data) return; - const form = document.querySelector('[data-settings-form]'); - form.sid.value = data.sid || ''; - form.token.value = data.token || ''; - form.from.value = data.from || ''; - form.webhook.value = data.webhook || ''; - }; - - loadDashboard(); + renderContacts(); }; document.addEventListener('DOMContentLoaded', () => { if (document.body.dataset.page === 'agent') initFrontend(); - if (document.body.dataset.page === 'admin') initAdmin(); }); \ No newline at end of file