kongbai-84 commited on
Commit
c9e9f5e
·
verified ·
1 Parent(s): 3c5eaad

Upload 提示词.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. 提示词.html +1566 -0
提示词.html ADDED
@@ -0,0 +1,1566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>SD人物提示词管理器 (Pro增强版)</title>
7
+ <style>
8
+ :root {
9
+ /* 默认浅色主题 */
10
+ --bg-color: #ffffff;
11
+ --sidebar-bg: #f8f9fa;
12
+ --text-color: #333333;
13
+ --border-color: #dee2e6;
14
+ --hover-color: #e9ecef;
15
+ --selected-bg: #cce5ff;
16
+ --selected-border: #b8daff;
17
+ --accent-color: #0d6efd;
18
+ --btn-text: #212529;
19
+ --btn-bg: #f8f9fa;
20
+ --tag-bg: #e2e3e5;
21
+ --tag-text: #383d41;
22
+ --tag-cn-text: #666;
23
+ --tag-placeholder: #adb5bd;
24
+ --input-bg: #ffffff;
25
+ --img-preview-bg: #e9ecef;
26
+ }
27
+
28
+ [data-theme="dark"] {
29
+ /* 深色主题 */
30
+ --bg-color: #212529;
31
+ --sidebar-bg: #2c3034;
32
+ --text-color: #f8f9fa;
33
+ --border-color: #495057;
34
+ --hover-color: #343a40;
35
+ --selected-bg: #375a7f;
36
+ --selected-border: #0d6efd;
37
+ --accent-color: #0d6efd;
38
+ --btn-text: #f8f9fa;
39
+ --btn-bg: #343a40;
40
+ --tag-bg: #495057;
41
+ --tag-text: #e9ecef;
42
+ --tag-cn-text: #adb5bd;
43
+ --tag-placeholder: #6c757d;
44
+ --input-bg: #212529;
45
+ --img-preview-bg: #343a40;
46
+ }
47
+
48
+ body {
49
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
50
+ margin: 0;
51
+ padding: 0;
52
+ background-color: var(--bg-color);
53
+ color: var(--text-color);
54
+ height: 100vh;
55
+ display: flex;
56
+ flex-direction: column;
57
+ overflow: hidden;
58
+ transition: background-color 0.2s, color 0.2s;
59
+ }
60
+
61
+ /* --- Header --- */
62
+ header {
63
+ background-color: var(--sidebar-bg);
64
+ padding: 10px 20px;
65
+ border-bottom: 1px solid var(--border-color);
66
+ display: flex;
67
+ justify-content: space-between;
68
+ align-items: center;
69
+ user-select: none;
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ h1 { margin: 0; font-size: 1.2rem; display: flex; align-items: center; gap: 10px; }
74
+ .toolbar { display: flex; gap: 8px; }
75
+
76
+ button {
77
+ background-color: var(--btn-bg);
78
+ color: var(--btn-text);
79
+ border: 1px solid var(--border-color);
80
+ padding: 6px 12px;
81
+ cursor: pointer;
82
+ border-radius: 4px;
83
+ font-size: 0.9rem;
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 6px;
87
+ transition: all 0.15s;
88
+ }
89
+ button:hover { background-color: var(--hover-color); }
90
+ button.primary { background-color: var(--accent-color); color: white; border: none; }
91
+ button.primary:hover { opacity: 0.9; }
92
+ button.danger { color: #dc3545; border-color: #dc3545; }
93
+ button.danger:hover { background-color: #dc3545; color: white; }
94
+
95
+ /* --- Layout --- */
96
+ .container { display: flex; flex: 1; overflow: hidden; }
97
+
98
+ /* --- Sidebar --- */
99
+ .sidebar-container {
100
+ width: 340px;
101
+ display: flex;
102
+ flex-direction: column;
103
+ background-color: var(--sidebar-bg);
104
+ border-right: 1px solid var(--border-color);
105
+ flex-shrink: 0;
106
+ }
107
+
108
+ /* Search Bar */
109
+ .search-box {
110
+ padding: 10px;
111
+ border-bottom: 1px solid var(--border-color);
112
+ }
113
+ .search-box input {
114
+ width: 100%;
115
+ padding: 8px;
116
+ border-radius: 4px;
117
+ border: 1px solid var(--border-color);
118
+ background-color: var(--input-bg);
119
+ color: var(--text-color);
120
+ box-sizing: border-box;
121
+ }
122
+
123
+ .sidebar-tools {
124
+ padding: 8px;
125
+ border-bottom: 1px solid var(--border-color);
126
+ display: flex;
127
+ gap: 5px;
128
+ flex-wrap: wrap;
129
+ }
130
+ .sidebar-tools button { flex: 1; justify-content: center; font-size: 0.8rem; padding: 4px; white-space: nowrap;}
131
+
132
+ .tree-view {
133
+ flex: 1;
134
+ overflow-y: auto;
135
+ padding: 5px 0;
136
+ }
137
+
138
+ .tree-node-container { margin-bottom: 2px; }
139
+ .tree-content {
140
+ padding: 6px 10px;
141
+ display: flex;
142
+ align-items: center;
143
+ border: 1px solid transparent;
144
+ margin: 1px 5px;
145
+ border-radius: 4px;
146
+ cursor: pointer;
147
+ white-space: nowrap;
148
+ user-select: none;
149
+ }
150
+ .tree-content:hover { background-color: var(--hover-color); }
151
+ .tree-content.selected { background-color: var(--selected-bg); border-color: var(--selected-border); }
152
+ .tree-content.hidden-node { display: none !important; }
153
+
154
+ /* Drag Visuals for Tree */
155
+ .tree-content.drag-over { background-color: var(--hover-color); outline: 2px dashed var(--accent-color); }
156
+
157
+ .icon { margin-right: 8px; display: inline-block; width: 20px; text-align: center;}
158
+ .collapsed > .children-container { display: none; }
159
+
160
+ /* --- Main Area --- */
161
+ .main-area {
162
+ flex: 1;
163
+ padding: 0;
164
+ display: flex;
165
+ flex-direction: column;
166
+ overflow-y: auto;
167
+ background-color: var(--bg-color);
168
+ }
169
+
170
+ .content-scroll { padding: 30px; flex: 1; }
171
+
172
+ .char-header {
173
+ border-bottom: 2px solid var(--border-color);
174
+ padding-bottom: 15px;
175
+ margin-bottom: 20px;
176
+ }
177
+ .char-title { font-size: 2rem; font-weight: bold; margin-bottom: 10px; }
178
+
179
+ /* Image Section */
180
+ .image-section {
181
+ background-color: var(--sidebar-bg);
182
+ border: 1px solid var(--border-color);
183
+ border-radius: 8px;
184
+ padding: 15px;
185
+ margin-bottom: 20px;
186
+ display: flex;
187
+ gap: 20px;
188
+ align-items: flex-start;
189
+ }
190
+ .image-preview-box {
191
+ width: 150px;
192
+ height: 150px;
193
+ background-color: var(--img-preview-bg);
194
+ border-radius: 6px;
195
+ border: 1px dashed var(--border-color);
196
+ display: flex;
197
+ justify-content: center;
198
+ align-items: center;
199
+ overflow: hidden;
200
+ flex-shrink: 0;
201
+ cursor: pointer;
202
+ }
203
+ .image-preview-box img {
204
+ width: 100%;
205
+ height: 100%;
206
+ object-fit: cover;
207
+ }
208
+ .image-preview-box span { color: #888; font-size: 0.8rem; text-align: center; padding: 5px;}
209
+
210
+ .image-controls { flex: 1; display: flex; flex-direction: column; gap: 10px; }
211
+ .image-path-input {
212
+ width: 100%;
213
+ padding: 8px;
214
+ background: var(--input-bg);
215
+ border: 1px solid var(--border-color);
216
+ color: var(--text-color);
217
+ border-radius: 4px;
218
+ box-sizing: border-box;
219
+ }
220
+
221
+ /* Editor Section */
222
+ .skin-tabs-container {
223
+ display: flex;
224
+ gap: 5px;
225
+ margin-bottom: 0;
226
+ flex-wrap: wrap;
227
+ }
228
+ .skin-tab {
229
+ padding: 8px 16px;
230
+ background-color: var(--btn-bg);
231
+ border: 1px solid var(--border-color);
232
+ border-bottom: none;
233
+ cursor: pointer;
234
+ border-radius: 6px 6px 0 0;
235
+ position: relative;
236
+ top: 1px;
237
+ user-select: none;
238
+ }
239
+ .skin-tab.active {
240
+ background-color: var(--bg-color);
241
+ border-bottom: 1px solid var(--bg-color);
242
+ color: var(--accent-color);
243
+ font-weight: bold;
244
+ z-index: 2;
245
+ }
246
+ .skin-tab.drag-over-tab {
247
+ border-left: 3px solid var(--accent-color);
248
+ background-color: var(--hover-color);
249
+ }
250
+
251
+ .skin-tab .close-tab { margin-left: 8px; opacity: 0.5; font-size: 0.8em; }
252
+ .skin-tab .close-tab:hover { opacity: 1; color: red; }
253
+
254
+ .skin-tab:not(.add-btn):hover::after {
255
+ content: "双击重命名";
256
+ position: absolute;
257
+ bottom: 100%;
258
+ left: 50%;
259
+ transform: translateX(-50%);
260
+ background: rgba(0,0,0,0.7);
261
+ color: #fff;
262
+ padding: 2px 6px;
263
+ border-radius: 4px;
264
+ font-size: 0.7rem;
265
+ white-space: nowrap;
266
+ pointer-events: none;
267
+ opacity: 0;
268
+ transition: opacity 0.2s;
269
+ }
270
+ .skin-tab:not(.add-btn):hover:hover::after { opacity: 1; }
271
+
272
+ .editor-box {
273
+ border: 1px solid var(--border-color);
274
+ border-radius: 0 4px 4px 4px;
275
+ padding: 20px;
276
+ background: var(--bg-color);
277
+ display: flex;
278
+ flex-direction: column;
279
+ min-height: 300px;
280
+ }
281
+
282
+ textarea {
283
+ width: 100%;
284
+ min-height: 150px;
285
+ background-color: var(--input-bg);
286
+ color: var(--text-color);
287
+ border: 1px solid var(--border-color);
288
+ padding: 15px;
289
+ font-family: 'Consolas', monospace;
290
+ resize: vertical;
291
+ border-radius: 4px;
292
+ box-sizing: border-box;
293
+ margin-bottom: 15px;
294
+ }
295
+
296
+ .preview-area {
297
+ padding: 15px;
298
+ background-color: var(--sidebar-bg);
299
+ border-radius: 4px;
300
+ min-height: 40px;
301
+ display: flex;
302
+ flex-wrap: wrap;
303
+ gap: 8px;
304
+ align-content: flex-start;
305
+ }
306
+
307
+ /* Tag Style */
308
+ .tag {
309
+ background-color: var(--tag-bg);
310
+ color: var(--tag-text);
311
+ padding: 4px 10px;
312
+ border-radius: 6px;
313
+ font-size: 0.9rem;
314
+ border: 1px solid var(--border-color);
315
+ cursor: pointer;
316
+ display: flex;
317
+ flex-direction: column;
318
+ align-items: center;
319
+ line-height: 1.2;
320
+ transition: transform 0.1s, background-color 0.2s, border-color 0.2s;
321
+ user-select: none;
322
+ max-width: 250px;
323
+ position: relative;
324
+ overflow: visible;
325
+ }
326
+ .tag:active { transform: scale(0.98); }
327
+ /* Highlight when hovered for shortcuts */
328
+ .tag.hovered-for-shortcut {
329
+ border-color: var(--accent-color);
330
+ background-color: var(--hover-color);
331
+ box-shadow: 0 0 5px rgba(13, 110, 253, 0.3);
332
+ }
333
+ .tag.drag-over-tag {
334
+ border-left: 3px solid var(--accent-color);
335
+ margin-left: 5px;
336
+ }
337
+ .tag.editing { cursor: default; background-color: var(--bg-color); border-color: var(--accent-color); }
338
+ .tag.copied {
339
+ background-color: #d1e7dd !important;
340
+ border-color: #badbcc !important;
341
+ color: #0f5132 !important;
342
+ }
343
+
344
+ .tag-en { font-weight: 500; text-align: center; border-bottom: 1px dotted transparent; }
345
+ .tag-en:hover { border-bottom-color: var(--text-color); }
346
+
347
+ .tag-cn {
348
+ font-size: 0.75rem;
349
+ color: var(--tag-cn-text);
350
+ margin-top: 2px;
351
+ white-space: nowrap;
352
+ overflow: hidden;
353
+ text-overflow: ellipsis;
354
+ max-width: 100%;
355
+ cursor: text;
356
+ border-bottom: 1px dotted transparent;
357
+ min-height: 14px;
358
+ }
359
+ .tag-cn.placeholder {
360
+ color: var(--tag-placeholder);
361
+ font-style: italic;
362
+ }
363
+ .tag-cn:hover { border-bottom-color: var(--tag-cn-text); }
364
+
365
+ .tag-input {
366
+ width: 80px;
367
+ font-size: 0.75rem;
368
+ padding: 1px;
369
+ margin-top: 2px;
370
+ text-align: center;
371
+ border: 1px solid var(--accent-color);
372
+ border-radius: 2px;
373
+ background: var(--input-bg);
374
+ color: var(--text-color);
375
+ }
376
+
377
+ .tag-close-btn {
378
+ position: absolute;
379
+ top: -6px;
380
+ right: -6px;
381
+ width: 16px;
382
+ height: 16px;
383
+ background-color: #dc3545;
384
+ color: white;
385
+ border-radius: 50%;
386
+ font-size: 12px;
387
+ line-height: 14px;
388
+ text-align: center;
389
+ cursor: pointer;
390
+ opacity: 0;
391
+ transition: opacity 0.2s;
392
+ z-index: 10;
393
+ display: none;
394
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
395
+ }
396
+ .tag:hover .tag-close-btn { display: block; opacity: 1; }
397
+
398
+ .empty-state {
399
+ display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #888;
400
+ }
401
+ .hidden { display: none !important; }
402
+
403
+ /* Modal */
404
+ .modal-overlay {
405
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
406
+ background: rgba(0,0,0,0.5); z-index: 1000;
407
+ display: none; justify-content: center; align-items: center;
408
+ }
409
+ .modal {
410
+ background: var(--bg-color); padding: 25px; border-radius: 8px; width: 480px;
411
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
412
+ max-height: 90vh; overflow-y: auto;
413
+ }
414
+ .modal h3 { margin-top: 0; }
415
+ .modal input[type="text"], .modal input[type="password"], .modal input[type="number"] {
416
+ width: 100%; padding: 8px; margin: 5px 0;
417
+ background: var(--input-bg); color:var(--text-color);
418
+ border:1px solid var(--border-color); box-sizing: border-box;
419
+ }
420
+
421
+ .loading-spinner {
422
+ display: inline-block;
423
+ width: 12px; height: 12px;
424
+ border: 2px solid rgba(0,0,0,0.3);
425
+ border-radius: 50%;
426
+ border-top-color: var(--accent-color);
427
+ animation: spin 1s ease-in-out infinite;
428
+ margin-right: 5px;
429
+ }
430
+ @keyframes spin { to { transform: rotate(360deg); } }
431
+
432
+ .settings-group { margin-bottom: 20px; border-bottom: 1px solid var(--border-color); padding-bottom: 15px; }
433
+ .settings-group:last-child { border: none; }
434
+
435
+ .shortcut-config-row {
436
+ display: flex;
437
+ align-items: center;
438
+ justify-content: space-between;
439
+ margin-bottom: 8px;
440
+ font-size: 0.9rem;
441
+ }
442
+ .shortcut-config-row input {
443
+ width: 60px !important;
444
+ text-align: center;
445
+ font-family: monospace;
446
+ text-transform: uppercase;
447
+ }
448
+
449
+ /* Backup List Style */
450
+ .backup-list {
451
+ max-height: 150px;
452
+ overflow-y: auto;
453
+ border: 1px solid var(--border-color);
454
+ border-radius: 4px;
455
+ margin-top: 5px;
456
+ }
457
+ .backup-item {
458
+ padding: 8px;
459
+ border-bottom: 1px solid var(--border-color);
460
+ display: flex;
461
+ justify-content: space-between;
462
+ align-items: center;
463
+ font-size: 0.85rem;
464
+ }
465
+ .backup-item:last-child { border-bottom: none; }
466
+ .backup-item:hover { background-color: var(--hover-color); }
467
+
468
+ /* Shortcut Info */
469
+ .shortcut-info {
470
+ background: var(--hover-color);
471
+ padding: 8px;
472
+ border-radius: 4px;
473
+ font-size: 0.8rem;
474
+ color: var(--text-color);
475
+ margin-top: 10px;
476
+ }
477
+ .kbd {
478
+ background: var(--bg-color);
479
+ border: 1px solid var(--border-color);
480
+ border-radius: 3px;
481
+ padding: 0 4px;
482
+ font-family: monospace;
483
+ font-weight: bold;
484
+ display: inline-block;
485
+ min-width: 15px;
486
+ text-align: center;
487
+ }
488
+ </style>
489
+ </head>
490
+ <body>
491
+
492
+ <header>
493
+ <h1>SD 提示词管理 <span style="font-size:0.8em; font-weight:normal; color:#888; margin-left:10px;" id="filePathDisplay"></span></h1>
494
+ <div class="toolbar">
495
+ <button id="themeToggleBtn" onclick="toggleTheme()">🌙</button>
496
+ <button onclick="openSettingsModal()" title="设置 API、备份和快捷键">⚙️ 设置</button>
497
+ <div style="width: 1px; background: var(--border-color); margin: 0 5px;"></div>
498
+ <button onclick="openFile()" title="选择本地 JSON 文件">📂 打开文件</button>
499
+ <button onclick="saveFile()" class="primary" id="saveBtn" title="保存修改到原文件">💾 保存 (覆盖)</button>
500
+ <button onclick="exportJson()" title="另存为新文件">📥 另存为</button>
501
+ </div>
502
+ </header>
503
+
504
+ <div class="container">
505
+ <div class="sidebar-container">
506
+ <div class="search-box">
507
+ <input type="text" id="searchInput" placeholder="🔍 搜索人物或文件夹..." oninput="handleSearch()">
508
+ </div>
509
+ <div class="sidebar-tools">
510
+ <button onclick="createNewFolder()" title="在当前位置新建文件夹">📁 新建文件夹</button>
511
+ <button onclick="createNewCharacter()" title="在当前位置新建人物">👤 新建人物</button>
512
+ <button onclick="deleteSelectedNodes()" class="danger" title="删除选中项">🗑️ 删除</button>
513
+ <button onclick="sortTreeNodes()" title="按名称排序 (文件夹优先)">🔤 排序</button>
514
+ </div>
515
+ <div class="tree-view" id="treeRoot" onclick="deselectAll(event)">
516
+ <div style="padding:30px 20px; text-align:center; color:#888; line-height:1.6;">
517
+ 请点击上方<br><strong>“📂 打开文件”</strong><br>加载数据
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ <div class="main-area">
523
+ <div id="emptyState" class="empty-state">
524
+ <div style="font-size: 4rem; margin-bottom: 20px; opacity:0.3;">🖼️</div>
525
+ <p>请在左侧选择一个人物或文件夹查看详情</p>
526
+ </div>
527
+
528
+ <div id="contentPanel" class="content-scroll hidden">
529
+ <div class="char-header">
530
+ <div class="char-title" id="nodeTitleDisplay">名称</div>
531
+ <div style="font-size:0.9rem; color:#888;">类型: <span id="nodeTypeDisplay">人物</span></div>
532
+ </div>
533
+
534
+ <div class="image-section">
535
+ <div class="image-preview-box" id="imgPreviewContainer" onclick="triggerFileSelect()">
536
+ <span id="imgPlaceholder">无图片<br>(点击选择)</span>
537
+ <img id="imgPreview" class="hidden" src="" alt="预览">
538
+ </div>
539
+ <div class="image-controls">
540
+ <label style="font-weight:bold;">🖼️ 封面图片</label>
541
+ <div style="display:flex; gap:5px;">
542
+ <input type="text" class="image-path-input" id="imgPathInput" placeholder="输入图片路径 或 点击右侧按钮选择..." oninput="updateImageFromInput()">
543
+ <input type="file" id="hiddenFileInput" style="display:none;" accept="image/*" onchange="handleFileSelect(this)">
544
+ <button onclick="triggerFileSelect()">📂 选择图片</button>
545
+ </div>
546
+ </div>
547
+ </div>
548
+
549
+ <div id="promptSection" class="hidden">
550
+ <div class="skin-tabs-container" id="skinTabs">
551
+ </div>
552
+ <div class="editor-box">
553
+ <textarea id="promptEditor" placeholder="在此输入提示词..." oninput="handlePromptInput()"></textarea>
554
+
555
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
556
+ <span style="font-size:0.85rem; color:#888; display:flex; align-items:center;">
557
+ 🏷️ 提示词预览 (单击复制 | 拖拽排序 | 双击中文可直接修改 | 鼠标悬停用快捷键):
558
+ </span>
559
+ <div style="display:flex; gap: 8px;">
560
+ <button onclick="translateCurrentTags()" title="使用 DeepSeek 翻译当前页面缺失的 Tag" style="font-size:0.8rem;">
561
+ <span id="transBtnIcon">🌏</span> AI翻译
562
+ </button>
563
+ <button onclick="copyPrompts()" style="font-size:0.8rem;">📋 复制全部</button>
564
+ </div>
565
+ </div>
566
+
567
+ <div class="preview-area" id="tagsPreview"></div>
568
+ </div>
569
+ </div>
570
+
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ <div id="inputModal" class="modal-overlay">
576
+ <div class="modal" style="width: 350px;">
577
+ <h3 id="modalTitle">输入名称</h3>
578
+ <input type="text" id="modalInput">
579
+ <div style="display:flex; justify-content:flex-end; gap:10px; margin-top:10px;">
580
+ <button onclick="closeModal()">取消</button>
581
+ <button onclick="confirmModal()" class="primary">确定</button>
582
+ </div>
583
+ </div>
584
+ </div>
585
+
586
+ <div id="settingsModal" class="modal-overlay">
587
+ <div class="modal">
588
+ <h3>⚙️ 设置</h3>
589
+
590
+ <div class="settings-group">
591
+ <label style="font-weight:bold; display:block; margin-bottom:5px;">🔑 DeepSeek API Key (可选)</label>
592
+ <input type="password" id="apiKeyInput" placeholder="sk-...">
593
+ <div style="font-size:0.8rem; color:#888;">用于自动翻译英文提示词。</div>
594
+ </div>
595
+
596
+ <div class="settings-group">
597
+ <label style="font-weight:bold; display:block; margin-bottom:10px;">⌨️ 快捷键自定义 (鼠标悬停在 Tag 上时生效)</label>
598
+ <div class="shortcut-config-row">
599
+ <span>复制 (Copy)</span>
600
+ <input type="text" id="scInputCopy" maxlength="1">
601
+ </div>
602
+ <div class="shortcut-config-row">
603
+ <span>删除 (Delete)</span>
604
+ <input type="text" id="scInputDel" maxlength="1">
605
+ </div>
606
+ <div class="shortcut-config-row">
607
+ <span>编辑英文 (Edit En)</span>
608
+ <input type="text" id="scInputEn" maxlength="1">
609
+ </div>
610
+ <div class="shortcut-config-row">
611
+ <span>编辑中文 (Edit Cn)</span>
612
+ <input type="text" id="scInputCn" maxlength="1">
613
+ </div>
614
+ </div>
615
+
616
+ <div class="settings-group">
617
+ <label style="font-weight:bold; display:block; margin-bottom:5px;">🛡️ 备份管理 (浏览器本地存储)</label>
618
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom:5px;">
619
+ <span style="font-size:0.9rem;">保留删除备份数量:</span>
620
+ <input type="number" id="backupCountInput" style="width:60px;" min="1" max="20" value="4">
621
+ </div>
622
+ <div id="backupListContainer" class="backup-list">
623
+ </div>
624
+ <div style="font-size:0.8rem; color:#888; margin-top:5px;">
625
+ 自动备份:3小时一次 (保留1份)<br>删除备份:每次删除操作前自动触发
626
+ </div>
627
+ </div>
628
+
629
+ <div style="display:flex; justify-content:flex-end; gap:10px; margin-top:10px;">
630
+ <button onclick="closeSettingsModal()">关闭</button>
631
+ <button onclick="saveSettings()" class="primary">保存设置</button>
632
+ </div>
633
+ </div>
634
+ </div>
635
+
636
+ <script>
637
+ // --- Configuration: Defaults ---
638
+ let SHORTCUT_CONFIG = {
639
+ copy: 'c',
640
+ delete: 'd',
641
+ editEn: 'q',
642
+ editCn: 'w'
643
+ };
644
+
645
+ let AUTO_BACKUP_INTERVAL = 3 * 60 * 60 * 1000; // 3小时
646
+ let MAX_DELETE_BACKUPS = 4;
647
+
648
+ // --- Data & State ---
649
+ let appData = { tree: [], prompts: {}, translations: {} };
650
+ let fileHandle = null;
651
+ let apiKey = localStorage.getItem('sd_prompt_apikey') || '';
652
+
653
+ // Load Saved Shortcuts & Settings
654
+ const savedShortcuts = localStorage.getItem('sd_shortcuts');
655
+ if (savedShortcuts) {
656
+ try { SHORTCUT_CONFIG = JSON.parse(savedShortcuts); } catch(e) {}
657
+ }
658
+ const savedMaxBackups = localStorage.getItem('sd_max_backups');
659
+ if (savedMaxBackups) {
660
+ MAX_DELETE_BACKUPS = parseInt(savedMaxBackups, 10) || 4;
661
+ }
662
+
663
+ let selectedNodeId = null;
664
+ let selectedNodesSet = new Set();
665
+ let lastSelectedNodeUI = null;
666
+ let theme = 'light';
667
+ let dragSrcTab = null;
668
+ let dragSrcTag = null;
669
+
670
+ // Hover State for Shortcuts
671
+ let hoveredTagIndex = -1;
672
+ let isEditingTag = false;
673
+
674
+ // --- DOM Refs ---
675
+ const treeRoot = document.getElementById('treeRoot');
676
+ const contentPanel = document.getElementById('contentPanel');
677
+ const emptyState = document.getElementById('emptyState');
678
+ const nodeTitleDisplay = document.getElementById('nodeTitleDisplay');
679
+ const nodeTypeDisplay = document.getElementById('nodeTypeDisplay');
680
+ const imgPathInput = document.getElementById('imgPathInput');
681
+ const imgPreview = document.getElementById('imgPreview');
682
+ const imgPlaceholder = document.getElementById('imgPlaceholder');
683
+ const hiddenFileInput = document.getElementById('hiddenFileInput');
684
+ const promptSection = document.getElementById('promptSection');
685
+ const skinTabs = document.getElementById('skinTabs');
686
+ const promptEditor = document.getElementById('promptEditor');
687
+ const tagsPreview = document.getElementById('tagsPreview');
688
+
689
+ // --- Initialization ---
690
+ if(apiKey) console.log("DeepSeek Key Loaded.");
691
+
692
+ // Start Auto Backup Timer
693
+ setInterval(() => {
694
+ if (appData.tree.length > 0) performBackup('auto');
695
+ }, AUTO_BACKUP_INTERVAL);
696
+
697
+ // --- Backup System (LocalStorage) ---
698
+ function performBackup(type) {
699
+ if(!appData || !appData.tree || appData.tree.length === 0) return;
700
+
701
+ try {
702
+ // 重要:在任何修改发生前序列化数据
703
+ const dataStr = JSON.stringify(appData);
704
+ const timestamp = new Date().toLocaleString();
705
+
706
+ if (type === 'auto') {
707
+ const backupObj = { time: timestamp, data: dataStr, type: '自动' };
708
+ localStorage.setItem('sd_backup_auto', JSON.stringify(backupObj));
709
+ console.log("已执行自动备份");
710
+ } else if (type === 'delete') {
711
+ let list = JSON.parse(localStorage.getItem('sd_backup_del_list') || '[]');
712
+ list.unshift({ time: timestamp, data: dataStr, type: '删除前' });
713
+ // 使用用户配置的最大备份数量
714
+ if (list.length > MAX_DELETE_BACKUPS) list = list.slice(0, MAX_DELETE_BACKUPS);
715
+ localStorage.setItem('sd_backup_del_list', JSON.stringify(list));
716
+ console.log("已执行删除前备份");
717
+ }
718
+ } catch (e) {
719
+ console.error("备份失败 (可能是存储空间不足):", e);
720
+ }
721
+ }
722
+
723
+ function renderBackupList() {
724
+ const container = document.getElementById('backupListContainer');
725
+ container.innerHTML = '';
726
+
727
+ const backups = [];
728
+
729
+ // Load Auto Backup
730
+ const auto = localStorage.getItem('sd_backup_auto');
731
+ if(auto) backups.push(JSON.parse(auto));
732
+
733
+ // Load Delete Backups
734
+ const delList = JSON.parse(localStorage.getItem('sd_backup_del_list') || '[]');
735
+ backups.push(...delList);
736
+
737
+ if(backups.length === 0) {
738
+ container.innerHTML = '<div style="padding:10px; text-align:center; color:#888;">暂无备份</div>';
739
+ return;
740
+ }
741
+
742
+ // Sort by time roughly (though they are prepended)
743
+ backups.forEach((bk, idx) => {
744
+ const div = document.createElement('div');
745
+ div.className = 'backup-item';
746
+ div.innerHTML = `
747
+ <div>
748
+ <strong>[${bk.type}]</strong> ${bk.time}
749
+ <div style="font-size:0.75em; color:#888;">${(bk.data.length/1024).toFixed(1)} KB</div>
750
+ </div>
751
+ <div style="display:flex; gap:5px;">
752
+ <button onclick="downloadBackupString(this)" data-json='${bk.data.replace(/'/g, "&#39;")}' style="font-size:0.75rem; padding:2px 6px;">📥 下载</button>
753
+ <button onclick="restoreBackup(this)" data-json='${bk.data.replace(/'/g, "&#39;")}' class="danger" style="font-size:0.75rem; padding:2px 6px;">🔄 还原</button>
754
+ </div>
755
+ `;
756
+ container.appendChild(div);
757
+ });
758
+ }
759
+
760
+ function downloadBackupString(btn) {
761
+ const jsonStr = btn.getAttribute('data-json');
762
+ const blob = new Blob([jsonStr], {type: "application/json"});
763
+ const url = URL.createObjectURL(blob);
764
+ const a = document.createElement('a'); a.href = url; a.download = `backup_${Date.now()}.json`;
765
+ a.click();
766
+ }
767
+
768
+ function restoreBackup(btn) {
769
+ if(!confirm("⚠️ 确定要还原此备份吗?当前未保存的修改将丢失!")) return;
770
+ try {
771
+ const jsonStr = btn.getAttribute('data-json');
772
+ appData = JSON.parse(jsonStr);
773
+ addIdsToTree(appData.tree); // Ensure IDs
774
+ renderTree();
775
+ contentPanel.classList.add('hidden');
776
+ emptyState.classList.remove('hidden');
777
+ closeSettingsModal();
778
+ alert("✅ 备份已还原,请重新选择人物。");
779
+ } catch(e) {
780
+ alert("还原失败: " + e.message);
781
+ }
782
+ }
783
+
784
+ // --- Keyboard Shortcuts Listener ---
785
+ document.addEventListener('keydown', (e) => {
786
+ // Ignore if typing in an input
787
+ const tag = e.target.tagName;
788
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
789
+ if (isEditingTag) return; // Ignore if editing a tag inline
790
+ if (hoveredTagIndex === -1) return; // Only trigger if hovering a tag
791
+
792
+ const key = e.key.toLowerCase();
793
+
794
+ if (key === SHORTCUT_CONFIG.delete.toLowerCase()) {
795
+ e.preventDefault();
796
+ performBackup('delete'); // 备份
797
+ deleteTag(hoveredTagIndex);
798
+ } else if (key === SHORTCUT_CONFIG.copy.toLowerCase()) {
799
+ e.preventDefault();
800
+ copyTagByIndex(hoveredTagIndex);
801
+ } else if (key === SHORTCUT_CONFIG.editEn.toLowerCase()) {
802
+ e.preventDefault();
803
+ triggerTagEdit(hoveredTagIndex, 'en');
804
+ } else if (key === SHORTCUT_CONFIG.editCn.toLowerCase()) {
805
+ e.preventDefault();
806
+ triggerTagEdit(hoveredTagIndex, 'cn');
807
+ }
808
+ });
809
+
810
+ // --- Logic Helpers for Shortcuts ---
811
+ function copyTagByIndex(index) {
812
+ const charName = nodeTitleDisplay.innerText;
813
+ if (!charName || !currentSkin) return;
814
+ const text = appData.prompts[charName][currentSkin] || "";
815
+ const tags = text.split(',').map(t => t.trim()).filter(t => t);
816
+ if (tags[index]) {
817
+ navigator.clipboard.writeText(tags[index]);
818
+ // Find element to show effect
819
+ const el = tagsPreview.children[index];
820
+ if(el) {
821
+ el.classList.add('copied');
822
+ setTimeout(()=>el.classList.remove('copied'), 600);
823
+ }
824
+ }
825
+ }
826
+
827
+ function triggerTagEdit(index, type) {
828
+ const el = tagsPreview.children[index];
829
+ if(!el) return;
830
+
831
+ const charName = nodeTitleDisplay.innerText;
832
+ const text = appData.prompts[charName][currentSkin] || "";
833
+ const tags = text.split(',').map(t => t.trim()).filter(t => t);
834
+ const tagText = tags[index];
835
+
836
+ if(type === 'en') {
837
+ const span = el.querySelector('.tag-en');
838
+ editEnglishTag(el, tagText, span, index);
839
+ } else {
840
+ const span = el.querySelector('.tag-cn');
841
+ enableTagEditing(el, tagText, span);
842
+ }
843
+ }
844
+
845
+
846
+ // --- File IO ---
847
+ async function openFile() {
848
+ if (!window.showOpenFilePicker) { alert("浏览器不支持 File System API,请使用 Chrome/Edge。"); return; }
849
+ try {
850
+ [fileHandle] = await window.showOpenFilePicker({ types: [{ accept: { 'application/json': ['.json'] } }] });
851
+ const file = await fileHandle.getFile();
852
+ document.getElementById('filePathDisplay').innerText = file.name;
853
+ const text = await file.text();
854
+ appData = JSON.parse(text);
855
+
856
+ if (!appData.translations) appData.translations = {};
857
+ addIdsToTree(appData.tree);
858
+ renderTree();
859
+ } catch (e) { console.error(e); }
860
+ }
861
+
862
+ async function saveFile(silent = false) {
863
+ if (!fileHandle) {
864
+ if(!silent) alert("请先打开文件,无法保存。");
865
+ return;
866
+ }
867
+ try {
868
+ // Save Backup (Auto type) as well just in case
869
+ performBackup('auto');
870
+
871
+ const jsonStr = JSON.stringify(appData, (key, value) => {
872
+ if (key === '_isExpanded') return undefined;
873
+ return value;
874
+ }, 2);
875
+
876
+ const writable = await fileHandle.createWritable();
877
+ await writable.write(jsonStr);
878
+ await writable.close();
879
+
880
+ if (!silent) {
881
+ const btn = document.getElementById('saveBtn');
882
+ const oldText = btn.innerText; btn.innerText = "✅ 已保存";
883
+ setTimeout(()=>btn.innerText=oldText, 2000);
884
+ }
885
+ } catch (e) { if(!silent) alert("保存失败: " + e.message); }
886
+ }
887
+
888
+ function exportJson() {
889
+ const jsonStr = JSON.stringify(appData, (key, value) => {
890
+ if (key === '_isExpanded') return undefined;
891
+ return value;
892
+ }, 2);
893
+ const blob = new Blob([jsonStr], {type: "application/json"});
894
+ const url = URL.createObjectURL(blob);
895
+ const a = document.createElement('a'); a.href = url; a.download = "prompt_data.json";
896
+ a.click();
897
+ }
898
+
899
+ function addIdsToTree(nodes) {
900
+ nodes.forEach(node => {
901
+ if (!node._id) node._id = Math.random().toString(36).substr(2, 9);
902
+ if (node.children && node._isExpanded === undefined) node._isExpanded = false;
903
+ if (node.children) addIdsToTree(node.children);
904
+ });
905
+ }
906
+
907
+ // --- Search Logic ---
908
+ function handleSearch() {
909
+ const query = document.getElementById('searchInput').value.toLowerCase();
910
+ function checkNode(domNode) {
911
+ const content = domNode.querySelector('.tree-content');
912
+ if(!content) return false;
913
+ const name = content.innerText.toLowerCase();
914
+ const childrenContainer = domNode.querySelector('.children-container');
915
+ let selfMatch = name.includes(query);
916
+ let childMatch = false;
917
+ if (childrenContainer) {
918
+ const children = childrenContainer.children;
919
+ for (let child of children) { if (checkNode(child)) childMatch = true; }
920
+ }
921
+ if (query === "") {
922
+ content.classList.remove('hidden-node');
923
+ return true;
924
+ }
925
+ if (selfMatch || childMatch) {
926
+ content.classList.remove('hidden-node');
927
+ if (childMatch && childrenContainer) domNode.classList.remove('collapsed');
928
+ return true;
929
+ } else {
930
+ content.classList.add('hidden-node');
931
+ return false;
932
+ }
933
+ }
934
+ const topLevelNodes = treeRoot.firstElementChild ? treeRoot.firstElementChild.children : [];
935
+ for (let node of topLevelNodes) { checkNode(node); }
936
+ }
937
+
938
+ // --- Tree Render ---
939
+ function renderTree() {
940
+ const scrollTop = treeRoot.scrollTop;
941
+ treeRoot.innerHTML = '';
942
+ const container = document.createElement('div');
943
+ buildTree(appData.tree, container, 0);
944
+ treeRoot.appendChild(container);
945
+ if(document.getElementById('searchInput').value) handleSearch();
946
+ setTimeout(() => treeRoot.scrollTop = scrollTop, 0);
947
+ }
948
+
949
+ function buildTree(nodes, parentEl, depth) {
950
+ nodes.forEach(node => {
951
+ const isFolder = !!node.children;
952
+ const wrapper = document.createElement('div');
953
+ wrapper.className = 'tree-node-container';
954
+
955
+ const content = document.createElement('div');
956
+ content.className = 'tree-content';
957
+ content.dataset.id = node._id;
958
+ content.style.paddingLeft = (depth * 20 + 10) + 'px';
959
+ content.draggable = true;
960
+
961
+ const icon = document.createElement('span');
962
+ icon.className = 'icon';
963
+ icon.innerText = isFolder ? (node._isExpanded ? '📂' : '📁') : '👤';
964
+
965
+ const text = document.createElement('span');
966
+ text.innerText = node.name;
967
+
968
+ content.append(icon, text);
969
+ content.onclick = (e) => handleNodeClick(e, node, content);
970
+
971
+ if(isFolder) {
972
+ content.ondblclick = (e) => { e.stopPropagation(); toggleFolder(node, wrapper, icon); };
973
+ icon.onclick = (e) => { e.stopPropagation(); toggleFolder(node, wrapper, icon); }
974
+ }
975
+
976
+ content.ondragstart = (e) => handleDragStart(e, node);
977
+ content.ondragover = (e) => handleDragOver(e, node, content);
978
+ content.ondrop = (e) => handleDrop(e, node);
979
+
980
+ wrapper.appendChild(content);
981
+
982
+ if (isFolder) {
983
+ if (!node._isExpanded) wrapper.classList.add('collapsed');
984
+ const childBox = document.createElement('div');
985
+ childBox.className = 'children-container';
986
+ buildTree(node.children, childBox, depth + 1);
987
+ wrapper.appendChild(childBox);
988
+ }
989
+ parentEl.appendChild(wrapper);
990
+ if (selectedNodesSet.has(node._id)) content.classList.add('selected');
991
+ });
992
+ }
993
+
994
+ function toggleFolder(node, wrapper, icon) {
995
+ node._isExpanded = !node._isExpanded;
996
+ if (node._isExpanded) {
997
+ wrapper.classList.remove('collapsed');
998
+ icon.innerText = '📂';
999
+ } else {
1000
+ wrapper.classList.add('collapsed');
1001
+ icon.innerText = '📁';
1002
+ }
1003
+ }
1004
+
1005
+ function handleNodeClick(e, node, el) {
1006
+ if (e.ctrlKey) {
1007
+ if (selectedNodesSet.has(node._id)) { selectedNodesSet.delete(node._id); el.classList.remove('selected'); }
1008
+ else { selectedNodesSet.add(node._id); el.classList.add('selected'); }
1009
+ } else if (e.shiftKey && lastSelectedNodeUI) {
1010
+ selectedNodesSet.add(node._id); el.classList.add('selected');
1011
+ } else {
1012
+ document.querySelectorAll('.tree-content.selected').forEach(x => x.classList.remove('selected'));
1013
+ selectedNodesSet.clear();
1014
+ selectedNodesSet.add(node._id);
1015
+ el.classList.add('selected');
1016
+ lastSelectedNodeUI = el;
1017
+ loadNodeData(node);
1018
+ }
1019
+ }
1020
+
1021
+ function sortTreeNodes() {
1022
+ if (!appData.tree || appData.tree.length === 0) return;
1023
+ function sortRecursive(nodes) {
1024
+ nodes.sort((a, b) => {
1025
+ const aIsFolder = !!a.children;
1026
+ const bIsFolder = !!b.children;
1027
+ if (aIsFolder && !bIsFolder) return -1;
1028
+ if (!aIsFolder && bIsFolder) return 1;
1029
+ return a.name.localeCompare(b.name, 'zh-CN');
1030
+ });
1031
+ nodes.forEach(node => { if (node.children) sortRecursive(node.children); });
1032
+ }
1033
+ sortRecursive(appData.tree);
1034
+ renderTree();
1035
+ saveFile(true);
1036
+ }
1037
+
1038
+ let draggedNodes = [];
1039
+ function handleDragStart(e, node) {
1040
+ if (!selectedNodesSet.has(node._id)) { selectedNodesSet.clear(); selectedNodesSet.add(node._id); }
1041
+ draggedNodes = Array.from(selectedNodesSet);
1042
+ e.dataTransfer.effectAllowed = 'move';
1043
+ }
1044
+ function handleDragOver(e, node, el) {
1045
+ if (node.children) { e.preventDefault(); el.classList.add('drag-over'); }
1046
+ }
1047
+ document.addEventListener('dragend', () => document.querySelectorAll('.drag-over').forEach(x=>x.classList.remove('drag-over')));
1048
+
1049
+ function handleDrop(e, targetNode) {
1050
+ e.preventDefault();
1051
+ if (!targetNode.children) return;
1052
+ targetNode._isExpanded = true;
1053
+ const moveList = [];
1054
+ const idSet = new Set(draggedNodes);
1055
+ function removeRecursive(list) {
1056
+ for(let i=list.length-1; i>=0; i--) {
1057
+ if(idSet.has(list[i]._id)) {
1058
+ if(list[i] !== targetNode) moveList.push(list[i]);
1059
+ list.splice(i, 1);
1060
+ } else if(list[i].children) removeRecursive(list[i].children);
1061
+ }
1062
+ }
1063
+ removeRecursive(appData.tree);
1064
+ targetNode.children.push(...moveList);
1065
+ renderTree();
1066
+ selectedNodesSet.clear();
1067
+ saveFile(true);
1068
+ }
1069
+
1070
+ function loadNodeData(node) {
1071
+ selectedNodeId = node._id;
1072
+ emptyState.classList.add('hidden');
1073
+ contentPanel.classList.remove('hidden');
1074
+ nodeTitleDisplay.innerText = node.name;
1075
+ nodeTypeDisplay.innerText = node.children ? "文件夹" : "人物";
1076
+ const imgPath = node.image || "";
1077
+ imgPathInput.value = imgPath;
1078
+ updatePreview(imgPath);
1079
+ if (!node.children) {
1080
+ promptSection.classList.remove('hidden');
1081
+ loadCharacterPrompts(node.name);
1082
+ } else {
1083
+ promptSection.classList.add('hidden');
1084
+ }
1085
+ }
1086
+
1087
+ function triggerFileSelect() { hiddenFileInput.click(); }
1088
+ function handleFileSelect(input) {
1089
+ if (input.files && input.files[0]) {
1090
+ const file = input.files[0];
1091
+ const blobUrl = URL.createObjectURL(file);
1092
+ updatePreview(blobUrl);
1093
+ imgPathInput.value = file.name;
1094
+ saveImageToNode(file.name);
1095
+ }
1096
+ input.value = '';
1097
+ }
1098
+ function updateImageFromInput() {
1099
+ const val = imgPathInput.value;
1100
+ updatePreview(val);
1101
+ saveImageToNode(val);
1102
+ }
1103
+ function updatePreview(src) {
1104
+ if (src) {
1105
+ imgPreview.src = src;
1106
+ imgPreview.classList.remove('hidden');
1107
+ imgPlaceholder.classList.add('hidden');
1108
+ } else {
1109
+ imgPreview.src = '';
1110
+ imgPreview.classList.add('hidden');
1111
+ imgPlaceholder.classList.remove('hidden');
1112
+ }
1113
+ }
1114
+ function saveImageToNode(path) {
1115
+ const node = findNodeById(appData.tree, selectedNodeId);
1116
+ if (node) { node.image = path; saveFile(true); }
1117
+ }
1118
+ function findNodeById(list, id) {
1119
+ for(let node of list) {
1120
+ if(node._id === id) return node;
1121
+ if(node.children) { const res = findNodeById(node.children, id); if(res) return res; }
1122
+ }
1123
+ return null;
1124
+ }
1125
+
1126
+ // --- Prompt & Tag Logic ---
1127
+ let currentSkin = null;
1128
+
1129
+ function loadCharacterPrompts(charName) {
1130
+ skinTabs.innerHTML = '';
1131
+ if (!appData.prompts[charName]) appData.prompts[charName] = {"基础": ""};
1132
+ const skins = Object.keys(appData.prompts[charName]);
1133
+ if (!currentSkin || !skins.includes(currentSkin)) { currentSkin = skins.length > 0 ? skins[0] : null; }
1134
+
1135
+ skins.forEach((skin, index) => {
1136
+ const tab = document.createElement('div');
1137
+ tab.className = `skin-tab ${skin === currentSkin ? 'active' : ''}`;
1138
+ tab.innerHTML = `${skin} <span class="close-tab" onclick="deleteSkin(event, '${skin}')">✖</span>`;
1139
+ tab.onclick = () => { currentSkin = skin; loadCharacterPrompts(charName); };
1140
+ tab.ondblclick = (e) => {
1141
+ e.stopPropagation();
1142
+ showInput(`重命名 "${skin}" 为:`, (newName) => renameSkin(charName, skin, newName));
1143
+ };
1144
+ tab.draggable = true;
1145
+ tab.ondragstart = (e) => { dragSrcTab = { skin: skin, index: index }; e.dataTransfer.effectAllowed = 'move'; tab.style.opacity = '0.4'; };
1146
+ tab.ondragend = () => { tab.style.opacity = '1'; document.querySelectorAll('.skin-tab').forEach(t => t.classList.remove('drag-over-tab')); };
1147
+ tab.ondragover = (e) => { e.preventDefault(); tab.classList.add('drag-over-tab'); return false; };
1148
+ tab.ondragleave = () => { tab.classList.remove('drag-over-tab'); };
1149
+ tab.ondrop = (e) => { e.stopPropagation(); if (dragSrcTab.skin !== skin) { reorderSkins(charName, dragSrcTab.skin, index); } return false; };
1150
+ skinTabs.appendChild(tab);
1151
+ });
1152
+
1153
+ const addBtn = document.createElement('div');
1154
+ addBtn.className = 'skin-tab add-btn';
1155
+ addBtn.style.color = 'var(--accent-color)';
1156
+ addBtn.innerText = '+';
1157
+ addBtn.onclick = () => {
1158
+ showInput('新建子内容名称', name => {
1159
+ if(name && !appData.prompts[charName][name]) {
1160
+ appData.prompts[charName][name] = "";
1161
+ currentSkin = name;
1162
+ loadCharacterPrompts(charName);
1163
+ saveFile(true);
1164
+ }
1165
+ });
1166
+ };
1167
+ skinTabs.appendChild(addBtn);
1168
+
1169
+ if (currentSkin) {
1170
+ const text = appData.prompts[charName][currentSkin] || "";
1171
+ promptEditor.value = text;
1172
+ promptEditor.disabled = false;
1173
+ renderTags(text);
1174
+ } else {
1175
+ promptEditor.value = "";
1176
+ promptEditor.disabled = true;
1177
+ renderTags("");
1178
+ }
1179
+ }
1180
+
1181
+ function renameSkin(charName, oldName, newName) {
1182
+ if (!newName || newName === oldName) return;
1183
+ if (appData.prompts[charName][newName] !== undefined) { alert("该名称已存在!"); return; }
1184
+ const keys = Object.keys(appData.prompts[charName]);
1185
+ const newObj = {};
1186
+ keys.forEach(key => {
1187
+ if (key === oldName) newObj[newName] = appData.prompts[charName][oldName];
1188
+ else newObj[key] = appData.prompts[charName][key];
1189
+ });
1190
+ appData.prompts[charName] = newObj;
1191
+ if (currentSkin === oldName) currentSkin = newName;
1192
+ loadCharacterPrompts(charName);
1193
+ saveFile(true);
1194
+ }
1195
+ function reorderSkins(charName, draggedSkinName, targetIndex) {
1196
+ const keys = Object.keys(appData.prompts[charName]);
1197
+ const oldIndex = keys.indexOf(draggedSkinName);
1198
+ if (oldIndex < 0) return;
1199
+ keys.splice(oldIndex, 1);
1200
+ keys.splice(targetIndex, 0, draggedSkinName);
1201
+ const newObj = {};
1202
+ keys.forEach(key => newObj[key] = appData.prompts[charName][key]);
1203
+ appData.prompts[charName] = newObj;
1204
+ loadCharacterPrompts(charName);
1205
+ saveFile(true);
1206
+ }
1207
+
1208
+ function handlePromptInput() {
1209
+ const charName = nodeTitleDisplay.innerText;
1210
+ if (charName && currentSkin) {
1211
+ let val = promptEditor.value;
1212
+ const cursor = promptEditor.selectionStart;
1213
+ let newVal = val.replace(/,/g, ',').replace(/,{2,}/g, ',');
1214
+ if (newVal !== val) {
1215
+ promptEditor.value = newVal;
1216
+ const diff = val.length - newVal.length;
1217
+ const newCursor = Math.max(0, cursor - diff);
1218
+ promptEditor.setSelectionRange(newCursor, newCursor);
1219
+ }
1220
+ appData.prompts[charName][currentSkin] = newVal;
1221
+ renderTags(newVal);
1222
+ }
1223
+ }
1224
+
1225
+ function renderTags(text) {
1226
+ tagsPreview.innerHTML = '';
1227
+ hoveredTagIndex = -1; // reset on render
1228
+ if (!text) return;
1229
+
1230
+ const tags = text.split(',').map(t => t.trim()).filter(t => t);
1231
+
1232
+ tags.forEach((t, index) => {
1233
+ const tagEl = document.createElement('div');
1234
+ tagEl.className = 'tag';
1235
+ tagEl.draggable = true;
1236
+ tagEl.dataset.value = t;
1237
+
1238
+ // Hover Event for Shortcuts
1239
+ tagEl.onmouseenter = () => {
1240
+ hoveredTagIndex = index;
1241
+ tagEl.classList.add('hovered-for-shortcut');
1242
+ tagEl.title = `双击编辑中文 | 快捷键: ${SHORTCUT_CONFIG.delete.toUpperCase()}(删) ${SHORTCUT_CONFIG.copy.toUpperCase()}(复) ${SHORTCUT_CONFIG.editEn.toUpperCase()}(英) ${SHORTCUT_CONFIG.editCn.toUpperCase()}(中)`;
1243
+ };
1244
+ tagEl.onmouseleave = () => {
1245
+ hoveredTagIndex = -1;
1246
+ tagEl.classList.remove('hovered-for-shortcut');
1247
+ };
1248
+
1249
+ // Delete Button
1250
+ const closeBtn = document.createElement('div');
1251
+ closeBtn.className = 'tag-close-btn';
1252
+ closeBtn.innerText = '×';
1253
+ closeBtn.onclick = (e) => {
1254
+ e.stopPropagation();
1255
+ if(confirm(`确定删除提示词 "${t}" 吗?`)) {
1256
+ performBackup('delete'); // Backup trigger
1257
+ deleteTag(index);
1258
+ }
1259
+ };
1260
+ tagEl.appendChild(closeBtn);
1261
+
1262
+ // English
1263
+ const enSpan = document.createElement('span');
1264
+ enSpan.className = 'tag-en';
1265
+ enSpan.innerText = t;
1266
+ enSpan.ondblclick = (e) => { e.stopPropagation(); editEnglishTag(tagEl, t, enSpan, index); };
1267
+
1268
+ // Chinese
1269
+ const cnSpan = document.createElement('span');
1270
+ cnSpan.className = 'tag-cn';
1271
+ const trans = appData.translations && appData.translations[t.toLowerCase()];
1272
+
1273
+ if (trans) {
1274
+ cnSpan.innerText = trans;
1275
+ } else {
1276
+ cnSpan.innerText = "双击添加";
1277
+ cnSpan.classList.add('placeholder');
1278
+ }
1279
+
1280
+ cnSpan.ondblclick = (e) => { e.stopPropagation(); enableTagEditing(tagEl, t, cnSpan); };
1281
+
1282
+ // Copy
1283
+ tagEl.onclick = (e) => {
1284
+ if(tagEl.classList.contains('editing')) return;
1285
+ navigator.clipboard.writeText(t).then(() => {
1286
+ tagEl.classList.add('copied');
1287
+ setTimeout(() => tagEl.classList.remove('copied'), 600);
1288
+ });
1289
+ };
1290
+
1291
+ tagEl.appendChild(enSpan);
1292
+ tagEl.appendChild(cnSpan);
1293
+
1294
+ // Drag
1295
+ tagEl.ondragstart = (e) => {
1296
+ if(tagEl.classList.contains('editing')) { e.preventDefault(); return; }
1297
+ dragSrcTag = { el: tagEl, index: index, value: t };
1298
+ e.dataTransfer.effectAllowed = 'move';
1299
+ e.dataTransfer.setData('text/plain', t);
1300
+ tagEl.style.opacity = '0.5';
1301
+ };
1302
+ tagEl.ondragend = () => { tagEl.style.opacity = '1'; document.querySelectorAll('.tag').forEach(el => el.classList.remove('drag-over-tag')); };
1303
+ tagEl.ondragover = (e) => { e.preventDefault(); if(!tagEl.classList.contains('editing')) tagEl.classList.add('drag-over-tag'); };
1304
+ tagEl.ondragleave = () => { tagEl.classList.remove('drag-over-tag'); };
1305
+ tagEl.ondrop = (e) => { e.stopPropagation(); if (dragSrcTag && dragSrcTag.el !== tagEl && !tagEl.classList.contains('editing')) { handleTagDrop(dragSrcTag.index, index); } return false; };
1306
+
1307
+ tagsPreview.appendChild(tagEl);
1308
+ });
1309
+ }
1310
+
1311
+ function deleteTag(indexToRemove) {
1312
+ const charName = nodeTitleDisplay.innerText;
1313
+ if (!charName || !currentSkin) return;
1314
+ let text = appData.prompts[charName][currentSkin] || "";
1315
+ let tags = text.split(',').map(t => t.trim()).filter(t => t);
1316
+ if (indexToRemove >= 0 && indexToRemove < tags.length) {
1317
+ tags.splice(indexToRemove, 1);
1318
+ const newText = tags.join(', ');
1319
+ appData.prompts[charName][currentSkin] = newText;
1320
+ promptEditor.value = newText;
1321
+ renderTags(newText);
1322
+ saveFile(true);
1323
+ }
1324
+ }
1325
+
1326
+ function editEnglishTag(tagEl, oldVal, spanEl, indexToEdit) {
1327
+ tagEl.draggable = false;
1328
+ tagEl.classList.add('editing');
1329
+ isEditingTag = true; // Lock shortcuts
1330
+
1331
+ const input = document.createElement('input');
1332
+ input.type = 'text';
1333
+ input.className = 'tag-input';
1334
+ input.style.width = '120px';
1335
+ input.value = oldVal;
1336
+
1337
+ spanEl.innerHTML = '';
1338
+ spanEl.appendChild(input);
1339
+ input.focus();
1340
+ input.select(); // Auto Select All
1341
+
1342
+ const finishEdit = () => {
1343
+ const newVal = input.value.trim().replace(/,/g, ',');
1344
+ tagEl.classList.remove('editing');
1345
+ tagEl.draggable = true;
1346
+ isEditingTag = false; // Unlock shortcuts
1347
+
1348
+ if (!newVal || newVal === oldVal) {
1349
+ const charName = nodeTitleDisplay.innerText;
1350
+ renderTags(appData.prompts[charName][currentSkin]);
1351
+ return;
1352
+ }
1353
+ const charName = nodeTitleDisplay.innerText;
1354
+ let text = appData.prompts[charName][currentSkin] || "";
1355
+ let tags = text.split(',').map(t => t.trim()).filter(t => t);
1356
+ if (indexToEdit >= 0 && indexToEdit < tags.length) {
1357
+ tags[indexToEdit] = newVal;
1358
+ const newText = tags.join(', ');
1359
+ appData.prompts[charName][currentSkin] = newText;
1360
+ promptEditor.value = newText;
1361
+ renderTags(newText);
1362
+ saveFile(true);
1363
+ }
1364
+ };
1365
+
1366
+ input.onblur = finishEdit;
1367
+ input.onkeydown = (e) => { if(e.key === 'Enter') input.blur(); e.stopPropagation(); };
1368
+ input.onclick = e => e.stopPropagation();
1369
+ }
1370
+
1371
+ function enableTagEditing(tagEl, engTag, spanEl) {
1372
+ tagEl.draggable = false;
1373
+ tagEl.classList.add('editing');
1374
+ isEditingTag = true;
1375
+ // 如果是占位符文本,则清空,否则显示原值
1376
+ const oldVal = spanEl.classList.contains('placeholder') ? '' : spanEl.innerText;
1377
+
1378
+ const input = document.createElement('input');
1379
+ input.type = 'text';
1380
+ input.className = 'tag-input';
1381
+ input.value = oldVal;
1382
+ input.placeholder = "输入中文";
1383
+
1384
+ spanEl.innerHTML = '';
1385
+ spanEl.appendChild(input);
1386
+ input.focus();
1387
+
1388
+ // 结束编辑的回调
1389
+ const finishEdit = () => {
1390
+ const newVal = input.value.trim();
1391
+ if (newVal) {
1392
+ // 保存新翻译
1393
+ appData.translations[engTag.toLowerCase()] = newVal;
1394
+ saveFile(true);
1395
+ } else if (newVal === '' && oldVal !== '') {
1396
+ // 允许用户清空翻译(如果需要的话,也可以选择不保存)
1397
+ // delete appData.translations[engTag.toLowerCase()];
1398
+ // saveFile(true);
1399
+ }
1400
+
1401
+ tagEl.classList.remove('editing');
1402
+ tagEl.draggable = true;
1403
+ isEditingTag = false;
1404
+
1405
+ // 重新渲染以更新显示
1406
+ const charName = nodeTitleDisplay.innerText;
1407
+ if(charName && currentSkin) renderTags(appData.prompts[charName][currentSkin]);
1408
+ };
1409
+
1410
+ input.onblur = finishEdit;
1411
+ input.onkeydown = (e) => {
1412
+ if(e.key === 'Enter') input.blur();
1413
+ e.stopPropagation();
1414
+ };
1415
+ input.onclick = e => e.stopPropagation();
1416
+ }
1417
+
1418
+ function handleTagDrop(fromIndex, toIndex) {
1419
+ const charName = nodeTitleDisplay.innerText;
1420
+ const text = appData.prompts[charName][currentSkin];
1421
+ let tags = text.split(',').map(t => t.trim()).filter(t => t);
1422
+ const [moved] = tags.splice(fromIndex, 1);
1423
+ tags.splice(toIndex, 0, moved);
1424
+ const newText = tags.join(', ');
1425
+ appData.prompts[charName][currentSkin] = newText;
1426
+ promptEditor.value = newText;
1427
+ renderTags(newText);
1428
+ saveFile(true);
1429
+ }
1430
+
1431
+ function deleteSkin(e, skin) {
1432
+ e.stopPropagation();
1433
+ const charName = nodeTitleDisplay.innerText;
1434
+ if(confirm(`删除 ${skin}?`)) {
1435
+ delete appData.prompts[charName][skin];
1436
+ currentSkin = null;
1437
+ loadCharacterPrompts(charName);
1438
+ saveFile(true);
1439
+ }
1440
+ }
1441
+
1442
+ // --- Settings & Translation ---
1443
+ function openSettingsModal() {
1444
+ document.getElementById('apiKeyInput').value = apiKey;
1445
+
1446
+ // Populate Shortcut Inputs
1447
+ document.getElementById('scInputCopy').value = SHORTCUT_CONFIG.copy;
1448
+ document.getElementById('scInputDel').value = SHORTCUT_CONFIG.delete;
1449
+ document.getElementById('scInputEn').value = SHORTCUT_CONFIG.editEn;
1450
+ document.getElementById('scInputCn').value = SHORTCUT_CONFIG.editCn;
1451
+
1452
+ document.getElementById('backupCountInput').value = MAX_DELETE_BACKUPS;
1453
+
1454
+ document.getElementById('settingsModal').style.display = 'flex';
1455
+ renderBackupList(); // Load backups
1456
+ }
1457
+ function closeSettingsModal() { document.getElementById('settingsModal').style.display = 'none'; }
1458
+
1459
+ function saveSettings() {
1460
+ apiKey = document.getElementById('apiKeyInput').value.trim();
1461
+ localStorage.setItem('sd_prompt_apikey', apiKey);
1462
+
1463
+ // Save Shortcuts
1464
+ SHORTCUT_CONFIG.copy = (document.getElementById('scInputCopy').value || 'c').toLowerCase();
1465
+ SHORTCUT_CONFIG.delete = (document.getElementById('scInputDel').value || 'd').toLowerCase();
1466
+ SHORTCUT_CONFIG.editEn = (document.getElementById('scInputEn').value || 'q').toLowerCase();
1467
+ SHORTCUT_CONFIG.editCn = (document.getElementById('scInputCn').value || 'w').toLowerCase();
1468
+ localStorage.setItem('sd_shortcuts', JSON.stringify(SHORTCUT_CONFIG));
1469
+
1470
+ // Save Backup Count
1471
+ const count = parseInt(document.getElementById('backupCountInput').value);
1472
+ if (count > 0) {
1473
+ MAX_DELETE_BACKUPS = count;
1474
+ localStorage.setItem('sd_max_backups', MAX_DELETE_BACKUPS);
1475
+ }
1476
+
1477
+ closeSettingsModal();
1478
+ alert("设置已保存");
1479
+
1480
+ // Refresh UI to show new shortcuts tooltip
1481
+ const charName = nodeTitleDisplay.innerText;
1482
+ if(currentSkin && appData.prompts[charName]) renderTags(appData.prompts[charName][currentSkin]);
1483
+ }
1484
+
1485
+ async function translateCurrentTags() {
1486
+ if (!apiKey) { alert("请先在设置中配置 API Key"); return; }
1487
+ const charName = nodeTitleDisplay.innerText;
1488
+ const text = appData.prompts[charName][currentSkin];
1489
+ if(!text) return;
1490
+ const tags = text.split(',').map(t => t.trim()).filter(t => t);
1491
+ const missingTags = tags.filter(t => !appData.translations[t.toLowerCase()]);
1492
+ if (missingTags.length === 0) return alert("当前页面没有需要翻译的标签");
1493
+
1494
+ const icon = document.getElementById('transBtnIcon');
1495
+ const originalIcon = icon.innerText;
1496
+ icon.className = 'loading-spinner'; icon.innerText = '';
1497
+
1498
+ try {
1499
+ const systemPrompt = "You are a translator for Stable Diffusion prompts. Translate the provided English tags into concise Chinese. Return strictly a JSON object where keys are the English tags and values are the Chinese translations.";
1500
+ const userContent = JSON.stringify(missingTags);
1501
+ const response = await fetch("https://api.deepseek.com/chat/completions", {
1502
+ method: "POST",
1503
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
1504
+ body: JSON.stringify({ model: "deepseek-chat", messages: [{ role: "system", content: systemPrompt }, { role: "user", content: `Tags: ${userContent}` }], response_format: { type: "json_object" } })
1505
+ });
1506
+ const data = await response.json();
1507
+ const resultObj = JSON.parse(data.choices[0].message.content);
1508
+ for (const [en, cn] of Object.entries(resultObj)) { if (cn) appData.translations[en.toLowerCase()] = cn; }
1509
+ renderTags(appData.prompts[charName][currentSkin]);
1510
+ saveFile(true);
1511
+ } catch (e) { console.error(e); alert("翻译出错: " + e.message); }
1512
+ finally { icon.className = ''; icon.innerText = originalIcon; }
1513
+ }
1514
+
1515
+ // --- Misc ---
1516
+ function createNewFolder() { showInput('文件夹名称', name => addItem(name, true)); }
1517
+ function createNewCharacter() { showInput('人物名称', name => addItem(name, false)); }
1518
+ function addItem(name, isFolder) {
1519
+ let list = appData.tree;
1520
+ let parentNode = null;
1521
+ if(selectedNodesSet.size === 1) {
1522
+ const id = Array.from(selectedNodesSet)[0];
1523
+ const node = findNodeById(appData.tree, id);
1524
+ if(node && node.children) { list = node.children; parentNode = node; }
1525
+ }
1526
+ const newNode = { name, _id: Math.random().toString(36).substr(2,9), _isExpanded: false };
1527
+ if(isFolder) newNode.children = [];
1528
+ else if(!appData.prompts[name]) appData.prompts[name] = {"基础": ""};
1529
+ list.push(newNode);
1530
+ if (parentNode) parentNode._isExpanded = true;
1531
+ selectedNodesSet.clear(); selectedNodesSet.add(newNode._id);
1532
+ renderTree();
1533
+ if (!isFolder) loadNodeData(newNode);
1534
+ saveFile(true);
1535
+ }
1536
+ function deleteSelectedNodes() {
1537
+ if(!selectedNodesSet.size || !confirm("确认删除选中项?")) return;
1538
+
1539
+ // 关键:在删除逻辑执行前调用备份
1540
+ performBackup('delete');
1541
+
1542
+ const ids = new Set(selectedNodesSet);
1543
+ function clean(list) {
1544
+ for(let i=list.length-1; i>=0; i--) {
1545
+ if(ids.has(list[i]._id)) {
1546
+ if(!list[i].children) delete appData.prompts[list[i].name];
1547
+ list.splice(i, 1);
1548
+ } else if(list[i].children) clean(list[i].children);
1549
+ }
1550
+ }
1551
+ clean(appData.tree);
1552
+ selectedNodesSet.clear();
1553
+ emptyState.classList.remove('hidden'); contentPanel.classList.add('hidden');
1554
+ renderTree(); saveFile(true);
1555
+ }
1556
+ let modalCb = null;
1557
+ function showInput(title, cb) { document.getElementById('modalTitle').innerText = title; document.getElementById('modalInput').value = ''; document.getElementById('inputModal').style.display = 'flex'; document.getElementById('modalInput').focus(); modalCb = cb; }
1558
+ function confirmModal() { if(modalCb && document.getElementById('modalInput').value) modalCb(document.getElementById('modalInput').value); closeModal(); }
1559
+ function closeModal() { document.getElementById('inputModal').style.display = 'none'; }
1560
+ document.getElementById('modalInput').onkeyup = e => e.key === 'Enter' && confirmModal();
1561
+ function toggleTheme() { theme = theme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', theme); document.getElementById('themeToggleBtn').innerText = theme==='light' ? '🌙' : '☀️'; }
1562
+ function deselectAll(e) { if(e.target === treeRoot) { selectedNodesSet.clear(); document.querySelectorAll('.selected').forEach(x=>x.classList.remove('selected')); contentPanel.classList.add('hidden'); emptyState.classList.remove('hidden'); } }
1563
+ function copyPrompts() { promptEditor.select(); document.execCommand('copy'); }
1564
+ </script>
1565
+ </body>
1566
+ </html>