diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..f9c4a17 --- /dev/null +++ b/admin.php @@ -0,0 +1,213 @@ + + + + + + + SMS Chat — 管理后台 + + + + + + + + + + + + + + + + + + + +
+
+
S
+
+ SMS Chat +
管理后台
+
+
+
+ ● 系统正常 + 管理员:Admin + 返回工作台 +
+
+ +
+ + +
+
+
今日概览
+
+
+
+
今日发送
+
0
+
+
+
+
+
今日接收
+
0
+
+
+
+
+
活跃客户
+
0
+
+
+
+
+ +
+
客户管理
+
+ + + + + + + + + + +
手机号标签状态操作
+
+
+ +
+
消息记录
+
+ + + + + + + + + + +
手机号方向内容时间
+
+
+ +
+
发送短信
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
自动回复
+
+
+ + +
+
+ + +
+
+ +
+
+
    +
    + +
    +
    Twilio 配置
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + +
    +
    系统设置
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + + + + diff --git a/api/_bootstrap.php b/api/_bootstrap.php new file mode 100644 index 0000000..f7fcb69 --- /dev/null +++ b/api/_bootstrap.php @@ -0,0 +1,84 @@ +exec(" + CREATE TABLE IF NOT EXISTS contacts ( + id INT AUTO_INCREMENT PRIMARY KEY, + phone VARCHAR(32) NOT NULL UNIQUE, + name VARCHAR(64) NULL, + tags VARCHAR(255) NULL, + status ENUM('normal','blocked') DEFAULT 'normal', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + contact_id INT NOT NULL, + direction ENUM('in','out') NOT NULL DEFAULT 'out', + body TEXT NOT NULL, + is_read TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (contact_id) REFERENCES contacts(id) + ) + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + category VARCHAR(32) NOT NULL, + name VARCHAR(64) NOT NULL, + value TEXT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_setting (category, name) + ) + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS auto_reply ( + id INT AUTO_INCREMENT PRIMARY KEY, + keyword VARCHAR(120) NOT NULL, + reply TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + "); + + $count = (int)$pdo->query("SELECT COUNT(*) FROM contacts")->fetchColumn(); + if ($count === 0) { + $stmt = $pdo->prepare("INSERT INTO contacts (phone, name, tags, status) VALUES (?, ?, ?, ?)"); + $stmt->execute(['+86 138 0013 8000', '华北客户', 'VIP,续费', 'normal']); + $stmt->execute(['+86 139 0009 1212', '深圳客户', '新线索', 'normal']); + $stmt->execute(['+1 415 555 0198', '海外客户', '海外', 'normal']); + + $contacts = $pdo->query("SELECT id, phone FROM contacts")->fetchAll(); + $msgStmt = $pdo->prepare("INSERT INTO messages (contact_id, direction, body, is_read, created_at) VALUES (?, ?, ?, ?, ?)"); + foreach ($contacts as $contact) { + $msgStmt->execute([$contact['id'], 'in', '您好,我想了解套餐价格。', 0, date('Y-m-d H:i:s', strtotime('-2 hours'))]); + $msgStmt->execute([$contact['id'], 'out', '您好,这里是客服,请问您关注哪种短信套餐?', 1, date('Y-m-d H:i:s', strtotime('-90 minutes'))]); + } + } + + $done = true; +} + +function read_json(): array { + $raw = file_get_contents('php://input'); + $data = json_decode($raw, true); + if (is_array($data)) return $data; + return $_POST ?? []; +} diff --git a/api/auto_reply.php b/api/auto_reply.php new file mode 100644 index 0000000..5423a7c --- /dev/null +++ b/api/auto_reply.php @@ -0,0 +1,21 @@ + false, 'error' => 'Missing fields']); + } + $stmt = $pdo->prepare("INSERT INTO auto_reply (keyword, reply) VALUES (?, ?)"); + $stmt->execute([$keyword, $reply]); + json_response(['success' => true]); +} + +$rules = $pdo->query("SELECT id, keyword, reply, created_at FROM auto_reply ORDER BY created_at DESC")->fetchAll(); +json_response(['rules' => $rules]); diff --git a/api/contacts.php b/api/contacts.php new file mode 100644 index 0000000..08606fa --- /dev/null +++ b/api/contacts.php @@ -0,0 +1,64 @@ + false, 'error' => 'Missing phone']); + + $stmt = $pdo->prepare("SELECT * FROM contacts WHERE phone = ?"); + $stmt->execute([$phone]); + $contact = $stmt->fetch(); + + if (!$contact) { + $stmt = $pdo->prepare("INSERT INTO contacts (phone) VALUES (?)"); + $stmt->execute([$phone]); + $id = $pdo->lastInsertId(); + $stmt = $pdo->prepare("SELECT * FROM contacts WHERE id = ?"); + $stmt->execute([$id]); + $contact = $stmt->fetch(); + } + + json_response(['success' => true, 'contact' => $contact]); + } + + if ($action === 'update') { + $id = (int)($input['id'] ?? 0); + if ($id <= 0) json_response(['success' => false, 'error' => 'Missing id']); + $fields = []; + $params = []; + foreach (['name', 'tags', 'status'] as $field) { + if (array_key_exists($field, $input)) { + $fields[] = "$field = ?"; + $params[] = $input[$field]; + } + } + if (!$fields) json_response(['success' => false, 'error' => 'No changes']); + $params[] = $id; + $stmt = $pdo->prepare("UPDATE contacts SET " . implode(', ', $fields) . " WHERE id = ?"); + $stmt->execute($params); + json_response(['success' => true]); + } + + json_response(['success' => false, 'error' => 'Unknown action']); +} + +$sql = " + SELECT c.id, c.phone, c.name, c.tags, c.status, c.updated_at, + (SELECT body FROM messages m WHERE m.contact_id = c.id ORDER BY m.created_at DESC LIMIT 1) AS last_message, + (SELECT created_at FROM messages m WHERE m.contact_id = c.id ORDER BY m.created_at DESC LIMIT 1) AS last_time, + (SELECT COUNT(*) FROM messages m WHERE m.contact_id = c.id AND m.direction = 'in' AND m.is_read = 0) AS unread_count + FROM contacts c + ORDER BY last_time DESC, c.updated_at DESC +"; +$contacts = $pdo->query($sql)->fetchAll(); + +json_response(['contacts' => $contacts]); \ No newline at end of file diff --git a/api/messages.php b/api/messages.php new file mode 100644 index 0000000..053f0e4 --- /dev/null +++ b/api/messages.php @@ -0,0 +1,73 @@ + false, 'error' => 'Empty body']); + + $direction = $input['direction'] ?? 'out'; + $contactId = (int)($input['contact_id'] ?? 0); + $phone = trim((string)($input['phone'] ?? '')); + + if ($contactId <= 0 && $phone === '') { + json_response(['success' => false, 'error' => 'Missing contact']); + } + + if ($contactId <= 0 && $phone !== '') { + $stmt = $pdo->prepare("SELECT id FROM contacts WHERE phone = ?"); + $stmt->execute([$phone]); + $contactId = (int)($stmt->fetchColumn() ?: 0); + if ($contactId <= 0) { + $insert = $pdo->prepare("INSERT INTO contacts (phone, name, tags, status) VALUES (?, ?, ?, 'normal')"); + $insert->execute([$phone, $input['name'] ?? null, $input['tags'] ?? null]); + $contactId = (int)$pdo->lastInsertId(); + } + } + + $stmt = $pdo->prepare("INSERT INTO messages (contact_id, direction, body, is_read) VALUES (?, ?, ?, ?)"); + $stmt->execute([$contactId, $direction, $body, $direction === 'in' ? 0 : 1]); + json_response(['success' => true, 'contact_id' => $contactId]); +} + +if (isset($_GET['all'])) { + $phoneFilter = trim((string)($_GET['phone'] ?? '')); + if ($phoneFilter !== '') { + $stmt = $pdo->prepare(" + SELECT m.id, m.direction, m.body, m.created_at, c.phone + FROM messages m + JOIN contacts c ON c.id = m.contact_id + WHERE c.phone LIKE ? + ORDER BY m.created_at DESC + LIMIT 200 + "); + $stmt->execute(['%' . $phoneFilter . '%']); + $messages = $stmt->fetchAll(); + } else { + $messages = $pdo->query(" + SELECT m.id, m.direction, m.body, m.created_at, c.phone + FROM messages m + JOIN contacts c ON c.id = m.contact_id + ORDER BY m.created_at DESC + LIMIT 200 + ")->fetchAll(); + } + json_response(['messages' => $messages]); +} + +$contactId = (int)($_GET['contact_id'] ?? 0); +if ($contactId <= 0) { + json_response(['messages' => []]); +} + +$stmt = $pdo->prepare("SELECT id, direction, body, created_at FROM messages WHERE contact_id = ? ORDER BY created_at ASC"); +$stmt->execute([$contactId]); +$messages = $stmt->fetchAll(); + +$pdo->prepare("UPDATE messages SET is_read = 1 WHERE contact_id = ? AND direction = 'in'")->execute([$contactId]); + +json_response(['messages' => $messages]); diff --git a/api/settings.php b/api/settings.php new file mode 100644 index 0000000..eb9136c --- /dev/null +++ b/api/settings.php @@ -0,0 +1,34 @@ + false, 'error' => 'Invalid data']); + } + $stmt = $pdo->prepare(" + INSERT INTO settings (category, name, value) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = CURRENT_TIMESTAMP + "); + foreach ($data as $key => $value) { + $stmt->execute([$category, $key, (string)$value]); + } + json_response(['success' => true]); +} + +$category = $_GET['category'] ?? 'general'; +$stmt = $pdo->prepare("SELECT name, value FROM settings WHERE category = ?"); +$stmt->execute([$category]); +$settings = $stmt->fetchAll(); +$out = []; +foreach ($settings as $row) { + $out[$row['name']] = $row['value']; +} +json_response(['settings' => $out]); diff --git a/api/stats.php b/api/stats.php new file mode 100644 index 0000000..4075abd --- /dev/null +++ b/api/stats.php @@ -0,0 +1,16 @@ +query("SELECT COUNT(*) FROM messages WHERE direction = 'out' AND DATE(created_at) = CURDATE()")->fetchColumn(); +$received = (int)$pdo->query("SELECT COUNT(*) FROM messages WHERE direction = 'in' AND DATE(created_at) = CURDATE()")->fetchColumn(); +$active = (int)$pdo->query("SELECT COUNT(DISTINCT contact_id) FROM messages WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)")->fetchColumn(); + +json_response([ + 'sent_today' => $sent, + 'received_today' => $received, + 'active_contacts' => $active, +]); diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..182a833 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,353 @@ +@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 { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + font-family: 'Inter', system-ui, -apple-system, Segoe UI, sans-serif; + background: var(--bg); + color: var(--text); } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.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; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.brand { + display: flex; + align-items: center; + gap: 0.75rem; + font-weight: 700; + font-size: 1.1rem; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.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: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.chat-actions .btn { + border-radius: 999px; + font-size: 0.8rem; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); +.chat-body { + flex: 1; + padding: 1.25rem; + overflow-y: auto; + background: #fafafa; } .message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + max-width: 70%; + padding: 0.65rem 0.9rem; + border-radius: 14px; + margin-bottom: 0.6rem; + font-size: 0.9rem; + line-height: 1.4; } -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } +.message.in { + background: #ffffff; + border: 1px solid var(--border); } -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; +.message.out { + background: rgba(37, 211, 102, 0.18); + margin-left: auto; + border: 1px solid rgba(37, 211, 102, 0.3); } -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; +.message .time { + font-size: 0.7rem; + color: var(--muted); + margin-top: 0.35rem; } -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); +.chat-input { + border-top: 1px solid var(--border); + padding: 1rem 1.25rem; + background: var(--surface); } -.chat-input-area form { +.chat-input .form-control { + border-radius: 999px; +} + +.quick-replies { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.quick-replies .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.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { + gap: 0.5rem; overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); + } } -.history-table { - width: 100%; +/* Emoji Button Alignment */ +.chat-input .position-relative { + display: flex; + align-items: center; } -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; +[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; } -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; +emoji-picker { + --emoji-size: 1.5rem; + --indicator-color: var(--primary); + --border-color: var(--border); + width: 320px; + height: 400px; } - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/countries.js b/assets/js/countries.js new file mode 100644 index 0000000..17d171b --- /dev/null +++ b/assets/js/countries.js @@ -0,0 +1,18 @@ +const countries = [ + { name: "马来西亚", code: "+60" }, + { name: "中国", code: "+86" }, + { name: "美国", code: "+1" }, + { name: "英国", code: "+44" }, + { name: "日本", code: "+81" }, + { name: "德国", code: "+49" }, + { name: "法国", code: "+33" }, + { name: "加拿大", code: "+1" }, + { name: "澳大利亚", code: "+61" }, + { name: "俄罗斯", code: "+7" }, + { name: "韩国", code: "+82" }, + { name: "巴西", code: "+55" }, + { name: "印度", code: "+91" }, + { name: "意大利", code: "+39" }, + { name: "西班牙", code: "+34" }, + { name: "墨西哥", code: "+52" } +]; \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..6d63bcd 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,196 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +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 appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; +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); +}; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; +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' }); +}; - appendMessage(message, 'visitor'); - chatInput.value = ''; +const initAgent = () => { + const listEl = document.querySelector('[data-contacts]'); + const chatTitle = document.querySelector('[data-chat-title]'); + 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 shortcutList = document.getElementById('shortcutList'); + const newShortcutInput = document.getElementById('newShortcut'); + const addShortcutBtn = document.getElementById('addShortcut'); + const shortcutContainer = document.querySelector('[data-shortcuts]'); - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + let contacts = []; + let activeId = null; + let shortcuts = JSON.parse(localStorage.getItem('shortcuts') || '["你好", "请问有什么可以帮您?", "谢谢"]'); + + // Country selector + const countryList = document.getElementById('countryList'); + const countrySearch = document.getElementById('countrySearch'); + const selectedCode = document.getElementById('selectedCode'); + const phoneInput = document.getElementById('phoneNumber'); + + const renderCountryList = (filter = '') => { + countryList.innerHTML = ''; + countries.filter(c => c.name.includes(filter) || c.code.includes(filter)).forEach(c => { + const item = document.createElement('div'); + item.className = 'country-item'; + item.textContent = `${c.name} (${c.code})`; + item.onclick = () => { + selectedCode.textContent = c.code; + countrySearch.value = c.name; + renderCountryList(); + }; + countryList.appendChild(item); + }); + }; + + countrySearch.addEventListener('input', e => renderCountryList(e.target.value)); + renderCountryList(); + + document.getElementById('startChat').onclick = async () => { + const phone = selectedCode.textContent + phoneInput.value; + if (!phoneInput.value) return; + + try { + // Send to API to create/get contact + const res = await apiFetch('/api/contacts.php', { method: 'POST', body: { phone } }); + const newContact = res.contact; + contacts.unshift(newContact); + renderContacts(); + setActive(newContact.id); + bootstrap.Modal.getInstance(document.getElementById('countryModal')).hide(); + } catch (err) { + showToast('无法启动聊天', 'danger'); + } + }; + + const renderShortcuts = () => { + shortcutContainer.innerHTML = shortcuts.map(s => + `` + ).join(''); + + shortcutList.innerHTML = shortcuts.map((s, i) => ` +
  • + ${s} +
  • + `).join(''); + }; + + window.removeShortcut = (i) => { + shortcuts.splice(i, 1); + localStorage.setItem('shortcuts', JSON.stringify(shortcuts)); + renderShortcuts(); + }; + + addShortcutBtn.addEventListener('click', () => { + if(newShortcutInput.value) { + shortcuts.push(newShortcutInput.value); + localStorage.setItem('shortcuts', JSON.stringify(shortcuts)); + newShortcutInput.value = ''; + renderShortcuts(); + } + }); + + emojiTrigger.addEventListener('click', () => emojiPicker.classList.toggle('d-none')); + emojiPicker.addEventListener('emoji-click', e => { + input.value += e.detail.unicode; + emojiPicker.classList.add('d-none'); + }); + + const renderContacts = () => { + listEl.innerHTML = ''; + contacts.forEach((contact) => { + const item = document.createElement('div'); + item.className = `contact-item ${contact.id === activeId ? 'active' : ''}`; + item.style.padding = '10px'; + item.style.borderBottom = '1px solid #eee'; + item.style.cursor = 'pointer'; + item.innerHTML = ` +
    +
    ${contact.phone}
    +
    ${contact.last_message || '暂无消息'}
    +
    + `; + item.addEventListener('click', () => setActive(contact.id)); + listEl.appendChild(item); }); -}); + }; + + const renderMessages = (messages) => { + chatBody.innerHTML = ''; + messages.forEach((msg) => { + const item = document.createElement('div'); + item.className = `message ${msg.direction === 'out' ? 'out' : 'in'}`; + item.innerHTML = `
    ${msg.body}
    ${formatTime(msg.created_at)}
    `; + chatBody.appendChild(item); + }); + chatBody.scrollTop = chatBody.scrollHeight; + }; + + 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}`); + renderMessages(data.messages || []); + }; + + const setActive = async (id) => { + activeId = id; + const contact = contacts.find(c => c.id === id); + chatTitle.textContent = contact ? contact.phone : '未知联系人'; + renderContacts(); + await loadMessages(); + }; + + 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(); + }); + + renderShortcuts(); + loadContacts(); + setInterval(loadContacts, 3000); +}; + +document.addEventListener('DOMContentLoaded', () => { + if (document.body.dataset.page === 'agent') initAgent(); +}); \ No newline at end of file diff --git a/assets/pasted-20260318-114609-f43679b2.png b/assets/pasted-20260318-114609-f43679b2.png new file mode 100644 index 0000000..ffed0ad Binary files /dev/null and b/assets/pasted-20260318-114609-f43679b2.png differ diff --git a/assets/pasted-20260318-115351-9232ce20.png b/assets/pasted-20260318-115351-9232ce20.png new file mode 100644 index 0000000..8da68c5 Binary files /dev/null and b/assets/pasted-20260318-115351-9232ce20.png differ diff --git a/assets/pasted-20260318-120057-dbde59ee.png b/assets/pasted-20260318-120057-dbde59ee.png new file mode 100644 index 0000000..5cb2691 Binary files /dev/null and b/assets/pasted-20260318-120057-dbde59ee.png differ diff --git a/assets/pasted-20260318-120348-619c1f5d.png b/assets/pasted-20260318-120348-619c1f5d.png new file mode 100644 index 0000000..e563c15 Binary files /dev/null and b/assets/pasted-20260318-120348-619c1f5d.png differ diff --git a/index.php b/index.php index 7205f3d..2cdb4f0 100644 --- a/index.php +++ b/index.php @@ -3,148 +3,112 @@ declare(strict_types=1); @ini_set('display_errors', '1'); @error_reporting(E_ALL); @date_default_timezone_set('UTC'); - -$phpVersion = PHP_VERSION; -$now = date('Y-m-d H:i:s'); ?> - + - New Style - - - - - - - - - - - - - - - - - - + SMS Chat — 前端工作台 + + + - -
    -
    -

    Analyzing your requirements and generating your website…

    -
    - Loading… + +
    +
    +
    S
    +
    + SMS Chat +
    前端聊天工作台
    -

    AI is collecting your requirements and applying the first changes.

    -

    This page will update automatically as the plan is implemented.

    -

    Runtime: PHP — UTC

    +
    + ● 在线 + 客服:Lina + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    请选择联系人
    +
    状态
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + + +
    +
    +
    +
    -
    - Page updated: (UTC) -
    + + + + + + +
    + + + + - + \ No newline at end of file