@flun/html-template 4.3.1 → 4.4.0

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.
package/f-CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # 变更日志
2
+ ## [4.4.0] - 2026-05-31 14:25
3
+ ### 优化
4
+ - 修改辅助功能(在线编辑css文件)中预览逻辑:删除单预览按钮,增加上、下、左、右和单页面预览按钮,及相关逻辑的全面优化,让体验更好;
2
5
  ## [4.3.1] - 2026-05-30 11:42
3
6
  ### 优化
4
7
  - https启用成功后,不在打印证书路径,只给出成功提示;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/html-template",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "description": "一个HTML模板工具包,提供开发服务器和模板编译功能,支持自定义标签和快捷输入,变量定义,包含文件引用,帮助开发者模块化处理HTML;",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/static/script.css CHANGED
@@ -1,4 +1,10 @@
1
- /* script.css */
1
+ /* ========== 全局与容器样式 ========== */
2
+ .container {
3
+ max-width: 100%;
4
+ margin: 0px;
5
+ padding: 0px;
6
+ }
7
+
2
8
  .modal {
3
9
  position: fixed;
4
10
  top: 0;
@@ -7,32 +13,106 @@
7
13
  height: 100%;
8
14
  background-color: rgba(0, 0, 0, 0.7);
9
15
  z-index: 1000;
16
+ display: none;
17
+ }
18
+
19
+ /* ========== 布局工具栏 ========== */
20
+ .layout-toolbar {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ gap: 10px;
25
+ padding: 8px 16px;
26
+ background: #2c2e3e;
27
+ border-bottom: 1px solid #44475a;
28
+ border-radius: 8px;
29
+ width: 100%;
30
+ box-sizing: border-box;
31
+ position: relative;
32
+ z-index: 1000;
33
+ flex-wrap: wrap;
34
+ }
35
+
36
+ .layout-btn {
37
+ background: #44475a;
38
+ color: var(--text-color);
39
+ border: none;
40
+ padding: 6px 16px;
41
+ border-radius: 30px;
42
+ font-size: 13px;
43
+ cursor: pointer;
44
+ transition: 0.2s;
45
+ }
46
+
47
+ .layout-btn.active {
48
+ background: #14712a;
49
+ }
50
+
51
+ .layout-btn:hover {
52
+ background: #6272a4;
53
+ transform: translateY(-1px);
54
+ }
55
+
56
+ #cancelPreviewBtn {
57
+ display: none;
58
+ background: #d94bec;
59
+ }
60
+
61
+ #cancelPreviewBtn:hover {
62
+ background-color: #821d90;
63
+ }
64
+
65
+ /* ========== 主工作区 ========== */
66
+ .workspace {
67
+ display: flex;
68
+ flex: 1;
69
+ gap: 5px;
70
+ overflow: hidden;
71
+ background: #282a36;
72
+ transition: all 0.2s ease;
73
+ width: 100%;
74
+ height: calc(100vh - 50px);
75
+ }
76
+
77
+ .workspace > #cssEditor,
78
+ .workspace > #preview {
79
+ flex: 1 1 0;
80
+ min-width: 0;
81
+ min-height: 0;
82
+ }
83
+
84
+ .workspace.single-preview #cssEditor {
85
+ display: none;
86
+ }
87
+
88
+ .workspace.single-preview #preview {
89
+ width: 100%;
90
+ flex: 1;
10
91
  }
11
92
 
93
+ /* ========== 编辑器面板 #cssEditor ========== */
12
94
  #cssEditor {
13
95
  display: flex;
14
96
  flex-direction: column;
15
97
  background: rgb(175, 63, 63);
16
- padding: 20px;
17
- border-radius: 10px;
18
- width: 90%;
19
- max-width: 700px;
20
- height: 800px;
21
- min-width: 300px;
22
- min-height: 200px;
23
- overflow: auto;
24
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
25
- position: fixed;
26
- top: 50%;
27
- left: 50%;
28
- cursor: grab;
98
+ padding: 0;
99
+ border-radius: 0;
100
+ width: 100%;
101
+ height: 100%;
29
102
  box-sizing: border-box;
30
- resize: none;
31
- z-index: 1001;
103
+ overflow: auto;
104
+ position: relative;
105
+ top: auto;
106
+ left: auto;
107
+ transform: none;
108
+ max-width: none;
109
+ box-shadow: none;
110
+ cursor: default;
111
+ z-index: auto;
32
112
  }
33
113
 
34
114
  #cssEditor:active {
35
- cursor: grabbing;
115
+ cursor: default;
36
116
  }
37
117
 
38
118
  #cssEditor .header {
@@ -40,17 +120,18 @@
40
120
  justify-content: space-between;
41
121
  align-items: center;
42
122
  margin-bottom: 10px;
43
- padding-bottom: 10px;
123
+ padding: 15px 20px 10px 20px;
44
124
  border-bottom: 1px solid #eee;
45
- cursor: grab;
125
+ cursor: default;
126
+ background: inherit;
46
127
  }
47
128
 
48
129
  #cssEditor .header:active {
49
- cursor: grabbing;
130
+ cursor: default;
50
131
  }
51
132
 
52
133
  #cssEditor h2 {
53
- margin-bottom: 8px;
134
+ margin: 0;
54
135
  color: #2c3e50;
55
136
  }
56
137
 
@@ -67,30 +148,28 @@
67
148
 
68
149
  .editor-info {
69
150
  background: #069d57;
70
- padding: 10px;
71
- border-radius: 5px;
72
- margin-bottom: 15px;
151
+ padding: 8px 20px;
152
+ margin: 0;
73
153
  font-size: 14px;
74
154
  color: #333aa6;
75
155
  }
76
156
 
77
- /* 隐藏原始 textarea */
78
157
  #cssContent {
79
158
  display: none;
80
159
  }
81
160
 
82
- /* CodeMirror 容器 */
161
+ /* CodeMirror 基础样式 */
83
162
  #cssEditor .CodeMirror {
84
163
  flex: 1;
85
164
  width: 100%;
86
165
  height: auto;
87
166
  min-height: 200px;
88
- border: 1px solid #444;
89
- border-radius: 5px;
167
+ border: none;
168
+ border-radius: 0;
90
169
  font-family: 'Courier New', monospace;
91
170
  font-size: 14px;
92
171
  line-height: 1.6;
