You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

278 regels
12 KiB

  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.0">
  6. <title>登录 - 肠愈同行患者关爱</title>
  7. <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
  8. <style>
  9. *,*::before,*::after{margin:0;padding:0;box-sizing:border-box;}
  10. :root{
  11. --primary:#E8751A;
  12. --primary-dark:#C96012;
  13. --primary-light:#F5A623;
  14. --blue:#1A3550;
  15. --white:#fff;
  16. --bg:#f0f2f5;
  17. --text:#303133;
  18. --text-secondary:#909399;
  19. --border:#dcdfe6;
  20. --danger:#f56c6c;
  21. --font-cn:"Source Han Sans SC","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif;
  22. }
  23. html,body{height:100%;font-family:var(--font-cn);color:var(--text);}
  24. .login-wrapper{display:flex;height:100vh;}
  25. .login-left{
  26. flex:0 0 50%;display:flex;flex-direction:column;align-items:center;justify-content:center;
  27. background:linear-gradient(135deg,var(--primary) 0%,var(--primary-dark) 100%);
  28. position:relative;overflow:hidden;
  29. }
  30. .login-left::before{
  31. content:"";position:absolute;width:600px;height:600px;border-radius:50%;
  32. background:rgba(255,255,255,.06);top:-120px;right:-180px;
  33. }
  34. .login-left::after{
  35. content:"";position:absolute;width:400px;height:400px;border-radius:50%;
  36. background:rgba(255,255,255,.04);bottom:-80px;left:-100px;
  37. }
  38. .brand{position:relative;z-index:1;text-align:center;color:var(--white);padding:0 60px;}
  39. .brand-logo{width:120px;height:120px;border-radius:50%;background:rgba(255,255,255,.95);
  40. display:flex;align-items:center;justify-content:center;margin:0 auto 28px;
  41. box-shadow:0 4px 24px rgba(0,0,0,.1);}
  42. .brand-logo img{width:80px;height:80px;object-fit:contain;}
  43. .brand-logo.no-img{background:rgba(255,255,255,.15);backdrop-filter:blur(10px);}
  44. .brand-logo.no-img svg{width:44px;height:44px;}
  45. .brand h1{font-size:28px;font-weight:700;margin-bottom:12px;letter-spacing:1px;}
  46. .brand p{font-size:14px;opacity:.75;line-height:1.8;max-width:360px;}
  47. .login-right{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg);}
  48. .login-box{
  49. width:400px;background:var(--white);border-radius:12px;
  50. box-shadow:0 2px 12px rgba(0,0,0,.08);padding:48px 40px;
  51. animation:fadeUp .5s ease both;
  52. }
  53. @keyframes fadeUp{from{opacity:0;transform:translateY(24px);}to{opacity:1;transform:translateY(0);}}
  54. .login-box h2{font-size:22px;font-weight:600;color:var(--text);margin-bottom:6px;}
  55. .login-box .subtitle{font-size:13px;color:var(--text-secondary);margin-bottom:32px;}
  56. </style>
  57. </head>
  58. <body>
  59. <div style="display:none;">
  60. <!-- split styles to avoid autofix issues -->
  61. </div>
  62. <style>
  63. .form-item{margin-bottom:22px;}
  64. .form-item label{display:block;font-size:13px;color:var(--text);font-weight:500;margin-bottom:8px;}
  65. .input-wrap{
  66. position:relative;display:flex;align-items:center;
  67. border:1px solid var(--border);border-radius:8px;transition:.2s;background:var(--white);
  68. }
  69. .input-wrap:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px rgba(232,117,26,.1);}
  70. .input-wrap .icon{position:absolute;left:12px;color:var(--text-secondary);display:flex;align-items:center;}
  71. .input-wrap .icon svg{width:18px;height:18px;}
  72. .input-wrap input{
  73. width:100%;border:none;outline:none;padding:11px 12px 11px 40px;
  74. font-size:14px;font-family:var(--font-cn);color:var(--text);background:transparent;border-radius:8px;
  75. }
  76. .input-wrap input::placeholder{color:#c0c4cc;}
  77. .input-wrap .toggle-pwd{
  78. position:absolute;right:12px;cursor:pointer;color:var(--text-secondary);
  79. display:flex;align-items:center;background:none;border:none;padding:0;
  80. }
  81. .input-wrap .toggle-pwd:hover{color:var(--primary);}
  82. .input-wrap .toggle-pwd svg{width:18px;height:18px;}
  83. .captcha-row{display:flex;gap:10px;align-items:center;}
  84. .captcha-row .input-wrap{flex:1;}
  85. .captcha-img{height:40px;border-radius:8px;cursor:pointer;border:1px solid var(--border);flex-shrink:0;}
  86. .form-options{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;}
  87. .remember{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;color:var(--text-secondary);}
  88. .remember input[type="checkbox"]{width:16px;height:16px;accent-color:var(--primary);cursor:pointer;}
  89. .btn-login{
  90. width:100%;padding:12px 0;border:none;border-radius:8px;
  91. background:linear-gradient(135deg,var(--primary),var(--primary-dark));
  92. color:var(--white);font-size:15px;font-weight:600;cursor:pointer;
  93. transition:.25s;letter-spacing:1px;font-family:var(--font-cn);
  94. display:flex;align-items:center;justify-content:center;gap:8px;
  95. }
  96. .btn-login:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(232,117,26,.35);}
  97. .btn-login:active{transform:translateY(0);}
  98. .btn-login:disabled{opacity:.6;cursor:not-allowed;transform:none;box-shadow:none;}
  99. .btn-login .spinner{
  100. width:16px;height:16px;border:2px solid rgba(255,255,255,.3);
  101. border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:none;
  102. }
  103. .btn-login.loading .spinner{display:block;}
  104. @keyframes spin{to{transform:rotate(360deg);}}
  105. .login-footer{text-align:center;margin-top:24px;font-size:12px;color:var(--text-secondary);}
  106. .error-msg{color:var(--danger);font-size:13px;margin-bottom:16px;display:none;}
  107. .error-msg.show{display:block;}
  108. @media(max-width:960px){
  109. .login-left{display:none;}
  110. .login-right{background:linear-gradient(135deg,#fdf0e6 0%,var(--bg) 100%);}
  111. }
  112. @media(max-width:480px){
  113. .login-box{width:100%;margin:0 20px;padding:36px 28px;}
  114. }
  115. </style>
  116. <div class="login-wrapper">
  117. <div class="login-left">
  118. <div class="brand">
  119. <div class="brand-logo">
  120. <img src="/static/images/logo.png" alt="肠愈同行患者关爱">
  121. </div>
  122. <h1>肠愈同行患者关爱</h1>
  123. <p>患者关爱管理平台<br>为患者提供全流程关爱服务</p>
  124. </div>
  125. </div>
  126. <div class="login-right">
  127. <div class="login-box">
  128. <h2>欢迎回来</h2>
  129. <p class="subtitle">请登录您的管理员账号</p>
  130. <form id="loginForm">
  131. <div class="error-msg" id="errorMsg"></div>
  132. <div class="form-item">
  133. <label>用户名</label>
  134. <div class="input-wrap">
  135. <span class="icon">
  136. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
  137. </span>
  138. <input type="text" name="username" id="username" placeholder="请输入用户名" autocomplete="username" required>
  139. </div>
  140. </div>
  141. <div class="form-item">
  142. <label>密码</label>
  143. <div class="input-wrap">
  144. <span class="icon">
  145. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
  146. </span>
  147. <input type="password" name="password" id="password" placeholder="请输入密码" autocomplete="current-password" required>
  148. <button type="button" class="toggle-pwd" onclick="togglePassword()" aria-label="显示密码">
  149. <svg id="eyeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
  150. </button>
  151. </div>
  152. </div>
  153. <div class="form-item">
  154. <label>验证码</label>
  155. <div class="captcha-row">
  156. <div class="input-wrap">
  157. <span class="icon">
  158. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 9h.01M15 15h.01M9 15l6-6"/></svg>
  159. </span>
  160. <input type="text" name="captcha" id="captcha" placeholder="请输入验证码" autocomplete="off" maxlength="4" required>
  161. </div>
  162. <img class="captcha-img" id="captchaImg" src="/admin/auth/captcha" alt="验证码" title="点击刷新" onclick="this.src='/admin/auth/captcha?t='+Date.now()">
  163. </div>
  164. </div>
  165. <div class="form-options">
  166. <label class="remember">
  167. <input type="checkbox" name="remember" id="remember"> 记住我
  168. </label>
  169. </div>
  170. <button type="submit" class="btn-login" id="submitBtn">
  171. <span class="spinner"></span>
  172. <span class="btn-text">登 录</span>
  173. </button>
  174. </form>
  175. <div class="login-footer">
  176. © {{ now_year }} 肠愈同行患者关爱
  177. </div>
  178. </div>
  179. </div>
  180. </div>
  181. <script>
  182. var REMEMBER_KEY = 'cytx_admin_remember';
  183. // 页面加载时回填记住的账号密码
  184. (function() {
  185. try {
  186. var saved = localStorage.getItem(REMEMBER_KEY);
  187. if (saved) {
  188. var data = JSON.parse(saved);
  189. if (data.username) document.getElementById('username').value = data.username;
  190. if (data.password) document.getElementById('password').value = data.password;
  191. document.getElementById('remember').checked = true;
  192. }
  193. } catch(e) {}
  194. })();
  195. function togglePassword(){
  196. var input = document.getElementById('password');
  197. input.type = input.type === 'password' ? 'text' : 'password';
  198. }
  199. function refreshCaptcha(){
  200. document.getElementById('captchaImg').src = '/admin/auth/captcha?t=' + Date.now();
  201. }
  202. document.getElementById('loginForm').addEventListener('submit', function(e) {
  203. e.preventDefault();
  204. var btn = document.getElementById('submitBtn');
  205. var errorMsg = document.getElementById('errorMsg');
  206. var username = document.getElementById('username').value.trim();
  207. var password = document.getElementById('password').value;
  208. var captcha = document.getElementById('captcha').value.trim();
  209. var remember = document.getElementById('remember').checked;
  210. if (!username || !password) {
  211. errorMsg.textContent = '请输入用户名和密码';
  212. errorMsg.classList.add('show');
  213. return;
  214. }
  215. if (!captcha) {
  216. errorMsg.textContent = '请输入验证码';
  217. errorMsg.classList.add('show');
  218. return;
  219. }
  220. btn.disabled = true;
  221. btn.classList.add('loading');
  222. btn.querySelector('.btn-text').textContent = '登录中...';
  223. errorMsg.classList.remove('show');
  224. fetch('/admin/auth/login', {
  225. method: 'POST',
  226. headers: { 'Content-Type': 'application/json' },
  227. body: JSON.stringify({ username: username, password: password, captcha: captcha, remember: remember })
  228. }).then(function(res) {
  229. return res.json();
  230. }).then(function(data) {
  231. if (data.code === 0) {
  232. // 登录成功:记住我则存 localStorage,否则清除
  233. if (remember) {
  234. localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username: username, password: password }));
  235. } else {
  236. localStorage.removeItem(REMEMBER_KEY);
  237. }
  238. window.location.href = '/admin/dashboard.html';
  239. } else {
  240. errorMsg.textContent = data.msg || '登录失败';
  241. errorMsg.classList.add('show');
  242. refreshCaptcha();
  243. document.getElementById('captcha').value = '';
  244. btn.disabled = false;
  245. btn.classList.remove('loading');
  246. btn.querySelector('.btn-text').textContent = '登 录';
  247. }
  248. }).catch(function() {
  249. errorMsg.textContent = '网络错误,请稍后重试';
  250. errorMsg.classList.add('show');
  251. refreshCaptcha();
  252. btn.disabled = false;
  253. btn.classList.remove('loading');
  254. btn.querySelector('.btn-text').textContent = '登 录';
  255. });
  256. });
  257. </script>
  258. </body>
  259. </html>