/// 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 `

概览

系统运行状态与近期预约一览

查看 API
${[ { 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) => `
${s.label}
${s.value}
${s.foot}
`).join('')}

最近预约

按创建时间倒序,最多 6 条

查看全部
${upcoming.length === 0 ? '
暂无预约数据
' : `
${upcoming.map((a) => ` `).join('')}
患者科室日期状态提交时间
${escapeHtml(a.patientName)}
${escapeHtml(a.patientPhone)} · ${a.patientAge}岁
${escapeHtml(a.departmentName || '-')} ${escapeHtml(a.date || '-')} ${APPT_STATUS[a.status]?.label || a.status} ${formatDate(a.createdAt)}
`}
`; } // ============ 页面:预约管理 ============ 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) => `
${s.label}
${s.value}
`).join('')}
${list.length === 0 ? '
暂无意向客户
' : ` ${list.map((l) => ` `).join('')}
提交时间姓名手机来源得分症状备注操作
${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 || '')}
`}
`; } 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 ? `
📍 ${q.location.address || (q.location.latitude + ',' + q.location.longitude)}
` : ''; const authTimeHtml = q.authTime ? `
${formatDate(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: `
${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} 分
${authTimeHtml} ${locationHtml}
${escapeHtml(l.symptom || '-')}
${Number(q.total) || 0} 分
${ansHtml}
${formatDate(l.createdAt)}
${escapeHtml(l.id)}
`, 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) => ` `).join('')}
预览标题链接排序状态操作
${escapeHtml(b.title || '-')}
${escapeHtml(b.subtitle || '')}
${escapeHtml(b.link || '-')} ${b.sort} ${b.enabled ? '启用' : '停用'}
`}
`; }; const bannerForm = (b = {}) => `

支持选择本地文件自动上传, 也可直接粘贴外链 URL (后端会落盘到 /uploads/)

`; 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 ? '
暂无中心, 点击右上角"新增"
' : ` ${list.map((c) => ` `).join('')}
图标名称slug简介排序状态操作
${c.iconImage ? `` : `${escapeHtml(c.icon || '🩺')}`} ${escapeHtml(c.name)} ${escapeHtml(c.slug)} ${escapeHtml(c.intro || '-')} ${c.sort} ${c.enabled ? '启用' : '停用'}
`}
`; }; 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 中心卡下方), 单张长图, 可竖向滚动浏览

支持选择本地文件自动上传, 也可粘贴外链 URL

`; }; // ---------- 父页 ---------- 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) => ` `).join('')}
预览说明排序状态操作
${escapeHtml(it.caption || '-')} ${it.sort} ${it.enabled ? '启用' : '停用'}
`}
`; } 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 `

系统设置

维护小程序底栏"电话咨询"号码 & 微信手机号授权配置

咨询电话

此号码会在小程序底栏最右"电话咨询"按钮上使用, 用户点击直接拨号

支持格式: 座机 (区号-号码) 或 11 位手机号, 用户点击时小程序会自动清除非数字字符后拨号。此号码也用于预约页"健康咨询热线"

显示在预约页面底部

显示在小程序首页导航栏标题,留空则使用默认值

📱 微信手机号授权

开启后,用户首次打开小程序时会弹出授权手机号弹窗,授权后自动填充到预约表单

开启后用户打开小程序时弹出微信官方手机号授权弹窗,关闭则不弹出

在微信公众平台 → 开发管理 → 开发设置 中获取

在微信公众平台 → 开发管理 → 开发设置 中获取,重置后需同步更新

免费申请:登录 lbs.amap.com → 控制台 → 创建应用 → 添加 Key(Web服务类型)· 不填则只记录经纬度

开启后在用户授权手机号时同时获取模糊定位(市级),需要先在微信公众平台开通"模糊定位"接口权限。未开通请保持关闭,否则会报错

🔔 通知设置

新预约、新意向客户时自动推送消息到企业微信群

开启后,每次新预约或新意向客户时自动推送消息

在企业微信群 → 群设置 → 群机器人 → 添加机器人 → 复制 Webhook 地址。每行一个,可配置多个群

📡 接口自检

点击"测试"按钮可直接拉取最新数据, 验证小程序能看到


        
`; } 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(); } })();