93
- margin-bottom: 20px;
172
+ margin-bottom: 0;
94
173
  background: #282a36;
95
174
  }
96
175
 
@@ -103,12 +182,11 @@
103
182
  color: #6272a4;
104
183
  }
105
184
 
106
- /* 注释颜色 */
107
185
  .CodeMirror .cm-comment {
108
186
  color: #3b9e3b !important;
109
187
  }
110
188
 
111
- /* ---------- 颜色色块组件样式 ---------- */
189
+ /* 颜色部件样式 */
112
190
  .cm-color-widget {
113
191
  display: inline-flex;
114
192
  align-items: center;
@@ -139,7 +217,7 @@
139
217
  }
140
218
 
141
219
  .cm-color-text {
142
- color: #f8f8f2;
220
+ color: var(--text-color);
143
221
  background: rgba(0, 0, 0, 0.2);
144
222
  padding: 0 4px;
145
223
  border-radius: 2px;
@@ -150,35 +228,17 @@
150
228
  cursor: text;
151
229
  }
152
230
 
153
- /* -------------------------------------- */
154
-
231
+ /* 按钮区域 */
155
232
  #cssEditor .actions {
156
233
  display: flex;
157
234
  justify-content: flex-end;
158
235
  gap: 10px;
159
- margin-top: 10px;
236
+ padding: 15px 20px;
237
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
160
238
  }
161
239
 
162
- #previewBtn {
163
- background: #3498db;
164
- color: var(--text-color);
165
- padding: 12px 24px;
166
- border: none;
167
- border-radius: 5px;
168
- cursor: pointer;
169
- font-size: 16px;
170
- transition: all 0.3s;
171
- min-width: 120px;
172
- }
173
-
174
- #previewBtn:hover {
175
- background-color: #2980b9;
176
- transform: translateY(-2px);
177
- }
178
-
179
- #cancelPreviewBtn {
180
- display: none;
181
- background: #d94bec;
240
+ #saveBtn {
241
+ background: #0ca55e;
182
242
  color: var(--text-color);
183
243
  padding: 12px 24px;
184
244
  border: none;
@@ -189,23 +249,11 @@
189
249
  min-width: 120px;
190
250
  }
191
251
 
192
- #cancelPreviewBtn:hover {
193
- background-color: #821d90;
252
+ #saveBtn:hover {
253
+ background-color: #17723d;
194
254
  transform: translateY(-2px);
195
255
  }
196
256
 
197
- #saveBtn {
198
- background: #0ca55e;
199
- color: var(--text-color);
200
- padding: 12px 24px;
201
- border: none;
202
- border-radius: 5px;
203
- cursor: pointer;
204
- font-size: 16px;
205
- transition: all 0.3s;
206
- min-width: 120px;
207
- }
208
-
209
257
  #cancelBtn {
210
258
  background: #bdb330;
211
259
  color: var(--text-color);
@@ -218,53 +266,52 @@
218
266
  min-width: 120px;
219
267
  }
220
268
 
221
- #saveBtn:hover {
222
- background-color: #17723d;
223
- transform: translateY(-2px);
224
- }
225
-
226
269
  #cancelBtn:hover {
227
270
  background-color: #566214;
228
271
  transform: translateY(-2px);
229
272
  }
230
273
 
274
+ /* 编辑器附加强制左对齐 */
275
+ #cssEditor .CodeMirror,
276
+ #cssEditor .CodeMirror *,
277
+ .CodeMirror pre,
278
+ .CodeMirror-lines,
279
+ .CodeMirror-line {
280
+ text-align: left !important;
281
+ }
282
+
283
+ /* ========== 预览容器 #preview ========== */
231
284
  #preview {
232
285
  display: none;
233
- position: fixed;
234
- width: 800px;
235
- height: 600px;
236
- top: 40px;
237
- left: 20px;
286
+ flex-direction: column;
238
287
  background: #181f3a;
239
- border-radius: 10px;
240
- box-shadow: 0 5px 25px rgba(0, 0, 0, 0.5);
241
- z-index: 1001;
288
+ border-radius: 0;
242
289
  overflow: hidden;
243
- transition: opacity 0.3s ease;
244
- }
245
-
246
- #preview::before {
247
- content: '';
248
- position: absolute;
249
- top: 0;
250
- left: 0;
251
290
  width: 100%;
252
291
  height: 100%;
253
- z-index: 1002;
254
- background: transparent;
255
- cursor: grab;
292
+ position: relative;
293
+ box-shadow: none;
294
+ z-index: auto;
256
295
  }
257
296
 
258
- #preview:active::before {
259
- cursor: grabbing;
297
+ .preview-header {
298
+ padding: 8px 16px;
299
+ background: #2c2e3e;
300
+ color: #8be9fd;
301
+ font-size: 13px;
302
+ border-bottom: 1px solid #444;
303
+ display: flex;
304
+ justify-content: space-between;
260
305
  }
261
306
 
262
307
  #previewFrame {
263
308
  width: 100%;
264
309
  height: 100%;
265
310
  border: none;
311
+ flex: 1;
266
312
  }
267
313
 
314
+ /* ========== 加载指示器 ========== */
268
315
  #loader {
269
316
  display: none;
270
317
  }
@@ -283,22 +330,8 @@
283
330
  text-align: center;
284
331
  }
285
332
 
286
- @keyframes spin {
287
- 0% {
288
- transform: rotate(0deg);
289
- }
290
-
291
- 100% {
292
- transform: rotate(360deg);
293
- }
294
- }
295
-
333
+ /* ========== 响应式布局 ========== */
296
334
  @media (max-width: 768px) {
297
- #cssEditor {
298
- width: 95%;
299
- height: 100%;
300
- }
301
-
302
335
  #cssEditor .actions {
303
336
  flex-wrap: wrap;
304
337
  }
@@ -307,7 +340,6 @@
307
340
  font-size: 18px;
308
341
  }
309
342
 
310
- #previewBtn,
311
343
  #cancelPreviewBtn,
312
344
  #saveBtn,
313
345
  #cancelBtn {
