@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,933 @@
1
+
2
+ // 长按设置背景色功能实现
3
+ class LongPressBackground {
4
+ constructor() {
5
+ this.pressTimer = null, this.longPressDuration = 2000, this.isLongPress = false; // 定时器,长按时间 2秒,是否长按
6
+ this.visualFeedbackTimer = null, this.visualFeedbackDelay = 600; // 视觉反馈定时器
7
+ this.nowElement = null, this.longPressedElement = null; this.lastSelect = null; // 当前元素,长按元素,最后下拉框对象
8
+ this.dragThreshold = 10, this.startX = 0, this.startY = 0, this.isDragging = false; // 添加拖动检测相关变量
9
+ const pressIndicator = document.createElement('div'), picker = document.createElement('div'), api = '/api/longPic';
10
+ pressIndicator.className = 'pressIndicator', picker.id = 'picker', picker.className = 'picker';
11
+
12
+ // 添加颜色选择器容器,长按指示器
13
+ document.body.append(pressIndicator, picker), this.pressIndicator = pressIndicator, this.picker = picker;
14
+ this.currentMode = 'background'; // 当前模式:初始为background
15
+
16
+ // 提前绑定所有需要的事件处理器
17
+ this.handleStart = this.handleStart.bind(this), this.handleEnd = this.handleEnd.bind(this);
18
+ this.handleCancel = this.handleCancel.bind(this), this.handleMove = this.handleMove.bind(this);
19
+ this.handleDocumentClick = this.handleDocumentClick.bind(this), this.handleGlobalClick = this.handleGlobalClick.bind(this);
20
+
21
+ getStyle(this.picker, api), mouseOrTouch(this.picker, null, api); // 获取储存样式,绑定元素移动事件
22
+ this.init();
23
+ }
24
+
25
+ init() {
26
+ // 使用事件委托,避免为每个元素单独绑定事件
27
+ document.addEventListener('mousedown', this.handleStart, true);
28
+ document.addEventListener('mouseup', this.handleEnd, true);
29
+ document.addEventListener('mouseleave', this.handleCancel, true);
30
+ document.addEventListener('mousemove', this.handleMove);
31
+
32
+ document.addEventListener('touchstart', this.handleStart);
33
+ document.addEventListener('touchend', this.handleEnd);
34
+ document.addEventListener('touchcancel', this.handleCancel);
35
+ document.addEventListener('touchmove', this.handleMove, { passive: true });
36
+
37
+ this.initPicker(); // 初始化选择器
38
+ document.addEventListener('click', this.handleDocumentClick);
39
+ document.addEventListener('touchend', this.handleDocumentClick);
40
+ document.addEventListener('click', this.handleGlobalClick, true); // 阻止长按后的单击
41
+ }
42
+
43
+ // 坐标获取
44
+ getEventCoordinates(e) {
45
+ let x, y;
46
+ if (e.touches && e.touches.length > 0) x = e.touches[0].pageX, y = e.touches[0].pageY;
47
+ else if (e.changedTouches && e.changedTouches.length > 0) x = e.changedTouches[0].pageX, y = e.changedTouches[0].pageY;
48
+ else x = e.pageX, y = e.pageY;
49
+
50
+ return { x, y };
51
+ }
52
+
53
+ // 移动事件处理
54
+ handleMove(e) {
55
+ if (!this.pressTimer) return;
56
+
57
+ // 检测是否拖动
58
+ const { x, y } = this.getEventCoordinates(e), deltaX = Math.abs(x - this.startX), deltaY = Math.abs(y - this.startY);
59
+ // 如果拖动距离超过阈值,取消长按
60
+ if (deltaX > this.dragThreshold || deltaY > this.dragThreshold) this.handleCancel(), this.isDragging = true;
61
+ }
62
+
63
+ // 全局单击事件处理
64
+ handleGlobalClick(e) {
65
+ if (this.isLongPress) {
66
+ this.preventingDefault(e), e.stopImmediatePropagation();
67
+ return false;
68
+ }
69
+ }
70
+
71
+ // 设置跳过元素
72
+ shouldSkipElement(_e) {
73
+ return false; // 默认允许所有元素长按
74
+ }
75
+
76
+ // 阻止默认事件行为
77
+ preventingDefault(e) {
78
+ if (e.cancelable) e.preventDefault();
79
+ }
80
+
81
+ // 长按开始
82
+ handleStart(e) {
83
+ const target = e.target;
84
+
85
+ if (this.shouldSkipElement(target)) return;
86
+ // 记录起始位置和标记长按元素
87
+ const { x, y } = this.getEventCoordinates(e);
88
+ this.startX = x, this.startY = y, this.isDragging = false, this.isLongPress = false, this.nowElement = target;
89
+
90
+ if (this.isLongPress && e.type === 'touchstart') this.preventingDefault(e);
91
+ // 设置视觉反馈定时器
92
+ this.visualFeedbackTimer = setTimeout(() => this.addVisualFeedback(target, e), this.visualFeedbackDelay);
93
+ // 设置长按定时器
94
+ this.pressTimer = setTimeout(() => {
95
+ this.isLongPress = true, this.showPicker(e);
96
+ if (e.type === 'touchstart' || e.type === 'touchmove') e.preventDefault(); // 长按成功后,才阻止后续默认行为
97
+ }, this.longPressDuration);
98
+ }
99
+
100
+ handleEnd(e) {
101
+ // 如果是拖动,不执行长按后的操作
102
+ if (this.isDragging) {
103
+ this.handleCancel();
104
+ return;
105
+ }
106
+ if (this.visualFeedbackTimer) clearTimeout(this.visualFeedbackTimer), this.visualFeedbackTimer = null;
107
+ if (this.pressTimer) clearTimeout(this.pressTimer), this.pressTimer = null; // 清除定时器
108
+ if (this.isLongPress && e.type === 'touchend') this.preventingDefault(e);
109
+ this.removeVisualFeedback(); // 移除视觉反馈
110
+ if (!this.isLongPress) return; // 如果不是长按,直接返回
111
+ }
112
+
113
+ // 取消长按
114
+ handleCancel(e) {
115
+ if (this.visualFeedbackTimer) clearTimeout(this.visualFeedbackTimer), this.visualFeedbackTimer = null;
116
+ if (this.pressTimer) clearTimeout(this.pressTimer), this.pressTimer = null;
117
+ this.removeVisualFeedback(), this.isLongPress = false;
118
+ }
119
+
120
+ // 处理文档点击事件(关闭选择器)
121
+ handleDocumentClick(e) {
122
+ // 如果选择器正在显示,并且点击的不是选择器内部,也不是长按元素
123
+ if (this.picker.style.display === 'grid' && !this.picker.contains(e.target)
124
+ && this.nowElement !== this.longPressedElement) this.hidepicker();
125
+ }
126
+
127
+ // 添加长按视觉反馈
128
+ addVisualFeedback(element, e) {
129
+ element.classList.add('element-highlight');
130
+
131
+ // 创建指示器
132
+ const { x, y } = this.getEventCoordinates(e);
133
+ if (x && y) {
134
+ // 添加倒计时条
135
+ const countdownBar = document.createElement('div');
136
+ this.pressIndicator.classList.add('long-press-indicator');
137
+ this.pressIndicator.style.left = `${x - 30}px`, this.pressIndicator.style.top = `${y - 30}px`;
138
+ this.pressIndicator.innerHTML = '长按中<br><span style="font-size:10px">2秒</span>';
139
+ countdownBar.className = 'countdown-bar', this.pressIndicator.append(countdownBar);
140
+ }
141
+ }
142
+
143
+ // 移除指示器及长按视觉反馈
144
+ removeVisualFeedback() {
145
+ if (this.nowElement) this.nowElement.classList.remove('element-highlight');
146
+ this.pressIndicator.classList.remove('long-press-indicator'), this.pressIndicator.innerHTML = '';
147
+ }
148
+
149
+ // 显示选择器并保存长按元素
150
+ showPicker(e) {
151
+ this.picker.style.display = 'grid';
152
+
153
+ // 定位到事件发生位置并确保选择器不会超出屏幕
154
+ const { x, y } = this.getEventCoordinates(e), rect = this.picker.getBoundingClientRect(),
155
+ viewportWidth = window.innerWidth, viewportHeight = window.innerHeight;
156
+
157
+ let left = x, top = y;
158
+ if (x + rect.width > viewportWidth) left = viewportWidth - rect.width - 20;
159
+ if (y + rect.height > viewportHeight) top = viewportHeight - rect.height - 20;
160
+
161
+ this.picker.style.left = `${left}px`, this.picker.style.top = `${top}px`, this.picker.style.transform = 'none';
162
+ this.longPressedElement = e.target;
163
+ }
164
+
165
+ // 隐藏颜色选择器(重置状态,移除视觉反馈)
166
+ hidepicker() {
167
+ if (this.lastSelect) {
168
+ const { btn, list, txt, arrow } = this.lastSelect;
169
+ btn.textContent = txt, btn.append(arrow), list.style.display = 'none'
170
+ };
171
+ this.picker.style.display = 'none', this.isLongPress = false, this.nowElement = null, this.longPressedElement = null;
172
+ this.removeVisualFeedback();
173
+ }
174
+
175
+ // =================================================================================================================
176
+ initPicker() {
177
+ // 创建模式切换按钮组和添加跳转到编辑器按钮
178
+ const modeBtns = [{ text: '背景', mode: 'background', isActive: true }, { text: '字体', mode: 'font', isActive: false }
179
+ , { text: '边框', mode: 'border', isActive: false }, { text: '宽高', mode: 'size', isActive: false }],
180
+ modeToggle = document.createElement('div'), saveEditor = document.createElement('div'),
181
+ toEditorBtn = document.createElement('button'), toSaveBtn = document.createElement('button');
182
+
183
+ modeToggle.className = 'mode-toggle';
184
+ modeBtns.forEach(value => {
185
+ const btn = document.createElement('button');
186
+ btn.type = 'button', btn.className = `mode-btn ${value.isActive ? 'active' : ''}`;
187
+ btn.textContent = value.text, btn.setAttribute('data-mode', value.mode);
188
+ addTapSupport(btn, () => this.switchMode(value.mode)), modeToggle.append(btn);
189
+ });
190
+ saveEditor.className = 'saveEditor';
191
+ toEditorBtn.className = 'toEditorBtn', toEditorBtn.textContent = '去编辑样式', toEditorBtn.type = 'button';
192
+ toSaveBtn.className = 'toSaveBtn', toSaveBtn.textContent = '保存到文件', toSaveBtn.type = 'button';
193
+
194
+ this.picker.append(modeToggle), this.createBackgroundContent(); // 初始化默认内容(背景模式)
195
+ addTapSupport(toEditorBtn, () => this.navigateToEditor()), addTapSupport(toSaveBtn, () => this.saveStylesToFile());
196
+ saveEditor.append(toEditorBtn, toSaveBtn), this.picker.append(saveEditor);
197
+ }
198
+
199
+ // 切换模式
200
+ switchMode(mode) {
201
+ if (this.currentMode === mode) return;
202
+
203
+ // 更新所有模式按钮的状态
204
+ const modeBtns = this.picker.querySelectorAll('.mode-btn'), modeToggle = this.picker.querySelector('.mode-toggle'),
205
+ saveEditor = this.picker.querySelector('.saveEditor');
206
+
207
+ modeBtns.forEach(modeBtn => {
208
+ const btnMode = modeBtn.getAttribute('data-mode');
209
+ if (btnMode === mode) modeBtn.classList.add('active');
210
+ else modeBtn.classList.remove('active');
211
+ });
212
+
213
+ this.currentMode = mode, this.picker.innerHTML = '', this.picker.append(modeToggle);// 清空picker内容(除了模式按钮和编辑器按钮)
214
+
215
+ // 根据模式创建内容
216
+ if (mode === 'background') this.createBackgroundContent();
217
+ else if (mode === 'font') this.createFontContent();
218
+ else if (mode === 'border') this.createBorderContent();
219
+ else if (mode === 'size') this.createSizeContent();
220
+
221
+ this.picker.append(saveEditor);
222
+ }
223
+
224
+ // 创建自定义输入组件
225
+ createCustomInput(options) {
226
+ const { labelText = '', inputType = 'text', inputId = '', inputValue = '', placeholder = '', accept = '', buttonText = null,
227
+ onApply = null, containerClass = 'custom' } = options, container = document.createElement('div'),
228
+ label = document.createElement('label'), input = document.createElement('input'), btn = document.createElement('button');
229
+
230
+ container.className = containerClass;
231
+ // 设置标签
232
+ if (inputId) input.id = inputId, label.setAttribute('for', inputId);
233
+ label.textContent = labelText, label.style.marginRight = '10px';
234
+
235
+ // 设置输入框
236
+ if (inputType === 'range') input.min = '0', input.max = inputValue, input.step = '1';
237
+ if (inputValue) input.value = inputValue;
238
+ if (placeholder) input.placeholder = placeholder;
239
+ if (accept) input.accept = accept;
240
+ input.type = inputType, input.style.marginRight = '10px';
241
+
242
+ btn.type = 'button', btn.textContent = buttonText, btn.setAttribute('aria-label', '应用按钮');
243
+ if (!buttonText) btn.style.display = 'none';
244
+
245
+ // 绑定监听事件
246
+ if (typeof onApply === 'function') {
247
+ if (inputType === 'text') addTapSupport(btn, () => {
248
+ const value = input.value.trim();
249
+ if (value) onApply(value), input.value = '';
250
+ });
251
+ else if (inputType === 'color') input.addEventListener('input', e => onApply(input.value, e));
252
+ else input.addEventListener('change', e => onApply(input.value, e));
253
+ }
254
+
255
+ container.append(label, input, btn), this.picker.append(container);
256
+ }
257
+
258
+ // 创建背景内容
259
+ createBackgroundContent() {
260
+ // 预定义颜色选项
261
+ const bgColors = [
262
+ '#FF5733', '#33FF57', '#3357FF', '#F333FF', '#FF33A1', '#33FFF6', '#FFC733', '#8D33FF',
263
+ '#33FF8D', '#FF3333', '#33A1FF', '#FF8D33', '#FFFFFF', '#E0E0E0', '#9E9E9E', '#000000',
264
+ '#FFEAA7', '#74B9FF', '#55EFC4', '#FD79A8', '#FDCB6E', '#E17055', '#00CEC9', '#6C5CE7'
265
+ ];
266
+
267
+ this.createColorItem(bgColors, '背景', 'background'); // 添加背景颜色选项
268
+ // 自定义颜色输入
269
+ this.createCustomInput({
270
+ labelText: '自定义颜色:', inputType: 'color', inputId: 'customColorInput', inputValue: '#3498db',
271
+ onApply: color => { this.resetOpacity(), this.setStyle('background', color) }
272
+ });
273
+ // 添加背景透明度输入框
274
+ this.createCustomInput({
275
+ labelText: '背景透明度:', inputType: 'range', inputId: 'bgOpacityInput', inputValue: '255', placeholder: '0-255之间',
276
+ onApply: opacit => {
277
+ // 获取当前自定义颜色输入框的值
278
+ const colorInput = this.picker.querySelector('#customColorInput'), baseColor = colorInput.value,
279
+ opacityHex = parseInt(opacit).toString(16).padStart(2, '0').toUpperCase();
280
+
281
+ this.setStyle('background', `${baseColor}${opacityHex}`);
282
+ }
283
+ });
284
+
285
+ this.createImagePickerDiv(); // 添加背景图选择器触发容器
286
+ }
287
+
288
+ // 获取图片列表
289
+ async getImageList() {
290
+ try {
291
+ const response = await fetch('/api/images');
292
+ if (!response.ok) throw new Error('获取图片列表失败');
293
+ return await response.json();
294
+ } catch (error) {
295
+ console.error('获取图片列表失败:', error);
296
+ return null;
297
+ }
298
+ }
299
+
300
+ // 创建图片选择器触发容器(标题,选择按钮,移除按钮)
301
+ createImagePickerDiv() {
302
+ const container = document.createElement('div'), title = document.createElement('span'),
303
+ selectBtn = document.createElement('button'), removeBgBtn = document.createElement('button');
304
+
305
+ container.id = 'imagePickerDiv', title.textContent = '背景图:', title.style.fontWeight = 'bold';
306
+ selectBtn.type = 'button', selectBtn.className = 'selectImageBtn';
307
+ selectBtn.innerHTML = `
308
+ <span style="display:inline-flex;align-items:center;gap:8px;">
309
+ 选择
310
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
311
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
312
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
313
+ <polyline points="21 15 16 10 5 21"></polyline>
314
+ </svg>
315
+ </span>`;
316
+ removeBgBtn.className = 'selectImageBtn remove-bg-btn', removeBgBtn.textContent = '移除', removeBgBtn.type = 'button';
317
+
318
+ addTapSupport(removeBgBtn, () => this.setStyle('background-image', 'none'));
319
+ addTapSupport(selectBtn, () => this.showImagePickerOverlay());
320
+ container.append(title, selectBtn, removeBgBtn), this.picker.append(container);
321
+ }
322
+
323
+ // 显示半透明遮罩图片选择器(头部->标题,关闭按钮;搜索筛选->输入框,搜索按钮;图片网格;按钮容器->取消按钮,应用按钮)
324
+ async showImagePickerOverlay() {
325
+ this.picker.style.display = 'none';
326
+
327
+ const overlay = document.createElement('div'), pickerContainer = document.createElement('div'),
328
+ header = document.createElement('div'), title = document.createElement('div'),
329
+ closeBtn = document.createElement('button'), filterSection = document.createElement('div'),
330
+ searchInput = document.createElement('input'), searchBtn = document.createElement('button'),
331
+ imageGrid = document.createElement('div'), actions = document.createElement('div'),
332
+ cancelBtn = document.createElement('button'), applyBtn = document.createElement('button'),
333
+ removeAndShow = () => {
334
+ overlay.remove(), setTimeout(() => this.showPicker({ target: this.longPressedElement }), 50); // 延迟确保DOM更新完成
335
+ };
336
+
337
+ overlay.className = 'image-picker-overlay', pickerContainer.className = 'image-picker-container';
338
+ header.className = 'image-picker-header', title.className = 'image-picker-title', title.textContent = '选择背景图片';
339
+ closeBtn.className = 'close-image-btn', closeBtn.type = 'button'; closeBtn.innerHTML = '&times;';
340
+
341
+ // 搜索和筛选区域
342
+ filterSection.className = 'image-filter-section', searchInput.type = 'text', searchInput.placeholder = '输入图片名称...';
343
+ searchInput.className = 'image-search-input', searchInput.name = "fullname"
344
+ searchBtn.className = 'selectImageBtn image-search-btn', searchBtn.type = 'button', searchBtn.textContent = '搜索';
345
+
346
+ imageGrid.className = 'image-grid-overlay'; // 图片网格容器
347
+ // 底部操作区域
348
+ actions.className = 'image-picker-actions', cancelBtn.className = 'cancel-image-btn', cancelBtn.type = 'button';
349
+ cancelBtn.textContent = '取消', applyBtn.className = 'apply-image-btn', applyBtn.type = 'button';
350
+ applyBtn.textContent = '应用选择', applyBtn.disabled = true;
351
+
352
+ // 事件监听
353
+ closeBtn.onclick = removeAndShow, cancelBtn.onclick = removeAndShow;
354
+
355
+ // 搜索按钮点击事件
356
+ searchBtn.onclick = () => this.filterImages(imageGrid, searchInput.value);
357
+ searchInput.addEventListener('keydown', (e) => {
358
+ if (e.key === 'Enter') this.filterImages(imageGrid, searchInput.value);
359
+ });
360
+
361
+ // 点击遮罩背景关闭
362
+ overlay.addEventListener('click', (e) => {
363
+ if (e.target === overlay) removeAndShow();
364
+ });
365
+
366
+ // ESC键关闭
367
+ document.addEventListener('keydown', (e) => {
368
+ if (e.key === 'Escape') removeAndShow();
369
+ }, { once: true });
370
+
371
+ // 组装容器
372
+ header.append(title, closeBtn), filterSection.append(searchInput, searchBtn), actions.append(cancelBtn, applyBtn);
373
+ pickerContainer.append(header, filterSection, imageGrid, actions);
374
+ overlay.append(pickerContainer), document.body.append(overlay);
375
+
376
+ this.loadImagesToOverlay(imageGrid, applyBtn, removeAndShow); // 加载图片
377
+ }
378
+
379
+ // 加载图片到遮罩层
380
+ async loadImagesToOverlay(container, applyBtn, removeAndShow) {
381
+ container.innerHTML = '<div class="loading-overlay">加载图片中...</div>';
382
+
383
+ try {
384
+ const images = await this.getImageList(), path = '/static/img/';
385
+ if (!images) return container.innerHTML = `<div class="no-images-overlay">暂无图片,请上传图片到 ${path} 目录</div>`;
386
+ container.innerHTML = '';
387
+
388
+ let selectedImage = null;
389
+ // 创建图片项(缩略图->原图;图片名称,选择指示器)
390
+ images.forEach(imgName => {
391
+ const imgItem = document.createElement('div'), thumbnail = document.createElement('div'),
392
+ img = document.createElement('img'), N = document.createElement('div'), checkmark = document.createElement('div');
393
+
394
+ imgItem.className = 'image-item-overlay', imgItem.dataset.image = imgName, imgItem.title = "双击预览原图";
395
+ thumbnail.className = 'image-thumbnail-overlay', img.src = `${path}${imgName}`, img.alt = imgName;
396
+ img.onerror = () => thumbnail.innerHTML = '<span>加载失败</span>', N.className = 'image-name-overlay';
397
+ N.textContent = imgName, checkmark.className = 'image-checkmark', checkmark.innerHTML = '✓';
398
+ thumbnail.append(img), imgItem.append(thumbnail, N, checkmark), container.append(imgItem);
399
+
400
+ // 点击选择图片(移除其他图片的选择状态并添加自己为选中)
401
+ imgItem.addEventListener('click', () => {
402
+ container.querySelectorAll('.image-item-overlay').forEach(item => item.classList.remove('selected'));
403
+ imgItem.classList.add('selected'), selectedImage = imgName, applyBtn.disabled = false;
404
+ });
405
+
406
+ // 双击预览大图
407
+ imgItem.addEventListener('dblclick', () => this.previewImageOverlay(`${path}${imgName}`, imgName, removeAndShow));
408
+ });
409
+
410
+ // 应用按钮事件
411
+ applyBtn.onclick = () => {
412
+ if (selectedImage) {
413
+ const imagePath = `${path}${selectedImage}`;
414
+ this.setStyle('background-image', `url(${imagePath})`), removeAndShow();
415
+ }
416
+ };
417
+ } catch (error) {
418
+ container.innerHTML = '<div class="error-overlay">加载图片失败,请检查网络连接</div>';
419
+ }
420
+ }
421
+
422
+ // 图片搜索过滤
423
+ filterImages(container, searchTerm) {
424
+ const items = container.querySelectorAll('.image-item-overlay'), term = searchTerm.toLowerCase().trim();
425
+
426
+ for (const item of items) {
427
+ const imageName = item.dataset.image.toLowerCase();
428
+ if (imageName.includes(term)) return item.click();
429
+ };
430
+ alert('没有找到匹配图片!');
431
+ }
432
+
433
+ // 原始大图预览功能
434
+ previewImageOverlay(imageUrl, imageName, removeAndShow) {
435
+ // 创建预览层
436
+ const previewOverlay = document.createElement('div'),
437
+ previewContent = `
438
+ <div class="preview-modal">
439
+ <div class="preview-header">
440
+ <h3>${imageName}</h3>
441
+ <button class="close-preview">&times;</button>
442
+ </div>
443
+ <div class="preview-body">
444
+ <img src="${imageUrl}" alt="${imageName}" class="preview-image" />
445
+ </div>
446
+ <div class="preview-footer">
447
+ <button class="use-this-image">使用此图片</button>
448
+ <button class="close-preview-btn">关闭</button>
449
+ </div>
450
+ </div>`, closePreview = () => previewOverlay.remove();
451
+
452
+ previewOverlay.className = 'image-preview-overlay', previewOverlay.innerHTML = previewContent;
453
+ document.body.append(previewOverlay);
454
+
455
+ // 事件处理
456
+ previewOverlay.querySelectorAll('.close-preview, .close-preview-btn').forEach(btn => btn.onclick = closePreview);
457
+ previewOverlay.querySelector('.use-this-image').onclick = () => {
458
+ this.setStyle('background-image', `url(${imageUrl})`), closePreview(), removeAndShow(); // 关闭预览,遮罩层并显示选择器
459
+ };
460
+
461
+ // 点击背景关闭
462
+ previewOverlay.addEventListener('click', e => {
463
+ if (e.target === previewOverlay) closePreview();
464
+ });
465
+
466
+ // ESC键关闭
467
+ document.addEventListener('keydown', e => {
468
+ if (e.key === 'Escape') closePreview();
469
+ }, { once: true });
470
+ }
471
+
472
+ // 创建字体内容
473
+ createFontContent() {
474
+ // 字体颜色选项
475
+ const fontColors = [
476
+ '#000000', '#333333', '#666666', '#999999', '#CCCCCC', '#FFFFFF', '#FF5733', '#33FF57',
477
+ '#3357FF', '#F333FF', '#FF33A1', '#33FFF6', '#FFC733', '#8D33FF', '#33FF8D'
478
+ ],
479
+ // 常用字体列表
480
+ fontFamilies = [
481
+ { name: '默认', value: '' }, { name: 'Arial', value: 'Arial, sans-serif' },
482
+ { name: '微软雅黑', value: 'Microsoft YaHei, sans-serif' }, { name: '宋体', value: 'SimSun, serif' },
483
+ { name: '黑体', value: 'SimHei, sans-serif' }, { name: '楷体', value: 'KaiTi, serif' },
484
+ { name: 'Times New Roman', value: 'Times New Roman, serif' }, { name: 'Verdana', value: 'Verdana, sans-serif' },
485
+ { name: 'Georgia', value: 'Georgia, serif' }, { name: 'Courier New', value: 'Courier New, monospace' },
486
+ { name: 'Comic Sans MS', value: 'Comic Sans MS, cursive' }, { name: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' }
487
+ ],
488
+ // 水平对齐方式列表
489
+ textAlignments = [{ name: '左对齐', value: 'left' }, { name: '居中对齐', value: 'center' },
490
+ { name: '右对齐', value: 'right' }, { name: '两端对齐', value: 'justify' }, { name: '起始对齐', value: 'start' },
491
+ { name: '结束对齐', value: 'end' }, { name: '继承父元素', value: 'inherit' }],
492
+ // 垂直对齐方式列表
493
+ verticalAlignments = [{ name: '基线对齐', value: 'baseline' }, { name: '顶部对齐', value: 'top' },
494
+ { name: '中间对齐', value: 'middle' }, { name: '底部对齐', value: 'bottom' }, { name: '文本顶部', value: 'text-top' },
495
+ { name: '文本底部', value: 'text-bottom' }, { name: '上标', value: 'super' }, { name: '下标', value: 'sub' },
496
+ { name: '继承父元素', value: 'inherit' }, { name: '初始值', value: 'initial' }];
497
+
498
+ this.createColorItem(fontColors, '字体', 'color'); // 添加字体颜色选项
499
+ // 自定义边框颜色输入
500
+ this.createCustomInput({
501
+ labelText: '自定义颜色:', inputType: 'color', inputId: 'fontColorInput', inputValue: '#3498db',
502
+ onApply: color => this.setStyle('color', color)
503
+ });
504
+ // 创建字体大小输入
505
+ this.createCustomInput({
506
+ labelText: '字体大小:', inputType: 'text', inputId: 'fontSizeLabel', placeholder: '例如:16px,1rem', buttonText: '应用',
507
+ onApply: fontSize => this.setStyle('font-size', fontSize)
508
+ });
509
+ // 创建字体选择器
510
+ this.createSelectContainer(fontFamilies, '选择字体', 'font-family', font => this.setStyle('font-family', font));
511
+ // 创建水平对齐方式选择器
512
+ this.createSelectContainer(textAlignments, '水平对齐', 'text-align', align => this.setStyle('text-align', align));
513
+ // 创建垂直对齐方式选择器
514
+ this.createSelectContainer(verticalAlignments, '垂直对齐', 'vertical-align', align => this.setStyle('vertical-align', align));
515
+ // 添加垂直对齐数值输入
516
+ this.createCustomInput({
517
+ labelText: '垂直对齐值:', inputType: 'text', inputId: 'verticalAlignInput', placeholder: '例如:10px, -5px, 50%',
518
+ buttonText: '应用', onApply: value => { this.setStyle('vertical-align', value); }
519
+ });
520
+ }
521
+
522
+ // 创建边框内容
523
+ createBorderContent() {
524
+
525
+ const styleNames = {
526
+ 'solid': '实线', 'dashed': '虚线', 'dotted': '点线', 'double': '双线', 'groove': '凹槽', 'ridge': '凸起',
527
+ 'inset': '内嵌', 'outset': '外凸', 'none': '无'
528
+ }, container = document.createElement('div'),
529
+ borderColors = [
530
+ '#000000', '#333333', '#666666', '#999999', '#CCCCCC', '#FF5733', '#33FF57', '#3357FF', '#F333FF',
531
+ '#FF33A1', '#33FFF6', '#FFC733', '#8D33FF', '#33FF8D', '#FF3333'
532
+ ], title = document.createElement('h3'),
533
+ clearBtn = document.createElement('button'); // 清除按钮
534
+
535
+ // 边框样式选择
536
+ container.className = 'borderCustom';
537
+ for (const style in styleNames) {
538
+ const option = document.createElement('div');
539
+ option.className = 'borderOption', option.textContent = styleNames[style];
540
+ option.style.border = `2px ${style} #333`, option.style.background = '#666666';
541
+ option.setAttribute('role', 'button'), option.setAttribute('tabindex', '0');
542
+ option.setAttribute('aria-label', `选择边框样式 ${styleNames[style]}`);
543
+ addTapSupport(option, () => this.setStyle('borderStyle', style));
544
+ container.append(option);
545
+ }
546
+ this.picker.append(container);
547
+
548
+ // 边框颜色项选择
549
+ title.textContent = '边框颜色', title.style.gridColumn = '1 / -1', title.style.marginTop = '-3px';
550
+ this.picker.append(title);
551
+ this.createColorItem(borderColors, '边框', 'borderColor'); // 添加边框颜色选项
552
+
553
+ // 自定义边框颜色
554
+ this.createCustomInput({
555
+ labelText: '自定义颜色:', inputType: 'color', inputId: 'borderColorInput', inputValue: '#3498db',
556
+ onApply: color => this.setStyle('borderColor', color)
557
+ });
558
+
559
+ // 边框宽度设置
560
+ this.createCustomInput({
561
+ labelText: '边框宽度:', inputType: 'text', inputId: 'borderWidthInput', placeholder: '例如: 1px, 2px, 0.5rem',
562
+ buttonText: '应用', onApply: width => this.setStyle('borderWidth', width)
563
+ });
564
+
565
+ // 边框圆角设置
566
+ this.createCustomInput({
567
+ labelText: '圆角大小:', inputType: 'text', inputId: 'borderRadiusInput', placeholder: '例如: 5px, 10px, 50%',
568
+ buttonText: '应用', onApply: radius => this.setStyle('borderRadius', radius)
569
+ });
570
+
571
+ // 添加清除边框按钮
572
+ clearBtn.className = 'clearBorder', clearBtn.type = 'button', clearBtn.textContent = '清除边框';
573
+ addTapSupport(clearBtn, () => this.clearAllBorders());
574
+
575
+ this.picker.append(clearBtn);
576
+ }
577
+
578
+ // 清除所有边框设置
579
+ clearAllBorders() {
580
+ const borderProperties = ['border', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft', 'borderWidth', 'borderStyle',
581
+ 'borderColor', 'borderRadius'];
582
+
583
+ borderProperties.forEach(prop => this.longPressedElement.style[prop] = '');
584
+ }
585
+
586
+ // 创建下拉选择器
587
+ createSelectContainer(arr, btnText = '', property = '', onApply = null) {
588
+ const container = document.createElement('div'), btn = document.createElement('button'),
589
+ arrow = document.createElement('span'), list = document.createElement('div');
590
+
591
+ container.className = 'custom', btn.type = 'button', btn.className = 'select-btn', btn.textContent = btnText;
592
+ arrow.className = 'dropdown-arrow', arrow.innerHTML = '▼', list.className = 'select-list', list.style.display = 'none';
593
+
594
+ // 创建字体选项
595
+ arr?.forEach(obj => {
596
+ const item = document.createElement('div');
597
+ item.className = 'select-item', item.textContent = obj.name;
598
+ item.setAttribute('select-item-data', obj.value);
599
+
600
+ if (obj.value) item.style[property] = obj.value;
601
+ // 添加点击事件
602
+ addTapSupport(item, e => {
603
+ if (onApply) onApply(obj.value);
604
+ this.closeList(obj.name, btn, list, arrow);
605
+ document.querySelectorAll('.select-item').forEach(item => item.classList.remove('selected'));
606
+ item.classList.add('selected');
607
+ });
608
+
609
+ list.append(item);
610
+ });
611
+
612
+ // 下拉按钮点击事件
613
+ addTapSupport(btn, e => {
614
+ if (this.lastSelect && this.lastSelect.btn !== btn) {
615
+ const { btn: lBtn, list: lList, txt: lTxt, arrow: lArrow } = this.lastSelect;
616
+ this.closeList(lTxt, lBtn, lList, lArrow);
617
+ lList.querySelectorAll('.select-item').forEach(item => item.classList.remove('selected'));
618
+ }
619
+
620
+ if (list.style.display === 'none') {
621
+ this.openList(list, arrow), this.lastSelect = { btn, list, txt: btnText, arrow };
622
+ // 点击picker其他部分时关闭字体列表(执行一次)
623
+ setTimeout(() => {
624
+ this.picker.addEventListener('click', e => {
625
+ if (!container.contains(e.target))
626
+ this.closeList(btnText, btn, list, arrow), this.lastSelect = null;
627
+ }, { once: true });
628
+ }, 0);
629
+ }
630
+ else this.closeList(btnText, btn, list, arrow), this.lastSelect = null;
631
+ });
632
+
633
+ btn.append(arrow), container.append(btn, list); this.picker.append(container);
634
+ }
635
+
636
+ // 创建元素大小内容
637
+ createSizeContent() {
638
+ // 设置宽度输入
639
+ this.createCustomInput({
640
+ labelText: '宽度:', inputType: 'text', inputId: 'widthInput', placeholder: '单位:px, %,em, vw等',
641
+ buttonText: '应用', onApply: width => this.setStyle('width', width)
642
+ });
643
+ // 设置高度输入
644
+ this.createCustomInput({
645
+ labelText: '高度:', inputType: 'text', inputId: 'heightInput', placeholder: '单位:px, %,em, vh等',
646
+ buttonText: '应用', onApply: height => this.setStyle('height', height)
647
+ });
648
+ }
649
+
650
+ // 打开列表
651
+ openList(list, arrow) {
652
+ list.style.display = 'block', arrow.style.transform = 'rotate(180deg)';
653
+ }
654
+
655
+ // 关闭列表
656
+ closeList(btnText, btn, list, arrow) {
657
+ btn.textContent = btnText, btn.append(arrow), list.style.display = 'none', arrow.style.transform = 'rotate(0deg)';
658
+ }
659
+
660
+ // 循环创建颜色数组项
661
+ createColorItem = (arr, labelTxt, property) => {
662
+ arr.forEach(color => {
663
+ const customOption = document.createElement('div');
664
+ customOption.className = 'custom-option', customOption.style.background = color;
665
+ customOption.setAttribute('role', 'button'), customOption.setAttribute('tabindex', '0');
666
+ customOption.setAttribute('aria-label', `选择${labelTxt}颜色 ${color}`);
667
+ if (property === 'background') {
668
+ addTapSupport(customOption, () => {
669
+ this.resetOpacity(), this.setStyle(property, color);
670
+ });
671
+ }
672
+ else addTapSupport(customOption, () => this.setStyle(property, color));
673
+ this.picker.append(customOption);
674
+ });
675
+ };
676
+ // 重置透明度
677
+ resetOpacity() {
678
+ const opacity = this.picker.querySelector('#bgOpacityInput');
679
+ opacity.value = '255';
680
+ }
681
+
682
+ // 设置元素样式
683
+ setStyle = (property, value) => {
684
+ if (this.longPressedElement) this.longPressedElement.style[property] = value;// 如果长按元素存在,设置其属性值;
685
+ };
686
+
687
+ // =================================================================================================================
688
+ // 跳转到编辑器页面
689
+ navigateToEditor() {
690
+ // 获取当前页面的外部样式文件
691
+ const styleFileDir = this.getCurrentStyleFile();
692
+ if (!styleFileDir) return;
693
+
694
+ // 构建跳转URL并以当前页面作为返回地址
695
+ const params = new URLSearchParams({ fileDir: styleFileDir, return: window.location.href });
696
+ window.location.href = `script.html?${params.toString()}`;
697
+ }
698
+
699
+ // 获取当前页面的样式文件
700
+ getCurrentStyleFile(fileDir) {
701
+ // 如果全局函数已定义,则调用它来获取样式文件路径
702
+ if (typeof window.getCurrentStyleFile === 'function') return window.getCurrentStyleFile();
703
+
704
+ // 否则尝试从link标签获取
705
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
706
+ for (let link of links) if (link.href && link.href.includes('/static/')) return link.href;
707
+
708
+ return null;
709
+ }
710
+ // =================================================================================================================
711
+ // 保存样式到文件
712
+ async saveStylesToFile() {
713
+ const lement = this.longPressedElement; // 长按的元素
714
+ if (!lement) return alert('请先长按一个元素以设置样式!');
715
+
716
+ try {
717
+ // 1. 获取当前页面使用的CSS文件
718
+ const styleFileDir = this.getCurrentStyleFile();
719
+ if (!styleFileDir) return alert('无法获取当前页面的CSS文件!');
720
+
721
+ // 2. 生成元素的选择器
722
+ const selector = this.generateElementSelector(lement);
723
+ if (!selector) return alert('无法为元素生成有效的选择器!');
724
+
725
+ // 3. 获取当前元素的内联样式
726
+ const styles = this.getInlineStyles(lement);
727
+ if (Object.keys(styles).length === 0) return alert('当前元素没有通过长按设置的样式!');
728
+
729
+ // 5. 获取现有的CSS文件内容
730
+ const existingContent = await this.fetchCSSFile(styleFileDir);
731
+ if (existingContent === null) return alert('无法读取CSS文件!');
732
+
733
+ // 6. 更新CSS内容
734
+ const updatedContent = this.updateCSSContent(existingContent, selector, styles);
735
+
736
+ // 7. 保存到服务器
737
+ const result = await this.saveCSSFile(styleFileDir, updatedContent);
738
+
739
+ if (result.success) {
740
+ alert(`样式已保存到 ${styleFileDir}\n操作元素: ${selector}`);
741
+ setTimeout(() => this.applyStylesFromCSS(lement, styles), 100);// 移除元素的内联样式,让保存样式生效
742
+ }
743
+ else throw new Error(result.error || '保存失败');
744
+ } catch (error) {
745
+ alert(`保存失败: ${error.message}`);
746
+ }
747
+ }
748
+
749
+ // 生成元素的选择器
750
+ generateElementSelector(element) {
751
+ if (element === document.body) return 'body';
752
+ // CSS选择器生成器
753
+ const getSelector = (el, classLimit = 0) => {
754
+ if (el.id) return `#${CSS.escape(el.id)}`;
755
+
756
+ let selector = el.tagName.toLowerCase();
757
+ if (classLimit > 0 && el.classList?.length > 0) {
758
+ const classes = Array.from(el.classList).slice(0, classLimit).map(cls => `.${CSS.escape(cls)}`).join('');
759
+ selector += classes;
760
+ }
761
+
762
+ return selector;
763
+ },// 1. 优先使用ID
764
+ idSelector = getSelector(element);
765
+ if (idSelector.startsWith('#')) return idSelector;
766
+
767
+ // 2. 尝试使用所有类名的选择器
768
+ const fullClassSelector = getSelector(element, Infinity);
769
+ if (fullClassSelector !== element.tagName.toLowerCase()) {
770
+ const matches = document.querySelectorAll(fullClassSelector);
771
+ if (matches.length === 1) return fullClassSelector;// 类名唯一,直接使用
772
+ }
773
+
774
+ // 3. 生成层级路径(最多8层)
775
+ const path = [];
776
+ let current = element;
777
+ for (let depth = 0; depth < 8 && current && current !== document.body; depth++) {
778
+ let selector = getSelector(current, 2); // 最多两个类
779
+
780
+ // 为当前元素添加兄弟位置
781
+ if (!selector.startsWith('#')) {
782
+ const siblings = Array.from(current.parentElement?.children || []).filter(el => el.tagName === current.tagName);
783
+ if (siblings.length > 1) {
784
+ const index = siblings.indexOf(current) + 1;
785
+ selector += `:nth-of-type(${index})`;
786
+ }
787
+ }
788
+
789
+ path.unshift(selector);
790
+ if (current.id) break; // 如果当前元素有ID,停止向上
791
+ current = current.parentElement;
792
+ }
793
+
794
+ // 优化单元素路径
795
+ const parent = element.parentElement;
796
+ if (path.length === 1 && parent) {
797
+ const parentSelector = getSelector(parent, 1); // 最多一个类
798
+ path.unshift(parentSelector);
799
+ }
800
+
801
+ return path.join(' > ');
802
+ }
803
+
804
+ // 解析样式属性对字符串
805
+ parseStylePair(styleStr, targetObj = {}) {
806
+ // 更安全地解析style属性,处理包含分号的属性值(如Data URL)
807
+ let inQuotes = false, quoteChar = null, currentPair = '';
808
+ const addPairToObj = pairStr => {
809
+ const trimmed = pairStr.trim();
810
+ if (!trimmed) return;
811
+
812
+ const colonIndex = trimmed.indexOf(':');
813
+ if (colonIndex === -1) return;
814
+
815
+ const property = trimmed.slice(0, colonIndex).trim(), value = trimmed.slice(colonIndex + 1).trim();
816
+ if (property && value) targetObj[property] = value;
817
+ };
818
+
819
+ for (let i = 0; i < styleStr.length; i++) {
820
+ const char = styleStr[i];
821
+
822
+ // 处理引号
823
+ if (char === '"' || char === "'") {
824
+ if (!inQuotes) inQuotes = true, quoteChar = char;
825
+ else if (quoteChar === char) inQuotes = false, quoteChar = null;
826
+ }
827
+ // 只有不在引号内且遇到分号时,才分割属性对
828
+ if (char === ';' && !inQuotes) addPairToObj(currentPair), currentPair = '';
829
+ else currentPair += char;
830
+ }
831
+
832
+ addPairToObj(currentPair); // 处理最后一个属性对(没有结尾分号的情况)
833
+ return targetObj;
834
+ }
835
+
836
+ // 获取元素内联样式(长按设置的style属性)
837
+ getInlineStyles(element) {
838
+ const styles = {}, styleAttr = element.getAttribute('style');
839
+ if (!styleAttr) return styles;
840
+ return this.parseStylePair(styleAttr, styles);
841
+ }
842
+
843
+ // 生成CSS规则
844
+ generateCSSRule(selector, styles) {
845
+ const styleLines = Object.entries(styles).map(([property, value]) => ` ${property}: ${value};`).join('\n');
846
+ return `${selector} {\n${styleLines}\n}`;
847
+ }
848
+
849
+ // 获取CSS文件内容
850
+ async fetchCSSFile(fileDir) {
851
+ try {
852
+ const response = await fetch(`/api/css?fileDir=${encodeURIComponent(fileDir)}`);
853
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
854
+ return await response.text();
855
+ } catch (error) {
856
+ console.error('读取CSS文件失败:', fileDir, error.message);
857
+ return null;
858
+ }
859
+ }
860
+
861
+ // 更新CSS内容(添加新规则或更新现有规则)
862
+ updateCSSContent(cssContent, selector, newStyles) {
863
+ // 检查是否已经存在该选择器的规则
864
+ const lines = cssContent.split('\n');
865
+ let inTargetRule = false, targetStart = -1, targetEnd = -1, braceCount = 0;
866
+
867
+ // 找到现有规则的位置
868
+ for (let i = 0; i < lines.length; i++) {
869
+ const line = lines[i];
870
+
871
+ if (line.includes(selector) && line.includes('{')) inTargetRule = true, targetStart = i, braceCount = 1;
872
+ else if (inTargetRule) {
873
+ if (line.includes('{')) braceCount++;
874
+ if (line.includes('}')) {
875
+ braceCount--;
876
+ if (braceCount === 0) {
877
+ targetEnd = i;
878
+ break;
879
+ }
880
+ }
881
+ }
882
+ }
883
+
884
+ // 如果找到了现有规则,只更新特定属性
885
+ if (targetStart !== -1 && targetEnd !== -1) {
886
+ // 提取规则块内的所有行
887
+ const ruleLines = lines.slice(targetStart, targetEnd + 1), ruleText = ruleLines.join('\n'),
888
+ existingDeclarations = this.parseCSSDeclarations(ruleText), // 解析规则块,获取所有现有声明
889
+ mergedDeclarations = { ...existingDeclarations, ...newStyles }, // 合并现有声明和新声明(新声明覆盖旧声明)
890
+ updatedRule = this.generateCSSRule(selector, mergedDeclarations); // 生成更新后的规则
891
+
892
+ lines.splice(targetStart, ruleLines.length, updatedRule); // 替换旧规则
893
+ }
894
+ // 如果没有找到,创建一个新规则
895
+ else {
896
+ const newRule = this.generateCSSRule(selector, newStyles);
897
+ if (cssContent.trim().length > 0 && !cssContent.endsWith('\n')) lines.push(''); // 确保最后一行有换行符
898
+ lines.push(newRule); // 添加到文件末尾
899
+ }
900
+
901
+ return lines.join('\n');
902
+ }
903
+
904
+ // 解析CSS规则中的声明
905
+ parseCSSDeclarations(cssRule) {
906
+ const declarations = {}, ruleMatch = cssRule.match(/\{([^}]+)\}/s); // 提取规则块内容(大括号内的部分)
907
+ if (!ruleMatch) return declarations;
908
+ return this.parseStylePair(ruleMatch[1], declarations);
909
+ }
910
+
911
+ // 保存CSS文件到服务器
912
+ async saveCSSFile(fileDir, content) {
913
+ try {
914
+ const response = await fetch('/api/css', {
915
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileDir, content })
916
+ });
917
+
918
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
919
+ return await response.json();
920
+ } catch (error) {
921
+ console.error('保存CSS文件失败:', error);
922
+ return null;
923
+ }
924
+ }
925
+
926
+ // 从CSS文件中应用样式到元素(移除内联样式)
927
+ applyStylesFromCSS(element, styles) {
928
+ element.removeAttribute('style'), void element.offsetWidth; // 强制浏览器重新计算样式
929
+ }
930
+ }
931
+
932
+ // 监听用户中心准备就绪事件,初始化长按背景功能
933
+ document.addEventListener('userCenterReady', () => new LongPressBackground(), { once: true });