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 = `
-
`;
- 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 => `
+
+ `).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