package/static/script.js CHANGED
@@ -2,20 +2,25 @@
2
2
  function scriptFun() {
3
3
  const modal = document.querySelector('.modal'), cssEditor = document.getElementById('cssEditor'),
4
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 实例,由外部库创建
5
+ cancelBtn = document.getElementById('cancelBtn'), loader = document.getElementById('loader'),
6
+ preview = document.getElementById('preview'), previewFrame = document.getElementById('previewFrame'),
7
+ cancelPreviewBtn = document.getElementById('cancelPreviewBtn'),
8
+ urlParams = new URLSearchParams(window.location.search),
9
+ fileDir = urlParams.get('fileDir'), returnUrl = urlParams.get('return');
10
+
11
+ let cm = window.cssEditor;
12
+ if (!cm) {
13
+ console.error('CodeMirror not ready, retry');
14
+ setTimeout(scriptFun, 100);
15
+ return;
16
+ }
10
17
 
11
18
  let isEditingColor = false, editColorRange = null, isPreviewMode = false, globalColorPicker = null;
12
19
 
13
- // ----- 阻止 CodeMirror 编辑器区域事件冒泡,避免触发父容器拖拽和阻止默认菜单 -----
14
20
  cm.getWrapperElement().addEventListener('mousedown', e => e.stopPropagation());
15
21
  cm.getWrapperElement().addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
16
22
  cm.getWrapperElement().addEventListener('contextmenu', e => e.stopPropagation());
17
23
 
18
- // ---------- 🎨 全局颜色选择器 ----------
19
24
  function initGlobalColorPicker() {
20
25
  if (globalColorPicker) return;
21
26
  globalColorPicker = document.createElement('input');
@@ -32,51 +37,46 @@ function scriptFun() {
32
37
  }
33
38
  initGlobalColorPicker();
34
39
 
35
- // ---------- 颜色关键字映射 ----------
40
+ // 颜色关键字映射(完整)
36
41
  const COLOR_KEYWORDS = {
37
42
  'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF', 'aquamarine': '#7FFFD4', 'azure': '#F0FFFF',
38
43
  'beige': '#F5F5DC', 'bisque': '#FFE4C4', 'black': '#000000', 'blanchedalmond': '#FFEBCD', 'blue': '#0000FF',
39
44
  'blueviolet': '#8A2BE2', 'brown': '#A52A2A', 'burlywood': '#DEB887', 'cadetblue': '#5F9EA0', 'chartreuse': '#7FFF00',
40
45
  'chocolate': '#D2691E', 'coral': '#FF7F50', 'cornflowerblue': '#6495ED', 'cornsilk': '#FFF8DC', 'crimson': '#DC143C',
41
46
  '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',
47
+ 'darkgreen': '#006400', 'darkgrey': '#A9A9A9', 'darkkhaki': '#BDB76B', 'darkmagenta': '#8B008B', 'darkolivegreen': '#556B2F',
48
+ 'darkorange': '#FF8C00', 'darkorchid': '#9932CC', 'darkred': '#8B0000', 'darksalmon': '#E9967A', 'darkseagreen': '#8FBC8F',
45
49
  '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',
50
+ 'darkviolet': '#9400D3', 'deeppink': '#FF1493', 'deepskyblue': '#00BFFF', 'dimgray': '#696969', 'dimgrey': '#696969',
51
+ 'dodgerblue': '#1E90FF', 'firebrick': '#B22222', 'floralwhite': '#FFFAF0', 'forestgreen': '#228B22', 'fuchsia': '#FF00FF',
52
+ 'gainsboro': '#DCDCDC', 'ghostwhite': '#F8F8FF', 'gold': '#FFD700', 'goldenrod': '#DAA520', 'gray': '#808080',
53
+ 'green': '#008000', 'greenyellow': '#ADFF2F', 'grey': '#808080', 'honeydew': '#F0FFF0', 'hotpink': '#FF69B4',
54
+ 'indianred': '#CD5C5C', 'indigo': '#4B0082', 'ivory': '#FFFFF0', 'khaki': '#F0E68C', 'lavender': '#E6E6FA',
55
+ 'lavenderblush': '#FFF0F5', 'lawngreen': '#7CFC00', 'lemonchiffon': '#FFFACD', 'lightblue': '#ADD8E6', 'lightcoral': '#F08080',
56
+ 'lightcyan': '#E0FFFF', 'lightgoldenrodyellow': '#FAFAD2', 'lightgray': '#D3D3D3', 'lightgreen': '#90EE90', 'lightgrey': '#D3D3D3',
57
+ 'lightpink': '#FFB6C1', 'lightsalmon': '#FFA07A', 'lightseagreen': '#20B2AA', 'lightskyblue': '#87CEFA', 'lightslategray': '#778899',
58
+ 'lightslategrey': '#778899', 'lightsteelblue': '#B0C4DE', 'lightyellow': '#FFFFE0', 'lime': '#00FF00', 'limegreen': '#32CD32',
59
+ 'linen': '#FAF0E6', 'magenta': '#FF00FF', 'maroon': '#800000', 'mediumaquamarine': '#66CDAA', 'mediumblue': '#0000CD',
60
+ 'mediumorchid': '#BA55D3', 'mediumpurple': '#9370DB', 'mediumseagreen': '#3CB371', 'mediumslateblue': '#7B68EE',
61
+ 'mediumspringgreen': '#00FA9A', 'mediumturquoise': '#48D1CC', 'mediumvioletred': '#C71585', 'midnightblue': '#191970',
62
+ 'mintcream': '#F5FFFA', 'mistyrose': '#FFE4E1', 'moccasin': '#FFE4B5', 'navajowhite': '#FFDEAD', 'navy': '#000080',
63
+ 'oldlace': '#FDF5E6', 'olive': '#808000', 'olivedrab': '#6B8E23', 'orange': '#FFA500', 'orangered': '#FF4500',
64
+ 'orchid': '#DA70D6', 'palegoldenrod': '#EEE8AA', 'palegreen': '#98FB98', 'paleturquoise': '#AFEEEE', 'palevioletred': '#DB7093',
65
+ 'papayawhip': '#FFEFD5', 'peachpuff': '#FFDAB9', 'peru': '#CD853F', 'pink': '#FFC0CB', 'plum': '#DDA0DD',
66
+ 'powderblue': '#B0E0E6', 'purple': '#800080', 'rebeccapurple': '#663399', 'red': '#FF0000', 'rosybrown': '#BC8F8F',
67
+ 'royalblue': '#4169E1', 'saddlebrown': '#8B4513', 'salmon': '#FA8072', 'sandybrown': '#F4A460', 'seagreen': '#2E8B57',
65
68
  '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*)?\\)',
69
+ 'slategray': '#708090', 'slategrey': '#708090', 'snow': '#FFFAFA', 'springgreen': '#00FF7F', 'steelblue': '#4682B4',
70
+ 'tan': '#D2B48C', 'teal': '#008080', 'thistle': '#D8BFD8', 'tomato': '#FF6347', 'turquoise': '#40E0D0',
71
+ 'violet': '#EE82EE', 'wheat': '#F5DEB3', 'white': '#FFFFFF', 'whitesmoke': '#F5F5F5', 'yellow': '#FFFF00',
72
+ 'yellowgreen': '#9ACD32', 'transparent': 'transparent'
73
+ },
74
+ KEYWORDS = Object.keys(COLOR_KEYWORDS).join('|'), HEX = '#(?:[0-9a-fA-F]{3,4}){1,2}\\b',
75
+ RGB = 'rgba?\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
75
76
  HSL = 'hsla?\\(\\s*\\d+\\s*,\\s*\\d+%\\s*,\\s*\\d+%\\s*(?:,\\s*[\\d.]+\\s*)?\\)',
76
77
  COLOR_REGEX = new RegExp(`${HEX}|${RGB}|${HSL}|\\b(${KEYWORDS})\\b`, 'gi'),
77
78
  RGB_EXTRACT = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i;
78
79
 
79
- // ---------- 颜色工具函数 ----------
80
80
  function rgbToHex(r, g, b) {
81
81
  return '#' + [r, g, b].map(x => {
82
82
  const h = parseInt(x).toString(16);
@@ -87,40 +87,40 @@ function scriptFun() {
87
87
  function extractColorAndAlpha(c) {
88
88
  const l = c.toLowerCase(), kw = COLOR_KEYWORDS[l];
89
89
  let r = 0, g = 0, b = 0, a = 1, t = 'keyword';
90
-
91
- // 1. 颜色关键字(含 transparent)
92
90
  if (kw) {
93
91
  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('#')) {
92
+ const hex = kw.slice(1);
93
+ [r, g, b] = hex.match(/.{2}/g).map(v => parseInt(v, 16));
94
+ a = 1;
95
+ } else if (c.startsWith('#')) {
99
96
  t = 'hex';
100
97
  let h = c.slice(1).toLowerCase();
101
98
  if (h.length === 3 || h.length === 4) h = h.split('').map(x => x + x).join('');
102
99
  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')) {
100
+ if (hexMatch) {
101
+ [r, g, b] = hexMatch.slice(1, 4).map(v => parseInt(v, 16));
102
+ a = hexMatch[4] ? parseInt(hexMatch[4], 16) / 255 : 1;
103
+ }
104
+ } else if (c.startsWith('rgb')) {
108
105
  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')) {
106
+ if (m) {
107
+ t = 'rgb';
108
+ [r, g, b] = [m[1], m[2], m[3]].map(Number);
109
+ a = m[4] ? parseFloat(m[4]) : 1;
110
+ }
111
+ } else if (c.startsWith('hsl')) {
113
112
  t = 'hsl';
114
113
  const d = document.createElement('div');
115
114
  d.style.color = c, document.body.append(d);
116
- const computed = window.getComputedStyle(d).color; // "rgb(r, g, b)" 或 "rgba(r, g, b, a)"
115
+ const computed = window.getComputedStyle(d).color;
117
116
  d.remove();
118
-
119
117
  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; // 解析失败时的默认值
118
+ if (m) {
119
+ [r, g, b] = [m[1], m[2], m[3]].map(Number);
120
+ a = m[4] ? parseFloat(m[4]) : 1;
121
+ }
122
+ else r = 128; g = 128; b = 128; a = 1;
122
123
  }
123
-
124
124
  return { r, g, b, a, t, o: c };
125
125
  }
126
126
 
@@ -128,134 +128,162 @@ function scriptFun() {
128
128
  const o = extractColorAndAlpha(c);
129
129
  if (o.tr || c.toLowerCase() === 'transparent') return 'transparent';
130
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;
131
+ if (c.startsWith('#')) return c;
132
+ if (c.startsWith('rgb')) return `rgb(${o.r},${o.g},${o.b})`;
133
+ return c;
134
134
  }
135
135
 
136
- // ---------- 创建颜色部件 ----------
137
136
  function createColorWidget(ct, fr, to) {
138
137
  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)';
138
+ w.className = 'cm-color-widget';
139
+ w.style.display = 'inline-flex';
140
+ w.style.alignItems = 'center';
141
+ w.style.margin = '0 2px';
142
+ w.style.padding = '2px 4px';
143
+ w.style.borderRadius = '3px';
144
+ w.style.backgroundColor = 'rgba(0,0,0,0.1)';
141
145
 
142
146
  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
+ s.className = 'cm-color-swatch';
148
+ s.style.backgroundColor = bg;
149
+ s.title = '点击修改颜色';
150
+ s.style.display = 'inline-block';
151
+ s.style.width = '16px';
152
+ s.style.height = '16px';
153
+ s.style.borderRadius = '3px';
154
+ s.style.marginRight = '6px';
155
+ s.style.border = bg === 'transparent' ? '1px dashed #999' : '1px solid rgba(255,255,255,1)';
156
+ s.style.cursor = 'pointer';
157
+ s.style.flexShrink = '0';
158
+
159
+ t.className = 'cm-color-text';
160
+ t.textContent = ct;
161
+ t.style.color = '#f8f8f2';
162
+ t.style.fontSize = '13px';
163
+ t.style.userSelect = 'text';
164
+ t.style.cursor = 'text';
165
+ t.style.marginRight = '8px';
166
+ w.append(s, t);
147
167
 
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
168
  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
- });
169
+ as.type = 'range';
170
+ as.min = 0;
171
+ as.max = 100;
172
+ as.value = Math.round(o.a * 100);
173
+ as.style.width = '120px';
174
+ as.style.marginRight = '8px';
175
+ as.title = '调整透明度 (0-100%)';
176
+ as.className = 'cm-alpha-slider';
177
+ av.className = 'cm-alpha-value';
178
+ av.textContent = `${Math.round(o.a * 100)}%`;
179
+ av.style.fontSize = '12px';
180
+ av.style.color = '#ccc';
181
+ av.style.minWidth = '30px';
182
+ av.style.textAlign = 'center';
183
+
184
+ const originalColor = ct, { a: originalAlpha } = extractColorAndAlpha(originalColor);
185
+ // 根据原始颜色和新透明度生成颜色字符串(仅供预览和生成新颜色)
186
+ function makeColorFromAlpha(alphaPercent) {
187
+ const newAlpha = alphaPercent / 100, lc = originalColor.toLowerCase();
188
+ if (lc === 'transparent') return `rgba(0,0,0,${newAlpha})`;
189
+ if (COLOR_KEYWORDS[lc]) {
190
+ const hex = COLOR_KEYWORDS[lc], r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16),
191
+ b = parseInt(hex.slice(5, 7), 16);
192
+ return `rgba(${r},${g},${b},${newAlpha})`;
193
+ }
194
+ if (originalColor.includes('rgba') || originalColor.includes('hsla')) {
195
+ if (originalColor.includes('rgba')) return originalColor.replace(RGB_EXTRACT, `rgba($1,$2,$3,${newAlpha})`);
196
+ else return originalColor.replace(/hsla?\((\d+,\s*\d+%,\s*\d+%)(?:,\s*[\d.]+)?\)/i, `hsla($1,${newAlpha})`);
197
+ }
198
+ if (originalColor.startsWith('rgb(')) return originalColor.replace('rgb(', 'rgba(').replace(')', `,${newAlpha})`);
199
+ if (originalColor.startsWith('hsl(')) return originalColor.replace('hsl(', 'hsla(').replace(')', `,${newAlpha})`);
200
+ if (originalColor.startsWith('#')) {
201
+ let hex = originalColor.slice(1);
202
+ let r, g, b;
203
+ if (hex.length === 3)
204
+ r = parseInt(hex[0] + hex[0], 16), g = parseInt(hex[1] + hex[1], 16), b = parseInt(hex[2] + hex[2], 16);
205
+ else if (hex.length === 6)
206
+ r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16);
207
+ else r = g = b = 128;
208
+
209
+ return `rgba(${r},${g},${b},${newAlpha})`;
210
+ }
211
+ return originalColor;
212
+ }
199
213
 
200
- // 阻止滑块上的鼠标/触摸事件冒泡,使滑块可拖动
201
- as.addEventListener('mousedown', e => e.stopPropagation());
202
- as.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
214
+ as.addEventListener('input', function () {
215
+ av.textContent = `${this.value}%`;
216
+ const previewColor = makeColorFromAlpha(this.value);
217
+ s.style.backgroundColor = getColorForSwatch(previewColor);
218
+ });
203
219
 
204
- w.append(as, av);
205
- }
220
+ as.addEventListener('change', function () {
221
+ const mark = w._colorMark;
222
+ if (!mark) return;
223
+ const pos = mark.find();
224
+ if (!pos) return;
225
+ const currentPercent = Number(this.value);
226
+ let newColor;
227
+ if (Math.abs(currentPercent - originalAlpha * 100) < 0.1) newColor = originalColor;
228
+ else newColor = makeColorFromAlpha(currentPercent);
229
+
230
+ cm.replaceRange(newColor, pos.from, pos.to);
231
+ setTimeout(() => updateColorWidgets(cm), 10);
232
+ });
233
+ as.addEventListener('mousedown', e => e.stopPropagation());
234
+ as.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });
235
+ w.append(as, av);
206
236
 
