ernoban commited on
Commit
523b298
·
verified ·
1 Parent(s): d2cc609

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1383 -19
index.html CHANGED
@@ -1,19 +1,1383 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OpenWebUI 模型头像URL修正工具 | 精致美化版</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ /* 基础变量(统一风格) */
11
+ :root {
12
+ --primary-color: #4A6CF7;
13
+ --primary-hover: #3a5af5;
14
+ --secondary-color: #6c757d;
15
+ --success-color: #10b981;
16
+ --warning-color: #f59e0b;
17
+ --error-color: #ef4444;
18
+ --background-color: #f8fafc;
19
+ --card-bg: #ffffff;
20
+ --text-color: #1e293b;
21
+ --light-text: #64748b;
22
+ --border-color: #e2e8f0;
23
+ --gap: 20px;
24
+ --card-padding: 25px;
25
+ --button-font-size: 14px;
26
+ --border-radius: 12px;
27
+ --box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05), 0 10px 10px -5px rgba(0,0,0,0.02);
28
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
29
+ --switch-bg: #e2e8f0;
30
+ --switch-active: var(--primary-color);
31
+ }
32
+
33
+ /* 基础重置 */
34
+ * {
35
+ margin: 0;
36
+ padding: 0;
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ body {
41
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
42
+ line-height: 1.6;
43
+ padding: 20px;
44
+ max-width: 1200px;
45
+ margin: 20px auto;
46
+ background-color: var(--background-color);
47
+ background-image: linear-gradient(135deg, #f0f4ff 0%, #f8fafc 100%);
48
+ color: var(--text-color);
49
+ }
50
+
51
+ /* 顶部标题 */
52
+ .header {
53
+ text-align: center;
54
+ margin-bottom: 30px;
55
+ padding: 20px;
56
+ }
57
+
58
+ .header h1 {
59
+ font-size: 2.2rem;
60
+ font-weight: 700;
61
+ margin-bottom: 8px;
62
+ background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
63
+ -webkit-background-clip: text;
64
+ -webkit-text-fill-color: transparent;
65
+ position: relative;
66
+ display: inline-block;
67
+ }
68
+
69
+ .header h1::after {
70
+ content: '';
71
+ position: absolute;
72
+ bottom: -10px;
73
+ left: 50%;
74
+ transform: translateX(-50%);
75
+ width: 80px;
76
+ height: 4px;
77
+ background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
78
+ border-radius: 2px;
79
+ }
80
+
81
+ .header p {
82
+ font-size: 1.1rem;
83
+ color: var(--light-text);
84
+ max-width: 700px;
85
+ margin: 0 auto;
86
+ line-height: 1.7;
87
+ }
88
+
89
+ /* 顶部标签栏(主功能切换) */
90
+ .main-tabs {
91
+ display: flex;
92
+ gap: 8px;
93
+ margin-bottom: var(--gap);
94
+ background: var(--card-bg);
95
+ border-radius: var(--border-radius);
96
+ padding: 6px;
97
+ box-shadow: var(--box-shadow);
98
+ }
99
+
100
+ .main-tab-button {
101
+ flex: 1;
102
+ border: none;
103
+ background: none;
104
+ padding: 14px 20px;
105
+ font-size: 16px;
106
+ font-weight: 600;
107
+ color: var(--light-text);
108
+ cursor: pointer;
109
+ border-radius: 8px;
110
+ transition: var(--transition);
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ gap: 8px;
115
+ }
116
+
117
+ .main-tab-button:hover {
118
+ background: #f1f5f9;
119
+ color: var(--primary-color);
120
+ }
121
+
122
+ .main-tab-button.active {
123
+ background: var(--primary-color);
124
+ color: white;
125
+ box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
126
+ }
127
+
128
+ /* 标签内容容器(统一卡片样式) */
129
+ .tab-content {
130
+ background-color: var(--card-bg);
131
+ border-radius: var(--border-radius);
132
+ box-shadow: var(--box-shadow);
133
+ padding: var(--card-padding);
134
+ margin-bottom: var(--gap);
135
+ border: 1px solid var(--border-color);
136
+ transition: var(--transition);
137
+ }
138
+
139
+ .tab-content:hover {
140
+ box-shadow: 0 20px 25px -5px rgba(0,0,0,0.08), 0 10px 10px -5px rgba(0,0,0,0.03);
141
+ }
142
+
143
+ .section-title {
144
+ font-size: 1.3rem;
145
+ font-weight: 600;
146
+ margin-bottom: 20px;
147
+ padding-bottom: 12px;
148
+ border-bottom: 1px solid var(--border-color);
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 10px;
152
+ color: var(--primary-color);
153
+ }
154
+
155
+ .section-title i {
156
+ font-size: 1.2rem;
157
+ }
158
+
159
+ /* 上传与结果标签(主流程) */
160
+ .upload-section .input-group {
161
+ margin-bottom: 20px;
162
+ }
163
+
164
+ .upload-section label {
165
+ font-size: 15px;
166
+ font-weight: 500;
167
+ color: var(--text-color);
168
+ margin-bottom: 10px;
169
+ display: block;
170
+ }
171
+
172
+ /* 文件上传区域美化 */
173
+ .file-upload-container {
174
+ position: relative;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 15px;
178
+ }
179
+
180
+ .file-upload-btn {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ gap: 10px;
185
+ padding: 16px 24px;
186
+ background: var(--primary-color);
187
+ color: white;
188
+ border-radius: var(--border-radius);
189
+ cursor: pointer;
190
+ transition: var(--transition);
191
+ border: none;
192
+ font-size: 16px;
193
+ font-weight: 600;
194
+ box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
195
+ }
196
+
197
+ .file-upload-btn:hover {
198
+ background: var(--primary-hover);
199
+ transform: translateY(-2px);
200
+ box-shadow: 0 6px 10px -1px rgba(74, 108, 247, 0.3), 0 4px 6px -1px rgba(74, 108, 247, 0.1);
201
+ }
202
+
203
+ .file-upload-btn i {
204
+ font-size: 18px;
205
+ }
206
+
207
+ .file-name {
208
+ font-size: 14px;
209
+ padding: 12px 15px;
210
+ background: #f8fafc;
211
+ border-radius: var(--border-radius);
212
+ border: 1px dashed var(--border-color);
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 10px;
216
+ }
217
+
218
+ .file-name i {
219
+ color: var(--success-color);
220
+ }
221
+
222
+ .file-name.empty {
223
+ color: var(--light-text);
224
+ }
225
+
226
+ .file-name.empty i {
227
+ color: var(--light-text);
228
+ }
229
+
230
+ /* 开关控件美化 */
231
+ .options-container {
232
+ display: flex;
233
+ flex-direction: column;
234
+ gap: 15px;
235
+ margin-bottom: 20px;
236
+ }
237
+
238
+ .option-item {
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: space-between;
242
+ padding: 15px;
243
+ background: #f8fafc;
244
+ border-radius: var(--border-radius);
245
+ border: 1px solid var(--border-color);
246
+ transition: var(--transition);
247
+ }
248
+
249
+ .option-item:hover {
250
+ border-color: var(--primary-color);
251
+ background: #eef2ff;
252
+ }
253
+
254
+ .option-label {
255
+ font-size: 15px;
256
+ font-weight: 500;
257
+ color: var(--text-color);
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 12px;
261
+ }
262
+
263
+ .option-label i {
264
+ color: var(--primary-color);
265
+ font-size: 18px;
266
+ }
267
+
268
+ .switch {
269
+ position: relative;
270
+ display: inline-block;
271
+ width: 50px;
272
+ height: 26px;
273
+ }
274
+
275
+ .switch input {
276
+ opacity: 0;
277
+ width: 0;
278
+ height: 0;
279
+ }
280
+
281
+ .slider {
282
+ position: absolute;
283
+ cursor: pointer;
284
+ top: 0;
285
+ left: 0;
286
+ right: 0;
287
+ bottom: 0;
288
+ background-color: var(--switch-bg);
289
+ transition: var(--transition);
290
+ border-radius: 34px;
291
+ }
292
+
293
+ .slider:before {
294
+ position: absolute;
295
+ content: "";
296
+ height: 20px;
297
+ width: 20px;
298
+ left: 3px;
299
+ bottom: 3px;
300
+ background-color: white;
301
+ transition: var(--transition);
302
+ border-radius: 50%;
303
+ }
304
+
305
+ input:checked + .slider {
306
+ background-color: var(--switch-active);
307
+ }
308
+
309
+ input:checked + .slider:before {
310
+ transform: translateX(24px);
311
+ }
312
+
313
+ /* 按钮样式 */
314
+ .action-buttons {
315
+ display: flex;
316
+ gap: var(--gap);
317
+ margin-bottom: 20px;
318
+ flex-wrap: wrap;
319
+ }
320
+
321
+ .button {
322
+ border: none;
323
+ border-radius: var(--border-radius);
324
+ padding: 14px 24px;
325
+ font-size: 15px;
326
+ cursor: pointer;
327
+ transition: var(--transition);
328
+ display: inline-flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ gap: 8px;
332
+ font-weight: 600;
333
+ }
334
+
335
+ .button:hover {
336
+ transform: translateY(-2px);
337
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.05);
338
+ }
339
+
340
+ .button-primary {
341
+ background-color: var(--primary-color);
342
+ color: white;
343
+ }
344
+
345
+ .button-primary:hover {
346
+ background-color: var(--primary-hover);
347
+ }
348
+
349
+ .button-success {
350
+ background-color: var(--success-color);
351
+ color: white;
352
+ }
353
+
354
+ .button-success:hover {
355
+ background-color: #059669;
356
+ }
357
+
358
+ .button-secondary {
359
+ background-color: #e2e8f0;
360
+ color: var(--text-color);
361
+ }
362
+
363
+ .button-secondary:hover {
364
+ background-color: #cbd5e1;
365
+ }
366
+
367
+ .button-danger {
368
+ background-color: var(--error-color);
369
+ color: white;
370
+ }
371
+
372
+ .button-danger:hover {
373
+ background-color: #dc2626;
374
+ }
375
+
376
+ /* 结果区域(日志+输出) */
377
+ .result-subtabs {
378
+ display: flex;
379
+ gap: 8px;
380
+ margin-bottom: 20px;
381
+ background: #f8fafc;
382
+ border-radius: var(--border-radius);
383
+ padding: 6px;
384
+ }
385
+
386
+ .result-subtab-button {
387
+ flex: 1;
388
+ border: none;
389
+ background: none;
390
+ padding: 12px 20px;
391
+ font-size: 14px;
392
+ font-weight: 500;
393
+ color: var(--light-text);
394
+ cursor: pointer;
395
+ border-radius: 8px;
396
+ transition: var(--transition);
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ gap: 8px;
401
+ }
402
+
403
+ .result-subtab-button:hover {
404
+ background: #eef2ff;
405
+ color: var(--primary-color);
406
+ }
407
+
408
+ .result-subtab-button.active {
409
+ background: var(--primary-color);
410
+ color: white;
411
+ box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
412
+ }
413
+
414
+ .log-container {
415
+ max-height: 300px;
416
+ overflow-y: auto;
417
+ padding: 15px;
418
+ border: 1px solid var(--border-color);
419
+ border-radius: var(--border-radius);
420
+ margin-bottom: 15px;
421
+ background: #f8fafc;
422
+ }
423
+
424
+ .log-summary {
425
+ font-size: 15px;
426
+ font-weight: 600;
427
+ margin-bottom: 15px;
428
+ padding: 12px 15px;
429
+ border-radius: var(--border-radius);
430
+ background: #e0e7ff;
431
+ color: var(--primary-color);
432
+ display: flex;
433
+ justify-content: space-between;
434
+ flex-wrap: wrap;
435
+ gap: 10px;
436
+ }
437
+
438
+ .log-summary span {
439
+ background: white;
440
+ padding: 5px 12px;
441
+ border-radius: 20px;
442
+ font-weight: 600;
443
+ }
444
+
445
+ .log-item {
446
+ font-size: 14px;
447
+ padding: 12px 15px;
448
+ margin-bottom: 10px;
449
+ border-radius: 8px;
450
+ display: flex;
451
+ align-items: center;
452
+ gap: 10px;
453
+ transition: var(--transition);
454
+ }
455
+
456
+ .log-item:hover {
457
+ transform: translateY(-2px);
458
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.02);
459
+ }
460
+
461
+ .log-item i {
462
+ font-size: 16px;
463
+ }
464
+
465
+ .log-update {
466
+ background-color: #d1fae5;
467
+ color: var(--success-color);
468
+ border-left: 4px solid var(--success-color);
469
+ }
470
+
471
+ .log-skip {
472
+ background-color: #fffbeb;
473
+ color: var(--warning-color);
474
+ border-left: 4px solid var(--warning-color);
475
+ }
476
+
477
+ .log-notfound {
478
+ background-color: #fee2e2;
479
+ color: var(--error-color);
480
+ border-left: 4px solid var(--error-color);
481
+ }
482
+
483
+ .output-container pre {
484
+ font-size: 14px;
485
+ padding: 15px;
486
+ border: 1px solid var(--border-color);
487
+ border-radius: var(--border-radius);
488
+ background-color: #f8fafc;
489
+ max-height: 300px;
490
+ overflow-y: auto;
491
+ margin-top: 10px;
492
+ line-height: 1.5;
493
+ font-family: 'Fira Code', 'Consolas', monospace;
494
+ }
495
+
496
+ /* 规则管理标签(优化后) */
497
+ .rules-section .rules-subtabs {
498
+ display: flex;
499
+ gap: 8px;
500
+ margin-bottom: 20px;
501
+ background: #f8fafc;
502
+ border-radius: var(--border-radius);
503
+ padding: 6px;
504
+ }
505
+
506
+ .rules-subtab-button {
507
+ flex: 1;
508
+ border: none;
509
+ background: none;
510
+ padding: 12px 20px;
511
+ font-size: 14px;
512
+ font-weight: 500;
513
+ color: var(--light-text);
514
+ cursor: pointer;
515
+ border-radius: 8px;
516
+ transition: var(--transition);
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ gap: 8px;
521
+ }
522
+
523
+ .rules-subtab-button:hover {
524
+ background: #eef2ff;
525
+ color: var(--primary-color);
526
+ }
527
+
528
+ .rules-subtab-button.active {
529
+ background: var(--primary-color);
530
+ color: white;
531
+ box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06);
532
+ }
533
+
534
+ /* 现有规则列表(合并添加规则) */
535
+ .rules-list {
536
+ max-height: 300px;
537
+ overflow-y: auto;
538
+ padding: 15px;
539
+ border: 1px solid var(--border-color);
540
+ border-radius: var(--border-radius);
541
+ margin-bottom: 20px;
542
+ background: #f8fafc;
543
+ }
544
+
545
+ .rules-list-header {
546
+ display: flex;
547
+ justify-content: space-between;
548
+ align-items: center;
549
+ margin-bottom: 15px;
550
+ padding-bottom: 10px;
551
+ border-bottom: 1px solid var(--border-color);
552
+ }
553
+
554
+ .rules-list-header h4 {
555
+ font-size: 16px;
556
+ font-weight: 600;
557
+ color: var(--text-color);
558
+ }
559
+
560
+ .rules-count {
561
+ background: var(--primary-color);
562
+ color: white;
563
+ padding: 3px 10px;
564
+ border-radius: 20px;
565
+ font-size: 13px;
566
+ font-weight: 500;
567
+ }
568
+
569
+ .rules-list-item {
570
+ display: flex;
571
+ justify-content: space-between;
572
+ align-items: center;
573
+ padding: 15px;
574
+ margin-bottom: 12px;
575
+ border: 1px solid var(--border-color);
576
+ border-radius: var(--border-radius);
577
+ background-color: white;
578
+ box-shadow: 0 2px 4px rgba(0,0,0,0.03);
579
+ transition: var(--transition);
580
+ }
581
+
582
+ .rules-list-item:hover {
583
+ transform: translateX(3px);
584
+ border-left: 3px solid var(--primary-color);
585
+ }
586
+
587
+ .rules-list-item .keyword {
588
+ font-weight: 600;
589
+ font-size: 15px;
590
+ color: var(--text-color);
591
+ background: #e0e7ff;
592
+ padding: 3px 10px;
593
+ border-radius: 20px;
594
+ display: inline-block;
595
+ }
596
+
597
+ .rules-list-item .url {
598
+ font-size: 14px;
599
+ color: var(--secondary-color);
600
+ text-decoration: none;
601
+ white-space: nowrap;
602
+ overflow: hidden;
603
+ text-overflow: ellipsis;
604
+ display: block;
605
+ margin-top: 10px;
606
+ padding-left: 5px;
607
+ }
608
+
609
+ .rules-list-item .delete-btn {
610
+ padding: 6px 12px;
611
+ font-size: 13px;
612
+ background-color: var(--error-color);
613
+ color: white;
614
+ border: none;
615
+ border-radius: 6px;
616
+ cursor: pointer;
617
+ transition: var(--transition);
618
+ display: flex;
619
+ align-items: center;
620
+ gap: 5px;
621
+ }
622
+
623
+ .rules-list-item .delete-btn:hover {
624
+ background-color: #dc2626;
625
+ transform: translateY(-2px);
626
+ }
627
+
628
+ /* 添加规则表单(合并至现有规则下方) */
629
+ .add-rule-form {
630
+ padding: 20px;
631
+ border: 1px solid var(--border-color);
632
+ border-radius: var(--border-radius);
633
+ margin-bottom: 20px;
634
+ background: white;
635
+ box-shadow: var(--box-shadow);
636
+ }
637
+
638
+ .add-rule-form h4 {
639
+ font-size: 16px;
640
+ margin-bottom: 15px;
641
+ color: var(--text-color);
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 10px;
645
+ }
646
+
647
+ .add-rule-form label {
648
+ font-size: 14px;
649
+ font-weight: 500;
650
+ color: var(--text-color);
651
+ margin-bottom: 8px;
652
+ display: block;
653
+ }
654
+
655
+ .add-rule-form input[type="text"] {
656
+ width: 100%;
657
+ padding: 12px 15px;
658
+ border: 1px solid var(--border-color);
659
+ border-radius: var(--border-radius);
660
+ font-size: 14px;
661
+ margin-bottom: 15px;
662
+ transition: var(--transition);
663
+ }
664
+
665
+ .add-rule-form input[type="text"]:focus {
666
+ outline: none;
667
+ border-color: var(--primary-color);
668
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
669
+ }
670
+
671
+ /* 原始JSON区域(集中规则操作按钮) */
672
+ .raw-editor textarea {
673
+ width: 100%;
674
+ height: 200px;
675
+ padding: 15px;
676
+ border: 1px solid var(--border-color);
677
+ border-radius: var(--border-radius);
678
+ font-size: 14px;
679
+ resize: vertical;
680
+ margin-bottom: 20px;
681
+ font-family: 'Fira Code', 'Consolas', monospace;
682
+ line-height: 1.5;
683
+ transition: var(--transition);
684
+ }
685
+
686
+ .raw-editor textarea:focus {
687
+ outline: none;
688
+ border-color: var(--primary-color);
689
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2);
690
+ }
691
+
692
+ .rules-action-buttons {
693
+ display: flex;
694
+ gap: 12px;
695
+ flex-wrap: wrap;
696
+ }
697
+
698
+ /* 状态提示 */
699
+ .status {
700
+ padding: 15px 20px;
701
+ border-radius: var(--border-radius);
702
+ font-size: 15px;
703
+ margin-bottom: var(--gap);
704
+ display: flex;
705
+ align-items: center;
706
+ gap: 12px;
707
+ box-shadow: var(--box-shadow);
708
+ position: relative;
709
+ overflow: hidden;
710
+ }
711
+
712
+ .status::before {
713
+ content: '';
714
+ position: absolute;
715
+ left: 0;
716
+ top: 0;
717
+ height: 100%;
718
+ width: 4px;
719
+ }
720
+
721
+ .status.success {
722
+ background-color: #d1fae5;
723
+ color: var(--success-color);
724
+ }
725
+
726
+ .status.success::before {
727
+ background-color: var(--success-color);
728
+ }
729
+
730
+ .status.error {
731
+ background-color: #fee2e2;
732
+ color: var(--error-color);
733
+ }
734
+
735
+ .status.error::before {
736
+ background-color: var(--error-color);
737
+ }
738
+
739
+ .status i {
740
+ font-size: 18px;
741
+ }
742
+
743
+ /* 页脚 */
744
+ .footer {
745
+ text-align: center;
746
+ margin-top: 40px;
747
+ padding: 20px;
748
+ color: var(--light-text);
749
+ font-size: 14px;
750
+ }
751
+
752
+ .footer a {
753
+ color: var(--primary-color);
754
+ text-decoration: none;
755
+ }
756
+
757
+ .footer a:hover {
758
+ text-decoration: underline;
759
+ }
760
+
761
+ /* 响应式调整 */
762
+ @media (max-width: 768px) {
763
+ .main-tabs {
764
+ flex-direction: column;
765
+ gap: 5px;
766
+ }
767
+
768
+ .result-subtabs {
769
+ flex-direction: column;
770
+ gap: 5px;
771
+ }
772
+
773
+ .rules-subtabs {
774
+ flex-direction: column;
775
+ gap: 5px;
776
+ }
777
+
778
+ .header h1 {
779
+ font-size: 1.8rem;
780
+ }
781
+
782
+ .header p {
783
+ font-size: 1rem;
784
+ }
785
+
786
+ .action-buttons, .rules-action-buttons {
787
+ flex-direction: column;
788
+ }
789
+
790
+ .button {
791
+ width: 100%;
792
+ }
793
+ }
794
+ </style>
795
+ </head>
796
+ <body>
797
+ <div id="app">
798
+ <!-- 顶部标题 -->
799
+ <div class="header">
800
+ <h1><i class="fas fa-cogs"></i> OpenWebUI 模型头像URL修正工具</h1>
801
+ <p>自动检测并修正模型配置文件中的头像URL,支持自定义规则管理,提升OpenWebUI界面美观度</p>
802
+ </div>
803
+
804
+ <!-- 全局状态提示 -->
805
+ <div v-if="statusMessage.text" :class="['status', statusMessage.type]">
806
+ <i :class="statusMessage.icon"></i>
807
+ <span>{{ statusMessage.text }}</span>
808
+ </div>
809
+
810
+ <!-- 顶部主标签栏(切换主功能) -->
811
+ <div class="main-tabs">
812
+ <button @click="activeMainTab='upload'" :class="['main-tab-button', { active: activeMainTab==='upload' }]">
813
+ <i class="fas fa-upload"></i> 上传与结果
814
+ </button>
815
+ <button @click="activeMainTab='rules'" :class="['main-tab-button', { active: activeMainTab==='rules' }]">
816
+ <i class="fas fa-list"></i> 规则管理
817
+ </button>
818
+ </div>
819
+
820
+ <!-- 主标签内容(动态切换) -->
821
+ <div class="tab-content">
822
+ <!-- 1. 上传与结果标签(主流程) -->
823
+ <div v-if="activeMainTab==='upload'">
824
+ <div class="upload-section">
825
+ <h3 class="section-title"><i class="fas fa-file-import"></i> 模型文件处理</h3>
826
+
827
+ <!-- 文件上传 - 美化版 -->
828
+ <div class="input-group">
829
+ <label>选择模型JSON文件:</label>
830
+ <div class="file-upload-container">
831
+ <input type="file" id="jsonFile" @change="handleFileChange" accept=".json" ref="fileInput" style="display: none;">
832
+ <button class="file-upload-btn" @click="triggerFileInput">
833
+ <i class="fas fa-folder-open"></i> 选择JSON文件
834
+ </button>
835
+ <div :class="['file-name', fileContent ? '' : 'empty']">
836
+ <i :class="fileContent ? 'fas fa-check-circle' : 'fas fa-info-circle'"></i>
837
+ {{ fileContent ? originalFileName : '未选择文件' }}
838
+ </div>
839
+ </div>
840
+ </div>
841
+
842
+ <!-- 功能选项 - 美化版 -->
843
+ <div class="options-container">
844
+ <div class="option-item">
845
+ <div class="option-label">
846
+ <i class="fas fa-binoculars"></i>
847
+ <span>仅预览模式(不修改文件)</span>
848
+ </div>
849
+ <label class="switch">
850
+ <input type="checkbox" v-model="isDryRun">
851
+ <span class="slider"></span>
852
+ </label>
853
+ </div>
854
+ <div class="option-item">
855
+ <div class="option-label">
856
+ <i class="fas fa-sync-alt"></i>
857
+ <span>覆盖已有非默认URL</span>
858
+ </div>
859
+ <label class="switch">
860
+ <input type="checkbox" v-model="overwriteExisting">
861
+ <span class="slider"></span>
862
+ </label>
863
+ </div>
864
+ </div>
865
+
866
+ <!-- 核心操作按钮 -->
867
+ <div class="action-buttons">
868
+ <button @click="processFile" class="button button-primary" :disabled="!fileContent">
869
+ <i class="fas fa-bolt"></i> {{ isDryRun ? '开始预览' : '开始处理' }}
870
+ </button>
871
+ <button @click="downloadJson" class="button button-success" :disabled="!outputJson">
872
+ <i class="fas fa-download"></i> 下载修正文件
873
+ </button>
874
+ <button @click="resetFile" class="button button-secondary">
875
+ <i class="fas fa-redo"></i> 重置
876
+ </button>
877
+ </div>
878
+ </div>
879
+
880
+ <!-- 结果区域(日志+输出,子标签切换) -->
881
+ <div v-if="logs.length > 0 || outputJson">
882
+ <h3 class="section-title"><i class="fas fa-chart-bar"></i> 处理结果</h3>
883
+
884
+ <div class="result-subtabs">
885
+ <button @click="activeResultSubtab='logs'" :class="['result-subtab-button', { active: activeResultSubtab==='logs' }]">
886
+ <i class="fas fa-list"></i> 处理日志
887
+ </button>
888
+ <button @click="activeResultSubtab='output'" :class="['result-subtab-button', { active: activeResultSubtab==='output' }]">
889
+ <i class="fas fa-code"></i> 输出JSON
890
+ </button>
891
+ </div>
892
+
893
+ <!-- 处理日志 -->
894
+ <div v-if="activeResultSubtab==='logs'" class="log-container">
895
+ <div class="log-summary">
896
+ 处理结果:共{{ totalModels }}条模型
897
+ <span>更新: {{ updatedCount }}</span>
898
+ <span>跳过: {{ skippedCount }}</span>
899
+ <span>未匹配: {{ notFoundCount }}</span>
900
+ </div>
901
+ <div v-for="(log, index) in logs" :key="index" :class="['log-item', `log-${log.type}`]">
902
+ <i :class="log.icon"></i>
903
+ <span>{{ log.message }}</span>
904
+ </div>
905
+ </div>
906
+
907
+ <!-- 输出JSON -->
908
+ <div v-if="activeResultSubtab==='output' && !isDryRun" class="output-container">
909
+ <pre>{{ outputJson }}</pre>
910
+ </div>
911
+ </div>
912
+ </div>
913
+
914
+ <!-- 2. 规则管理标签(优化后) -->
915
+ <div v-if="activeMainTab==='rules'">
916
+ <div class="rules-section">
917
+ <h3 class="section-title"><i class="fas fa-sliders-h"></i> 规则管理</h3>
918
+
919
+ <!-- 规则管理子标签 -->
920
+ <div class="rules-subtabs">
921
+ <button @click="activeRulesSubtab='list'" :class="['rules-subtab-button', { active: activeRulesSubtab==='list' }]">
922
+ <i class="fas fa-th-list"></i> 现有规则
923
+ </button>
924
+ <button @click="activeRulesSubtab='raw'" :class="['rules-subtab-button', { active: activeRulesSubtab==='raw' }]">
925
+ <i class="fas fa-code"></i> 原始JSON
926
+ </button>
927
+ </div>
928
+
929
+ <!-- 现有规则列表(合并添加规则) -->
930
+ <div v-if="activeRulesSubtab==='list'">
931
+ <div class="rules-list">
932
+ <div class="rules-list-header">
933
+ <h4>当前规则列表</h4>
934
+ <div class="rules-count">{{ providerMap.length }} 条规则</div>
935
+ </div>
936
+ <div v-if="providerMap.length === 0" class="empty-rules">
937
+ <p style="font-size:14px; color:var(--light-text); text-align:center; padding:20px;">
938
+ <i class="fas fa-inbox" style="font-size:24px; margin-bottom:10px;"></i><br>
939
+ 暂无规则,请添加新规则
940
+ </p>
941
+ </div>
942
+ <div v-for="(rule, index) in providerMap" :key="index" class="rules-list-item">
943
+ <div>
944
+ <span class="keyword">{{ rule[0] }}</span>
945
+ <a :href="rule[1]" target="_blank" class="url">{{ rule[1] }}</a>
946
+ </div>
947
+ <button @click="deleteRule(index)" class="delete-btn">
948
+ <i class="fas fa-trash-alt"></i> 删除
949
+ </button>
950
+ </div>
951
+ </div>
952
+
953
+ <!-- 添加规则表单 -->
954
+ <div class="add-rule-form">
955
+ <h4><i class="fas fa-plus-circle"></i> 添加新规则</h4>
956
+ <label>
957
+ 匹配关键词(如"gpt"):
958
+ <input type="text" v-model="newKeyword" placeholder="请输入关键词" required>
959
+ </label>
960
+ <label>
961
+ 头像URL(完整链接):
962
+ <input type="text" v-model="newUrl" placeholder="请输入图片URL" required>
963
+ </label>
964
+ <button @click="addRule" class="button button-primary" :disabled="!newKeyword || !newUrl">
965
+ <i class="fas fa-save"></i> 添加规则
966
+ </button>
967
+ </div>
968
+ </div>
969
+
970
+ <!-- 原始JSON区域(集中规则操作) -->
971
+ <div v-if="activeRulesSubtab==='raw'">
972
+ <div class="raw-editor">
973
+ <textarea v-model="rulesText" placeholder="规则格式:[['关键词1','URL1'],['关键词2','URL2']]"></textarea>
974
+ </div>
975
+ <div class="rules-action-buttons">
976
+ <button @click="saveRules" class="button button-primary">
977
+ <i class="fas fa-save"></i> 保存规则
978
+ </button>
979
+ <button @click="importRules" class="button button-secondary">
980
+ <i class="fas fa-file-import"></i> 导入规则
981
+ </button>
982
+ <button @click="exportRules" class="button button-secondary">
983
+ <i class="fas fa-file-export"></i> 导出规则
984
+ </button>
985
+ <button @click="resetRules" class="button button-danger">
986
+ <i class="fas fa-undo"></i> 重置为默认
987
+ </button>
988
+ </div>
989
+ </div>
990
+ </div>
991
+ </div>
992
+ </div>
993
+
994
+ <!-- 页脚 -->
995
+ <div class="footer">
996
+ <p>图像 CDN 服务由 lobechat 提供 再次感谢 lobechat 提供的图像 CDN 服务</p>
997
+ </div>
998
+ </div>
999
+
1000
+ <script>
1001
+ const { createApp, ref, reactive, watch, onMounted } = Vue;
1002
+
1003
+ // 默认规则(可自定义)
1004
+ const DEFAULT_RULES = [
1005
+ ["openrouter/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg"],
1006
+ ["aliyun/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/aliyun.svg"],
1007
+ ["gemini_pipe_new.", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.svg"],
1008
+ ["gpt", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
1009
+ ["openai", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
1010
+ ["o1-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
1011
+ ["o3-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
1012
+ ["whisper", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"],
1013
+ ["text-embedding-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"],
1014
+ ["tts-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"],
1015
+ ["dall-e", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"],
1016
+ ["claude", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude.svg"],
1017
+ ["gemini", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.svg"],
1018
+ ["ernie", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"],
1019
+ ["baidu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"],
1020
+ ["command", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"],
1021
+ ["cohere", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"],
1022
+ ["deepseek", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg"],
1023
+ ["grok", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/grok.svg"],
1024
+ ["llama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta.svg"],
1025
+ ["meta", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta.svg"],
1026
+ ["groq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq.svg"],
1027
+ ["qwq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
1028
+ ["qvq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
1029
+ ["qwen", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"],
1030
+ ["abab", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"],
1031
+ ["minimax", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"],
1032
+ ["mistral", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
1033
+ ["kimi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"],
1034
+ ["moonshot", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"],
1035
+ ["ollama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg"],
1036
+ ["hunyuan", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"],
1037
+ ["tencent", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"],
1038
+ ["yi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi.svg"],
1039
+ ["glm", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"],
1040
+ ["zhipu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"],
1041
+ ["open-mixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
1042
+ ["ministral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
1043
+ ["codestral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
1044
+ ["pixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"],
1045
+ ["doubao","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/doubao.svg"]
1046
+ ];
1047
+
1048
+ // 本地存储键
1049
+ const STORAGE_KEY = "openwebui_avatar_rules";
1050
+
1051
+ createApp({
1052
+ setup() {
1053
+ // 1. 标签切换状态
1054
+ const activeMainTab = ref("upload"); // 默认显示"上传与结果"
1055
+ const activeResultSubtab = ref("logs"); // 结果子标签(默认日志)
1056
+ const activeRulesSubtab = ref("list"); // 规则子标签(默认现有规则)
1057
+
1058
+ // 2. 核心功能状态
1059
+ const fileContent = ref(null);
1060
+ const originalFileName = ref("");
1061
+ const isDryRun = ref(false);
1062
+ const overwriteExisting = ref(false);
1063
+ const providerMap = ref([]);
1064
+ const rulesText = ref("");
1065
+ const newKeyword = ref("");
1066
+ const newUrl = ref("");
1067
+ const logs = ref([]);
1068
+ const outputJson = ref("");
1069
+ const error = ref("");
1070
+ const statusMessage = reactive({ text: "", type: "success", icon: "" });
1071
+ const fileInput = ref(null);
1072
+
1073
+ // 3. 统计数据
1074
+ const totalModels = ref(0);
1075
+ const updatedCount = ref(0);
1076
+ const skippedCount = ref(0);
1077
+ const notFoundCount = ref(0);
1078
+
1079
+ // 4. 初始化加载规则
1080
+ onMounted(() => {
1081
+ loadRules();
1082
+ });
1083
+
1084
+ // 5. 监听规则变化,同步原始JSON
1085
+ watch(providerMap, (newRules) => {
1086
+ rulesText.value = JSON.stringify(newRules, null, 2);
1087
+ }, { deep: true });
1088
+
1089
+ // 6. 规则管理函数
1090
+ const loadRules = () => {
1091
+ try {
1092
+ const stored = localStorage.getItem(STORAGE_KEY);
1093
+ providerMap.value = stored ? JSON.parse(stored) : [...DEFAULT_RULES];
1094
+ showStatus("规则加载成功", "success", "fas fa-check-circle");
1095
+ } catch (e) {
1096
+ console.error("加载规则失败:", e);
1097
+ providerMap.value = [...DEFAULT_RULES];
1098
+ showStatus("加载规则失败,使用默认规则", "error", "fas fa-exclamation-circle");
1099
+ }
1100
+ };
1101
+
1102
+ const saveRules = () => {
1103
+ try {
1104
+ if (activeRulesSubtab.value === "raw") {
1105
+ const parsed = JSON.parse(rulesText.value);
1106
+ if (!Array.isArray(parsed) || !parsed.every(item => Array.isArray(item) && item.length === 2)) {
1107
+ throw new Error("规则格式错误,请检查JSON结构");
1108
+ }
1109
+ providerMap.value = parsed;
1110
+ }
1111
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(providerMap.value));
1112
+ showStatus("规则保存成功", "success", "fas fa-check-circle");
1113
+ } catch (e) {
1114
+ console.error("保存规则失败:", e);
1115
+ showStatus(`保存失败:${e.message}`, "error", "fas fa-exclamation-circle");
1116
+ }
1117
+ };
1118
+
1119
+ const importRules = () => {
1120
+ const input = document.createElement("input");
1121
+ input.type = "file";
1122
+ input.accept = ".json";
1123
+ input.onchange = (e) => {
1124
+ const file = e.target.files[0];
1125
+ if (!file) return;
1126
+ const reader = new FileReader();
1127
+ reader.onload = (event) => {
1128
+ try {
1129
+ const rules = JSON.parse(event.target.result);
1130
+ if (Array.isArray(rules) && rules.every(item => Array.isArray(item) && item.length === 2)) {
1131
+ providerMap.value = rules;
1132
+ saveRules();
1133
+ showStatus("规则导入成功", "success", "fas fa-check-circle");
1134
+ } else {
1135
+ throw new Error("导入文件格式错误,需为规则JSON数组");
1136
+ }
1137
+ } catch (e) {
1138
+ console.error("导入失败:", e);
1139
+ showStatus(`导入失败:${e.message}`, "error", "fas fa-exclamation-circle");
1140
+ }
1141
+ };
1142
+ reader.readAsText(file);
1143
+ };
1144
+ input.click();
1145
+ };
1146
+
1147
+ const exportRules = () => {
1148
+ const blob = new Blob([JSON.stringify(providerMap.value, null, 2)], { type: "application/json" });
1149
+ const url = URL.createObjectURL(blob);
1150
+ const a = document.createElement("a");
1151
+ a.href = url;
1152
+ a.download = "openwebui_avatar_rules.json";
1153
+ a.click();
1154
+ URL.revokeObjectURL(url);
1155
+ showStatus("规则导出成功", "success", "fas fa-check-circle");
1156
+ };
1157
+
1158
+ const resetRules = () => {
1159
+ if (confirm("确定要重置为默认规则吗?当前规则将丢失")) {
1160
+ providerMap.value = [...DEFAULT_RULES];
1161
+ localStorage.removeItem(STORAGE_KEY);
1162
+ showStatus("规则已重置为默认", "success", "fas fa-check-circle");
1163
+ }
1164
+ };
1165
+
1166
+ const addRule = () => {
1167
+ if (!newKeyword.value || !newUrl.value) {
1168
+ showStatus("关键词和URL不能为空", "error", "fas fa-exclamation-circle");
1169
+ return;
1170
+ }
1171
+ if (!newUrl.value.startsWith('http')) {
1172
+ showStatus("URL格式无效,必须以http/https开头", "error", "fas fa-exclamation-circle");
1173
+ return;
1174
+ }
1175
+ const exists = providerMap.value.some(rule => rule[0].toLowerCase() === newKeyword.value.toLowerCase());
1176
+ if (exists) {
1177
+ showStatus("该关键词已存在,请修改后重试", "error", "fas fa-exclamation-circle");
1178
+ return;
1179
+ }
1180
+ providerMap.value.push([newKeyword.value, newUrl.value]);
1181
+ newKeyword.value = "";
1182
+ newUrl.value = "";
1183
+ saveRules();
1184
+ showStatus("规则添加成功", "success", "fas fa-check-circle");
1185
+ };
1186
+
1187
+ const deleteRule = (index) => {
1188
+ if (confirm("确定要删除该规则吗?")) {
1189
+ providerMap.value.splice(index, 1);
1190
+ saveRules();
1191
+ showStatus("规则删除成功", "success", "fas fa-check-circle");
1192
+ }
1193
+ };
1194
+
1195
+ // 7. 文件处理函数
1196
+ const triggerFileInput = () => {
1197
+ fileInput.value.click();
1198
+ };
1199
+
1200
+ const handleFileChange = (e) => {
1201
+ const file = e.target.files[0];
1202
+ if (!file) return;
1203
+ originalFileName.value = file.name;
1204
+ const reader = new FileReader();
1205
+ reader.onload = (event) => {
1206
+ fileContent.value = event.target.result;
1207
+ error.value = "";
1208
+ showStatus(`文件 "${file.name}" 加载成功`, "success", "fas fa-check-circle");
1209
+ };
1210
+ reader.onerror = () => {
1211
+ showStatus("文件读取失败,请检查文件完整性", "error", "fas fa-exclamation-circle");
1212
+ };
1213
+ reader.readAsText(file);
1214
+ };
1215
+
1216
+ const resetFile = () => {
1217
+ fileContent.value = null;
1218
+ originalFileName.value = "";
1219
+ logs.value = [];
1220
+ outputJson.value = "";
1221
+ fileInput.value.value = "";
1222
+ showStatus("文件已重置", "success", "fas fa-redo");
1223
+ };
1224
+
1225
+ const processFile = () => {
1226
+ if (!fileContent.value) {
1227
+ showStatus("请先选择模型JSON文件", "error", "fas fa-exclamation-circle");
1228
+ return;
1229
+ }
1230
+ if (providerMap.value.length === 0) {
1231
+ showStatus("请先添加或导入规则", "error", "fas fa-exclamation-circle");
1232
+ return;
1233
+ }
1234
+
1235
+ try {
1236
+ const data = JSON.parse(fileContent.value);
1237
+ if (!Array.isArray(data)) {
1238
+ throw new Error("JSON文件格式错误,顶层应为模型数组");
1239
+ }
1240
+
1241
+ // 初始化统计
1242
+ totalModels.value = data.length;
1243
+ updatedCount.value = 0;
1244
+ skippedCount.value = 0;
1245
+ notFoundCount.value = 0;
1246
+ logs.value = [];
1247
+
1248
+ // 处理每个模型
1249
+ const processed = data.map(model => {
1250
+ const clone = { ...model };
1251
+ const modelId = clone.id || "";
1252
+ const currentUrl = clone.meta?.profile_image_url || "";
1253
+
1254
+ // 添加base_model_id字段(与meta同级)
1255
+ if (clone.base_model_id === undefined) {
1256
+ clone.base_model_id = null;
1257
+ }
1258
+
1259
+ // 查找匹配的URL(按规则顺序匹配)
1260
+ let newUrl = null;
1261
+ for (const [keyword, url] of providerMap.value) {
1262
+ if (modelId.toLowerCase().includes(keyword.toLowerCase())) {
1263
+ newUrl = url;
1264
+ break;
1265
+ }
1266
+ }
1267
+
1268
+ // 处理URL逻辑
1269
+ if (newUrl) {
1270
+ if (currentUrl === newUrl) {
1271
+ skippedCount.value++;
1272
+ logs.value.push({
1273
+ type: "skip",
1274
+ message: `[跳过] ${modelId}:已使用正确URL`,
1275
+ icon: "fas fa-redo"
1276
+ });
1277
+ } else if (currentUrl === "" || currentUrl === "/static/favicon.png" || overwriteExisting.value) {
1278
+ if (!isDryRun.value) {
1279
+ clone.meta = clone.meta || {};
1280
+ clone.meta.profile_image_url = newUrl;
1281
+ }
1282
+ updatedCount.value++;
1283
+ logs.value.push({
1284
+ type: "update",
1285
+ message: `[更新] ${modelId}:旧URL→${currentUrl || "空"},新URL→${newUrl}`,
1286
+ icon: "fas fa-sync-alt"
1287
+ });
1288
+ } else {
1289
+ skippedCount.value++;
1290
+ logs.value.push({
1291
+ type: "skip",
1292
+ message: `[跳过] ${modelId}:已有非默认URL(未勾选覆盖)`,
1293
+ icon: "fas fa-ban"
1294
+ });
1295
+ }
1296
+ } else {
1297
+ notFoundCount.value++;
1298
+ logs.value.push({
1299
+ type: "notfound",
1300
+ message: `[未匹配] ${modelId}:无对应规则`,
1301
+ icon: "fas fa-search"
1302
+ });
1303
+ }
1304
+
1305
+ return clone;
1306
+ });
1307
+
1308
+ // 生成输出JSON(非预览模式)
1309
+ if (!isDryRun.value) {
1310
+ outputJson.value = JSON.stringify(processed, null, 2);
1311
+ } else {
1312
+ outputJson.value = "";
1313
+ }
1314
+
1315
+ showStatus(`处理完成:共${totalModels.value}条模型`, "success", "fas fa-check-circle");
1316
+ } catch (e) {
1317
+ console.error("处理失败:", e);
1318
+ showStatus(`处理失败:${e.message}`, "error", "fas fa-exclamation-circle");
1319
+ }
1320
+ };
1321
+
1322
+ const downloadJson = () => {
1323
+ if (!outputJson.value) return;
1324
+ const blob = new Blob([outputJson.value], { type: "application/json" });
1325
+ const url = URL.createObjectURL(blob);
1326
+ const a = document.createElement("a");
1327
+ a.href = url;
1328
+ a.download = `${originalFileName.value.replace(".json", "")}_modified.json`;
1329
+ a.click();
1330
+ URL.revokeObjectURL(url);
1331
+ showStatus("文件下载成功", "success", "fas fa-download");
1332
+ };
1333
+
1334
+ // 8. 状态提示函数
1335
+ const showStatus = (message, type = "success", icon = "fas fa-info-circle") => {
1336
+ statusMessage.text = message;
1337
+ statusMessage.type = type;
1338
+ statusMessage.icon = icon;
1339
+ setTimeout(() => {
1340
+ statusMessage.text = "";
1341
+ }, 4000);
1342
+ };
1343
+
1344
+ // 9. 返回响应式状态与函数
1345
+ return {
1346
+ activeMainTab,
1347
+ activeResultSubtab,
1348
+ activeRulesSubtab,
1349
+ fileContent,
1350
+ originalFileName,
1351
+ isDryRun,
1352
+ overwriteExisting,
1353
+ providerMap,
1354
+ rulesText,
1355
+ newKeyword,
1356
+ newUrl,
1357
+ logs,
1358
+ outputJson,
1359
+ error,
1360
+ statusMessage,
1361
+ fileInput,
1362
+ totalModels,
1363
+ updatedCount,
1364
+ skippedCount,
1365
+ notFoundCount,
1366
+ loadRules,
1367
+ saveRules,
1368
+ importRules,
1369
+ exportRules,
1370
+ resetRules,
1371
+ addRule,
1372
+ deleteRule,
1373
+ triggerFileInput,
1374
+ handleFileChange,
1375
+ resetFile,
1376
+ processFile,
1377
+ downloadJson
1378
+ };
1379
+ }
1380
+ }).mount("#app");
1381
+ </script>
1382
+ </body>
1383
+ </html>