@flun/html-template 4.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.env +9 -0
  2. package/LICENSE +15 -0
  3. package/build.js +3 -0
  4. package/compile.js +349 -0
  5. package/copy-files.js +200 -0
  6. package/customize/account.js +726 -0
  7. package/customize/data.json +484 -0
  8. package/customize/functions.js +48 -0
  9. package/customize/hotReloadInjector.js +25 -0
  10. package/customize/routes.js +141 -0
  11. package/customize/users.json +44 -0
  12. package/customize/variables.js +70 -0
  13. package/dev-server.js +344 -0
  14. package/dev.js +4 -0
  15. package/f-CHANGELOG.md +4 -0
  16. package/f-README.md +485 -0
  17. package/index.d.ts +133 -0
  18. package/index.js +4 -0
  19. package/package.json +77 -0
  20. package/restoreDefaults.js +8 -0
  21. package/services/templateService.js +962 -0
  22. package/static/about.css +118 -0
  23. package/static/auth.js +27 -0
  24. package/static/constants.css +138 -0
  25. package/static/img/dark.png +0 -0
  26. package/static/img/favicon.ico +0 -0
  27. package/static/img/light.png +0 -0
  28. package/static/img/top.png +0 -0
  29. package/static/index.css +86 -0
  30. package/static/mouseOrTouch.js +156 -0
  31. package/static/public.css +288 -0
  32. package/static/script.css +318 -0
  33. package/static/script.js +392 -0
  34. package/static/styling.css +874 -0
  35. package/static/styling.js +933 -0
  36. package/static/themeImg.css +10 -0
  37. package/static/themeImg.js +19 -0
  38. package/static/themeModule.js +222 -0
  39. package/static/topImg.css +19 -0
  40. package/static/topImg.js +21 -0
  41. package/static/utils/browser13.js +270 -0
  42. package/static/utils/closebrackets.js +166 -0
  43. package/static/utils/css-lint.js +308 -0
  44. package/static/utils/custom-css-hint.js +876 -0
  45. package/static/utils/foldgutter.js +141 -0
  46. package/static/utils/match-highlighter.js +70 -0
  47. package/templates/about.html +236 -0
  48. package/templates/account/2fa.html +184 -0
  49. package/templates/account/forgot-password.html +226 -0
  50. package/templates/account/login.html +230 -0
  51. package/templates/account/profile.html +977 -0
  52. package/templates/account/register.html +224 -0
  53. package/templates/account/reset-password.html +205 -0
  54. package/templates/account/verify-email.html +163 -0
  55. package/templates/base.html +71 -0
  56. package/templates/footer-content.html +5 -0
  57. package/templates/index.html +140 -0
  58. package/templates/script.html +209 -0
  59. package/templates/test-include.html +11 -0