207
- // 色块点击事件
208
237
  s.addEventListener('click', e => {
209
238
  e.stopPropagation();
210
239
  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`;
240
+ globalColorPicker.style.left = `${r.left}px`;
241
+ globalColorPicker.style.top = `${r.bottom + 5}px`;
212
242
  globalColorPicker.value = ch;
213
-
214
243
  if (globalColorPicker._ch) globalColorPicker.removeEventListener('change', globalColorPicker._ch);
215
- const chf = function () {
244
+ const chf = () => {
216
245
  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);
246
+ hg = parseInt(nh.slice(3, 5), 16), hb = parseInt(nh.slice(5, 7), 16), nhu = nh.toUpperCase(),
247
+ kw = Object.keys(COLOR_KEYWORDS).find(k => COLOR_KEYWORDS[k].toUpperCase() === nhu);
248
+
218
249
  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
250
  if (kw && na >= 1) nc = kw;
221
251
  cm.replaceRange(nc, fr, to), updateColorWidgets(cm), t.textContent = nc;
222
252
  s.style.backgroundColor = getColorForSwatch(nc);
253
+
223
254
  if (av) av.textContent = `${Math.round(na * 100)}%`;
224
255
  to = { line: to.line, ch: fr.ch + nc.length };
225
- this.removeEventListener('change', chf), globalColorPicker._ch = null;
256
+ this.removeEventListener('change', chf);
257
+ globalColorPicker._ch = null;
226
258
  };
227
- globalColorPicker._ch = chf, globalColorPicker.addEventListener('change', chf), globalColorPicker.getBoundingClientRect();
259
+ globalColorPicker._ch = chf;
260
+ globalColorPicker.addEventListener('change', chf);
228
261
  if (typeof globalColorPicker.showPicker === 'function') globalColorPicker.showPicker();
229
262
  else globalColorPicker.click();
230
263
  });
231
-
232
264
  return w;
233
265
  }
234
266
 
235
- // ---------- 扫描编辑器添加部件 ----------
236
267
  function updateColorWidgets(cm) {
237
- isEditingColor = false, editColorRange = null; // 退出编辑模式
238
-
268
+ isEditingColor = false, editColorRange = null;
239
269
  cm.getAllMarks().forEach(m => { if (m.isColorWidget) m.clear(); });
240
270
  const d = cm.getDoc(), lc = d.lineCount();
241
271
  for (let i = 0; i < lc; i++) {
242
- const l = d.getLine(i); let m;
272
+ const l = d.getLine(i);
273
+ let m;
243
274
  COLOR_REGEX.lastIndex = 0;
244
275
  while ((m = COLOR_REGEX.exec(l)) !== null) {
245
276
  const s = m.index, e = s + m[0].length, fr = { line: i, ch: s }, to = { line: i, ch: e };
246
277
  if (d.findMarksAt(fr).some(x => x.replacedWith)) continue;
247
-
248
278
  const w = createColorWidget(m[0], fr, to),
249
279
  mark = cm.markText(fr, to, {
250
280
  replacedWith: w, inclusiveLeft: false, inclusiveRight: false, clearOnEnter: true
251
281
  });
252
282
  mark.isColorWidget = true, w._colorMark = mark;
253
-
254
- // 为颜色值文本添加双击事件
255
283
  const textSpan = w.querySelector('.cm-color-text');
256
284
  if (textSpan) {
257
285
  textSpan.title = '双击编辑颜色值';
258
- textSpan.addEventListener('dblclick', (e) => {
286
+ textSpan.addEventListener('dblclick', e => {
259
287
  e.stopPropagation();
260
288
  const widget = e.target.closest('.cm-color-widget');
261
289
  if (!widget) return;
@@ -263,7 +291,6 @@ function scriptFun() {
263
291
  if (mark) {
264
292
  const pos = mark.find();
265
293
  if (pos) {
266
- // 记录当前正在编辑的颜色值范围
267
294
  isEditingColor = true;
268
295
  editColorRange = {
269
296
  from: { line: pos.from.line, ch: pos.from.ch },
@@ -279,92 +306,128 @@ function scriptFun() {
279
306
  }
280
307
 
281
308
  updateColorWidgets(cm);
282
-
283
- // 编辑期间不自动重建部件,但允许手动触发重建
284
309
  cm.on('change', () => {
285
310
  if (isEditingColor) return;
286
311
  setTimeout(() => updateColorWidgets(cm), 10);
287
312
  });
288
-
289
- // 监听光标活动,检测是否离开正在编辑的颜色值区域
290
313
  cm.on('cursorActivity', () => {
291
314
  if (isEditingColor && editColorRange) {
292
315
  const cursor = cm.getCursor(), range = editColorRange,
293
- // 检查光标是否离开了正在编辑的颜色值范围
294
316
  isOutsideRange = cursor.line < range.from.line || cursor.line > range.to.line ||
295
317
  (cursor.line === range.from.line && cursor.ch < range.from.ch) ||
296
318
  (cursor.line === range.to.line && cursor.ch > range.to.ch);
297
-
298
319
  if (isOutsideRange) isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
299
320
  }
300
321
  });
301
322
 
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
- }
323
+ // ================= 预览控制逻辑(使用静态取消预览按钮) =================
324
+ let styleUpdateHandler = null; // 编辑器 change 监听函数
310
325
 
326
+ // 更新预览样式
311
327
  function updatePreviewStyles() {
312
328
  if (!isPreviewMode || !previewFrame.contentWindow?.document) return;
313
329
  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); }
330
+ const css = cm.getValue(), doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
331
+ let styleEl = doc.getElementById('dynamic-css');
332
+ if (!styleEl) styleEl = doc.createElement('style'), styleEl.id = 'dynamic-css', doc.head.appendChild(styleEl);
333
+
334
+ styleEl.textContent = css;
335
+ } catch (e) {
336
+ console.log('预览更新失败', e);
337
+ }
319
338
  }
320
339
 
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);
340
+ // 开启预览模式
341
+ function startPreview() {
342
+ if (isPreviewMode) return;
343
+ isPreviewMode = true, preview.style.display = 'flex';
344
+
345
+ if (cancelPreviewBtn) cancelPreviewBtn.style.display = 'flex';
346
+ if (previewFrame.src !== returnUrl && returnUrl && returnUrl !== 'about:blank') previewFrame.src = returnUrl;
347
+
348
+ // 等待 iframe 加载完成后注入样式
349
+ const applyStyles = () => {
350
+ if (previewFrame.contentDocument) updatePreviewStyles();
351
+ else setTimeout(applyStyles, 50);
352
+ };
353
+ if (previewFrame.contentDocument && previewFrame.contentDocument.readyState === 'complete') updatePreviewStyles();
354
+ else {
355
+ previewFrame.addEventListener('load', function onLoad() {
356
+ previewFrame.removeEventListener('load', onLoad), updatePreviewStyles();
357
+ });
358
+ applyStyles();
359
+ }
360
+ if (styleUpdateHandler) cm.off('change', styleUpdateHandler);
361
+ styleUpdateHandler = () => { if (isPreviewMode) updatePreviewStyles(); };
362
+ cm.on('change', styleUpdateHandler);
324
363
  }
325
364
 
326
- addTapSupport(previewBtn, startPreview), addTapSupport(cancelPreviewBtn, cancelPreview);
365
+ // 关闭预览模式
366
+ function stopPreview() {
367
+ if (!isPreviewMode) return;
368
+ isPreviewMode = false, preview.style.display = 'none';
327
369
 
328
- // 获取储存样式并应用
329
- getStyle(cssEditor, api), getStyle(preview, pApi);
330
- modal.append(cssEditor, preview), loadCssContent(fileDir);
370
+ if (cancelPreviewBtn) cancelPreviewBtn.style.display = 'none';
371
+ if (styleUpdateHandler) cm.off('change', styleUpdateHandler), styleUpdateHandler = null;
331
372
 
332
- mouseOrTouch(cssEditor, () => { cssEditor.style.zIndex = '1002', preview.style.zIndex = '1001'; }, api);
333
- mouseOrTouch(preview, () => { preview.style.zIndex = '1002', cssEditor.style.zIndex = '1001'; }, pApi);
373
+ // 重置布局高亮和布局样式
374
+ const workspace = document.getElementById('workspace'),
375
+ layoutBtns = document.querySelectorAll('.layout-btn:not(#cancelPreviewBtn)');
334
376
 
335
- addTapSupport(saveBtn, saveCSS), addTapSupport(cancelBtn, cancelEdit), addTapSupport(closeBtn, cancelEdit);
377
+ layoutBtns.forEach(btn => btn.classList.remove('active'));
378
+ if (workspace) workspace.classList.remove('single-preview'), workspace.style.flexDirection = '';
379
+ }
336
380
 
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
- }
381
+ // 切换布局(不改变预览状态)
382
+ function setLayout(layout) {
383
+ const workspace = document.getElementById('workspace');
384
+ if (!workspace) return;
385
+ workspace.classList.remove('single-preview');
386
+ workspace.style.flexDirection = '';
387
+ switch (layout) {
388
+ case 'left': workspace.style.flexDirection = 'row-reverse'; break;
389
+ case 'right': workspace.style.flexDirection = 'row'; break;
390
+ case 'top': workspace.style.flexDirection = 'column-reverse'; break;
391
+ case 'bottom': workspace.style.flexDirection = 'column'; break;
392
+ case 'single': workspace.classList.add('single-preview'); break;
393
+ default: break;
349
394
  }
350
- if (e.ctrlKey && e.key === 's') e.preventDefault(), saveCSS();
351
- if (e.ctrlKey && e.key === 'p') e.preventDefault(), startPreview();
395
+ if (isPreviewMode && previewFrame.contentWindow) updatePreviewStyles();
396
+ }
397
+
398
+ // 为所有布局按钮绑定事件
399
+ const layoutBtns = document.querySelectorAll('.layout-btn:not(#cancelPreviewBtn)');
400
+ layoutBtns.forEach(btn => {
401
+ btn.addEventListener('click', () => {
402
+ const layout = btn.getAttribute('data-layout');
403
+ if (!layout) return;
404
+ layoutBtns.forEach(b => b.classList.remove('active'));
405
+ btn.classList.add('active');
406
+ if (!isPreviewMode) startPreview();
407
+ setLayout(layout);
408
+ });
352
409
  });
410
+ if (cancelPreviewBtn) cancelPreviewBtn.addEventListener('click', () => stopPreview());
353
411
 
354
- // ---------- 加载CSS内容 ----------
412
+ // ================= 样式保存/加载/颜色等功能 =================
413
+ function showLoader() { if (loader) loader.style.display = 'block'; }
414
+ function hideLoader() { if (loader) loader.style.display = 'none'; }
415
+ function updateEditorTitle(fd) {
416
+ const te = document.querySelector('#cssEditor h2');
417
+ if (te) te.textContent = `编辑文件: ${fd}`;
418
+ }
355
419
  async function loadCssContent(fd) {
356
420
  showLoader();
357
421
  try {
358
422
  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}`;
423
+ if (r.ok) {
424
+ const ct = await r.text();
425
+ cm.setValue(ct), updateEditorTitle(fd);
426
+ } else throw new Error(`无法加载CSS文件: ${fd}`);
427
+ } catch (err) {
428
+ alert(`加载CSS失败: ${err.message}`);
429
+ updateEditorTitle(fd);
430
+ } finally { hideLoader() }
368
431
  }
