/// app.js
/// 苏州肛泰中医院 · 运营管理后台
/// 描述:单页应用 - 登录 / 概览 / 首页内容/医院简介/系统设置/预约管理/意向客户/账号设置
/// 版本:v2.0
(function () {
'use strict';
// ============ 版本号 ============
const APP_VERSION = 'v2.0';
// ============ 配置 ============
// 允许通过 ?api= 覆盖 API 地址,例如 admin/index.html?api=http://localhost:3000
const params = new URLSearchParams(location.search);
const API_BASE = (params.get('api') || localStorage.getItem('yygh_api_base') || '').replace(/\/$/, '');
// 业务常量
const APPT_STATUS = {
pending: { label: '待确认', tag: 'tag-warning' },
confirmed: { label: '已确认', tag: 'tag-primary' },
completed: { label: '已完成', tag: 'tag-success' },
cancelled: { label: '已取消', tag: 'tag-error' }
};
// ============ 工具 ============
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const toast = (msg, type = '') => {
const el = $('#toast');
el.textContent = msg;
el.className = 'toast show ' + type;
clearTimeout(toast._t);
toast._t = setTimeout(() => { el.className = 'toast ' + type; }, 2400);
};
// API 版本前缀(Phase 1 起统一为 v1)
const API_VERSION = '/v1';
const api = async (path, opts = {}) => {
// 自动给以 '/api/' 开头的 path 加版本号前缀(向后兼容:已带 /v1 不重复加)
let versionedPath = path;
if (path.startsWith('/api/') && !path.startsWith('/api/v1/')) {
versionedPath = '/api' + API_VERSION + path.slice(4);
}
const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) };
const token = localStorage.getItem('yygh_token');
if (token) headers.Authorization = 'Bearer ' + token;
const url = (API_BASE || '') + versionedPath;
const res = await fetch(url, { ...opts, headers: { ...headers, ...(opts.headers || {}) } });
let data;
try { data = await res.json(); } catch (e) { data = null; }
if (res.status === 401) {
// 鉴权失效,回到登录
localStorage.removeItem('yygh_token');
localStorage.removeItem('yygh_admin');
showLogin();
throw new Error(data?.message || '请先登录');
}
if (!res.ok || (data && data.code && data.code !== 0)) {
throw new Error((data && data.message) || `请求失败 (${res.status})`);
}
return data ? data.data : null;
};
const formatDate = (s) => {
if (!s) return '-';
const d = new Date(s);
if (Number.isNaN(d.getTime())) return s;
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
// ============ 登录 ============
const showLogin = () => {
$('#loginPage').classList.remove('hidden');
$('#appPage').classList.add('hidden');
};
const showApp = () => {
$('#loginPage').classList.add('hidden');
$('#appPage').classList.remove('hidden');
const admin = JSON.parse(localStorage.getItem('yygh_admin') || '{}');
$('#userName').textContent = admin.nickname || admin.username || '管理员';
$('#userChip').querySelector('.user-avatar').textContent = (admin.nickname || admin.username || 'A').charAt(0).toUpperCase();
};
$('#loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = $('#loginUsername').value.trim();
const password = $('#loginPassword').value;
if (!username || !password) return toast('请输入账号和密码', 'error');
try {
const data = await api('/api/admin/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
localStorage.setItem('yygh_token', data.token);
localStorage.setItem('yygh_admin', JSON.stringify(data.admin));
showApp();
navigate(location.hash.replace('#', '') || 'dashboard');
toast('登录成功', 'success');
} catch (err) {
toast(err.message || '登录失败', 'error');
}
});
$('#logoutBtn').addEventListener('click', () => {
localStorage.removeItem('yygh_token');
localStorage.removeItem('yygh_admin');
showLogin();
toast('已退出登录');
});
// ============ Modal ============
const openModal = ({ title, body, footer, size = '' }) => {
$('#modalTitle').textContent = title;
$('#modalBody').innerHTML = body;
$('#modalFooter').innerHTML = footer || '';
$('#modal').classList.remove('hidden');
$('.modal-card').classList.toggle('modal-lg', size === 'lg');
};
const closeModal = () => { $('#modal').classList.add('hidden'); };
document.addEventListener('click', (e) => {
if (e.target.matches('[data-modal-close]')) closeModal();
});
// ============ 路由 ============
const routes = {
dashboard: renderDashboard,
appointments: renderAppointments,
homeContent: renderHomeContent,
hospitalIntro: renderHospitalIntro,
siteConfig: renderSiteConfig,
account: renderAccount,
leads: renderLeads
};
const navigate = (name) => {
if (!routes[name]) name = 'dashboard';
location.hash = name;
};
const renderCurrent = () => {
const name = location.hash.replace('#', '') || 'dashboard';
$$('.side-link').forEach((el) => el.classList.toggle('active', el.dataset.route === name));
const main = $('#appMain');
main.innerHTML = '
加载中…
';
Promise.resolve(routes[name]()).then((html) => { main.innerHTML = html; bindPage(name); })
.catch((err) => { main.innerHTML = `加载失败:${escapeHtml(err.message)}
`; });
};
window.addEventListener('hashchange', renderCurrent);
$$('.side-link').forEach((el) => el.addEventListener('click', (e) => { e.preventDefault(); navigate(el.dataset.route); }));
// ============ 页面:概览 ============
async function renderDashboard() {
const [stats, recent] = await Promise.all([
api('/api/appointments/stats').catch(() => ({ total: 0, pending: 0, confirmed: 0, completed: 0, cancelled: 0 })),
api('/api/appointments').catch(() => [])
]);
const upcoming = recent.slice(0, 6);
return `
${[
{ label: '累计预约', value: stats.total, foot: '所有时间段' },
{ label: '待确认', value: stats.pending, foot: '需要尽快处理', tag: 'tag-warning' },
{ label: '已确认', value: stats.confirmed, foot: '等待到诊', tag: 'tag-primary' },
{ label: '已完成', value: stats.completed, foot: '本月完成', tag: 'tag-success' }
].map((s) => `
`).join('')}
${upcoming.length === 0 ? '
暂无预约数据
' : `
| 患者 | 科室 | 日期 | 状态 | 提交时间 |
${upcoming.map((a) => `
|
${escapeHtml(a.patientName)}
${escapeHtml(a.patientPhone)} · ${a.patientAge}岁
|
${escapeHtml(a.departmentName || '-')} |
${escapeHtml(a.date || '-')} |
${APPT_STATUS[a.status]?.label || a.status} |
${formatDate(a.createdAt)} |
`).join('')}
`}
`;
}
// ============ 页面:预约管理 ============
async function renderAppointments() {
const list = await api('/api/appointments');
list.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
return `
${list.length === 0 ? '
暂无预约
' : `
| 患者 | 科室 | 预约日期 | 症状 | 状态 | 提交时间 | 操作 |
${list.map(aptRow).join('')}
`}
`;
}
const aptRow = (a) => `
|
${escapeHtml(a.patientName)}
${escapeHtml(a.patientPhone)} · ${a.patientAge}岁
|
${escapeHtml(a.departmentName || '-')} |
${escapeHtml(a.date || '-')} |
${escapeHtml(a.symptom || '-')} |
${APPT_STATUS[a.status]?.label || a.status} |
${formatDate(a.createdAt)} |
${a.status === 'pending' ? `` : ''}
${a.status === 'confirmed' ? `` : ''}
${['pending', 'confirmed'].includes(a.status) ? `` : ''}
|
`;
const bindAppointments = () => {
const filter = () => {
const q = $('#aptSearch').value.trim().toLowerCase();
const s = $('#aptStatus').value;
$$('#aptTbody tr').forEach((tr) => {
const ok = (!q || tr.dataset.phone.toLowerCase().includes(q) || tr.dataset.name.toLowerCase().includes(q))
&& (!s || tr.dataset.status === s);
tr.style.display = ok ? '' : 'none';
});
};
$('#aptSearch')?.addEventListener('input', filter);
$('#aptStatus')?.addEventListener('change', filter);
$$('#aptTbody button[data-act]').forEach((btn) => {
btn.addEventListener('click', async () => {
const tr = btn.closest('tr');
const id = tr.dataset.id;
const act = btn.dataset.act;
const actions = {
confirm: { method: 'PUT', path: `/api/appointments/${id}/confirm`, msg: '已确认' },
complete: { method: 'PUT', path: `/api/appointments/${id}/complete`, msg: '已标记完成' },
cancel: { method: 'PUT', path: `/api/appointments/${id}/cancel`, msg: '已取消' },
delete: { method: 'DELETE', path: `/api/admin/appointments/${id}`, msg: '已删除' }
};
if (act === 'delete' && !confirm('确定删除该预约?此操作不可恢复')) return;
try {
await api(actions[act].path, { method: actions[act].method });
toast(actions[act].msg, 'success');
reloadPage('appointments');
} catch (err) { toast(err.message, 'error'); }
});
});
};
// ============ 页面:意向客户 leads(Phase 3) ============
// 问卷题目映射(与前端 self-test 页面保持一致)
const QUESTION_MAP = [
{ text: '您最近是否出现过大便带血或便后滴血的情况?', options: { no: '无', sometimes: '偶尔', often: '经常' } },
{ text: '您是否有肛门疼痛、瘙痒或异物感?', options: { no: '无', sometimes: '偶尔', often: '经常' } },
{ text: '您是否长期便秘、腹泻或两者交替出现?', options: { no: '无', sometimes: '偶尔', often: '经常' } },
{ text: '您是否感到腹胀、腹痛或饭后不适?', options: { no: '无', sometimes: '偶尔', often: '经常' } },
{ text: '您是否出现过肛门肿物脱出?', options: { no: '无', self: '可自行回纳', manual: '需用手推回', none: '无法回纳' } },
{ text: '您的排便习惯是否近期发生明显变化?', options: { no: '无变化', sometimes: '偶有变化', obvious: '明显变化' } },
{ text: '您是否有胃肠疾病家族史?', options: { no: '无', unknown: '不清楚', yes: '有' } }
];
const LEAD_SOURCES = [
{ value: 'miniapp-phone', label: '小程序获取' },
{ value: 'self-test', label: '在线自测' },
{ value: 'home', label: '首页落地' },
{ value: 'appointment', label: '预约入口' },
{ value: 'other', label: '其他' }
];
async function renderLeads() {
const keyword = new URLSearchParams(location.search).get('kw') || '';
const source = new URLSearchParams(location.search).get('src') || '';
const params = new URLSearchParams();
if (keyword) params.set('keyword', keyword);
if (source) params.set('source', source);
params.set('limit', '50');
const qs = params.toString();
const data = await api(`/api/admin/leads${qs ? `?${qs}` : ''}`);
const list = data?.list || [];
const total = data?.total || 0;
const stats = await api('/api/leads/stats').catch(() => ({ total: 0, last24h: 0, bySource: {} }));
return `
意向客户
自主问卷等渠道收集的留资线索(累计 ${stats.total} 条 · 24h 新增 ${stats.last24h})
${[
{ label: '累计留资', value: stats.total },
{ label: '24h 新增', value: stats.last24h },
{ label: '本次查询', value: total }
].map((s) => `
`).join('')}
${list.length === 0 ? '
暂无意向客户
' : `
| 提交时间 | 姓名 | 手机 | 来源 | 得分 | 症状 | 备注 | 操作 |
${list.map((l) => `
${formatDate(l.createdAt)} |
${escapeHtml(l.name)} |
${escapeHtml(l.phone)} |
${LEAD_SOURCES.find((s) => s.value === l.source)?.label || l.source}${l.source === 'miniapp-phone' ? ' 📱' : ''} |
${Number(l.score) || 0} |
${escapeHtml(l.symptom || '-')} |
${escapeHtml(l.remark || '')} |
|
`).join('')}
`}
`;
}
const bindLeads = () => {
$('#leadFilter')?.addEventListener('submit', (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const params = new URLSearchParams();
const kw = fd.get('keyword'); if (kw) params.set('kw', String(kw));
const src = fd.get('source'); if (src) params.set('src', String(src));
location.search = params.toString();
});
$('button[data-act="reset"]')?.addEventListener('click', () => { location.search = ''; });
$$('#leadTbody button[data-act]').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = btn.closest('tr').dataset.id;
const act = btn.dataset.act;
try {
if (act === 'view') {
const l = await api(`/api/admin/leads/${id}`);
const q = l.questionnaire || {};
const locationHtml = q.location
? ``
: '';
const authTimeHtml = q.authTime
? ``
: '';
const ansHtml = Object.entries(q.answers || {})
.map(([k, v]) => {
const idx = Number(k);
const qItem = QUESTION_MAP[idx];
const qText = qItem ? qItem.text : `第 ${idx + 1} 题`;
const aText = qItem ? (qItem.options[v] || String(v)) : String(v);
return `Q${idx + 1}${escapeHtml(qText)}${escapeHtml(aText)}
`;
})
.join('') || '(无)
';
openModal({
size: 'lg',
title: '意向客户详情',
body: `
`,
footer: ``
});
} else if (act === 'remark') {
const l = await api(`/api/admin/leads/${id}`);
openModal({
title: `${l.remark ? '修改' : '添加'}备注`,
body: ``,
footer: ``
});
$('#remarkSave').addEventListener('click', async () => {
try {
const fd = new FormData($('#remarkForm'));
await api(`/api/admin/leads/${id}/remark`, { method: 'PUT', body: JSON.stringify({ remark: fd.get('remark') }) });
toast('已更新', 'success'); closeModal(); reloadPage('leads');
} catch (err) { toast(err.message, 'error'); }
});
} else if (act === 'delete') {
if (!confirm('确定删除该意向客户?此操作不可恢复。')) return;
try {
await api(`/api/admin/leads/${id}`, { method: 'DELETE' });
toast('已删除', 'success'); reloadPage('leads');
} catch (err) { toast(err.message, 'error'); }
}
} catch (err) { toast(err.message, 'error'); }
});
});
};
// ============ 通用图片上传 (file → /uploads/image → 返回可访问 URL) ============
const fileToBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const handleImageFile = async (input, previewSelector) => {
const file = input.files?.[0];
if (!file) return;
try {
const dataUrl = await fileToBase64(file);
const out = await api('/api/uploads/image', { method: 'POST', body: JSON.stringify({ base64: dataUrl }) });
input.dataset.url = out.url;
const prev = $(previewSelector);
if (prev) { prev.src = out.url; prev.style.display = 'block'; }
} catch (err) { toast(err.message || '上传失败', 'error'); }
input.value = '';
};
// ============ 页面:首页内容(含 3 子 tab:轮播 / 中心 / 长图)============
const HOME_CONTENT_TABS = [
{ id: 'banner', label: '顶部轮播图' },
{ id: 'center', label: '三个中心' },
{ id: 'intro', label: '简介长图' }
];
// ---------- 子 tab 1: 轮播图 ----------
const renderBannerList = async () => {
const list = await api('/api/banners');
return `
${list.length === 0 ? '
暂无 Banner, 点击右上角"新增"
' : `
| 预览 | 标题 | 链接 | 排序 | 状态 | 操作 |
${list.map((b) => `
}) |
${escapeHtml(b.title || '-')} ${escapeHtml(b.subtitle || '')} |
${escapeHtml(b.link || '-')} |
${b.sort} |
${b.enabled ? '启用' : '停用'} |
|
`).join('')}
`}
`;
};
const bannerForm = (b = {}) => `
`;
const readBannerForm = () => {
const fd = new FormData($('#bnForm'));
return { title: fd.get('title'), subtitle: fd.get('subtitle'), image: fd.get('image'),
link: fd.get('link'), sort: Number(fd.get('sort') || 99), enabled: fd.get('enabled') !== null };
};
// ---------- 子 tab 2: 三个中心 ----------
const renderCenterList = async () => {
const list = await api('/api/centers');
return `
${list.length === 0 ? '
暂无中心, 点击右上角"新增"
' : `
| 图标 | 名称 | slug | 简介 | 排序 | 状态 | 操作 |
${list.map((c) => `
${c.iconImage
? ` `
: `${escapeHtml(c.icon || '🩺')}`} |
${escapeHtml(c.name)} |
${escapeHtml(c.slug)} |
${escapeHtml(c.intro || '-')} |
${c.sort} |
${c.enabled ? '启用' : '停用'} |
|
`).join('')}
`}
`;
};
const centerForm = (c = {}) => `
`;
const readCenterForm = () => {
const fd = new FormData($('#ctForm'));
return { name: fd.get('name'), slug: fd.get('slug'), intro: fd.get('intro'),
icon: fd.get('icon'), iconImage: fd.get('iconImage'), image: fd.get('image'),
sort: Number(fd.get('sort') || 99), enabled: fd.get('enabled') !== null };
};
// ---------- 子 tab 3: 简介长图 ----------
const renderIntroEditor = async () => {
const cur = await api('/api/home-intro');
return `
首页简介长图
此图会显示在首页底部 (3 中心卡下方), 单张长图, 可竖向滚动浏览
`;
};
// ---------- 父页 ----------
async function renderHomeContent() {
return `
${HOME_CONTENT_TABS.map((t, i) => ``).join('')}
`;
}
const bindHomeContent = () => {
let activeTab = 'banner';
const renderPanel = async (tabId) => {
activeTab = tabId;
$$('#hcTabs .subtab').forEach((b) => b.classList.toggle('active', b.dataset.tab === tabId));
const panel = $('#hcPanel');
panel.innerHTML = '加载中…
';
let html = '';
if (tabId === 'banner') html = await renderBannerList();
else if (tabId === 'center') html = await renderCenterList();
else if (tabId === 'intro') html = await renderIntroEditor();
panel.innerHTML = html;
bindPanel(tabId);
};
const bindPanel = (tabId) => {
if (tabId === 'banner') {
$('button[data-act="add-banner"]')?.addEventListener('click', () => {
openModal({ title: '新增轮播图', body: bannerForm(),
footer: `` });
$('#bnFile').addEventListener('change', (e) => handleImageFile(e.target, '#bnPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#bnSave').addEventListener('click', async () => {
try { const body = readBannerForm(); if (!body.image) return toast('请上传图片', 'error');
await api('/api/banners', { method: 'POST', body: JSON.stringify(body) });
toast('已新增', 'success'); closeModal(); renderPanel('banner');
} catch (err) { toast(err.message, 'error'); }
});
});
$$('button[data-act="edit-banner"]').forEach((b) => b.addEventListener('click', async () => {
const id = b.closest('tr').dataset.id;
const list = await api('/api/banners');
const cur = list.find((x) => x.id === id);
openModal({ title: '编辑轮播图', body: bannerForm(cur),
footer: `` });
$('#bnFile').addEventListener('change', (e) => handleImageFile(e.target, '#bnPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#bnSave').addEventListener('click', async () => {
try { const body = readBannerForm(); if (!body.image) return toast('请上传图片', 'error');
await api(`/api/banners/${id}`, { method: 'PUT', body: JSON.stringify(body) });
toast('已更新', 'success'); closeModal(); renderPanel('banner');
} catch (err) { toast(err.message, 'error'); }
});
}));
$$('button[data-act="del-banner"]').forEach((b) => b.addEventListener('click', async () => {
if (!confirm('确定删除?')) return;
try { await api(`/api/banners/${b.closest('tr').dataset.id}`, { method: 'DELETE' });
toast('已删除', 'success'); renderPanel('banner');
} catch (err) { toast(err.message, 'error'); }
}));
} else if (tabId === 'center') {
$('button[data-act="add-center"]')?.addEventListener('click', () => {
openModal({ size: 'lg', title: '新增中心', body: centerForm(),
footer: `` });
$('#ctIconFile').addEventListener('change', (e) => handleImageFile(e.target, '#ctIconPreview').then(() => {
$('input[name="iconImage"]').value = e.target.dataset.url || '';
}));
$('#ctFile').addEventListener('change', (e) => handleImageFile(e.target, '#ctPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#ctSave').addEventListener('click', async () => {
try { const body = readCenterForm();
if (!body.iconImage && !body.icon) return toast('请上传图标图片或填写 emoji 图标', 'error');
await api('/api/centers', { method: 'POST', body: JSON.stringify(body) });
toast('已新增', 'success'); closeModal(); renderPanel('center');
} catch (err) { toast(err.message, 'error'); }
});
});
$$('button[data-act="edit-center"]').forEach((b) => b.addEventListener('click', async () => {
const id = b.closest('tr').dataset.id;
const list = await api('/api/centers');
const cur = list.find((x) => x.id === id);
openModal({ size: 'lg', title: '编辑中心', body: centerForm(cur),
footer: `` });
$('#ctIconFile').addEventListener('change', (e) => handleImageFile(e.target, '#ctIconPreview').then(() => {
$('input[name="iconImage"]').value = e.target.dataset.url || '';
}));
$('#ctFile').addEventListener('change', (e) => handleImageFile(e.target, '#ctPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#ctSave').addEventListener('click', async () => {
try { const body = readCenterForm();
await api(`/api/centers/${id}`, { method: 'PUT', body: JSON.stringify(body) });
toast('已更新', 'success'); closeModal(); renderPanel('center');
} catch (err) { toast(err.message, 'error'); }
});
}));
$$('button[data-act="del-center"]').forEach((b) => b.addEventListener('click', async () => {
if (!confirm('确定删除?')) return;
try { await api(`/api/centers/${b.closest('tr').dataset.id}`, { method: 'DELETE' });
toast('已删除', 'success'); renderPanel('center');
} catch (err) { toast(err.message, 'error'); }
}));
} else if (tabId === 'intro') {
$('#hiFile')?.addEventListener('change', (e) => handleImageFile(e.target, '#hiPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#hiSave')?.addEventListener('click', async () => {
try { const fd = new FormData($('#hiForm'));
const body = { image: fd.get('image'), caption: fd.get('caption'), enabled: fd.get('enabled') !== null };
if (!body.image) return toast('请上传长图', 'error');
await api('/api/home-intro', { method: 'PUT', body: JSON.stringify(body) });
toast('已保存', 'success');
} catch (err) { toast(err.message, 'error'); }
});
}
};
$$('#hcTabs .subtab').forEach((b) => b.addEventListener('click', () => renderPanel(b.dataset.tab)));
renderPanel('banner');
};
// ============ 页面:医院简介 (Hospital Intro) ============
async function renderHospitalIntro() {
const list = await api('/api/hospital-images');
return `
医院简介
管理小程序"医院简介" tab 页展示的图片列表
${list.length === 0 ? '
暂无图片
' : `
| 预览 | 说明 | 排序 | 状态 | 操作 |
${list.map((it) => `
}) |
${escapeHtml(it.caption || '-')} |
${it.sort} |
${it.enabled ? '启用' : '停用'} |
|
`).join('')}
`}
`;
}
const hospitalImageForm = (it = {}) => `
`;
const readHospitalImageForm = () => {
const fd = new FormData($('#hiImgForm'));
return { image: fd.get('image'), caption: fd.get('caption'),
sort: Number(fd.get('sort') || 99), enabled: fd.get('enabled') !== null };
};
const bindHospitalIntro = () => {
$('button[data-act="add-hi"]')?.addEventListener('click', () => {
openModal({ size: 'lg', title: '新增医院图片', body: hospitalImageForm(),
footer: `` });
$('#hiImgFile').addEventListener('change', (e) => handleImageFile(e.target, '#hiImgPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#hiImgSave').addEventListener('click', async () => {
try { const body = readHospitalImageForm(); if (!body.image) return toast('请上传图片', 'error');
await api('/api/hospital-images', { method: 'POST', body: JSON.stringify(body) });
toast('已新增', 'success'); closeModal(); reloadPage('hospitalIntro');
} catch (err) { toast(err.message, 'error'); }
});
});
$$('button[data-act="edit-hi"]').forEach((b) => b.addEventListener('click', async () => {
const id = b.closest('tr').dataset.id;
const list = await api('/api/hospital-images');
const cur = list.find((x) => x.id === id);
openModal({ size: 'lg', title: '编辑医院图片', body: hospitalImageForm(cur),
footer: `` });
$('#hiImgFile').addEventListener('change', (e) => handleImageFile(e.target, '#hiImgPreview').then(() => {
$('input[name="image"]').value = e.target.dataset.url || '';
}));
$('#hiImgSave').addEventListener('click', async () => {
try { const body = readHospitalImageForm(); if (!body.image) return toast('请上传图片', 'error');
await api(`/api/hospital-images/${id}`, { method: 'PUT', body: JSON.stringify(body) });
toast('已更新', 'success'); closeModal(); reloadPage('hospitalIntro');
} catch (err) { toast(err.message, 'error'); }
});
}));
$$('button[data-act="del-hi"]').forEach((b) => b.addEventListener('click', async () => {
if (!confirm('确定删除?')) return;
try { await api(`/api/hospital-images/${b.closest('tr').dataset.id}`, { method: 'DELETE' });
toast('已删除', 'success'); reloadPage('hospitalIntro');
} catch (err) { toast(err.message, 'error'); }
}));
};
// ============ 页面:系统设置(站点配置)============
async function renderSiteConfig() {
const cur = await api('/api/config/site');
return `
系统设置
维护小程序底栏"电话咨询"号码 & 微信手机号授权配置
咨询电话
此号码会在小程序底栏最右"电话咨询"按钮上使用, 用户点击直接拨号
📱 微信手机号授权
开启后,用户首次打开小程序时会弹出授权手机号弹窗,授权后自动填充到预约表单
🔔 通知设置
新预约、新意向客户时自动推送消息到企业微信群
📡 接口自检
点击"测试"按钮可直接拉取最新数据, 验证小程序能看到
`;
}
const bindSiteConfig = () => {
$('#saveSite')?.addEventListener('click', async () => {
try {
const phone = $('#sitePhone').value.trim();
const address = $('#siteAddress').value.trim();
const name = $('#siteName').value.trim();
const requirePhone = $('#requirePhone').checked;
const wechatAppid = $('#wechatAppid').value.trim();
const wechatAppsecret = $('#wechatAppsecret').value.trim();
const amapKey = $('#amapKey').value.trim();
const collectLocation = $('#collectLocation').checked;
const notifyEnabled = $('#notifyEnabled').checked;
const notifyWebhooks = $('#notifyWebhooks').value.trim();
if (!phone) return toast('咨询电话必填', 'error');
if (requirePhone && (!wechatAppid || !wechatAppsecret)) {
return toast('开启手机号获取需要填写 AppID 和 AppSecret', 'error');
}
await api('/api/config/site', { method: 'PUT', body: JSON.stringify({
phone, address, name, requirePhone, wechatAppid, wechatAppsecret, amapKey, collectLocation, notifyEnabled, notifyWebhooks
}) });
toast('已保存', 'success');
} catch (err) { toast(err.message, 'error'); }
});
$('#testDial')?.addEventListener('click', () => {
const p = $('#sitePhone').value.trim();
if (!p) return toast('请先填写咨询电话', 'error');
const cleaned = p.replace(/[^\d+]/g, '');
$('#testResult').textContent = '清洗后号码: ' + cleaned + '\n微信小程序会调用 Taro.makePhoneCall({ phoneNumber: "' + cleaned + '" })';
});
$('#testFetch')?.addEventListener('click', async () => {
try {
const r = await api('/api/config/site');
$('#testResult').textContent = JSON.stringify(r, null, 2);
} catch (err) {
$('#testResult').textContent = '错误: ' + err.message;
}
});
$('#testNotify')?.addEventListener('click', async () => {
const webhooks = $('#notifyWebhooks').value.trim();
if (!webhooks) return toast('请先填写 Webhook 地址', 'error');
try {
await api('/api/v1/notification/test', { method: 'POST' });
toast('测试通知已发送,请检查企业微信群', 'success');
} catch (err) {
toast('发送失败: ' + err.message, 'error');
}
});
};
// ============ 页面:账号设置 ============
async function renderAccount() {
const me = await api('/api/admin/auth/me');
return `
`;
}
const bindAccount = () => {
$('#pwdForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
await api('/api/admin/auth/change-password', { method: 'POST', body: JSON.stringify({
oldPassword: fd.get('oldPassword'),
newPassword: fd.get('newPassword')
}) });
toast('密码已修改', 'success');
e.target.reset();
} catch (err) { toast(err.message, 'error'); }
});
};
// ============ 通用绑定 ============
const bindPage = (name) => {
// 全局:data-go 链接
$$('[data-go]').forEach((el) => el.addEventListener('click', (e) => { e.preventDefault(); navigate(el.dataset.go); }));
if (name === 'appointments') bindAppointments();
else if (name === 'homeContent') bindHomeContent();
else if (name === 'hospitalIntro') bindHospitalIntro();
else if (name === 'siteConfig') bindSiteConfig();
else if (name === 'account') bindAccount();
else if (name === 'leads') bindLeads();
};
const reloadPage = (name) => {
if (!name) name = location.hash.replace('#', '') || 'dashboard';
if (routes[name]) {
const main = $('#appMain');
main.innerHTML = '加载中…
';
Promise.resolve(routes[name]()).then((html) => { main.innerHTML = html; bindPage(name); });
}
};
// ============ 启动 ============
if (localStorage.getItem('yygh_token')) {
showApp();
renderCurrent();
} else {
showLogin();
}
})();