@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,392 @@
1
+ // 根目录/static/script.js
2
+ function scriptFun() {
3
+ const modal = document.querySelector('.modal'), cssEditor = document.getElementById('cssEditor'),
4
+ closeBtn = document.querySelector('.close-btn'), saveBtn = document.getElementById('saveBtn'),
5
+ cancelBtn = document.getElementById('cancelBtn'), loader = document.getElementById('loader'), api = '/api/cssEditor',
6
+ previewBtn = document.getElementById('previewBtn'), cancelPreviewBtn = document.getElementById('cancelPreviewBtn'),
7
+ preview = document.getElementById('preview'), previewFrame = document.getElementById('previewFrame'), pApi = '/api/preview',
8
+ urlParams = new URLSearchParams(window.location.search), fileDir = urlParams.get('fileDir'),
9
+ returnUrl = urlParams.get('return'), cm = window.cssEditor; // cm 是 CodeMirror 实例,由外部库创建
10
+
11
+ let isEditingColor = false, editColorRange = null, isPreviewMode = false, globalColorPicker = null;
12
+
13
+ // ----- 阻止 CodeMirror 编辑器区域事件冒泡,避免触发父容器拖拽和阻止默认菜单 -----
14
+ cm.getWrapperElement().addEventListener('mousedown', e => e.stopPropagation());
15
+ cm.getWrapperElement().addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
16
+ cm.getWrapperElement().addEventListener('contextmenu', e => e.stopPropagation());
17
+
18
+ // ---------- 🎨 全局颜色选择器 ----------
19
+ function initGlobalColorPicker() {
20
+ if (globalColorPicker) return;
21
+ globalColorPicker = document.createElement('input');
22
+ globalColorPicker.type = 'color';
23
+ globalColorPicker.style.position = 'fixed';
24
+ globalColorPicker.style.width = '0';
25
+ globalColorPicker.style.height = '0';
26
+ globalColorPicker.style.opacity = '0';
27
+ globalColorPicker.style.pointerEvents = 'none';
28
+ globalColorPicker.style.zIndex = '9999';
29
+ globalColorPicker.style.left = '0px';
30
+ globalColorPicker.style.top = '0px';
31
+ document.body.append(globalColorPicker);
32
+ }
33
+ initGlobalColorPicker();
34
+
35
+ // ---------- 颜色关键字映射 ----------
36
+ const COLOR_KEYWORDS = {
37
+ 'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF', 'aquamarine': '#7FFFD4', 'azure': '#F0FFFF',
38
+ 'beige': '#F5F5DC', 'bisque': '#FFE4C4', 'black': '#000000', 'blanchedalmond': '#FFEBCD', 'blue': '#0000FF',
39
+ 'blueviolet': '#8A2BE2', 'brown': '#A52A2A', 'burlywood': '#DEB887', 'cadetblue': '#5F9EA0', 'chartreuse': '#7FFF00',
40
+ 'chocolate': '#D2691E', 'coral': '#FF7F50', 'cornflowerblue': '#6495ED', 'cornsilk': '#FFF8DC', 'crimson': '#DC143C',
41
+ 'cyan': '#00FFFF', 'darkblue': '#00008B', 'darkcyan': '#008B8B', 'darkgoldenrod': '#B8860B', 'darkgray': '#A9A9A9',
42
+ 'darkgreen': '#006400', 'darkgrey': '#A9A9A9', 'darkkhaki': '#BDB76B', 'darkred': '#8B0000', 'dimgray': '#696969',
43
+ 'dimgrey': '#696969', 'darkorange': '#FF8C00', 'darkmagenta': '#8B008B', 'deeppink': '#FF1493',
44
+ 'darkorchid': '#9932CC', 'darkolivegreen': '#556B2F', 'darksalmon': '#E9967A', 'darkseagreen': '#8FBC8F',
45
+ 'darkslateblue': '#483D8B', 'darkslategray': '#2F4F4F', 'darkslategrey': '#2F4F4F', 'darkturquoise': '#00CED1',
46
+ 'darkviolet': '#9400D3', 'deepskyblue': '#00BFFF', 'dodgerblue': '#1E90FF', 'firebrick': '#B22222',
47
+ 'floralwhite': '#FFFAF0', 'forestgreen': '#228B22', 'fuchsia': '#FF00FF', 'gainsboro': '#DCDCDC', 'gold': '#FFD700',
48
+ 'ghostwhite': '#F8F8FF', 'goldenrod': '#DAA520', 'gray': '#808080', 'green': '#008000', 'greenyellow': '#ADFF2F',
49
+ 'grey': '#808080', 'honeydew': '#F0FFF0', 'hotpink': '#FF69B4', 'indianred': '#CD5C5C', 'indigo': '#4B0082',
50
+ 'ivory': '#FFFFF0', 'khaki': '#F0E68C', 'lavender': '#E6E6FA', 'lavenderblush': '#FFF0F5', 'lawngreen': '#7CFC00',
51
+ 'lemonchiffon': '#FFFACD', 'lightblue': '#ADD8E6', 'lightcoral': '#F08080', 'lightcyan': '#E0FFFF',
52
+ 'lightgoldenrodyellow': '#FAFAD2', 'lightgray': '#D3D3D3', 'lightgreen': '#90EE90', 'lightgrey': '#D3D3D3',
53
+ 'lightpink': '#FFB6C1', 'lightsalmon': '#FFA07A', 'lightseagreen': '#20B2AA', 'lightskyblue': '#87CEFA',
54
+ 'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#B0C4DE', 'lightyellow': '#FFFFE0',
55
+ 'lime': '#00FF00', 'limegreen': '#32CD32', 'linen': '#FAF0E6', 'magenta': '#FF00FF', 'maroon': '#800000',
56
+ 'mediumaquamarine': '#66CDAA', 'mediumblue': '#0000CD', 'mediumorchid': '#BA55D3', 'mediumpurple': '#9370DB',
57
+ 'mediumseagreen': '#3CB371', 'mediumslateblue': '#7B68EE', 'mediumspringgreen': '#00FA9A', 'moccasin': '#FFE4B5',
58
+ 'mediumturquoise': '#48D1CC', 'mediumvioletred': '#C71585', 'midnightblue': '#191970', 'mintcream': '#F5FFFA',
59
+ 'mistyrose': '#FFE4E1', 'navajowhite': '#FFDEAD', 'navy': '#000080', 'oldlace': '#FDF5E6', 'olive': '#808000',
60
+ 'olivedrab': '#6B8E23', 'orange': '#FFA500', 'orangered': '#FF4500', 'orchid': '#DA70D6', 'peru': '#CD853F',
61
+ 'palegoldenrod': '#EEE8AA', 'palegreen': '#98FB98', 'paleturquoise': '#AFEEEE', 'palevioletred': '#DB7093',
62
+ 'papayawhip': '#FFEFD5', 'peachpuff': '#FFDAB9', 'pink': '#FFC0CB', 'plum': '#DDA0DD', 'powderblue': '#B0E0E6',
63
+ 'purple': '#800080', 'rebeccapurple': '#663399', 'red': '#FF0000', 'rosybrown': '#BC8F8F', 'royalblue': '#4169E1',
64
+ 'saddlebrown': '#8B4513', 'salmon': '#FA8072', 'sandybrown': '#F4A460', 'seagreen': '#2E8B57', 'snow': '#FFFAFA',
65
+ 'seashell': '#FFF5EE', 'sienna': '#A0522D', 'silver': '#C0C0C0', 'skyblue': '#87CEEB', 'slateblue': '#6A5ACD',
66
+ 'slategray': '#708090', 'slategrey': '#708090', 'springgreen': '#00FF7F', 'steelblue': '#4682B4', 'tan': '#D2B48C',
67
+ 'teal': '#008080', 'thistle': '#D8BFD8', 'tomato': '#FF6347', 'turquoise': '#40E0D0', 'violet': '#EE82EE',
68
+ 'wheat': '#F5DEB3', 'white': '#FFFFFF', 'whitesmoke': '#F5F5F5', 'yellow': '#FFFF00', 'yellowgreen': '#9ACD32',
69
+ 'transparent': 'transparent'
70
+ };
71
+
72
+ // ---------- 颜色正则(支持所有格式)----------
73
+ const KEYWORDS = Object.keys(COLOR_KEYWORDS).concat('transparent').join('|'),
74
+ HEX = '#(?:[0-9a-fA-F]{3,4}){1,2}\\b', RGB = 'rgba?\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
75
+ HSL = 'hsla?\\(\\s*\\d+\\s*,\\s*\\d+%\\s*,\\s*\\d+%\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
76
+ COLOR_REGEX = new RegExp(`${HEX}|${RGB}|${HSL}|\\b(${KEYWORDS})\\b`, 'gi'),
77
+ RGB_EXTRACT = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i;
78
+
79
+ // ---------- 颜色工具函数 ----------
80
+ function rgbToHex(r, g, b) {
81
+ return '#' + [r, g, b].map(x => {
82
+ const h = parseInt(x).toString(16);
83
+ return h.length === 1 ? '0' + h : h;
84
+ }).join('');
85
+ }
86
+
87
+ function extractColorAndAlpha(c) {
88
+ const l = c.toLowerCase(), kw = COLOR_KEYWORDS[l];
89
+ let r = 0, g = 0, b = 0, a = 1, t = 'keyword';
90
+
91
+ // 1. 颜色关键字(含 transparent)
92
+ if (kw) {
93
+ if (l === 'transparent') return { r: 0, g: 0, b: 0, a: 0, t, o: c, tr: true };
94
+ const hex = kw.slice(1); // "#ff0000" → "ff0000"
95
+ [r, g, b] = hex.match(/.{2}/g).map(v => parseInt(v, 16)), a = 1;
96
+ }
97
+ // 2. 十六进制 #RRGGBB / #RGB / #RRGGBBAA / #RGBA
98
+ else if (c.startsWith('#')) {
99
+ t = 'hex';
100
+ let h = c.slice(1).toLowerCase();
101
+ if (h.length === 3 || h.length === 4) h = h.split('').map(x => x + x).join('');
102
+ const hexMatch = h.match(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/);
103
+ if (hexMatch)
104
+ [r, g, b] = hexMatch.slice(1, 4).map(v => parseInt(v, 16)), a = hexMatch[4] ? parseInt(hexMatch[4], 16) / 255 : 1;
105
+ }
106
+ // 3. rgb() / rgba()
107
+ else if (c.startsWith('rgb')) {
108
+ const m = RGB_EXTRACT.exec(c);
109
+ if (m) t = 'rgb', [r, g, b] = [m[1], m[2], m[3]].map(Number), a = m[4] ? parseFloat(m[4]) : 1;
110
+ }
111
+ // 4. hsl() / hsla()
112
+ else if (c.startsWith('hsl')) {
113
+ t = 'hsl';
114
+ const d = document.createElement('div');
115
+ d.style.color = c, document.body.append(d);
116
+ const computed = window.getComputedStyle(d).color; // "rgb(r, g, b)" 或 "rgba(r, g, b, a)"
117
+ d.remove();
118
+
119
+ const m = RGB_EXTRACT.exec(computed);
120
+ if (m) [r, g, b] = [m[1], m[2], m[3]].map(Number), a = m[4] ? parseFloat(m[4]) : 1;
121
+ else r = 128, g = 128; b = 128, a = 1; // 解析失败时的默认值
122
+ }
123
+
124
+ return { r, g, b, a, t, o: c };
125
+ }
126
+
127
+ function getColorForSwatch(c) {
128
+ const o = extractColorAndAlpha(c);
129
+ if (o.tr || c.toLowerCase() === 'transparent') return 'transparent';
130
+ if (o.a < 1) return `rgba(${o.r},${o.g},${o.b},${o.a})`;
131
+ else if (c.startsWith('#')) return c;
132
+ else if (c.startsWith('rgb')) return `rgb(${o.r},${o.g},${o.b})`;
133
+ else return c;
134
+ }
135
+
136
+ // ---------- 创建颜色部件 ----------
137
+ function createColorWidget(ct, fr, to) {
138
+ const w = document.createElement('span'), s = document.createElement('span'), t = document.createElement('span');
139
+ w.className = 'cm-color-widget', w.style.display = 'inline-flex', w.style.alignItems = 'center'; w.style.margin = '0 2px',
140
+ w.style.padding = '2px 4px', w.style.borderRadius = '3px', w.style.backgroundColor = 'rgba(0,0,0,0.1)';
141
+
142
+ const bg = getColorForSwatch(ct);
143
+ s.className = 'cm-color-swatch', s.style.backgroundColor = bg, s.title = '点击修改颜色', s.style.display = 'inline-block',
144
+ s.style.width = '16px', s.style.height = '16px', s.style.borderRadius = '3px', s.style.marginRight = '6px',
145
+ s.style.border = bg === 'transparent' ? '1px dashed #999' : '1px solid rgba(255,255,255,1)';
146
+ s.style.cursor = 'pointer', s.style.flexShrink = '0';
147
+
148
+ t.className = 'cm-color-text', t.textContent = ct, t.style.color = '#f8f8f2', t.style.fontSize = '13px';
149
+ t.style.userSelect = 'text', t.style.cursor = 'text', t.style.marginRight = '8px', w.append(s, t);
150
+
151
+ // 透明度滑块
152
+ const o = extractColorAndAlpha(ct), as = document.createElement('input'), av = document.createElement('span');
153
+ if (as) {
154
+ as.type = 'range', as.min = 0, as.max = 100, as.value = Math.round(o.a * 100), as.style.width = '120px',
155
+ as.style.marginRight = '8px', as.title = '调整透明度 (0-100%)', as.className = 'cm-alpha-slider';
156
+ av.className = 'cm-alpha-value', av.textContent = `${Math.round(o.a * 100)}%`, av.style.fontSize = '12px',
157
+ av.style.color = '#ccc', av.style.minWidth = '30px', av.style.textAlign = 'center';
158
+
159
+ // 根据透明度计算新颜色(供 input/change 复用)
160
+ const getUpdatedColor = (alpha) => {
161
+ const na = alpha / 100;
162
+ let nc = ct;
163
+ const lc = ct.toLowerCase();
164
+ if (lc === 'transparent') nc = `rgba(0,0,0,${na})`;
165
+ if (COLOR_KEYWORDS[lc]) {
166
+ const h = COLOR_KEYWORDS[lc], hr = parseInt(h.slice(1, 3), 16), hg = parseInt(h.slice(3, 5), 16),
167
+ hb = parseInt(h.slice(5, 7), 16);
168
+ nc = `rgba(${hr},${hg},${hb},${na})`;
169
+ } else if (ct.includes('rgba') || ct.includes('hsla')) {
170
+ if (ct.includes('rgba')) nc = ct.replace(RGB_EXTRACT, `rgba($1,$2,$3,${na})`);
171
+ else nc = ct.replace(/hsla?\((\d+,\s*\d+%,\s*\d+%)(?:,\s*[\d.]+)?\)/i, `hsla($1,${na})`);
172
+ } else if (ct.startsWith('rgb(')) nc = ct.replace('rgb(', `rgba(`).replace(')', `,${na})`);
173
+ else if (ct.startsWith('hsl(')) nc = ct.replace('hsl(', `hsla(`).replace(')', `,${na})`);
174
+ else if (ct.startsWith('#')) {
175
+ let h = ct.slice(1), hr, hg, hb;
176
+ if (h.length === 3)
177
+ hr = parseInt(h[0] + h[0], 16), hg = parseInt(h[1] + h[1], 16), hb = parseInt(h[2] + h[2], 16);
178
+ else if (h.length === 6)
179
+ hr = parseInt(h.slice(0, 2), 16), hg = parseInt(h.slice(2, 4), 16), hb = parseInt(h.slice(4, 6), 16);
180
+ else hr = 128, hg = 128, hb = 128;
181
+ nc = `rgba(${hr},${hg},${hb},${na})`;
182
+ }
183
+ return nc;
184
+ };
185
+
186
+ // 实时更新 UI(色块、文本、百分比),但不修改文档
187
+ as.addEventListener('input', function () {
188
+ const na = this.value / 100;
189
+ av.textContent = `${this.value}%`;
190
+ const nc = getUpdatedColor(this.value);
191
+ s.style.backgroundColor = getColorForSwatch(nc);
192
+ });
193
+
194
+ // 滑块松开或失去焦点时,一次性更新文档
195
+ as.addEventListener('change', function () {
196
+ const nc = getUpdatedColor(this.value);
197
+ cm.replaceRange(nc, fr, to), updateColorWidgets(cm);
198
+ });
199
+
200
+ // 阻止滑块上的鼠标/触摸事件冒泡,使滑块可拖动
201
+ as.addEventListener('mousedown', e => e.stopPropagation());
202
+ as.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
203
+
204
+ w.append(as, av);
205
+ }
206
+
207
+ // 色块点击事件
208
+ s.addEventListener('click', e => {
209
+ e.stopPropagation();
210
+ const r = s.getBoundingClientRect(), cc = extractColorAndAlpha(ct), ch = rgbToHex(cc.r, cc.g, cc.b);
211
+ globalColorPicker.style.left = `${r.left}px`, globalColorPicker.style.top = `${r.bottom + 5}px`;
212
+ globalColorPicker.value = ch;
213
+
214
+ if (globalColorPicker._ch) globalColorPicker.removeEventListener('change', globalColorPicker._ch);
215
+ const chf = function () {
216
+ const nh = this.value, na = as ? as.value / 100 : cc.a, hr = parseInt(nh.slice(1, 3), 16),
217
+ hg = parseInt(nh.slice(3, 5), 16), hb = parseInt(nh.slice(5, 7), 16);
218
+ let nc = na < 1 ? `rgba(${hr},${hg},${hb},${na})` : nh;
219
+ const nhu = nh.toUpperCase(), kw = Object.keys(COLOR_KEYWORDS).find(k => COLOR_KEYWORDS[k].toUpperCase() === nhu);
220
+ if (kw && na >= 1) nc = kw;
221
+ cm.replaceRange(nc, fr, to), updateColorWidgets(cm), t.textContent = nc;
222
+ s.style.backgroundColor = getColorForSwatch(nc);
223
+ if (av) av.textContent = `${Math.round(na * 100)}%`;
224
+ to = { line: to.line, ch: fr.ch + nc.length };
225
+ this.removeEventListener('change', chf), globalColorPicker._ch = null;
226
+ };
227
+ globalColorPicker._ch = chf, globalColorPicker.addEventListener('change', chf), globalColorPicker.getBoundingClientRect();
228
+ if (typeof globalColorPicker.showPicker === 'function') globalColorPicker.showPicker();
229
+ else globalColorPicker.click();
230
+ });
231
+
232
+ return w;
233
+ }
234
+
235
+ // ---------- 扫描编辑器添加部件 ----------
236
+ function updateColorWidgets(cm) {
237
+ isEditingColor = false, editColorRange = null; // 退出编辑模式
238
+
239
+ cm.getAllMarks().forEach(m => { if (m.isColorWidget) m.clear(); });
240
+ const d = cm.getDoc(), lc = d.lineCount();
241
+ for (let i = 0; i < lc; i++) {
242
+ const l = d.getLine(i); let m;
243
+ COLOR_REGEX.lastIndex = 0;
244
+ while ((m = COLOR_REGEX.exec(l)) !== null) {
245
+ const s = m.index, e = s + m[0].length, fr = { line: i, ch: s }, to = { line: i, ch: e };
246
+ if (d.findMarksAt(fr).some(x => x.replacedWith)) continue;
247
+
248
+ const w = createColorWidget(m[0], fr, to),
249
+ mark = cm.markText(fr, to, {
250
+ replacedWith: w, inclusiveLeft: false, inclusiveRight: false, clearOnEnter: true
251
+ });
252
+ mark.isColorWidget = true, w._colorMark = mark;
253
+
254
+ // 为颜色值文本添加双击事件
255
+ const textSpan = w.querySelector('.cm-color-text');
256
+ if (textSpan) {
257
+ textSpan.title = '双击编辑颜色值';
258
+ textSpan.addEventListener('dblclick', (e) => {
259
+ e.stopPropagation();
260
+ const widget = e.target.closest('.cm-color-widget');
261
+ if (!widget) return;
262
+ const mark = widget._colorMark;
263
+ if (mark) {
264
+ const pos = mark.find();
265
+ if (pos) {
266
+ // 记录当前正在编辑的颜色值范围
267
+ isEditingColor = true;
268
+ editColorRange = {
269
+ from: { line: pos.from.line, ch: pos.from.ch },
270
+ to: { line: pos.to.line, ch: pos.to.ch }
271
+ };
272
+ mark.clear(), cm.setCursor(pos.from), cm.focus();
273
+ }
274
+ }
275
+ });
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ updateColorWidgets(cm);
282
+
283
+ // 编辑期间不自动重建部件,但允许手动触发重建
284
+ cm.on('change', () => {
285
+ if (isEditingColor) return;
286
+ setTimeout(() => updateColorWidgets(cm), 10);
287
+ });
288
+
289
+ // 监听光标活动,检测是否离开正在编辑的颜色值区域
290
+ cm.on('cursorActivity', () => {
291
+ if (isEditingColor && editColorRange) {
292
+ const cursor = cm.getCursor(), range = editColorRange,
293
+ // 检查光标是否离开了正在编辑的颜色值范围
294
+ isOutsideRange = cursor.line < range.from.line || cursor.line > range.to.line ||
295
+ (cursor.line === range.from.line && cursor.ch < range.from.ch) ||
296
+ (cursor.line === range.to.line && cursor.ch > range.to.ch);
297
+
298
+ if (isOutsideRange) isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
299
+ }
300
+ });
301
+
302
+ // ============================== 预览功能 ==============================
303
+ function startPreview() {
304
+ isPreviewMode = true, preview.style.display = 'block';
305
+ const er = cssEditor.getBoundingClientRect();
306
+ preview.style.width = `${er.width}px`, preview.style.height = `${er.height}px`;
307
+ previewBtn.style.display = 'none', cancelPreviewBtn.style.display = 'block';
308
+ previewFrame.src = returnUrl, cm.on('change', updatePreviewStyles);
309
+ }
310
+
311
+ function updatePreviewStyles() {
312
+ if (!isPreviewMode || !previewFrame.contentWindow?.document) return;
313
+ try {
314
+ const sc = cm.getValue(), id = previewFrame.contentDocument || previewFrame.contentWindow.document,
315
+ os = id.getElementById('dynamic-css'), s = id.createElement('style');
316
+ if (os) os.remove();
317
+ s.id = 'dynamic-css', s.textContent = sc, id.head.append(s);
318
+ } catch (e) { console.log('预览更新失败', e); }
319
+ }
320
+
321
+ function cancelPreview() {
322
+ isPreviewMode = false, preview.style.display = 'none', previewBtn.style.display = 'block';
323
+ cancelPreviewBtn.style.display = 'none', previewFrame.src = 'about:blank', cm.off('change', updatePreviewStyles);
324
+ }
325
+
326
+ addTapSupport(previewBtn, startPreview), addTapSupport(cancelPreviewBtn, cancelPreview);
327
+
328
+ // 获取储存样式并应用
329
+ getStyle(cssEditor, api), getStyle(preview, pApi);
330
+ modal.append(cssEditor, preview), loadCssContent(fileDir);
331
+
332
+ mouseOrTouch(cssEditor, () => { cssEditor.style.zIndex = '1002', preview.style.zIndex = '1001'; }, api);
333
+ mouseOrTouch(preview, () => { preview.style.zIndex = '1002', cssEditor.style.zIndex = '1001'; }, pApi);
334
+
335
+ addTapSupport(saveBtn, saveCSS), addTapSupport(cancelBtn, cancelEdit), addTapSupport(closeBtn, cancelEdit);
336
+
337
+ // 键盘事件
338
+ document.addEventListener('keydown', e => {
339
+ // Escape 优先处理编辑退出
340
+ if (e.key === 'Escape') {
341
+ if (isEditingColor) {
342
+ e.preventDefault(), isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
343
+ return;
344
+ }
345
+ if (cssEditor.style.display === 'flex') {
346
+ if (isPreviewMode) cancelPreview();
347
+ else cancelEdit();
348
+ }
349
+ }
350
+ if (e.ctrlKey && e.key === 's') e.preventDefault(), saveCSS();
351
+ if (e.ctrlKey && e.key === 'p') e.preventDefault(), startPreview();
352
+ });
353
+
354
+ // ---------- 加载CSS内容 ----------
355
+ async function loadCssContent(fd) {
356
+ showLoader();
357
+ try {
358
+ const r = await fetch(`/api/css?fileDir=${encodeURIComponent(fd)}`);
359
+ if (r.ok) { const ct = await r.text(); cm.setValue(ct), updateEditorTitle(fd); }
360
+ else throw new Error(`无法加载CSS文件:${fd}`);
361
+ } catch (err) { alert(`加载CSS失败:${err.message}`), updateEditorTitle(fd); }
362
+ finally { hideLoader(); }
363
+ }
364
+
365
+ function updateEditorTitle(fd) {
366
+ const te = document.querySelector('#cssEditor h2');
367
+ if (te) te.textContent = `编辑文件:${fd}`;
368
+ }
369
+
370
+ async function saveCSS() {
371
+ showLoader();
372
+ try {
373
+ const r = await fetch('/api/css', {
374
+ method: 'POST',
375
+ headers: { 'Content-Type': 'application/json' },
376
+ body: JSON.stringify({ fileDir, content: cm.getValue() })
377
+ });
378
+ if (r.ok) window.location.href = returnUrl;
379
+ else { const et = await r.text(); throw new Error(et || '保存失败'); }
380
+ } catch (err) { alert(`保存CSS失败:${err.message}`); }
381
+ finally { hideLoader(); }
382
+ }
383
+
384
+ function cancelEdit() { if (confirm('确定要取消编辑吗')) window.location.href = returnUrl; }
385
+
386
+ function showLoader() { if (loader) loader.style.display = 'block'; }
387
+ function hideLoader() { if (loader) loader.style.display = 'none'; }
388
+ }
389
+
390
+ // 页面加载启动
391
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scriptFun);
392
+ else scriptFun();