369
432
 
370
433
  async function saveCSS() {
@@ -376,17 +439,52 @@ function scriptFun() {
376
439
  body: JSON.stringify({ fileDir, content: cm.getValue() })
377
440
  });
378
441
  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(); }
442
+ else {
443
+ const et = await r.text();
444
+ throw new Error(et || '保存失败');
445
+ }
446
+ } catch (err) {
447
+ alert(`保存CSS失败: ${err.message}`);
448
+ } finally { hideLoader() }
382
449
  }
383
450
 
384
- function cancelEdit() { if (confirm('确定要取消编辑吗')) window.location.href = returnUrl; }
451
+ function cancelEdit() {
452
+ if (confirm('确定要取消编辑吗')) window.location.href = returnUrl;
453
+ }
385
454
 
386
- function showLoader() { if (loader) loader.style.display = 'block'; }
387
- function hideLoader() { if (loader) loader.style.display = 'none'; }
455
+ addTapSupport(saveBtn, saveCSS);
456
+ addTapSupport(cancelBtn, cancelEdit);
457
+ addTapSupport(closeBtn, cancelEdit);
458
+
459
+ // 键盘快捷键
460
+ document.addEventListener('keydown', e => {
461
+ if (e.key === 'Escape') {
462
+ if (isEditingColor) {
463
+ e.preventDefault(), isEditingColor = false, editColorRange = null, updateColorWidgets(cm);
464
+ return;
465
+ }
466
+ isPreviewMode ? stopPreview() : cancelEdit();
467
+ }
468
+ if (e.ctrlKey && e.key === 's') { e.preventDefault(); saveCSS(); }
469
+ if (e.ctrlKey && e.key === 'p') {
470
+ e.preventDefault();
471
+ if (!isPreviewMode) {
472
+ startPreview(), setLayout('right');
473
+ layoutBtns.forEach(btn => {
474
+ if (btn.getAttribute('data-layout') === 'right') btn.classList.add('active');
475
+ else btn.classList.remove('active');
476
+ });
477
+ }
478
+ }
479
+ });
480
+
481
+ if (fileDir) loadCssContent(fileDir);
482
+ else console.warn('未提供 fileDir 参数');
388
483
  }
