Autosave: 20260318-133151
This commit is contained in:
parent
adaa57d3e7
commit
678400ba66
33
admin.php
33
admin.php
@ -1,8 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
@ -10,25 +11,6 @@ declare(strict_types=1);
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SMS Chat — 管理后台</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="assets/css/custom.css?v=<?= time() ?>" rel="stylesheet">
|
||||
</head>
|
||||
@ -44,7 +26,8 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<div class="meta">
|
||||
<span class="status-pill">● 系统正常</span>
|
||||
<span>管理员:Admin</span>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="index.php">返回工作台</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="logout.php">退出登录</a>
|
||||
<a class="btn btn-sm btn-outline-secondary ms-2" href="index.php">返回工作台</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -210,4 +193,4 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
<script src="assets/js/main.js?v=<?= time() ?>" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@ -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; }
|
||||
|
||||
@ -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 = `<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);
|
||||
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} <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);
|
||||
// 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 => `
|
||||
<tr>
|
||||
<td>${c.phone}</td>
|
||||
<td>${c.tags || '-'}</td>
|
||||
<td><span class="badge ${c.status === 'blocked' ? 'bg-danger' : 'bg-success'}">${c.status}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="toggleBlock('${c.id}', '${c.status}')">切换拉黑</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteContact('${c.id}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).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 => `
|
||||
<tr>
|
||||
<td>${m.phone}</td>
|
||||
<td>${m.direction}</td>
|
||||
<td>${m.body}</td>
|
||||
<td>${m.created_at}</td>
|
||||
</tr>
|
||||
`).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 => `
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<div><b>${r.keyword}</b>: ${r.reply}</div>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteRule('${r.id}')">删除</button>
|
||||
</li>
|
||||
`).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 => `
|
||||
<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;
|
||||
};
|
||||
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();
|
||||
});
|
||||
BIN
assets/pasted-20260318-130451-33711195.png
Normal file
BIN
assets/pasted-20260318-130451-33711195.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/pasted-20260318-132245-b6b02e61.png
Normal file
BIN
assets/pasted-20260318-132245-b6b02e61.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
31
db/migrations/001_create_tables.sql
Normal file
31
db/migrations/001_create_tables.sql
Normal file
@ -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
|
||||
);
|
||||
6
db/migrations/run.php
Normal file
6
db/migrations/run.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$pdo = db();
|
||||
$sql = file_get_contents(__DIR__ . '/001_create_tables.sql');
|
||||
$pdo->exec($sql);
|
||||
echo "Migration completed.\n";
|
||||
47
login.php
Normal file
47
login.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
session_start();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
// 默认账号密码设置:admin / admin123
|
||||
// 后续建议移动到环境变量或数据库加密存储
|
||||
if ($username === 'admin' && $password === 'admin123') {
|
||||
$_SESSION['loggedin'] = true;
|
||||
$_SESSION['username'] = $username;
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = '账号或密码错误';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>登录 - 管理后台</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #f8f9fa; }
|
||||
.login-card { width: 100%; max-width: 400px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h4 class="mb-3">后台登录</h4>
|
||||
<?php if (isset($error)): ?><div class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">账号</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">登入</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
5
logout.php
Normal file
5
logout.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
@ -105,5 +105,5 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
<script src="assets/js/countries.js?v=1773838803" defer></script>
|
||||
<script src="assets/js/main.js?v=1773838803" defer></script>
|
||||
<script>if(window.FL_SHOW_BUDGE!==false){document.body.style.position='relative';var w=document.createElement('a');w.href='https://flatlogic.com';w.target='_blank';w.innerHTML='<svg width="160" height="32" viewBox="0 0 320 65" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="320" height="65" rx="28" fill="#F5F6FA"/><g transform="translate(32 10) scale(1.5)"><rect x="15" y="10" width="7" height="5" rx="1" fill="#142C65"/><rect x="0" y="2" width="22" height="5" rx="1" fill="#5C7EF1"/><rect x="0" y="18" width="6" height="5" rx="1" fill="#8C9DFF"/><rect x="9" y="18" width="13" height="5" rx="1" fill="#142C65"/><rect x="0" y="10" width="12" height="5" rx="1" fill="#FFB229"/></g><text x="80" y="40" fill="#142C65" font-size="24" font-weight="600" font-family="Mulish,Arial,sans-serif">Built with Flatlogic</text></svg>';w.style.cssText='position:absolute!important;bottom:30px!important;right:30px!important;background:transparent!important;z-index:2147483647!important;user-select:none!important;text-decoration:none!important;cursor:pointer!important;border:none!important;padding:0!important;margin:0!important';document.body.appendChild(w);var o=new MutationObserver(function(m){m.forEach(function(n){if(n.type==='childList'&&n.removedNodes.length>0){for(var i=0;i<n.removedNodes.length;i++){if(n.removedNodes[i]===w||(n.removedNodes[i].innerHTML&&n.removedNodes[i].innerHTML.indexOf('Powered by')>-1)){setTimeout(function(){document.body.appendChild(w.cloneNode(true))},100);break}}}if(n.target===document.body&&n.type==='attributes'&&n.attributeName==='style'){if(document.body.style.position!=='relative'){document.body.style.position='relative'}}})});o.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['style']})}</script></body>
|
||||
</html>
|
||||
|
||||
</body></html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user