admin.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>纪检管理后台 · 安建医院纪检</title>
  7. <style>
  8. :root{
  9. --primary:#a51f23;--primary-dark:#8a1a1d;--primary-soft:#f8ecec;
  10. --ink:#1f1f1f;--muted:#8a8a8a;--line:#e8e8e8;--bg:#f3f4f6;--card:#fff;
  11. --radius:10px;--shadow:0 2px 12px rgba(0,0,0,.06);--side:220px;
  12. }
  13. *{box-sizing:border-box;margin:0;padding:0}
  14. body{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;background:var(--bg);color:var(--ink);min-height:100vh;line-height:1.5}
  15. .layout{display:flex;min-height:100vh}
  16. .sidebar{width:var(--side);background:linear-gradient(180deg,#2a1516,#1a1011);color:#fff;position:fixed;inset:0 auto 0 0;display:flex;flex-direction:column;z-index:50}
  17. .brand{padding:22px 18px 16px;border-bottom:1px solid rgba(255,255,255,.08)}
  18. .brand img{width:40px;height:40px;border-radius:8px;background:#fff;object-fit:contain;padding:4px;display:block;margin-bottom:10px}
  19. .brand h1{font-size:15px;font-weight:600}
  20. .brand p{margin-top:4px;font-size:11px;color:rgba(255,255,255,.5)}
  21. .nav{padding:12px 10px;flex:1}
  22. .nav button{width:100%;display:flex;align-items:center;gap:10px;padding:11px 12px;margin-bottom:4px;border:0;border-radius:8px;background:transparent;color:rgba(255,255,255,.72);font:inherit;font-size:14px;cursor:pointer;text-align:left}
  23. .nav button:hover{background:rgba(255,255,255,.06);color:#fff}
  24. .nav button.active{background:var(--primary);color:#fff;font-weight:600}
  25. .nav svg{width:18px;height:18px;fill:currentColor;flex-shrink:0}
  26. .side-foot{padding:14px 18px 20px;font-size:11px;color:rgba(255,255,255,.4);border-top:1px solid rgba(255,255,255,.06);line-height:1.6}
  27. .main{margin-left:var(--side);flex:1;min-width:0}
  28. .topbar{height:56px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 24px;position:sticky;top:0;z-index:40}
  29. .topbar h2{font-size:17px}
  30. .topbar .user{font-size:13px;color:var(--muted)}
  31. .topbar .user b{color:var(--ink)}
  32. .content{padding:20px 24px 40px}
  33. .tip{margin-bottom:14px;padding:10px 14px;background:#fff8e8;border:1px solid #f0e0b2;border-radius:8px;font-size:12px;color:#8a6d1d}
  34. .panel{display:none}.panel.active{display:block}
  35. .stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:18px}
  36. @media(max-width:1100px){.stats{grid-template-columns:1fr 1fr}}
  37. .stat{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 20px}
  38. .stat .label{font-size:13px;color:var(--muted)}
  39. .stat .value{margin-top:6px;font-size:28px;font-weight:700;color:var(--primary)}
  40. .stat .sub{margin-top:4px;font-size:12px;color:#aaa}
  41. .toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:14px}
  42. .toolbar .spacer{flex:1}
  43. input[type=search],input[type=text],input[type=email],input[type=url],select,textarea{height:36px;border:1px solid var(--line);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;background:#fff;outline:none;color:var(--ink)}
  44. input[type=search]{width:220px}
  45. textarea{height:auto;min-height:88px;padding:10px 12px;width:100%;resize:vertical}
  46. input:focus,select:focus,textarea:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(165,31,35,.1)}
  47. .btn{height:36px;padding:0 14px;border-radius:8px;border:1px solid var(--line);background:#fff;font:inherit;font-size:13px;cursor:pointer;color:var(--ink)}
  48. .btn:hover{background:#fafafa}
  49. .btn-primary{background:var(--primary);border-color:var(--primary);color:#fff}
  50. .btn-primary:hover{background:var(--primary-dark)}
  51. .btn-ghost{color:var(--primary);border-color:rgba(165,31,35,.35);background:var(--primary-soft)}
  52. .btn-sm{height:28px;padding:0 10px;font-size:12px}
  53. .card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
  54. .card-head{padding:14px 18px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
  55. .card-head h3{font-size:15px}
  56. .table-wrap{overflow-x:auto}
  57. table{width:100%;border-collapse:collapse;font-size:13px}
  58. th,td{padding:12px 14px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
  59. th{background:#fafafa;color:#666;font-weight:600;font-size:12px;white-space:nowrap}
  60. tr:hover td{background:#fcf8f8}
  61. .ellipsis{max-width:240px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  62. .empty{padding:40px;text-align:center;color:var(--muted)}
  63. .tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600}
  64. .tag-warn{background:#fff1f0;color:#cf1322}
  65. .tag-ok{background:#f6ffed;color:#389e0d}
  66. .tag-gray{background:#f5f5f5;color:#666}
  67. .tag-blue{background:#e6f4ff;color:#1677ff}
  68. .grid-2{display:grid;grid-template-columns:1.2fr .8fr;gap:16px}
  69. @media(max-width:960px){.grid-2{grid-template-columns:1fr}}
  70. .form-card,.preview-card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 22px 24px}
  71. .form-card h3,.preview-card h3{font-size:15px;margin-bottom:4px}
  72. .form-card .desc,.preview-card .desc{font-size:12px;color:var(--muted);margin-bottom:18px}
  73. .field{margin-bottom:14px}
  74. .field label{display:block;font-size:13px;margin-bottom:6px;font-weight:500;color:#444}
  75. .field input{width:100%;height:40px}
  76. .actions{margin-top:16px;display:flex;flex-wrap:wrap;gap:10px}
  77. .hint{margin-top:10px;font-size:12px;color:#999;line-height:1.6}
  78. .upload{border:1px dashed #ccc;border-radius:8px;padding:16px;text-align:center;color:var(--muted);font-size:13px;cursor:pointer;background:#fafafa;display:block}
  79. .upload:hover{border-color:var(--primary);color:var(--primary);background:var(--primary-soft)}
  80. .upload input{display:none}
  81. .qr-box{width:220px;height:220px;margin:0 auto 14px;border:1px solid var(--line);border-radius:10px;display:flex;align-items:center;justify-content:center;background:#fff;position:relative;overflow:hidden}
  82. .qr-box canvas{width:200px;height:200px}
  83. .qr-box .logo{position:absolute;width:40px;height:40px;background:#fff;border-radius:6px;padding:3px;box-shadow:0 1px 4px rgba(0,0,0,.1)}
  84. .qr-box .logo img{width:100%;height:100%;object-fit:contain}
  85. .preview-card{text-align:center}
  86. .mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100}
  87. .mask.show{display:block}
  88. .drawer{position:fixed;top:0;right:0;width:min(480px,100%);height:100%;background:#fff;z-index:110;transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;box-shadow:-8px 0 24px rgba(0,0,0,.12)}
  89. .drawer.open{transform:translateX(0)}
  90. .drawer-head{height:56px;padding:0 18px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
  91. .drawer-head h3{font-size:16px}
  92. .drawer-close{border:0;background:transparent;font-size:22px;cursor:pointer;color:#999;width:36px;height:36px}
  93. .drawer-body{flex:1;overflow-y:auto;padding:18px}
  94. .block{margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--line)}
  95. .block:last-child{border-bottom:0}
  96. .block .dt{font-size:12px;color:var(--muted);margin-bottom:8px;font-weight:600}
  97. .dgrid{display:grid;grid-template-columns:72px 1fr;gap:8px 10px;font-size:13px}
  98. .dgrid .k{color:#999}
  99. .dgrid .v{word-break:break-all}
  100. .dcontent{font-size:14px;line-height:1.7;white-space:pre-wrap;background:#fafafa;border-radius:8px;padding:12px}
  101. .files{display:flex;flex-wrap:wrap;gap:8px}
  102. .file{padding:6px 10px;background:var(--primary-soft);color:var(--primary);border-radius:6px;font-size:12px}
  103. .toast{position:fixed;left:50%;bottom:40px;transform:translateX(-50%) translateY(12px);background:rgba(0,0,0,.8);color:#fff;padding:10px 18px;border-radius:20px;font-size:13px;opacity:0;pointer-events:none;transition:.25s;z-index:200}
  104. .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
  105. </style>
  106. </head>
  107. <body>
  108. <div class="layout">
  109. <aside class="sidebar">
  110. <div class="brand">
  111. <img src="logo1.png" alt="" onerror="this.style.display='none'" />
  112. <h1>纪检管理后台</h1>
  113. <p>安建医院纪检</p>
  114. </div>
  115. <nav class="nav">
  116. <button type="button" class="active" data-panel="dashboard" onclick="goPanel('dashboard')">
  117. <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>工作台
  118. </button>
  119. <button type="button" data-panel="feedback" onclick="goPanel('feedback')">
  120. <svg viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-1 6h2v6h-2V7zm0 8h2v2h-2v-2z"/></svg>在线反馈
  121. </button>
  122. <button type="button" data-panel="suggest" onclick="goPanel('suggest')">
  123. <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.42l-2.34-2.34a1 1 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.82z"/></svg>建言献策
  124. </button>
  125. <button type="button" data-panel="hotline" onclick="goPanel('hotline')">
  126. <svg viewBox="0 0 24 24"><path d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.01-.24c1.12.37 2.33.57 3.58.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.4 21 3 13.6 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.46.57 3.58a1 1 0 0 1-.25 1.02l-2.2 2.19z"/></svg>热线配置
  127. </button>
  128. <button type="button" data-panel="qrcode" onclick="goPanel('qrcode')">
  129. <svg viewBox="0 0 24 24"><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm10 2h2v2h-2zm4-2h-4v4h2v-2h2v-2zm-2-2h2v2h-2zm4 6h-2v2h4v-4h-2z"/></svg>二维码配置
  130. </button>
  131. </nav>
  132. <div class="side-foot">演示原型 · 非正式业务系统<br/>对接后端后替换为真实数据</div>
  133. </aside>
  134. <div class="main">
  135. <header class="topbar">
  136. <h2 id="pageTitle">工作台</h2>
  137. <div class="user">当前账号:<b>纪检管理员</b></div>
  138. </header>
  139. <div class="content">
  140. <div class="tip">本页为建设方案演示用管理端。列表为示例数据;热线与二维码配置写入浏览器本地存储,便于演示。</div>
  141. <section class="panel active" id="panel-dashboard">
  142. <div class="stats">
  143. <div class="stat"><div class="label">在线反馈总数</div><div class="value" id="statFeedback">0</div><div class="sub">含投诉 / 表扬</div></div>
  144. <div class="stat"><div class="label">投诉待办</div><div class="value" id="statComplaint">0</div><div class="sub">需及时核实办理</div></div>
  145. <div class="stat"><div class="label">建言献策</div><div class="value" id="statSuggest">0</div><div class="sub">群众建议条目</div></div>
  146. <div class="stat"><div class="label">本周新增</div><div class="value" id="statWeek">0</div><div class="sub">反馈 + 建议合计</div></div>
  147. </div>
  148. <div class="card">
  149. <div class="card-head"><h3>最近反馈</h3><button class="btn btn-sm btn-ghost" type="button" onclick="goPanel('feedback')">查看全部</button></div>
  150. <div class="table-wrap"><table><thead><tr><th>编号</th><th>类型</th><th>内容摘要</th><th>提交时间</th><th>状态</th><th>操作</th></tr></thead><tbody id="dashBody"></tbody></table></div>
  151. </div>
  152. </section>
  153. <section class="panel" id="panel-feedback">
  154. <div class="toolbar">
  155. <input type="search" id="fbSearch" placeholder="搜索内容 / 姓名 / 单位" oninput="renderFeedback()" />
  156. <select id="fbType" onchange="renderFeedback()"><option value="">全部类型</option><option value="投诉">投诉</option><option value="表扬">表扬</option></select>
  157. <select id="fbStatus" onchange="renderFeedback()"><option value="">全部状态</option><option value="待处理">待处理</option><option value="处理中">处理中</option><option value="已办结">已办结</option></select>
  158. <div class="spacer"></div>
  159. <button class="btn" type="button" onclick="toast('演示环境:导出功能待对接')">导出</button>
  160. </div>
  161. <div class="card"><div class="table-wrap"><table>
  162. <thead><tr><th>编号</th><th>类型</th><th>被反馈人</th><th>内容摘要</th><th>举报人</th><th>附件</th><th>提交时间</th><th>状态</th><th>操作</th></tr></thead>
  163. <tbody id="feedbackBody"></tbody>
  164. </table></div></div>
  165. </section>
  166. <section class="panel" id="panel-suggest">
  167. <div class="toolbar">
  168. <input type="search" id="sgSearch" placeholder="搜索建议内容 / 联系人" oninput="renderSuggest()" />
  169. <select id="sgStatus" onchange="renderSuggest()"><option value="">全部状态</option><option value="待查阅">待查阅</option><option value="已采纳">已采纳</option><option value="已回复">已回复</option></select>
  170. <div class="spacer"></div>
  171. <button class="btn" type="button" onclick="toast('演示环境:导出功能待对接')">导出</button>
  172. </div>
  173. <div class="card"><div class="table-wrap"><table>
  174. <thead><tr><th>编号</th><th>建议内容</th><th>联系人</th><th>单位 / 职务</th><th>联系方式</th><th>附件</th><th>提交时间</th><th>状态</th><th>操作</th></tr></thead>
  175. <tbody id="suggestBody"></tbody>
  176. </table></div></div>
  177. </section>
  178. <section class="panel" id="panel-hotline">
  179. <div class="form-card" style="max-width:640px">
  180. <h3>监督热线配置</h3>
  181. <p class="desc">配置后用于移动端「监督热线」页面展示(演示写入本地存储)。</p>
  182. <div class="field"><label>监督电话</label><input id="cfgPhone" type="text" placeholder="如:0551-12345678" /></div>
  183. <div class="field"><label>电子邮箱</label><input id="cfgEmail" type="email" placeholder="如:jjb@ajyy.example.com" /></div>
  184. <div class="field"><label>通讯地址</label><textarea id="cfgAddress" placeholder="请填写纪检室通讯地址"></textarea></div>
  185. <div class="field"><label>受理时间说明(选填)</label><textarea id="cfgHours" placeholder="如:工作日 08:30–11:30、14:30–17:30"></textarea></div>
  186. <div class="actions">
  187. <button class="btn btn-primary" type="button" onclick="saveHotline()">保存配置</button>
  188. <button class="btn" type="button" onclick="resetHotline()">恢复默认</button>
  189. </div>
  190. <p class="hint">正式环境需对接后台接口,同步到移动端热线页。</p>
  191. </div>
  192. </section>
  193. <section class="panel" id="panel-qrcode">
  194. <div class="grid-2">
  195. <div class="form-card">
  196. <h3>二维码配置</h3>
  197. <p class="desc">设置扫码落地页,或上传自定义二维码图片,供打印张贴与移动端分享使用。</p>
  198. <div class="field"><label>扫码跳转链接</label><input id="cfgQrUrl" type="url" placeholder="https://jjb.ajyy.example.com" oninput="refreshQr()" /></div>
  199. <div class="field"><label>二维码说明(选填)</label><input id="cfgQrTitle" type="text" placeholder="如:安建医院纪检 · 扫码反馈" oninput="refreshQr()" /></div>
  200. <div class="field">
  201. <label>自定义二维码图片(选填)</label>
  202. <label class="upload">点击上传 PNG / JPG(上传后优先使用图片)<input type="file" accept="image/png,image/jpeg,image/webp" onchange="onQrUpload(event)" /></label>
  203. <p class="hint" id="qrUploadName"></p>
  204. </div>
  205. <div class="actions">
  206. <button class="btn btn-primary" type="button" onclick="saveQr()">保存配置</button>
  207. <button class="btn" type="button" onclick="clearQrImage()">清除自定义图片</button>
  208. <button class="btn" type="button" onclick="downloadQr()">下载预览图</button>
  209. </div>
  210. </div>
  211. <div class="preview-card">
  212. <h3>效果预览</h3>
  213. <p class="desc" id="qrTitle">安建医院纪检 · 扫码反馈</p>
  214. <div class="qr-box" id="qrBox">
  215. <canvas id="qrCanvas" width="200" height="200"></canvas>
  216. <div class="logo" id="qrLogo"><img src="logo1.png" alt="" onerror="this.parentElement.style.display='none'" /></div>
  217. <img id="qrCustom" alt="" style="display:none;width:100%;height:100%;object-fit:contain" />
  218. </div>
  219. <p class="hint" id="qrUrlText" style="word-break:break-all"></p>
  220. </div>
  221. </div>
  222. </section>
  223. </div>
  224. </div>
  225. </div>
  226. <div class="mask" id="mask" onclick="closeDrawer()"></div>
  227. <aside class="drawer" id="drawer">
  228. <div class="drawer-head"><h3 id="drawerTitle">详情</h3><button class="drawer-close" type="button" onclick="closeDrawer()">×</button></div>
  229. <div class="drawer-body" id="drawerBody"></div>
  230. </aside>
  231. <div class="toast" id="toast"></div>
  232. <script>
  233. const feedbackList=[
  234. {id:"FK20260825001",type:"投诉",targetName:"张某某",targetUnit:"门诊部",targetPost:"护士",content:"反映个别窗口服务态度生硬,排队指引不清晰,建议加强窗口文明服务培训。",reporterName:"李某",reporterUnit:"患者家属",reporterPost:"",reporterPhone:"138****6621",files:["现场照片1.jpg","说明.docx"],time:"2026-08-25 09:18",status:"待处理"},
  235. {id:"FK20260824012",type:"表扬",targetName:"王某某",targetUnit:"急诊科",targetPost:"医生",content:"夜间急诊接诊及时、解释耐心,家属非常感动,特此表扬。",reporterName:"",reporterUnit:"",reporterPost:"",reporterPhone:"",files:[],time:"2026-08-24 21:06",status:"已办结"},
  236. {id:"FK20260823007",type:"投诉",targetName:"赵某某",targetUnit:"信息科",targetPost:"工程师",content:"反映信息系统升级后挂号偶发失败,影响就医体验,请尽快排查。",reporterName:"匿名",reporterUnit:"",reporterPost:"",reporterPhone:"",files:["截图.png"],time:"2026-08-23 14:42",status:"处理中"},
  237. {id:"FK20260822003",type:"投诉",targetName:"",targetUnit:"后勤保障部",targetPost:"",content:"住院楼电梯长时间排队,高峰时段拥堵明显,建议优化调度。",reporterName:"周某",reporterUnit:"住院患者",reporterPost:"",reporterPhone:"159****1108",files:["视频.mp4"],time:"2026-08-22 11:05",status:"待处理"},
  238. {id:"FK20260821009",type:"表扬",targetName:"陈某某",targetUnit:"药剂科",targetPost:"药师",content:"发药窗口核对细致,用药交代清楚,服务规范热情。",reporterName:"吴某",reporterUnit:"",reporterPost:"",reporterPhone:"",files:[],time:"2026-08-21 16:33",status:"已办结"}
  239. ];
  240. const suggestList=[
  241. {id:"JY20260825002",content:"建议在门诊大厅增设纪检宣传展板和扫码反馈指引,方便群众知情监督。",name:"刘某",unit:"行政办",post:"科员",phone:"137****8820",files:["展板示意.pdf"],time:"2026-08-25 10:02",status:"待查阅"},
  242. {id:"JY20260824005",content:"建议对重点岗位开展廉洁风险排查,并建立季度回头看机制。",name:"",unit:"",post:"",phone:"",files:[],time:"2026-08-24 15:20",status:"已采纳"},
  243. {id:"JY20260823011",content:"建议优化公车使用审批流程,提高透明度和可追溯性。",name:"孙某",unit:"财务科",post:"会计",phone:"186****3344",files:["流程建议.docx"],time:"2026-08-23 09:47",status:"已回复"},
  244. {id:"JY20260821004",content:"建议将常见廉政提醒纳入新职工入职培训必修内容。",name:"匿名",unit:"",post:"",phone:"",files:[],time:"2026-08-21 08:15",status:"待查阅"}
  245. ];
  246. const defaultHotline={phone:"0551-XXXXXXXX",email:"jjb@ajyy.example.com",address:"安徽省合肥市 · 安徽建工集团医院纪检室(示例地址)",hours:"工作日 08:30–11:30、14:30–17:30\\n紧急事项可通过电话随时联系值班人员。"};
  247. const defaultQr={url:"https://jjb.ajyy.example.com",title:"安建医院纪检 · 扫码反馈",image:""};
  248. let qrImage="";
  249. const titles={dashboard:"工作台",feedback:"在线反馈",suggest:"建言献策",hotline:"热线配置",qrcode:"二维码配置"};
  250. function toast(msg){const el=document.getElementById("toast");el.textContent=msg;el.classList.add("show");clearTimeout(toast._t);toast._t=setTimeout(()=>el.classList.remove("show"),2200)}
  251. function esc(s){return String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
  252. function typeTag(t){return t==="投诉"?'<span class="tag tag-warn">投诉</span>':'<span class="tag tag-ok">表扬</span>'}
  253. function statusTag(s){if(s==="待处理"||s==="待查阅")return '<span class="tag tag-warn">'+s+"</span>";if(s==="处理中")return '<span class="tag tag-blue">'+s+"</span>";return '<span class="tag tag-gray">'+s+"</span>"}
  254. function goPanel(name){
  255. document.querySelectorAll(".panel").forEach(p=>p.classList.remove("active"));
  256. document.querySelectorAll(".nav button").forEach(b=>b.classList.remove("active"));
  257. document.getElementById("panel-"+name).classList.add("active");
  258. document.querySelector('.nav button[data-panel="'+name+'"]').classList.add("active");
  259. document.getElementById("pageTitle").textContent=titles[name];
  260. if(name==="qrcode")refreshQr();
  261. }
  262. function renderDashboard(){
  263. document.getElementById("statFeedback").textContent=feedbackList.length;
  264. document.getElementById("statComplaint").textContent=feedbackList.filter(x=>x.type==="投诉"&&x.status!=="已办结").length;
  265. document.getElementById("statSuggest").textContent=suggestList.length;
  266. document.getElementById("statWeek").textContent=feedbackList.length+suggestList.length;
  267. document.getElementById("dashBody").innerHTML=feedbackList.slice(0,5).map(r=>`<tr>
  268. <td>${r.id}</td><td>${typeTag(r.type)}</td>
  269. <td class="ellipsis" title="${esc(r.content)}">${esc(r.content)}</td>
  270. <td>${r.time}</td><td>${statusTag(r.status)}</td>
  271. <td><button class="btn btn-sm btn-ghost" type="button" onclick="openFeedback('${r.id}')">查看</button></td>
  272. </tr>`).join("");
  273. }
  274. function renderFeedback(){
  275. const q=(document.getElementById("fbSearch").value||"").trim();
  276. const type=document.getElementById("fbType").value;
  277. const status=document.getElementById("fbStatus").value;
  278. const list=feedbackList.filter(r=>{
  279. if(type&&r.type!==type)return false;
  280. if(status&&r.status!==status)return false;
  281. if(!q)return true;
  282. return [r.id,r.content,r.targetName,r.targetUnit,r.reporterName].join(" ").includes(q);
  283. });
  284. const body=document.getElementById("feedbackBody");
  285. if(!list.length){body.innerHTML='<tr><td colspan="9"><div class="empty">暂无匹配数据</div></td></tr>';return}
  286. body.innerHTML=list.map(r=>`<tr>
  287. <td>${r.id}</td><td>${typeTag(r.type)}</td>
  288. <td>${esc(r.targetName||"未填写")}<div style="color:#999;font-size:12px">${esc([r.targetUnit,r.targetPost].filter(Boolean).join(" · ")||"-")}</div></td>
  289. <td class="ellipsis" title="${esc(r.content)}">${esc(r.content)}</td>
  290. <td>${esc(r.reporterName||"匿名")}</td>
  291. <td>${r.files.length?r.files.length+"个":"-"}</td>
  292. <td>${r.time}</td><td>${statusTag(r.status)}</td>
  293. <td><button class="btn btn-sm btn-ghost" type="button" onclick="openFeedback('${r.id}')">详情</button></td>
  294. </tr>`).join("");
  295. }
  296. function renderSuggest(){
  297. const q=(document.getElementById("sgSearch").value||"").trim();
  298. const status=document.getElementById("sgStatus").value;
  299. const list=suggestList.filter(r=>{
  300. if(status&&r.status!==status)return false;
  301. if(!q)return true;
  302. return [r.id,r.content,r.name,r.unit,r.phone].join(" ").includes(q);
  303. });
  304. const body=document.getElementById("suggestBody");
  305. if(!list.length){body.innerHTML='<tr><td colspan="9"><div class="empty">暂无匹配数据</div></td></tr>';return}
  306. body.innerHTML=list.map(r=>`<tr>
  307. <td>${r.id}</td>
  308. <td class="ellipsis" title="${esc(r.content)}">${esc(r.content)}</td>
  309. <td>${esc(r.name||"匿名")}</td>
  310. <td>${esc([r.unit,r.post].filter(Boolean).join(" / ")||"-")}</td>
  311. <td>${esc(r.phone||"-")}</td>
  312. <td>${r.files.length?r.files.length+"个":"-"}</td>
  313. <td>${r.time}</td><td>${statusTag(r.status)}</td>
  314. <td><button class="btn btn-sm btn-ghost" type="button" onclick="openSuggest('${r.id}')">详情</button></td>
  315. </tr>`).join("");
  316. }
  317. function openDrawer(){document.getElementById("mask").classList.add("show");document.getElementById("drawer").classList.add("open")}
  318. function closeDrawer(){document.getElementById("mask").classList.remove("show");document.getElementById("drawer").classList.remove("open")}
  319. function openFeedback(id){
  320. const r=feedbackList.find(x=>x.id===id);if(!r)return;
  321. document.getElementById("drawerTitle").textContent="反馈详情 · "+r.id;
  322. document.getElementById("drawerBody").innerHTML=`
  323. <div class="block"><div class="dt">基本信息</div><div class="dgrid">
  324. <div class="k">类型</div><div class="v">${typeTag(r.type)}</div>
  325. <div class="k">状态</div><div class="v">${statusTag(r.status)}</div>
  326. <div class="k">时间</div><div class="v">${r.time}</div></div></div>
  327. <div class="block"><div class="dt">被反馈人</div><div class="dgrid">
  328. <div class="k">姓名</div><div class="v">${esc(r.targetName||"未填写")}</div>
  329. <div class="k">单位</div><div class="v">${esc(r.targetUnit||"-")}</div>
  330. <div class="k">职务</div><div class="v">${esc(r.targetPost||"-")}</div></div></div>
  331. <div class="block"><div class="dt">反馈内容</div><div class="dcontent">${esc(r.content)}</div></div>
  332. <div class="block"><div class="dt">附件</div><div class="files">${r.files.length?r.files.map(f=>'<span class="file">'+esc(f)+"</span>").join(""):'<span style="color:#999;font-size:13px">无</span>'}</div></div>
  333. <div class="block"><div class="dt">举报人信息</div><div class="dgrid">
  334. <div class="k">姓名</div><div class="v">${esc(r.reporterName||"匿名")}</div>
  335. <div class="k">单位</div><div class="v">${esc(r.reporterUnit||"-")}</div>
  336. <div class="k">职务</div><div class="v">${esc(r.reporterPost||"-")}</div>
  337. <div class="k">联系方式</div><div class="v">${esc(r.reporterPhone||"-")}</div></div></div>
  338. <div class="actions">
  339. <button class="btn btn-primary" type="button" onclick="toast('演示:已标记处理中')">标记处理中</button>
  340. <button class="btn" type="button" onclick="toast('演示:已办结')">办结</button>
  341. </div>`;
  342. openDrawer();
  343. }
  344. function openSuggest(id){
  345. const r=suggestList.find(x=>x.id===id);if(!r)return;
  346. document.getElementById("drawerTitle").textContent="建议详情 · "+r.id;
  347. document.getElementById("drawerBody").innerHTML=`
  348. <div class="block"><div class="dt">基本信息</div><div class="dgrid">
  349. <div class="k">状态</div><div class="v">${statusTag(r.status)}</div>
  350. <div class="k">时间</div><div class="v">${r.time}</div></div></div>
  351. <div class="block"><div class="dt">建议内容</div><div class="dcontent">${esc(r.content)}</div></div>
  352. <div class="block"><div class="dt">附件</div><div class="files">${r.files.length?r.files.map(f=>'<span class="file">'+esc(f)+"</span>").join(""):'<span style="color:#999;font-size:13px">无</span>'}</div></div>
  353. <div class="block"><div class="dt">联系人信息</div><div class="dgrid">
  354. <div class="k">姓名</div><div class="v">${esc(r.name||"匿名")}</div>
  355. <div class="k">单位</div><div class="v">${esc(r.unit||"-")}</div>
  356. <div class="k">职务</div><div class="v">${esc(r.post||"-")}</div>
  357. <div class="k">联系方式</div><div class="v">${esc(r.phone||"-")}</div></div></div>
  358. <div class="actions">
  359. <button class="btn btn-primary" type="button" onclick="toast('演示:已标记采纳')">标记采纳</button>
  360. <button class="btn" type="button" onclick="toast('演示:已回复')">标记已回复</button>
  361. </div>`;
  362. openDrawer();
  363. }
  364. function loadHotline(){
  365. let cfg={...defaultHotline};
  366. try{const raw=localStorage.getItem("ajyy_hotline");if(raw)cfg={...cfg,...JSON.parse(raw)}}catch(e){}
  367. document.getElementById("cfgPhone").value=cfg.phone;
  368. document.getElementById("cfgEmail").value=cfg.email;
  369. document.getElementById("cfgAddress").value=cfg.address;
  370. document.getElementById("cfgHours").value=cfg.hours.replace(/\\n/g,"\n");
  371. }
  372. function saveHotline(){
  373. const cfg={phone:document.getElementById("cfgPhone").value.trim(),email:document.getElementById("cfgEmail").value.trim(),address:document.getElementById("cfgAddress").value.trim(),hours:document.getElementById("cfgHours").value.trim()};
  374. localStorage.setItem("ajyy_hotline",JSON.stringify(cfg));
  375. toast("热线配置已保存");
  376. }
  377. function resetHotline(){localStorage.removeItem("ajyy_hotline");loadHotline();toast("已恢复默认配置")}
  378. function drawQr(canvas,text){
  379. const ctx=canvas.getContext("2d");const size=canvas.width,cells=25,cell=size/cells;
  380. ctx.fillStyle="#fff";ctx.fillRect(0,0,size,size);
  381. let h=0;for(let i=0;i<text.length;i++)h=(h*31+text.charCodeAt(i))>>>0;
  382. function finder(x,y){return x===0||y===0||x===6||y===6||(x>=2&&x<=4&&y>=2&&y<=4)}
  383. function bit(x,y){
  384. if(x<7&&y<7)return finder(x,y);
  385. if(x>=cells-7&&y<7)return finder(x-(cells-7),y);
  386. if(x<7&&y>=cells-7)return finder(x,y-(cells-7));
  387. return ((h^(x*73856093)^(y*19349663))>>>0)%3!==0;
  388. }
  389. ctx.fillStyle="#1a1a1a";
  390. for(let y=0;y<cells;y++)for(let x=0;x<cells;x++)if(bit(x,y))ctx.fillRect(x*cell,y*cell,Math.ceil(cell),Math.ceil(cell));
  391. }
  392. function refreshQr(){
  393. const url=document.getElementById("cfgQrUrl").value.trim()||defaultQr.url;
  394. const title=document.getElementById("cfgQrTitle").value.trim()||defaultQr.title;
  395. document.getElementById("qrTitle").textContent=title;
  396. document.getElementById("qrUrlText").textContent=url;
  397. const canvas=document.getElementById("qrCanvas");
  398. const img=document.getElementById("qrCustom");
  399. const logo=document.getElementById("qrLogo");
  400. if(qrImage){canvas.style.display="none";logo.style.display="none";img.style.display="block";img.src=qrImage}
  401. else{canvas.style.display="block";logo.style.display="block";img.style.display="none";drawQr(canvas,url)}
  402. }
  403. function loadQr(){
  404. let cfg={...defaultQr};
  405. try{const raw=localStorage.getItem("ajyy_qr");if(raw)cfg={...cfg,...JSON.parse(raw)}}catch(e){}
  406. document.getElementById("cfgQrUrl").value=cfg.url;
  407. document.getElementById("cfgQrTitle").value=cfg.title;
  408. qrImage=cfg.image||"";
  409. document.getElementById("qrUploadName").textContent=qrImage?"已上传自定义二维码图片":"";
  410. refreshQr();
  411. }
  412. function saveQr(){
  413. const cfg={url:document.getElementById("cfgQrUrl").value.trim()||defaultQr.url,title:document.getElementById("cfgQrTitle").value.trim()||defaultQr.title,image:qrImage||""};
  414. localStorage.setItem("ajyy_qr",JSON.stringify(cfg));
  415. toast("二维码配置已保存");refreshQr();
  416. }
  417. function onQrUpload(e){
  418. const file=e.target.files&&e.target.files[0];if(!file)return;
  419. const reader=new FileReader();
  420. reader.onload=()=>{qrImage=reader.result;document.getElementById("qrUploadName").textContent="已选择:"+file.name;refreshQr();toast("自定义二维码已加载,请点击保存")};
  421. reader.readAsDataURL(file);e.target.value="";
  422. }
  423. function clearQrImage(){qrImage="";document.getElementById("qrUploadName").textContent="";refreshQr();toast("已清除自定义图片")}
  424. function downloadQr(){
  425. const canvas=document.getElementById("qrCanvas");const img=document.getElementById("qrCustom");
  426. const a=document.createElement("a");
  427. a.href=qrImage&&img.style.display!=="none"?qrImage:canvas.toDataURL("image/png");
  428. a.download="ajyy-qrcode.png";a.click();toast("已开始下载");
  429. }
  430. renderDashboard();renderFeedback();renderSuggest();loadHotline();loadQr();
  431. </script>
  432. </body>
  433. </html>