389
484
 
390
- // 页面加载启动
391
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scriptFun);
392
- else scriptFun();
485
+ function waitForEditor(callback) {
486
+ if (window.cssEditor) callback();
487
+ else setTimeout(() => waitForEditor(callback), 50);
488
+ }
489
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => waitForEditor(scriptFun));
490
+ else waitForEditor(scriptFun);
@@ -13,10 +13,22 @@
13
13
  [~style]
14
14
 
15
15
  [!my] [~my]
16
- [!header][~header]
16
+ [!header]
17
+ <!-- 布局工具栏 -->
18
+ <div class="layout-toolbar">
19
+ <button data-layout="left" class="layout-btn" type="button">◀ 左边预览</button>
20
+ <button data-layout="right" class="layout-btn" type="button">右边预览 ▶</button>
21
+ <button data-layout="top" class="layout-btn" type="button">▲ 上边预览</button>
22
+ <button data-layout="bottom" class="layout-btn" type="button">下边预览 ▼</button>
23
+ <button data-layout="single" class="layout-btn" type="button">📺 单页预览</button>
24
+ <button id="cancelPreviewBtn" class="layout-btn" type="button">✖ 取消预览</button>
25
+ </div>
26
+ [~header]
27
+
17
28
  [!content]
18
- <!-- CSS编辑器模态框 <label for="code">代码编辑区:</label> -->
19
- <div class="modal">
29
+ <!-- 主工作区 -->
30
+ <div id="workspace" class="workspace">
31
+ <!-- 编辑器面板 -->
20
32
  <div id="cssEditor">