@@ -0,0 +1,141 @@
1
+ // CodeMirror库 - 折叠处理逻辑的优化和扩展 (https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/fold/foldgutter.min.js)
2
+
3
+ (function (mod) {
4
+ if (typeof exports == "object" && typeof module == "object") mod(require("../../lib/codemirror"), require("./foldcode"));
5
+ else if (typeof define == "function" && define.amd) define(["../../lib/codemirror", "./foldcode"], mod);
6
+ else mod(CodeMirror);
7
+ })((CodeMirror) => {
8
+ "use strict";
9
+
10
+ class State {
11
+ constructor(options) {
12
+ this.options = options, this.from = 0, this.to = 0;
13
+ }
14
+ };
15
+
16
+ CodeMirror.defineOption("foldGutter", false, (cm, val, old) => {
17
+ if (old && old != CodeMirror.Init) {
18
+ cm.clearGutter(cm.state.foldGutter.options.gutter), cm.state.foldGutter = null;
19
+ cm.off("gutterClick", onGutterClick).off("changes", onChange).off("viewportChange", onViewportChange)
20
+ .off("fold", onFold).off("unfold", onFold).off("swapDoc", onChange);
21
+ }
22
+ if (val) {
23
+ cm.state.foldGutter = new State(parseOptions(val)), updateInViewport(cm, cm.state.foldGutter);
24
+ cm.on("gutterClick", onGutterClick), cm.on("changes", onChange), cm.on("viewportChange", onViewportChange);
25
+ cm.on("fold", onFold), cm.on("unfold", onFold), cm.on("swapDoc", onChange);
26
+ }
27
+ });
28
+
29
+ const Pos = CodeMirror.Pos,
30
+ parseOptions = opts => {
31
+ if (opts === true) opts = {};
32
+ if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
33
+ if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
34
+ if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
35
+ return opts;
36
+ },
37
+
38
+ // 安全获取 state 并注入回调
39
+ withFoldState = fn => (cm, ...args) => {
40
+ const state = cm.state.foldGutter;
41
+ if (!state) return;
42
+ return fn(cm, state, ...args);
43
+ },
44
+
45
+ isFolded = (cm, line) => {
46
+ const marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
47
+ for (let i = 0; i < marks.length; ++i) {
48
+ if (marks[i].__isFold) {
49
+ const fromPos = marks[i].find(-1);
50
+ if (fromPos && fromPos.line === line) return marks[i];
51
+ }
52
+ }
53
+ },
54
+
55
+ marker = spec => {
56
+ if (typeof spec == "string") {
57
+ const elt = document.createElement("div");
58
+ elt.className = `${spec} CodeMirror-guttermarker-subtle`;
59
+ return elt;
60
+ }
61
+ else return spec.cloneNode(true);
62
+ },
63
+ classTest = cls => new RegExp(`(^|\\s)${cls}(?:$|\\s)\\s*`),
64
+ updateFoldInfo = (cm, state, from, to) => {
65
+ const opts = state.options, minSize = cm.foldOption(opts, "minFoldSize"), func = cm.foldOption(opts, "rangeFinder"),
66
+ { indicatorFolded, indicatorOpen, gutter } = opts, clsOpen = typeof indicatorOpen == "string" && classTest(indicatorOpen),
67
+ clsFolded = typeof indicatorFolded == "string" && classTest(indicatorFolded);
68
+
69
+ // 根据条件返回应使用的标记元素
70
+ const getMarker = (shouldShow, clsTest, indicator, title, old) => {
71
+ if (shouldShow) {
72
+ const { className, nodeType } = old || {};
73
+ if (clsTest && old && clsTest.test(className)) {
74
+ if (nodeType === 1) old.title = title;
75
+ return old;
76
+ }
77
+ const mark = marker(indicator);
78
+ if (mark?.nodeType === 1) mark.title = title;
79
+ return mark;
80
+ }
81
+ return null;
82
+ };
83
+
84
+ let cur = from - 1;
85
+ cm.eachLine(from, to, line => {
86
+ ++cur;
87
+ const old = line.gutterMarkers?.[gutter], folded = isFolded(cm, cur);
88
+ let range = null, shouldShow = false, clsTest, indicator, title;
89
+
90
+ // 已折叠 → 应显示折叠标记
91
+ if (folded) shouldShow = true, clsTest = clsFolded, indicator = indicatorFolded, title = "点击展开";
92
+ // 未折叠,检查是否有可折叠范围
93
+ else {
94
+ range = func?.(cm, Pos(cur, 0));
95
+ if (range?.to.line - range?.from.line >= minSize)
96
+ shouldShow = true, clsTest = clsOpen, indicator = indicatorOpen, title = "点击折叠";
97
+ }
98
+
99
+ const newMarker = getMarker(shouldShow, clsTest, indicator, title, old);
100
+ if (newMarker !== old) cm.setGutterMarker(line, gutter, newMarker);
101
+ });
102
+ },
103
+
104
+ updateInViewport = (cm, state) => {
105
+ const { from, to } = cm.getViewport();
106
+ cm.operation(() => updateFoldInfo(cm, state, from, to)), state.from = from, state.to = to;
107
+ },
108
+
109
+ onGutterClick = withFoldState((cm, state, line, gutter) => {
110
+ const opts = state.options;
111
+ if (gutter != opts.gutter) return;
112
+ const folded = isFolded(cm, line);
113
+ if (folded) folded.clear();
114
+ else cm.foldCode(Pos(line, 0), opts);
115
+ }),
116
+
117
+ onChange = withFoldState((cm, state) => {
118
+ const { foldOnChangeTimeSpan } = state.options;
119
+ state.from = state.to = 0, clearTimeout(state.changeUpdate);
120
+ state.changeUpdate = setTimeout(() => updateInViewport(cm, state), foldOnChangeTimeSpan || 600);
121
+ }),
122
+
123
+ onViewportChange = withFoldState((cm, state) => {
124
+ const { options, changeUpdate, from: sFrom, to: sTo } = state, { updateViewportTimeSpan } = options;
125
+ clearTimeout(changeUpdate);
126
+ state.changeUpdate = setTimeout(() => {
127
+ const { from, to } = cm.getViewport();
128
+ if (sFrom == sTo || from - sTo > 20 || sFrom - to > 20) updateInViewport(cm, state);
129
+ else
130
+ cm.operation(() => {
131
+ if (from < sFrom) updateFoldInfo(cm, state, from, sFrom), state.from = from;
132
+ if (to > sTo) updateFoldInfo(cm, state, sTo, to), state.to = to;
133
+ });
134
+ }, updateViewportTimeSpan || 400);
135
+ }),
136
+
137
+ onFold = withFoldState((cm, state, from) => {
138
+ const { from: stateFrom, to } = state, line = from.line;
139
+ if (line >= stateFrom && line < to) updateFoldInfo(cm, state, line, line + 1);
140
+ });
141
+ });
@@ -0,0 +1,70 @@
1
+ // 根目录/utils/match-highlighter.js
2
+ /**
3
+ * 为 CodeMirror 编辑器附加自定义高亮功能;
4
+ * 当光标停留在某个单词上时,自动高亮编辑器中所有与之相同的独立单词;
5
+ *
6
+ * @param {Object} editor - CodeMirror 编辑器实例
7
+ * @returns {void}
8
+ */
9
+ function attachCustomHighlight(editor) {
10
+ let currentMarks = []; // 存储当前高亮标记
11
+
12
+ function clearHighlights() {
13
+ currentMarks.forEach(mark => mark.clear()), currentMarks = [];
14
+ }
15
+
16
+ // 定义分隔符:空白字符 + CSS语法标点(不包括 . # - _ 这些可能出现在标识符中的字符)
17
+ function isSeparator(ch) {
18
+ if (!ch) return false;
19
+ if (/\s/.test(ch)) return true; // 空白字符
20
+ return '!"%&\'()*+,/:;<=>?@[\\]^{|}~`$'.indexOf(ch) !== -1; // 常见CSS分隔符
21
+ }
22
+
23
+ // 从光标位置获取完整的单词
24
+ function getWordAtCursor() {
25
+ const cursor = editor.getCursor(), line = editor.getLine(cursor.line), ch = cursor.ch;
26
+
27
+ // 如果光标在行尾或当前字符是分隔符,则返回空字符串(不高亮)
28
+ if (ch >= line.length) return '';
29
+ if (isSeparator(line[ch])) return '';
30
+
31
+ let start = ch, end = ch;
32
+ while (start > 0 && !isSeparator(line[start - 1])) start--; // 向左扩展直到遇到分隔符
33
+ while (end < line.length && !isSeparator(line[end])) end++; // 向右扩展直到遇到分隔符
34
+
35
+ return line.slice(start, end);
36
+ }
37
+
38
+ // 高亮指定单词
39
+ function highlightWord(word) {
40
+ clearHighlights();
41
+ if (!word || word.trim().length === 0) return;
42
+
43
+ const wordLen = word.length, lines = editor.lineCount();
44
+ for (let i = 0; i < lines; i++) {
45
+ const line = editor.getLine(i);
46
+ let pos = 0;
47
+ while (true) {
48
+ const index = line.indexOf(word, pos);
49
+ if (index === -1) break;
50
+ const endPos = index + wordLen, beforeChar = index > 0 ? line[index - 1] : '',
51
+ afterChar = endPos < line.length ? line[endPos] : '',
52
+ beforeOk = !beforeChar || isSeparator(beforeChar), afterOk = !afterChar || isSeparator(afterChar);
53
+
54
+ if (beforeOk && afterOk) {
55
+ const from = { line: i, ch: index }, to = { line: i, ch: endPos },
56
+ mark = editor.markText(from, to, { className: 'custom-highlight' });
57
+ currentMarks.push(mark);
58
+ }
59
+
60
+ pos = index + 1;
61
+ }
62
+ }
63
+ }
64
+
65
+ // 绑定光标活动事件
66
+ editor.on('cursorActivity', () => {
67
+ const word = getWordAtCursor();
68
+ highlightWord(word);
69
+ });
70
+ }
@@ -0,0 +1,236 @@
1
+ [extends base.html]<!-- 继承基础模板 -->
2
+
3
+ [!title]关于我们 - 模板功能测试[~title]
4
+
5
+ [!style]
6
+ <link rel="stylesheet" href="/static/styling.css">
7
+ <link rel="stylesheet" href="/static/about.css">
8
+ [~style]
9
+
10
+ [!content]
11
+ [!test]<h1>关于我们的公司 - 模板功能测试</h1>[~test]
12
+
13
+ <div class="abt_content_main">
14
+ <h2>主内容区</h2>
15
+ <p><a href="index.html">← 返回首页</a></p>
16
+ </div>
17
+
18
+ <!-- 复杂功能测试区域 -->
19
+ <div class="new-feature-test">
20
+ <h3>复杂功能测试</h3>
21
+
22
+ <div class="test-result">
23
+ <h4>1. 复杂条件表达式测试</h4>
24
+
25
+ <!-- 复杂条件表达式测试 - 使用 else if 语法 -->
26
+ <div class="control-flow">
27
+ <h5>用户权限测试</h5>
28
+ {{if user.isLoggedIn && user.membershipLevel === 'VIP'}}
29
+ <p style="color:green;">✓ 您是已登录的VIP用户</p>
30
+ {{else if user.isLoggedIn || user.isGuest}}
31
+ <p style="color:blue;">✓ 您是已登录用户或访客</p>
32
+ {{else}}
33
+ <p style="color:red;">✗ 您未登录</p>
34
+ {{endif}}
35
+ <hr>
36
+ <h5>产品状态测试</h5>
37
+ {{if product.stock > 0 && product.price < 100}}
38
+ <p style="color:green;">✓ 产品有库存且价格低于100元</p>
39
+ {{else if product.stock === 0 || product.price >= 100}}
40
+ <p style="color:orange;">⚠ 产品缺货或价格较高</p>
41
+ {{endif}}
42
+ <hr>
43
+ <h5>团队技能测试</h5>
44
+ {{if teamMembers.length > 2 && teamMembers[0].skills.includes('JavaScript')}}
45
+ <p style="color:green;">✓ 团队规模大于2人且第一个成员懂JavaScript</p>
46
+ {{endif}}
47
+ </div>
48
+ </div>
49
+
50
+ <div class="test-result">
51
+ <h4>2. 循环控制语句测试</h4>
52
+
53
+ <div class="control-flow">
54
+ <h5>Break 语句测试 (遇到价格超过250元的产品停止)</h5>
55
+ <div class="product-list">
56
+ {{for product in products}}
57
+ {{if product.price > 250}}
58
+ {{break}}
59
+ {{endif}}
60
+ <div class="product-card">
61
+ <h5>{{product.name}}</h5>
62
+ <p>价格: ¥{{product.price}}</p>
63
+ <p>库存: {{product.stock}}件</p>
64
+ </div>
65
+ {{endfor}}
66
+ </div>
67
+ <hr>
68
+ <h5>Continue 语句测试 (跳过缺货产品)</h5>
69
+ <div class="product-list">
70
+ {{for product in products}}
71
+ {{if product.stock === 0}}
72
+ {{continue}}
73
+ {{endif}}
74
+ <div class="product-card">
75
+ <h5>{{product.name}}</h5>
76
+ <p>价格: ¥{{product.price}}</p>
77
+ <p>库存: {{product.stock}}件</p>
78
+ </div>
79
+ {{endfor}}
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <div class="test-result">
85
+ <h4>3. 空状态处理测试</h4>
86
+
87
+ <div class="empty-state">
88
+ <h5>非空数组测试</h5>
89
+ {{for product in products}}
90
+ <div class="product-card" style="margin: 10px;">
91
+ <h5>{{product.name}}</h5>
92
+ <p>价格: ¥{{product.price}}</p>
93
+ </div>
94
+ {{empty}}
95
+ <p style="color:red;">✗ 错误: 应该显示产品列表</p>
96
+ {{endfor}}
97
+ <hr>
98
+ <h5>空数组测试</h5>
99
+ {{for item in emptyArray}}
100
+ <div class="product-card">
101
+ <h5>{{item.name}}</h5>
102
+ </div>
103
+ {{empty}}
104
+ <p style="color:green;">✓ 正确: 空数组显示空状态消息</p>
105
+ <p>暂无数据,请稍后再试</p>
106
+ {{endfor}}
107
+ <hr>
108
+ <h5>空对象测试</h5>
109
+ {{for key, value in emptyObject}}
110
+ <p><strong>{{key}}:</strong> {{value}}</p>
111
+ {{empty}}
112
+ <p style="color:green;">✓ 正确: 空对象显示空状态消息</p>
113
+ <p>暂无相关信息</p>
114
+ {{endfor}}
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- 条件判断和循环测试区域 -->
120
+ <div class="logic-test">
121
+ <h3>条件判断和循环功能测试</h3>
122
+
123
+ <div class="test-result">
124
+ <h4>1. 条件判断测试</h4>
125
+
126
+ <!-- 用户登录状态判断 -->
127
+ <h5>嵌套条件判断</h5>
128
+ {{if user.isLoggedIn}}
129
+ <div class="user-status logged-in">
130
+ <p>欢迎回来, {{user.name}}!</p>
131
+ <p>您的会员等级: {{user.membershipLevel}}</p>
132
+ {{if user.membershipLevel === 'VIP'}}
133
+ <p>🎉 您是VIP会员,享受专属特权!</p>
134
+ {{endif}}
135
+ </div>
136
+ {{else}}
137
+ <div class="user-status logged-out">
138
+ <p>您尚未登录,<a href="/login">请先登录</a></p>
139
+ </div>
140
+ {{endif}}
141
+ <hr>
142
+ <h5>简单条件判断</h5>
143
+ <!-- 库存状态判断 -->
144
+ {{if product.stock > 0}}
145
+ <p>库存状态: <span style="color:rgb(54, 185, 54);">有货 ({{product.stock}}件)</span></p>
146
+ {{else}}
147
+ <p>库存状态: <span style="color:red;">缺货</span></p>
148
+ {{endif}}
149
+ </div>
150
+
151
+ <div class="test-result">
152
+ <h4>2. 循环测试 - 产品列表</h4>
153
+
154
+ <div class="product-list">
155
+ {{for product in products}}
156
+ <div class="product-card">
157
+ <h5>{{product.name}}</h5>
158
+ <p>价格: ¥{{product.price}}</p>
159
+ <p>库存: {{product.stock}}件</p>
160
+ {{if product.isNew}}
161
+ <span style="color:rgb(118, 62, 103);font-weight:bold;">新品!</span>
162
+ {{endif}}
163
+ </div>
164
+ {{endfor}}
165
+ </div>
166
+ </div>
167
+
168
+ <div class="test-result">
169
+ <h4>3. 循环测试 - 团队成员</h4>
170
+
171
+ <div class="team-list">
172
+ {{for member in teamMembers}}
173
+ <div class="team-member">
174
+ <h5>{{member.name}} {{member_isFirst ? '(团队负责人)' : ''}}</h5>
175
+ <p>职位: {{member.position}}</p>
176
+ <p>部门: {{member.department}}</p>
177
+ {{if member.skills && member.skills.length > 0}}
178
+ <p>技能: {{member.skills.join(', ')}}</p>
179
+ {{endif}}
180
+ </div>
181
+ {{endfor}}
182
+ </div>
183
+ </div>
184
+
185
+ <div class="test-result">
186
+ <h4>4. 键值对循环测试</h4>
187
+
188
+ <div style="background: #b13c3c; padding: 10px; border-radius: 5px;">
189
+ {{for key, value in companyInfo}}
190
+ <p><strong>{{key}}:</strong> {{value}}</p>
191
+ {{endfor}}
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- 模板功能测试区域 -->
197
+ <div class="template-test">
198
+ <h3>关于页模板功能测试</h3>
199
+
200
+ <div class="test-result">
201
+ <h4>1. 模板继承与样式测试</h4>
202
+ <p>状态: <span style="color: green;">✓ 成功</span></p>
203
+ <p>此页面通过 <code>"[extends base.html]"</code> 正确继承了基础模板</p>
204
+ <p>此页面已成功添加了自定义样式区块</p>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- 包含文件测试 -->
209
+ <div style="background: #53a853; padding: 15px; margin: 20px 0; border-radius: 5px;">
210
+ <h3>包含文件测试</h3>
211
+ <p>下面将尝试测试包含文件:</p>
212
+ [include test-include.html]
213
+ [include footer-content.html]
214
+ </div>
215
+ [~content]
216
+
217
+ [!footer]
218
+ © {{year}} 我的网站 | 关于我们 | 模板测试
219
+ [~footer]
220
+
221
+ [!script]
222
+ <script src="/static/styling.js" defer></script>
223
+ <script>
224
+ // 获取当前样式文件路径
225
+ window.getCurrentStyleFile = () => {
226
+ return '/static/about.css';
227
+ }
228
+ // 关于页特定的模板测试脚本
229
+ console.log('关于页模板测试完成');
230
+
231
+ // 添加动态内容
232
+ const testDiv = document.createElement('div');
233
+ testDiv.innerHTML = '<p style="color:green;font-weight:bold;">动态内容添加测试成功!</p>';
234
+ document.querySelector('.template-test').appendChild(testDiv);
235
+ </script>
236
+ [~script]
@@ -0,0 +1,184 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>双因素验证处理页</title>
8
+ <!-- 引入公共主题变量与全局样式 -->
9
+ <link rel="stylesheet" href="/static/constants.css" /> <!-- 样式常量 -->
10
+ <link rel="stylesheet" href="/static/public.css" /> <!-- 公共样式 -->
11
+ <link rel="stylesheet" href="/static/themeImg.css" /> <!-- 主题图标 -->
12
+ <link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
13
+
14
+ <style>
15
+ /* 覆盖 body 布局:本页需要居中卡片,而非默认的纵向弹性布局 */
16
+ body {
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding: 20px;
21
+ /* 背景由公共变量接管,自动适配深浅色 */
22
+ background-color: var(--bg-color);
23
+ background-image: var(--body-bg);
24
+ }
25
+
26
+ /* 卡片容器 */
27
+ .card {
28
+ background: var(--container-bg);
29
+ border-radius: 12px;
30
+ box-shadow: 0 20px 40px var(--content-shadow);
31
+ width: 100%;
32
+ max-width: 400px;
33
+ padding: 40px 30px;
34
+ transition: background 0.5s ease, box-shadow 0.5s ease;
35
+ }
36
+
37
+ /* 卡片标题 */
38
+ .card h2 {
39
+ color: var(--h2-color);
40
+ margin-bottom: 20px;
41
+ text-align: center;
42
+ font-weight: 600;
43
+ font-size: 28px;
44
+ }
45
+
46
+ /* 提示文字 */
47
+ .card p {
48
+ text-align: center;
49
+ margin-bottom: 20px;
50
+ color: var(--p-color);
51
+ }
52
+
53
+ .form-group {
54
+ margin-bottom: 20px;
55
+ }
56
+
57
+ /* 验证码输入框 */
58
+ .form-group input {
59
+ width: 100%;
60
+ padding: 12px 16px;
61
+ border: 1px solid var(--content-border);
62
+ border-radius: 8px;
63
+ font-size: 16px;
64
+ text-align: center;
65
+ letter-spacing: 4px;
66
+ background: var(--li-bg);
67
+ color: var(--text-color);
68
+ transition: border-color 0.3s ease, background 0.5s ease, color 0.5s ease;
69
+ }
70
+
71
+ .form-group input:focus {
72
+ border-color: var(--link-color);
73
+ }
74
+
75
+ /* 验证按钮 */
76
+ .btn {
77
+ width: 100%;
78
+ display: block;
79
+ padding: 14px;
80
+ background: var(--btn-bg);
81
+ color: var(--text-color);
82
+ border: none;
83
+ border-radius: 8px;
84
+ font-size: 16px;
85
+ font-weight: 600;
86
+ cursor: pointer;
87
+ margin-top: 0;
88
+ box-shadow: 0 4px 12px var(--content-shadow);
89
+ transition: background 0.3s ease, transform 0.2s ease;
90
+ }
91
+
92
+ .btn:hover {
93
+ background: var(--btn-hover);
94
+ transform: translateY(-2px);
95
+ }
96
+
97
+ .btn:disabled {
98
+ background: #b0b0b0;
99
+ cursor: not-allowed;
100
+ opacity: 0.65;
101
+ transform: none;
102
+ }
103
+
104
+ /* 消息提示 */
105
+ .error {
106
+ color: #e53e3e;
107
+ font-size: 14px;
108
+ margin-top: 10px;
109
+ text-align: center;
110
+ }
111
+
112
+ /* 备用码区域 */
113
+ .backup-link {
114
+ margin-top: 20px;
115
+ text-align: center;
116
+ font-size: 14px;
117
+ color: var(--text-color);
118
+ }
119
+
120
+ .backup-link a {
121
+ color: var(--link-color);
122
+ text-decoration: none;
123
+ }
124
+
125
+ .backup-link a:hover {
126
+ color: var(--link-hover);
127
+ text-decoration: underline;
128
+ }
129
+ </style>
130
+ </head>
131
+
132
+ <body>
133
+ <div class="card">
134
+ <h2>双因素验证</h2>
135
+ <form onsubmit="return false;">
136
+ <p style="text-align:center; margin-bottom:20px; color:#666;">请输入身份验证器中的6位数字验证码</p>
137
+ <div class="form-group">
138
+ <input type="text" id="token" placeholder="在此输入6位验证码" maxlength="6" autofocus
139
+ autocomplete="one-time-code">
140
+ </div>
141
+ <button class="btn" id="verifyBtn">验证</button>
142
+ </form>
143
+ <div class="error" id="message"></div>
144
+ <div class="backup-link">
145
+ <a href="#" id="useBackup">使用备用码</a>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- 公共逻辑 -->
150
+ <script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
151
+ <script src="/static/mouseOrTouch.js" defer></script><!-- 引入鼠标或触摸操作 -->
152
+ <script src="/static/themeImg.js" defer></script> <!-- 引入主题图标模块 -->
153
+ <script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
154
+ <script>
155
+ const [verifyBtn, tokenEl, messageDiv, useBackup] = ['verifyBtn', 'token', 'message', 'useBackup']
156
+ .map(id => document.getElementById(id)),
157
+ handleError = msg => {
158
+ messageDiv.textContent = msg, tokenEl.value = '', verifyBtn.disabled = false, tokenEl.focus();
159
+ },
160
+ verify = async () => {
161
+ verifyBtn.disabled = true; messageDiv.textContent = '';
162
+ const token = tokenEl.value.trim();
163
+ if (!token) return handleError('请输入验证码');
164
+ try {
165
+ const response = await fetch('/api/verify-2fa', {
166
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token })
167
+ }), data = await response.json();
168
+ if (response.ok) tokenEl.value = '', window.location.href = '/';
169
+ else handleError(data.message);
170
+ } catch (err) { handleError('网络错误,请稍后重试') }
171
+ }
172
+ verifyBtn.addEventListener('click', verify);
173
+ useBackup.addEventListener('click', e => {
174
+ e.preventDefault(), tokenEl.placeholder = '请输入10位备用码', tokenEl.maxLength = 10, tokenEl.value = '';
175
+ useBackup.style.display = 'none'; tokenEl.focus();
176
+ });
177
+ tokenEl.addEventListener('keypress', e => {
178
+ if (e.key === 'Enter') verify();
179
+ });
180
+ tokenEl.addEventListener('input', () => messageDiv.textContent = '');
181
+ </script>
182
+ </body>
183
+
184
+ </html>