diff --git a/admin.php b/admin.php index f9c4a17..d09023f 100644 --- a/admin.php +++ b/admin.php @@ -1,8 +1,9 @@ @@ -10,25 +11,6 @@ declare(strict_types=1); SMS Chat — 管理后台 - - - - - - - - - - - - - - - @@ -44,7 +26,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
● 系统正常 管理员:Admin - 返回工作台 + 退出登录 + 返回工作台
@@ -210,4 +193,4 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; - + \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 47bfc91..df83b66 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,364 +1,16 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); - -:root { - --bg: #f5f7f9; - --surface: #ffffff; - --text: #111827; - --muted: #6b7280; - --border: #e5e7eb; - --primary: #25d366; - --primary-dark: #1ea856; - --sidebar-dark: #1f2937; - --sidebar-dark-muted: #9ca3af; - --shadow: 0 8px 24px rgba(15, 23, 42, 0.08); - --radius-sm: 8px; - --radius-md: 12px; -} - -* { - box-sizing: border-box; -} - -body { - font-family: 'Inter', system-ui, -apple-system, Segoe UI, sans-serif; - background: var(--bg); - color: var(--text); -} - .topbar { - height: 64px; - background: var(--surface); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 1.5rem; - position: sticky; - top: 0; - z-index: 10; -} - -.brand { - display: flex; - align-items: center; - gap: 0.75rem; - font-weight: 700; - font-size: 1.1rem; -} - -.brand-badge { - width: 36px; - height: 36px; - border-radius: 10px; - background: var(--primary); - color: #fff; - display: grid; - place-items: center; - font-weight: 700; -} - -.topbar .meta { - display: flex; - align-items: center; - gap: 1rem; - font-size: 0.9rem; - color: var(--muted); -} - -.status-pill { - display: inline-flex; - align-items: center; - gap: 0.4rem; - padding: 0.25rem 0.6rem; - border-radius: 999px; - background: rgba(37, 211, 102, 0.12); - color: var(--primary-dark); - font-weight: 600; - font-size: 0.75rem; -} - -.app-shell { - height: calc(100vh - 64px); - display: flex; - gap: 1rem; - padding: 1rem 1.5rem 1.5rem; -} - -.panel { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow); -} - -.contacts-panel { - width: 320px; - display: flex; - flex-direction: column; -} - -.contacts-panel .search { - padding: 1rem; - border-bottom: 1px solid var(--border); -} - -.contacts-list { - overflow-y: auto; -} - -.contact-item { - padding: 0.85rem 1rem; - border-bottom: 1px solid var(--border); - cursor: pointer; - display: flex; - justify-content: space-between; - gap: 0.75rem; - transition: background 0.15s ease; -} - -.contact-item:hover, -.contact-item.active { - background: rgba(37, 211, 102, 0.08); -} - -.contact-item .meta { - font-size: 0.75rem; - color: var(--muted); -} - -.unread { - background: var(--primary); - color: #fff; - border-radius: 999px; - font-size: 0.7rem; - padding: 0.1rem 0.45rem; - font-weight: 700; -} - -.chat-panel { - flex: 1; - display: flex; - flex-direction: column; -} - -.chat-header { - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.chat-actions .btn { - border-radius: 999px; - font-size: 0.8rem; -} - -.chat-body { - flex: 1; - padding: 1.25rem; - overflow-y: auto; - background: #fafafa; -} - -.message { - max-width: 70%; - padding: 0.65rem 0.9rem; - border-radius: 14px; - margin-bottom: 0.6rem; - font-size: 0.9rem; - line-height: 1.4; -} - -.message.in { - background: #ffffff; - border: 1px solid var(--border); -} - -.message.out { - background: rgba(37, 211, 102, 0.18); - margin-left: auto; - border: 1px solid rgba(37, 211, 102, 0.3); -} - -.message .time { - font-size: 0.7rem; - color: var(--muted); - margin-top: 0.35rem; -} - -.chat-input { - border-top: 1px solid var(--border); - padding: 1rem 1.25rem; - background: var(--surface); -} - -.chat-input .form-control { - border-radius: 999px; -} - -.shortcut-list { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.shortcut-list .btn { - border-radius: 999px; - font-size: 0.75rem; -} - -.admin-shell { - display: grid; - grid-template-columns: 240px 1fr; - height: calc(100vh - 64px); -} - -.admin-sidebar { - background: var(--sidebar-dark); - color: #fff; - padding: 1.5rem 1rem; -} - -.admin-sidebar .nav-link { - color: var(--sidebar-dark-muted); - border-radius: 8px; - margin-bottom: 0.3rem; - padding: 0.6rem 0.85rem; - font-weight: 600; -} - -.admin-sidebar .nav-link.active, -.admin-sidebar .nav-link:hover { - background: rgba(255, 255, 255, 0.08); - color: #fff; -} - -.admin-content { - padding: 1.5rem; - overflow-y: auto; -} - -.stat-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 1.25rem; - box-shadow: var(--shadow); -} - -.table thead th { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--muted); -} - -.badge-status { - padding: 0.35rem 0.6rem; - border-radius: 999px; - font-size: 0.7rem; -} - -.badge-status.normal { - background: rgba(37, 211, 102, 0.15); - color: var(--primary-dark); -} - -.badge-status.blocked { - background: rgba(239, 68, 68, 0.12); - color: #b91c1c; -} - -.section-card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 1.5rem; - box-shadow: var(--shadow); -} - -.toast-container { - position: fixed; - bottom: 1.5rem; - right: 1.5rem; - z-index: 1055; -} - -.form-control, -.form-select { - border-radius: 8px; -} - -.btn-primary { - background: var(--primary); - border-color: var(--primary); -} - -.btn-primary:hover { - background: var(--primary-dark); - border-color: var(--primary-dark); -} - -.muted { - color: var(--muted); -} - -.empty { - text-align: center; - color: var(--muted); - padding: 2rem 1rem; -} - -@media (max-width: 992px) { - .app-shell { - flex-direction: column; - height: auto; - } - .contacts-panel { - width: 100%; - } - .admin-shell { - grid-template-columns: 1fr; - } - .admin-sidebar { display: flex; - gap: 0.5rem; - overflow-x: auto; - } -} - -/* Emoji Button Alignment */ -.chat-input .position-relative { - display: flex; - align-items: center; -} - -[data-emoji-trigger] { - padding: 0.5rem 0.7rem; - font-size: 1.25rem; - line-height: 1; - background-color: var(--surface) !important; - border-color: var(--border) !important; - border-radius: 999px !important; -} - -emoji-picker { - --emoji-size: 1.5rem; - --indicator-color: var(--primary); - --border-color: var(--border); - width: 320px; - height: 400px; -} - -/* Force hide any potential branding that might be injected */ -[class*="Flatlogic"], -[id*="Flatlogic"], -.flatlogic-watermark, -.built-with-flatlogic { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - pointer-events: none !important; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background: #1f2937; + color: #fff; } +.brand { display: flex; align-items: center; gap: 10px; } +.brand-badge { background: #25D366; color: white; padding: 5px 12px; border-radius: 5px; font-weight: bold; } +.admin-shell { display: flex; min-height: 90vh; background: #f3f4f6; } +.admin-sidebar { width: 240px; background: white; padding: 20px; border-right: 1px solid #e5e7eb; } +.admin-content { flex: 1; padding: 20px; } +.section-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } +.stat-card { padding: 15px; border-radius: 8px; background: #f9fafb; border: 1px solid #e5e7eb; } +.toast-container { position: fixed; bottom: 20px; right: 20px; } diff --git a/assets/js/main.js b/assets/js/main.js index ebd13e4..7690a0e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -28,226 +28,137 @@ const showToast = (message, type = 'success') => { 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 initAdmin = () => { + const navLinks = document.querySelectorAll('[data-section-link]'); + const sections = document.querySelectorAll('[data-section]'); -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 = `${s}`; - 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); + 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(); + }); }); - }; - 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} (${c.code})`; - 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 = ` -
-
-
${contact.phone}
-
${contact.last_message || '暂无消息'}
-
- -
- `; - listEl.appendChild(item); + // Forms + document.querySelector('[data-send-form]')?.addEventListener('submit', async (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'); } }); - }; - 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(); - }; + 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'); } + }); - window.deleteContact = async (id) => { - if (!confirm('确定删除此联系人?')) return; - try { + 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' }); - await loadContacts(); - showToast('已删除联系人'); - if (activeId === id) { - activeId = null; - chatTitle.textContent = '请选择联系人'; - chatMeta.textContent = '状态'; - chatBody.innerHTML = ''; - } - } catch (e) { showToast('删除失败', 'danger'); } - }; + loadAdminContacts(); + }; - 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 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(''); + }; - const loadContacts = async () => { - const data = await apiFetch('/api/contacts.php'); - contacts = data.contacts || []; - renderContacts(); - }; + window.deleteRule = async (id) => { + await apiFetch(`/api/auto_reply.php?action=delete&id=${id}`, { method: 'POST' }); + loadAutoReplies(); + }; - const loadMessages = async () => { - if (!activeId) return; - const data = await apiFetch(`/api/messages.php?contact_id=${activeId}`); - chatBody.innerHTML = (data.messages || []).map(msg => ` -
    -
    ${msg.body}
    -
    ${formatTime(msg.created_at)}
    -
    - `).join(''); - chatBody.scrollTop = chatBody.scrollHeight; - }; + 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 || ''; + }; - 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); + loadDashboard(); }; document.addEventListener('DOMContentLoaded', () => { - if (document.body.dataset.page === 'agent') initAgent(); -}); -window.FL_SHOW_BUDGE = false; + if (document.body.dataset.page === 'admin') initAdmin(); +}); \ No newline at end of file diff --git a/assets/pasted-20260318-130451-33711195.png b/assets/pasted-20260318-130451-33711195.png new file mode 100644 index 0000000..9271b8b Binary files /dev/null and b/assets/pasted-20260318-130451-33711195.png differ diff --git a/assets/pasted-20260318-132245-b6b02e61.png b/assets/pasted-20260318-132245-b6b02e61.png new file mode 100644 index 0000000..4a40e8a Binary files /dev/null and b/assets/pasted-20260318-132245-b6b02e61.png differ diff --git a/db/migrations/001_create_tables.sql b/db/migrations/001_create_tables.sql new file mode 100644 index 0000000..aa8593a --- /dev/null +++ b/db/migrations/001_create_tables.sql @@ -0,0 +1,31 @@ +-- 客户信息表 +CREATE TABLE IF NOT EXISTS customers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 消息记录表 +CREATE TABLE IF NOT EXISTS messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + customer_id INT, + direction ENUM('inbound', 'outbound') NOT NULL, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customers(id) +); + +-- Twilio 和系统配置表 +CREATE TABLE IF NOT EXISTS settings ( + key_name VARCHAR(50) PRIMARY KEY, + key_value TEXT +); + +-- 自动回复设置 +CREATE TABLE IF NOT EXISTS auto_replies ( + id INT AUTO_INCREMENT PRIMARY KEY, + keyword VARCHAR(255), + response_text TEXT, + is_active BOOLEAN DEFAULT TRUE +); diff --git a/db/migrations/run.php b/db/migrations/run.php new file mode 100644 index 0000000..fd14e73 --- /dev/null +++ b/db/migrations/run.php @@ -0,0 +1,6 @@ +exec($sql); +echo "Migration completed.\n"; \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..feab053 --- /dev/null +++ b/login.php @@ -0,0 +1,47 @@ + + + + + + 登录 - 管理后台 + + + + +
    +

    后台登录

    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    + + \ No newline at end of file diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..37bc5ab --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ + - - \ No newline at end of file + +