21
33
  <div class="header">
22
34
  <h2>编辑CSS样式</h2>
@@ -27,17 +39,20 @@
27
39
  </h3>
28
40
  <textarea id="cssContent" placeholder="CSS内容将在这里显示..." spellcheck="false"></textarea>
29
41
  <div class="actions">
30
- <button id="previewBtn" type="button">预览</button>
31
- <button id="cancelPreviewBtn" type="button">取消预览</button>
42
+ <!-- 只保留保存和取消按钮,预览按钮已移除 -->
32
43
  <button id="saveBtn" type="button">保存并应用</button>
33
44
  <button id="cancelBtn" type="button">取消</button>
34
45
  </div>
35
46
  </div>
36
- </div>
37
47
 
38
- <!-- 预览容器 -->
39
- <div id="preview" class="preview-container">
40
- <iframe id="previewFrame" title="预览页面"></iframe>
48
+ <!-- 预览容器 -->
49
+ <div id="preview" class="preview-container">
50
+ <div class="preview-header">
51
+ <span>预览页面</span>
52
+ <span id="previewUrlHint"></span>
53
+ </div>
54
+ <iframe id="previewFrame" title="预览页面"></iframe>
55
+ </div>
41
56
  </div>
42
57
 
43
58
  <!-- 加载指示器 -->
@@ -62,7 +77,7 @@
62
77
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/fold/brace-fold.min.js"></script>
63
78
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/fold/comment-fold.min.js"></script>
64
79
 
65
- <!-- 语法检查依赖:CodeMirror lint 核心 + CSSTree -->
80
+ <!-- 语法检查依赖 -->
66
81
  <script src="/static/utils/css-lint.js"></script>
67
82
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/lint/lint.min.js"></script>
68
83
  <script src="https://cdn.jsdelivr.net/npm/css-tree@3.1.0/dist/csstree.min.js"></script>
@@ -77,9 +92,8 @@
77
92
  <script src="/static/utils/match-highlighter.js"></script>
78
93
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/addon/selection/active-line.min.js"></script>
79
94
 
80
- <!-- 库插件样式自定义 -->
81
95
  <style>
82
- /* 增大折叠按钮 */
96
+ /* 折叠按钮增大 */
83
97
  #cssEditor .CodeMirror .CodeMirror-foldgutter-open,
84
98
  #cssEditor .CodeMirror .CodeMirror-foldgutter-folded,
85
99
  #cssEditor .CodeMirror .CodeMirror-foldgutter-open::after,
@@ -87,17 +101,16 @@
87
101
  font-size: 25px;
88
102
  }
89
103
 
90
- /* 增加折叠 gutter 的宽度,避免按钮显示不全 */
91
104
  #cssEditor .CodeMirror .CodeMirror-foldgutter {
92
105
  width: 26px;
93
106
  }
94
107
 
95
- /* 强制 tooltip 显示的样式 */
108
+ /* ========== CodeMirror 扩展样式(提示、校验等) ========== */
96
109
  .CodeMirror-lint-tooltip {
97
110
  z-index: 10000;
98
111
  background-color: #ffd;
99
112
  border: 1px solid #000;
100
- color: #000;
113
+ color: var(--text-color);
101
114
  font-family: monospace;
102
115
  font-size: 12px;
103
116
  padding: 6px 10px;
@@ -105,10 +118,8 @@
105
118
  word-wrap: break-word;
106
119
  white-space: pre-wrap;
107
120
  line-height: 1.5;
108
- transition: none;
109
121
  }
110
122
 
111
- /* 自动补全列表样式 */
112
123
  .CodeMirror-hints {
113
124
  background-color: #282a36;
114
125
  border: 1px solid #6272a4;
@@ -122,7 +133,7 @@
122
133
  }
123
134
 
124
135
  .CodeMirror-hint {
125
- color: #f8f8f2;
136
+ color: var(--text-color);
126
137
  padding: 4px 8px;
127
138
  cursor: pointer;
128
139
  white-space: pre;
@@ -139,26 +150,26 @@
139
150
  color: #f8f8f2;
140
151
  }
141
152
 
142
- /* 匹配括号高亮 */
143
153
  .CodeMirror-matchingbracket {
144
154
  background-color: rgba(124, 136, 126, 1) !important;
145
155
  font-weight: bold;
146
156
  text-decoration: none !important;
147
157
  }
148
158
 
149
- /* 自定义高亮 */
150
159
  .custom-highlight {
151
160
  background-color: rgba(255, 255, 0, 0.3) !important;
152
161
  }
162
+
163
+ .CodeMirror-linenumber {
164
+ text-align: right !important;
165
+ }
153
166
  </style>
154
167
 
155
168
  <script>
156
- // 等待 DOM 加载完成后初始化编辑器
157
169
  (function () {
158
170
  function initEditor() {
159
171
  const textarea = document.getElementById('cssContent');
160
172
  if (!textarea) return;
161
-
162
173
  window.cssEditor = CodeMirror.fromTextArea(textarea, {
163
174
  lineNumbers: true,
164
175
  mode: 'css',
@@ -169,41 +180,27 @@
169
180
  matchBrackets: true,
170
181
  foldGutter: true,
171
182
  gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
172
- lint: {
173
- getAnnotations: window.customCssLint,
174
- async: true
175
- },
183
+ lint: { getAnnotations: window.customCssLint, async: true },
176
184
  styleActiveLine: true,
177
- hintOptions: {
178
- completeSingle: false, // 即使只有一个匹配也显示列表
179
- closeOnUnfocus: false
180
- }
185
+ hintOptions: { completeSingle: false, closeOnUnfocus: false }
181
186
  });
182
-
183
187
  window.cssEditor.setSize('100%', '100%');
184
-
185
- // ----- 输入时自动弹出补全 -----
186
188
  window.cssEditor.on("inputRead", (cm, change) => {
187
- // 如果输入内容包含冒号,立即触发补全(优先响应)
188
189
  if (change.text.join('').includes(':')) {
189
190
  clearTimeout(cm.autocompletionTimeout);
190
191
  cm.autocompletionTimeout = setTimeout(() => cm.showHint({ completeSingle: false }), 0);
191
192
  return;
192
193
  }
193
- // 其他输入(字母、数字、删除等)延迟触发
194
194
  clearTimeout(cm.autocompletionTimeout);
195
195
  cm.autocompletionTimeout = setTimeout(() => cm.showHint({ completeSingle: false }), 150);
196
196
  });
197
-
198
- attachCustomHighlight(window.cssEditor); // 高亮匹配选中词(自定义)
197
+ attachCustomHighlight(window.cssEditor);
199
198
  setTimeout(() => window.cssEditor.performLint(), 100);
200
199
  }
201
-
202
200
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initEditor);
203
201
  else initEditor();
204
202
  })();
205
203
  </script>
206
204
 
207
- <!-- 本地文件 -->
208
205
  <script src="/static/script.js"></script>
209
206
  [~script]