skill-forge-hub 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +77 -22
- package/annual-report/SKILL.md +10 -3
- package/annual-report/resources/disk-and-html.md +7 -7
- package/annual-report/resources/reference.html +135 -41
- package/annual-report/user-config.md +3 -2
- package/article-to-html/SKILL.md +23 -0
- package/article-to-html/design-system.md +86 -21
- package/article-to-html/user-config.md +18 -0
- package/cli.js +71 -23
- package/daily-report/SKILL.md +10 -3
- package/daily-report/resources/disk-and-html.md +2 -2
- package/daily-report/resources/reference.html +163 -65
- package/daily-report/user-config.md +3 -2
- package/deck-stage/SKILL.md +80 -0
- package/deck-stage/package-lock.json +172 -0
- package/deck-stage/package.json +15 -0
- package/deck-stage/resources/cover-template.html +139 -0
- package/deck-stage/resources/data-schema.md +111 -0
- package/deck-stage/resources/pptx-builder.js +394 -0
- package/deck-stage/resources/usage-guide.md +20 -0
- package/deck-stage/user-config.md +26 -0
- package/magic-deck/SKILL.md +27 -13
- package/magic-deck/resources/content-guide.md +58 -0
- package/magic-deck/resources/deck-spec.md +380 -30
- package/magic-deck/resources/reference.html +1803 -82
- package/magic-deck/resources/usage-guide.md +42 -0
- package/magic-deck/user-config.md +10 -7
- package/meta-forge/SKILL.md +2 -0
- package/meta-forge/resources/forge-rules.md +1 -0
- package/package.json +16 -4
- package/prompt-pouch/SKILL.md +81 -0
- package/prompt-pouch/resources/data-format.md +67 -0
- package/prompt-pouch/resources/reference.html +2038 -0
- package/prompt-pouch/resources/usage-guide.md +32 -0
- package/prompt-pouch/user-config.md +22 -0
- package/template/SKILL.md +1 -0
- package/weekly-report/SKILL.md +10 -3
- package/weekly-report/resources/disk-and-html.md +2 -2
- package/weekly-report/resources/reference.html +184 -95
- package/weekly-report/user-config.md +3 -2
|
@@ -0,0 +1,2038 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN" data-theme="gold">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>锦囊 Prompt Pouch — 提示词管理台</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ============ 锦囊 · 深棕金配色 ============ */
|
|
9
|
+
:root, [data-theme="gold"]{ /* 暖棕金(默认) */
|
|
10
|
+
--bg:#17110c; --bg2:#201710;
|
|
11
|
+
--panel:#251b12; --panel2:#2c2015;
|
|
12
|
+
--text:#f0e6d4; --muted:#9a8a72;
|
|
13
|
+
--gold:#e8b04b; --gold2:#d98a3d; --gold-dim:rgba(232,176,75,.16);
|
|
14
|
+
--border:rgba(232,176,75,.22);
|
|
15
|
+
--tag-bg:rgba(232,176,75,.1);
|
|
16
|
+
--star:#f7d9a0;
|
|
17
|
+
--glow:rgba(232,176,75,.35);
|
|
18
|
+
--btn-ink:#241505;
|
|
19
|
+
--bar-hi:rgba(23,17,12,.96); --bar-lo:rgba(23,17,12,.88);
|
|
20
|
+
}
|
|
21
|
+
[data-theme="tech"]{ /* 深蓝科技 */
|
|
22
|
+
--bg:#0a1929; --bg2:#0f2440;
|
|
23
|
+
--panel:#132f4c; --panel2:#1a3a5c;
|
|
24
|
+
--text:#e3f0ff; --muted:#8fa8c8;
|
|
25
|
+
--gold:#38bdf8; --gold2:#0ea5e9; --gold-dim:rgba(56,189,248,.16);
|
|
26
|
+
--border:rgba(56,189,248,.22);
|
|
27
|
+
--tag-bg:rgba(56,189,248,.1);
|
|
28
|
+
--star:#7dd3fc;
|
|
29
|
+
--glow:rgba(56,189,248,.35);
|
|
30
|
+
--btn-ink:#041018;
|
|
31
|
+
--bar-hi:rgba(10,25,41,.96); --bar-lo:rgba(10,25,41,.88);
|
|
32
|
+
}
|
|
33
|
+
/* ═══ mono 深黑极简:Linear 式黑画布 + 纯白强调 + 灰阶层次(黑底白字,货真价实的黑色主题) ═══ */
|
|
34
|
+
[data-theme="mono"]{
|
|
35
|
+
--bg:#0a0a0b; --bg2:#121215;
|
|
36
|
+
--panel:#17171a; --panel2:#1e1e22;
|
|
37
|
+
--text:#f5f5f7; --muted:#9b9ba4;
|
|
38
|
+
--gold:#ffffff; --gold2:#d9d9de; --gold-dim:rgba(255,255,255,.09);
|
|
39
|
+
--border:rgba(255,255,255,.11);
|
|
40
|
+
--tag-bg:rgba(255,255,255,.07);
|
|
41
|
+
--star:#f5f5f7;
|
|
42
|
+
--glow:rgba(255,255,255,.16);
|
|
43
|
+
--btn-ink:#0a0a0b;
|
|
44
|
+
--bar-hi:rgba(10,10,11,.96); --bar-lo:rgba(10,10,11,.88);
|
|
45
|
+
}
|
|
46
|
+
/* mono 深黑组件适配:魔法棒黑白化、白标签、白星标、深色弹层、输入区半透明白、黑点白描边 */
|
|
47
|
+
[data-theme="mono"] .lg{ filter:grayscale(1) brightness(.3); }
|
|
48
|
+
[data-theme="mono"] .nav-item .icon,
|
|
49
|
+
[data-theme="mono"] .search .kw{ filter:grayscale(1) brightness(1.35); }
|
|
50
|
+
[data-theme="mono"] .tpl-badge{ color:#f5f5f7; background:rgba(255,255,255,.12); }
|
|
51
|
+
[data-theme="mono"] .op.star-on{ color:#ffffff; border-color:rgba(255,255,255,.42); background:rgba(255,255,255,.15); }
|
|
52
|
+
[data-theme="mono"] .gpop{ background:#1e1e22; border-color:rgba(255,255,255,.13); box-shadow:0 12px 32px rgba(0,0,0,.55); }
|
|
53
|
+
[data-theme="mono"] .fp-preview,
|
|
54
|
+
[data-theme="mono"] .fp-field input,
|
|
55
|
+
[data-theme="mono"] .fp-field textarea,
|
|
56
|
+
[data-theme="mono"] .fp-field select,
|
|
57
|
+
[data-theme="mono"] .fp-live textarea,
|
|
58
|
+
[data-theme="mono"] .bk-item,
|
|
59
|
+
[data-theme="mono"] .detail-content{ background:rgba(255,255,255,.05); }
|
|
60
|
+
[data-theme="mono"] .theme-dock .th-dot[data-theme="mono"]{ border:1.5px solid rgba(255,255,255,.5); }
|
|
61
|
+
[data-theme="mint"]{ /* 薄荷绿 */
|
|
62
|
+
--bg:#0d1a12; --bg2:#12241a;
|
|
63
|
+
--panel:#182f22; --panel2:#1e3a2a;
|
|
64
|
+
--text:#e6f5ec; --muted:#8ab59a;
|
|
65
|
+
--gold:#4ade80; --gold2:#16a34a; --gold-dim:rgba(74,222,128,.16);
|
|
66
|
+
--border:rgba(74,222,128,.22);
|
|
67
|
+
--tag-bg:rgba(74,222,128,.1);
|
|
68
|
+
--star:#b9ffd8;
|
|
69
|
+
--glow:rgba(74,222,128,.35);
|
|
70
|
+
--btn-ink:#04150a;
|
|
71
|
+
--bar-hi:rgba(13,26,18,.96); --bar-lo:rgba(13,26,18,.88);
|
|
72
|
+
}
|
|
73
|
+
*{ box-sizing:border-box; margin:0; padding:0; }
|
|
74
|
+
html,body{ height:100%; }
|
|
75
|
+
body{
|
|
76
|
+
background:
|
|
77
|
+
radial-gradient(1200px 500px at 70% -10%, var(--gold-dim), transparent 60%),
|
|
78
|
+
repeating-linear-gradient(90deg, rgba(255,255,255,.012) 0 1px, transparent 1px 90px),
|
|
79
|
+
linear-gradient(160deg, var(--bg) 0%, var(--bg2) 100%);
|
|
80
|
+
color:var(--text);
|
|
81
|
+
font-family:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,'PingFang SC','Microsoft YaHei',monospace;
|
|
82
|
+
min-height:100%;
|
|
83
|
+
}
|
|
84
|
+
/* ============ 顶栏 ============ */
|
|
85
|
+
header{
|
|
86
|
+
position:sticky; top:0; z-index:30;
|
|
87
|
+
background:linear-gradient(180deg, var(--bar-hi), var(--bar-lo));
|
|
88
|
+
backdrop-filter:blur(8px);
|
|
89
|
+
border-bottom:1px solid var(--border);
|
|
90
|
+
padding:18px 28px 14px;
|
|
91
|
+
}
|
|
92
|
+
.hd-row{ display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
|
|
93
|
+
.logo{
|
|
94
|
+
width:42px; height:42px; flex:0 0 auto; border-radius:12px;
|
|
95
|
+
background:linear-gradient(135deg, var(--gold), var(--gold2));
|
|
96
|
+
display:flex; align-items:center; justify-content:center;
|
|
97
|
+
font-size:22px; box-shadow:0 0 18px var(--glow);
|
|
98
|
+
}
|
|
99
|
+
.title{
|
|
100
|
+
font-size:22px; font-weight:900; letter-spacing:3px;
|
|
101
|
+
background:linear-gradient(90deg,var(--star),var(--gold) 55%,var(--gold2));
|
|
102
|
+
-webkit-background-clip:text; background-clip:text; color:transparent;
|
|
103
|
+
}
|
|
104
|
+
.sub{ font-size:12px; color:var(--muted); letter-spacing:1px; margin-top:2px; }
|
|
105
|
+
.search{
|
|
106
|
+
margin-left:auto; display:flex; align-items:center; gap:8px;
|
|
107
|
+
background:var(--panel); border:1px solid var(--border); border-radius:999px;
|
|
108
|
+
padding:7px 16px; min-width:260px;
|
|
109
|
+
}
|
|
110
|
+
.search input{
|
|
111
|
+
flex:1; border:none; outline:none; background:none; color:var(--text);
|
|
112
|
+
font-family:inherit; font-size:13px; letter-spacing:.5px;
|
|
113
|
+
}
|
|
114
|
+
.search .kw{ font-size:13px; color:var(--muted); }
|
|
115
|
+
/* ============ 主体布局 ============ */
|
|
116
|
+
.layout{ display:flex; gap:22px; padding:22px 28px 64px; max-width:1440px; margin:0 auto; }
|
|
117
|
+
/* 侧栏 */
|
|
118
|
+
aside{ width:200px; flex:0 0 auto;
|
|
119
|
+
display:flex; flex-direction:column;
|
|
120
|
+
position:sticky; top:80px; align-self:flex-start;
|
|
121
|
+
max-height:calc(100vh - 100px);
|
|
122
|
+
}
|
|
123
|
+
/* 侧栏分组滚动区:收藏 + 全部分组局部滚动(「全部」与底部操作固定),窄滚动条 */
|
|
124
|
+
.nav-scroll{
|
|
125
|
+
flex:1 1 auto; min-height:0; overflow-y:auto;
|
|
126
|
+
margin:0 -4px 0 0; padding-right:4px;
|
|
127
|
+
scrollbar-width:thin; scrollbar-color:var(--border) transparent;
|
|
128
|
+
}
|
|
129
|
+
.nav-scroll::-webkit-scrollbar{ width:5px; }
|
|
130
|
+
.nav-scroll::-webkit-scrollbar-thumb{ background:var(--border); border-radius:999px; }
|
|
131
|
+
.nav-scroll::-webkit-scrollbar-thumb:hover{ background:var(--muted); }
|
|
132
|
+
.nav-item{
|
|
133
|
+
display:flex; align-items:center; gap:10px;
|
|
134
|
+
padding:10px 14px; border-radius:10px; cursor:pointer;
|
|
135
|
+
font-size:14px; letter-spacing:1px; color:var(--muted);
|
|
136
|
+
border:1px solid transparent; margin-bottom:4px;
|
|
137
|
+
transition:all .18s ease; user-select:none;
|
|
138
|
+
}
|
|
139
|
+
.nav-item:hover{ color:var(--text); background:var(--gold-dim); }
|
|
140
|
+
.nav-item.on{
|
|
141
|
+
color:var(--gold); background:var(--gold-dim);
|
|
142
|
+
border-color:var(--border); font-weight:700;
|
|
143
|
+
}
|
|
144
|
+
.nav-item .icon{ font-size:15px; }
|
|
145
|
+
.nav-item .cnt{
|
|
146
|
+
margin-left:auto; font-size:11px; color:var(--muted);
|
|
147
|
+
background:var(--panel2); border-radius:999px; padding:2px 8px;
|
|
148
|
+
}
|
|
149
|
+
.nav-item.on .cnt{ color:var(--gold); background:var(--gold-dim); }
|
|
150
|
+
.side-tip{
|
|
151
|
+
margin-top:16px; padding:12px 14px; border-radius:10px;
|
|
152
|
+
border:1px dashed var(--border); font-size:11px; line-height:1.9; color:var(--muted);
|
|
153
|
+
}
|
|
154
|
+
.side-tip b{ color:var(--gold); font-weight:700; }
|
|
155
|
+
/* 主区 */
|
|
156
|
+
main{ flex:1; min-width:0; }
|
|
157
|
+
.bar{
|
|
158
|
+
display:flex; align-items:center; gap:12px; margin-bottom:16px; flex-wrap:wrap;
|
|
159
|
+
}
|
|
160
|
+
.bar h2{
|
|
161
|
+
font-size:15px; letter-spacing:2px; color:var(--gold);
|
|
162
|
+
font-weight:900;
|
|
163
|
+
}
|
|
164
|
+
.bar .total{ font-size:12px; color:var(--muted); }
|
|
165
|
+
.sort-box{ margin-left:auto; display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); }
|
|
166
|
+
.sort-box select{
|
|
167
|
+
background:var(--panel2); border:1px solid var(--border); color:var(--text);
|
|
168
|
+
font-family:inherit; font-size:12px; letter-spacing:1px;
|
|
169
|
+
border-radius:999px; padding:5px 12px; outline:none; cursor:pointer;
|
|
170
|
+
}
|
|
171
|
+
.sort-box select:focus{ border-color:var(--gold); }
|
|
172
|
+
.sort-status{
|
|
173
|
+
font-size:12px; color:var(--muted);
|
|
174
|
+
display:flex; align-items:center; gap:10px;
|
|
175
|
+
}
|
|
176
|
+
.sort-status.show{ color:var(--gold); }
|
|
177
|
+
.btn{
|
|
178
|
+
border:1px solid var(--border); background:var(--gold-dim); color:var(--gold);
|
|
179
|
+
font-family:inherit; font-size:12px; letter-spacing:1px;
|
|
180
|
+
padding:6px 14px; border-radius:999px; cursor:pointer; transition:all .18s ease;
|
|
181
|
+
white-space:nowrap; flex:0 0 auto;
|
|
182
|
+
}
|
|
183
|
+
.btn:hover{ background:var(--border); box-shadow:0 0 12px var(--glow); }
|
|
184
|
+
.btn.primary{
|
|
185
|
+
background:linear-gradient(135deg, var(--gold), var(--gold2)); color:var(--btn-ink);
|
|
186
|
+
font-weight:900; border:none;
|
|
187
|
+
}
|
|
188
|
+
.btn.primary:hover{ box-shadow:0 0 16px var(--glow); }
|
|
189
|
+
.btn[disabled]{ opacity:.4; cursor:not-allowed; }
|
|
190
|
+
/* 卡片列表 */
|
|
191
|
+
.list{ display:grid; grid-template-columns:repeat(auto-fill,minmax(360px,1fr)); gap:14px; }
|
|
192
|
+
.card{
|
|
193
|
+
background:linear-gradient(180deg, var(--panel) 0%, var(--panel2) 100%);
|
|
194
|
+
border:1px solid var(--border); border-radius:14px;
|
|
195
|
+
padding:16px 18px 14px; position:relative;
|
|
196
|
+
transition:transform .18s ease, box-shadow .18s ease, border-color .18s ease;
|
|
197
|
+
cursor:grab; display:flex; flex-direction:column; gap:8px;
|
|
198
|
+
}
|
|
199
|
+
.card:hover{ transform:translateY(-3px); box-shadow:0 8px 24px rgba(0,0,0,.35); border-color:var(--glow); }
|
|
200
|
+
[data-theme="mono"] .card{ box-shadow:0 1px 0 rgba(255,255,255,.05) inset; }
|
|
201
|
+
[data-theme="mono"] .card:hover{ box-shadow:0 10px 30px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.09); }
|
|
202
|
+
.card.dragging{ opacity:.35; cursor:grabbing; }
|
|
203
|
+
.card.drop-target{ border-color:var(--gold); box-shadow:0 0 0 2px var(--gold-dim); }
|
|
204
|
+
.card-top{ display:flex; align-items:center; gap:8px; }
|
|
205
|
+
.usage-info{
|
|
206
|
+
margin-left:auto; flex:0 0 auto; font-size:10.5px; color:var(--muted);
|
|
207
|
+
letter-spacing:.5px; white-space:nowrap;
|
|
208
|
+
}
|
|
209
|
+
.card-title{
|
|
210
|
+
font-size:15px; font-weight:900; letter-spacing:1px; color:var(--text);
|
|
211
|
+
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* 搜索命中高亮(四主题令牌适配) */
|
|
215
|
+
mark.hl{ background:var(--gold-dim); color:var(--gold); border-radius:3px; padding:0 2px; font-weight:700; }
|
|
216
|
+
.hl-note{ font-size:10px; color:var(--gold); background:var(--gold-dim); border:1px solid var(--border); border-radius:4px; padding:0 5px; line-height:16px; margin-left:6px; vertical-align:middle; }
|
|
217
|
+
.gbadge{
|
|
218
|
+
flex:0 0 auto; font-size:10px; letter-spacing:1px; color:var(--gold);
|
|
219
|
+
background:var(--tag-bg); border:1px solid var(--border);
|
|
220
|
+
border-radius:999px; padding:2px 8px;
|
|
221
|
+
}
|
|
222
|
+
.card-body{
|
|
223
|
+
font-size:12.5px; line-height:1.85; color:var(--muted);
|
|
224
|
+
display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
|
|
225
|
+
word-break:break-word;
|
|
226
|
+
}
|
|
227
|
+
.card.expanded .card-body{ display:block; }
|
|
228
|
+
.card-expand{
|
|
229
|
+
border:none; background:none; color:var(--gold); cursor:pointer;
|
|
230
|
+
font-family:inherit; font-size:11px; letter-spacing:1px; padding:0;
|
|
231
|
+
}
|
|
232
|
+
.card-meta{ display:flex; align-items:center; gap:8px; }
|
|
233
|
+
.card-time{
|
|
234
|
+
margin-left:auto; flex:0 0 auto; font-size:10.5px; color:var(--muted);
|
|
235
|
+
letter-spacing:.5px; white-space:nowrap;
|
|
236
|
+
display:inline-flex; align-items:center; gap:4px;
|
|
237
|
+
}
|
|
238
|
+
.card-time .ct-ico{ color:var(--gold); flex:0 0 auto; }
|
|
239
|
+
.card-foot{ display:flex; align-items:center; gap:8px; margin-top:2px; }
|
|
240
|
+
.tag{
|
|
241
|
+
font-size:10px; color:var(--muted); background:var(--panel2);
|
|
242
|
+
border:1px solid var(--border); border-radius:999px; padding:2px 8px; letter-spacing:.5px;
|
|
243
|
+
}
|
|
244
|
+
.card-ops{ margin-left:auto; display:flex; gap:6px; opacity:0; transition:opacity .18s ease; }
|
|
245
|
+
.card:hover .card-ops{ opacity:1; }
|
|
246
|
+
.op{
|
|
247
|
+
width:26px; height:26px; border-radius:8px; border:1px solid var(--border);
|
|
248
|
+
background:var(--gold-dim); color:var(--gold); cursor:pointer;
|
|
249
|
+
font-size:12px; display:flex; align-items:center; justify-content:center; transition:all .15s ease;
|
|
250
|
+
}
|
|
251
|
+
.op:hover{ background:var(--border); }
|
|
252
|
+
.op.star-on{ color:var(--star); border-color:rgba(245,201,92,.5); background:rgba(245,201,92,.12); }
|
|
253
|
+
.gpop{
|
|
254
|
+
display:none; position:fixed; z-index:300; min-width:180px; max-width:260px;
|
|
255
|
+
background:#16191f; border:1px solid var(--border); border-radius:12px;
|
|
256
|
+
padding:8px; box-shadow:0 12px 32px rgba(0,0,0,.5);
|
|
257
|
+
}
|
|
258
|
+
.gpop.open{ display:block; }
|
|
259
|
+
.gpop-head{ font-size:11px; color:var(--muted); letter-spacing:1px; padding:2px 8px 6px; }
|
|
260
|
+
.gpop-item{
|
|
261
|
+
font-size:13px; color:var(--text); padding:7px 10px; border-radius:8px; cursor:pointer;
|
|
262
|
+
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
|
263
|
+
}
|
|
264
|
+
.gpop-item:hover{ background:var(--gold-dim); }
|
|
265
|
+
.gpop-item.cur{ color:var(--muted); cursor:default; }
|
|
266
|
+
.copy-feedback{
|
|
267
|
+
position:absolute; right:14px; top:-10px; font-size:11px; color:var(--gold);
|
|
268
|
+
background:var(--panel2); border:1px solid var(--border);
|
|
269
|
+
border-radius:999px; padding:3px 10px; opacity:0; pointer-events:none; transition:opacity .2s ease;
|
|
270
|
+
}
|
|
271
|
+
.copy-feedback.show{ opacity:1; }
|
|
272
|
+
.empty{
|
|
273
|
+
grid-column:1/-1; text-align:center; padding:70px 0; color:var(--muted); font-size:13px; letter-spacing:1px;
|
|
274
|
+
}
|
|
275
|
+
/* ============ 模板插值填写框 ============ */
|
|
276
|
+
.modal-mask{
|
|
277
|
+
position:fixed; inset:0; z-index:50; display:none; align-items:center; justify-content:center;
|
|
278
|
+
background:rgba(0,0,0,.62); backdrop-filter:blur(5px);
|
|
279
|
+
}
|
|
280
|
+
.modal-mask.open{ display:flex; }
|
|
281
|
+
.fill-panel{
|
|
282
|
+
width:540px; max-width:94vw; max-height:86vh; display:flex; flex-direction:column;
|
|
283
|
+
background:linear-gradient(180deg, var(--panel) 0%, var(--panel2) 100%);
|
|
284
|
+
border:1px solid var(--border); border-radius:16px; padding:22px 24px;
|
|
285
|
+
box-shadow:0 20px 60px rgba(0,0,0,.5);
|
|
286
|
+
}
|
|
287
|
+
.fp-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
|
|
288
|
+
.fp-title{
|
|
289
|
+
font-size:16px; font-weight:900; letter-spacing:2px;
|
|
290
|
+
background:linear-gradient(90deg,var(--star),var(--gold));
|
|
291
|
+
-webkit-background-clip:text; background-clip:text; color:transparent;
|
|
292
|
+
}
|
|
293
|
+
.fp-count{ font-size:12px; color:var(--muted); }
|
|
294
|
+
.fp-count b{ color:var(--gold); }
|
|
295
|
+
.fp-preview{
|
|
296
|
+
max-height:110px; overflow-y:auto; padding:10px 12px; margin-bottom:12px;
|
|
297
|
+
background:rgba(0,0,0,.22); border:1px solid var(--border); border-radius:10px;
|
|
298
|
+
font-size:12px; line-height:1.8; color:var(--muted); white-space:pre-wrap; word-break:break-word;
|
|
299
|
+
}
|
|
300
|
+
.var-badge{
|
|
301
|
+
display:inline-block; padding:0 6px; margin:0 1px; border-radius:5px;
|
|
302
|
+
background:var(--gold-dim); color:var(--gold); border:1px solid var(--border); font-weight:700;
|
|
303
|
+
}
|
|
304
|
+
.fp-fields{ display:flex; flex-direction:column; gap:10px; margin-bottom:12px; }
|
|
305
|
+
.fp-field label{
|
|
306
|
+
display:block; font-size:12px; font-weight:700; color:var(--gold);
|
|
307
|
+
margin-bottom:4px; letter-spacing:.5px; word-break:break-all;
|
|
308
|
+
}
|
|
309
|
+
.fp-field input{
|
|
310
|
+
width:100%; padding:9px 12px; border-radius:9px;
|
|
311
|
+
background:rgba(0,0,0,.25); border:1px solid var(--border); outline:none;
|
|
312
|
+
color:var(--text); font-family:inherit; font-size:13px; transition:border-color .18s ease, box-shadow .18s ease;
|
|
313
|
+
}
|
|
314
|
+
.fp-field input:focus{ border-color:var(--gold); box-shadow:0 0 0 3px var(--gold-dim); }
|
|
315
|
+
.fp-live{ margin-bottom:14px; }
|
|
316
|
+
.fp-live-label{
|
|
317
|
+
display:flex; align-items:center; justify-content:space-between;
|
|
318
|
+
font-size:11px; color:var(--muted); margin-bottom:5px; letter-spacing:.5px;
|
|
319
|
+
}
|
|
320
|
+
.fp-live textarea{
|
|
321
|
+
width:100%; height:96px; resize:none; padding:9px 12px; border-radius:9px;
|
|
322
|
+
background:rgba(0,0,0,.25); border:1px solid var(--border); outline:none;
|
|
323
|
+
color:var(--muted); font-family:inherit; font-size:12px; line-height:1.7;
|
|
324
|
+
}
|
|
325
|
+
.fp-ops{ display:flex; justify-content:flex-end; gap:10px; }
|
|
326
|
+
/* 备份与数据安全 */
|
|
327
|
+
.bk-wrap{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
328
|
+
.bk-status{ font-size:12px; color:var(--muted); letter-spacing:1px; }
|
|
329
|
+
.cache-badge{
|
|
330
|
+
font-size:11px; letter-spacing:1px; color:var(--gold);
|
|
331
|
+
background:var(--gold-dim); border:1px solid var(--border);
|
|
332
|
+
padding:4px 10px; border-radius:999px;
|
|
333
|
+
}
|
|
334
|
+
.bk-list{ display:flex; flex-direction:column; gap:8px; max-height:340px; overflow-y:auto; }
|
|
335
|
+
.bk-item{
|
|
336
|
+
display:flex; align-items:center; gap:10px; padding:10px 12px;
|
|
337
|
+
border:1px solid var(--border); border-radius:10px; background:rgba(0,0,0,.2);
|
|
338
|
+
}
|
|
339
|
+
.bk-item-main{ flex:1; display:flex; flex-direction:column; gap:2px; font-size:12px; color:var(--muted); }
|
|
340
|
+
.bk-item-main b{ color:var(--gold); font-size:13px; }
|
|
341
|
+
.bk-empty{ font-size:12px; color:var(--muted); padding:12px 4px; line-height:1.8; }
|
|
342
|
+
.tpl-badge{
|
|
343
|
+
flex:0 0 auto; font-size:10px; letter-spacing:1px; color:var(--btn-ink); font-weight:900;
|
|
344
|
+
background:linear-gradient(135deg, var(--gold), var(--gold2));
|
|
345
|
+
border-radius:999px; padding:2px 8px;
|
|
346
|
+
}
|
|
347
|
+
/* ═══ 底部帮助抽屉:默认收起为中央问号胶囊,点击展开条理化指南,ESC/× 收起;主题坞独立右下悬浮互不干扰 ═══ */
|
|
348
|
+
footer{
|
|
349
|
+
position:fixed; left:0; right:0; bottom:0; z-index:30;
|
|
350
|
+
display:flex; flex-direction:column; align-items:center; gap:6px;
|
|
351
|
+
pointer-events:none; /* 收起态不挡内容区滚动与点击 */
|
|
352
|
+
}
|
|
353
|
+
.help-trigger{
|
|
354
|
+
pointer-events:auto; cursor:pointer; user-select:none;
|
|
355
|
+
display:flex; align-items:center; gap:7px;
|
|
356
|
+
padding:6px 18px 7px; border-radius:999px 999px 0 0;
|
|
357
|
+
background:linear-gradient(0deg, var(--bar-hi), var(--bar-lo));
|
|
358
|
+
backdrop-filter:blur(8px);
|
|
359
|
+
border:1px solid var(--border); border-bottom:none;
|
|
360
|
+
font-size:12px; letter-spacing:1px; color:var(--muted);
|
|
361
|
+
transition:color .18s ease, transform .18s ease, box-shadow .18s ease;
|
|
362
|
+
}
|
|
363
|
+
.help-trigger:hover{ color:var(--gold); transform:translateY(-2px); box-shadow:0 -2px 12px var(--glow); }
|
|
364
|
+
.help-trigger .ht-icon{
|
|
365
|
+
width:15px; height:15px; border-radius:50%; flex:0 0 auto;
|
|
366
|
+
border:1.5px solid currentColor; display:flex; align-items:center; justify-content:center;
|
|
367
|
+
font-size:10px; font-weight:900; line-height:1;
|
|
368
|
+
}
|
|
369
|
+
.help-trigger .ht-arrow{ font-size:9px; line-height:1; transition:transform .25s ease; }
|
|
370
|
+
.help-panel{
|
|
371
|
+
pointer-events:auto; width:100%; max-width:1100px;
|
|
372
|
+
background:linear-gradient(0deg, var(--panel), var(--panel2));
|
|
373
|
+
border:1px solid var(--border); border-bottom:none; border-radius:16px 16px 0 0;
|
|
374
|
+
box-shadow:0 -10px 36px rgba(0,0,0,.2);
|
|
375
|
+
max-height:0; overflow:hidden; padding:0 24px;
|
|
376
|
+
transition:max-height .32s ease, padding .32s ease;
|
|
377
|
+
}
|
|
378
|
+
footer.open .help-panel{ max-height:320px; padding:18px 24px 16px; }
|
|
379
|
+
footer.open .ht-arrow{ transform:rotate(180deg); }
|
|
380
|
+
.hp-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
|
|
381
|
+
.hp-head b{ font-size:13px; letter-spacing:2px; color:var(--gold); }
|
|
382
|
+
.hp-close{
|
|
383
|
+
border:1px solid var(--border); background:var(--gold-dim); color:var(--gold);
|
|
384
|
+
width:24px; height:24px; border-radius:8px; cursor:pointer; font-size:13px; line-height:1;
|
|
385
|
+
display:flex; align-items:center; justify-content:center; transition:all .15s ease;
|
|
386
|
+
}
|
|
387
|
+
.hp-close:hover{ background:var(--border); }
|
|
388
|
+
.hp-grid{ display:grid; grid-template-columns:1fr 1fr; gap:10px 20px; }
|
|
389
|
+
.hp-item .hp-t{ font-size:12px; font-weight:900; letter-spacing:1px; color:var(--gold); margin-bottom:4px; }
|
|
390
|
+
.hp-item .hp-d{ font-size:11.5px; line-height:1.85; color:var(--muted); }
|
|
391
|
+
.hp-item .hp-d b{ color:var(--gold); }
|
|
392
|
+
@media (max-width:820px){
|
|
393
|
+
.hp-grid{ grid-template-columns:1fr; }
|
|
394
|
+
footer.open .help-panel{ max-height:62vh; overflow-y:auto; }
|
|
395
|
+
}
|
|
396
|
+
/* 页面直写:侧栏分组管理 / 提示词表单 / toast */
|
|
397
|
+
.nav-add{
|
|
398
|
+
display:flex; align-items:center; justify-content:center; gap:6px;
|
|
399
|
+
margin-top:10px; padding:10px 14px; border-radius:10px; cursor:pointer;
|
|
400
|
+
font-size:13px; letter-spacing:1px; color:var(--gold);
|
|
401
|
+
border:1px dashed var(--border); background:var(--gold-dim);
|
|
402
|
+
transition:all .18s ease; user-select:none;
|
|
403
|
+
}
|
|
404
|
+
.nav-add:hover{ background:var(--border); box-shadow:0 0 12px var(--glow); }
|
|
405
|
+
.nav-item .g-name{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
406
|
+
.nav-item .g-ops{ display:none; gap:4px; margin-left:6px; flex:0 0 auto; }
|
|
407
|
+
.nav-item:hover .g-ops{ display:flex; }
|
|
408
|
+
.nav-item .g-ops button{
|
|
409
|
+
width:20px; height:20px; border-radius:6px; border:1px solid var(--border);
|
|
410
|
+
background:var(--panel2); color:var(--muted); cursor:pointer; font-size:10px;
|
|
411
|
+
display:flex; align-items:center; justify-content:center; transition:all .15s ease;
|
|
412
|
+
}
|
|
413
|
+
.nav-item .g-ops button:hover{ color:var(--gold); border-color:var(--gold); }
|
|
414
|
+
.nav-item .g-ops .g-del:hover{ color:#e06c5b; border-color:#e06c5b; }
|
|
415
|
+
.fp-field textarea{
|
|
416
|
+
width:100%; min-height:170px; resize:vertical; padding:9px 12px; border-radius:9px;
|
|
417
|
+
background:rgba(0,0,0,.25); border:1px solid var(--border); outline:none;
|
|
418
|
+
color:var(--text); font-family:inherit; font-size:13px; line-height:1.7; transition:border-color .18s ease;
|
|
419
|
+
}
|
|
420
|
+
.fp-field textarea:focus{ border-color:var(--gold); box-shadow:0 0 0 3px var(--gold-dim); }
|
|
421
|
+
.fp-field select{
|
|
422
|
+
width:100%; padding:9px 12px; border-radius:9px;
|
|
423
|
+
background:rgba(0,0,0,.25); border:1px solid var(--border); outline:none;
|
|
424
|
+
color:var(--text); font-family:inherit; font-size:13px; cursor:pointer;
|
|
425
|
+
}
|
|
426
|
+
.fp-field select:focus{ border-color:var(--gold); }
|
|
427
|
+
.op-del:hover{ color:#e06c5b; border-color:#e06c5b; background:rgba(224,108,91,.12); }
|
|
428
|
+
/* 提示词详情弹窗 */
|
|
429
|
+
.detail-panel{ width:660px; }
|
|
430
|
+
/* 弹框尺寸:表单类加宽、内容区加高,避免局促 */
|
|
431
|
+
#itemMask .fill-panel, #fillMask .fill-panel{ width:640px; }
|
|
432
|
+
#bkMask .fill-panel{ width:560px; }
|
|
433
|
+
.detail-content{
|
|
434
|
+
max-height:56vh; overflow-y:auto; padding:14px 16px; margin-bottom:10px;
|
|
435
|
+
background:rgba(0,0,0,.22); border:1px solid var(--border); border-radius:10px;
|
|
436
|
+
font-size:13px; line-height:1.9; color:var(--text); white-space:pre-wrap; word-break:break-word;
|
|
437
|
+
}
|
|
438
|
+
.detail-vars{
|
|
439
|
+
display:flex; flex-wrap:wrap; align-items:center; gap:6px;
|
|
440
|
+
font-size:11px; color:var(--muted); margin-bottom:12px;
|
|
441
|
+
}
|
|
442
|
+
.detail-vars b{ color:var(--gold); }
|
|
443
|
+
.detail-times{
|
|
444
|
+
display:flex; flex-wrap:wrap; gap:4px 16px;
|
|
445
|
+
font-size:11px; color:var(--muted); margin-bottom:10px;
|
|
446
|
+
}
|
|
447
|
+
.toast{
|
|
448
|
+
position:fixed; left:50%; bottom:64px; transform:translateX(-50%) translateY(10px);
|
|
449
|
+
z-index:400; max-width:76vw; padding:10px 20px; border-radius:999px;
|
|
450
|
+
background:linear-gradient(135deg, var(--panel), var(--panel2));
|
|
451
|
+
border:1px solid var(--border); color:var(--gold); font-size:12px; letter-spacing:1px;
|
|
452
|
+
box-shadow:0 8px 28px rgba(0,0,0,.45); opacity:0; pointer-events:none; transition:all .25s ease;
|
|
453
|
+
}
|
|
454
|
+
.toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
|
|
455
|
+
.toast.warn{ color:#e06c5b; border-color:rgba(224,108,91,.5); }
|
|
456
|
+
@media (max-width:820px){
|
|
457
|
+
.layout{ flex-direction:column; padding:16px 16px 72px; }
|
|
458
|
+
aside{ width:100%; display:flex; flex-wrap:wrap; gap:6px; position:static; max-height:none; }
|
|
459
|
+
.nav-scroll{ display:contents; overflow:visible; }
|
|
460
|
+
.nav-item{ margin-bottom:0; }
|
|
461
|
+
.side-tip{ display:none; }
|
|
462
|
+
.search{ min-width:0; width:100%; margin-left:0; }
|
|
463
|
+
.list{ grid-template-columns:1fr; }
|
|
464
|
+
.card-ops{ opacity:1; }
|
|
465
|
+
}
|
|
466
|
+
/* 主题换肤坞:右下角四色点随点随换,记忆上次选择;打印隐藏 */
|
|
467
|
+
.theme-dock{ position:fixed; right:18px; bottom:18px; z-index:1001; display:flex; gap:8px;
|
|
468
|
+
padding:8px 10px; border-radius:999px; background:var(--panel2); border:1px solid var(--border);
|
|
469
|
+
box-shadow:0 4px 18px rgba(0,0,0,.3); }
|
|
470
|
+
.theme-dock .th-dot{ width:22px; height:22px; border-radius:50%; cursor:pointer;
|
|
471
|
+
border:2px solid transparent; padding:0; transition:transform .15s ease, border-color .15s ease; }
|
|
472
|
+
.theme-dock .th-dot:hover{ transform:scale(1.18); }
|
|
473
|
+
.theme-dock .th-dot.on{ border-color:var(--text); }
|
|
474
|
+
.theme-dock .th-dot[data-theme="gold"]{ background:#e8b04b; }
|
|
475
|
+
.theme-dock .th-dot[data-theme="tech"]{ background:#0ea5e9; }
|
|
476
|
+
.theme-dock .th-dot[data-theme="mono"]{ background:#111; }
|
|
477
|
+
.theme-dock .th-dot[data-theme="mint"]{ background:#4ade80; }
|
|
478
|
+
@media print{ .theme-dock{ display:none; } }
|
|
479
|
+
</style>
|
|
480
|
+
</head>
|
|
481
|
+
<body>
|
|
482
|
+
|
|
483
|
+
<header>
|
|
484
|
+
<div class="hd-row">
|
|
485
|
+
<div class="logo"><span class="lg">🪄</span></div>
|
|
486
|
+
<div>
|
|
487
|
+
<div class="title">锦囊 · PROMPT POUCH</div>
|
|
488
|
+
<div class="sub">妙计入囊 · 随取随用</div>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="search">
|
|
491
|
+
<span class="kw">🔍</span>
|
|
492
|
+
<input id="searchInput" type="text" placeholder="搜标题 / 内容 / 标签…" />
|
|
493
|
+
<span class="kw" id="searchClear" style="cursor:pointer;display:none;">✕</span>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
</header>
|
|
497
|
+
|
|
498
|
+
<div class="layout">
|
|
499
|
+
<aside id="sideNav"></aside>
|
|
500
|
+
|
|
501
|
+
<main>
|
|
502
|
+
<div class="bar">
|
|
503
|
+
<h2 id="curTitle">全部提示词</h2>
|
|
504
|
+
<span class="total" id="curTotal"></span>
|
|
505
|
+
<button class="btn primary" id="btnAddItem">+ 新增提示词</button>
|
|
506
|
+
<div class="sort-box">
|
|
507
|
+
<label for="sortSel">排序</label>
|
|
508
|
+
<select id="sortSel" title="切换卡片排序方式">
|
|
509
|
+
<option value="added">最新添加</option>
|
|
510
|
+
<option value="recent">最近使用</option>
|
|
511
|
+
<option value="freq">使用频率</option>
|
|
512
|
+
<option value="manual">手动顺序</option>
|
|
513
|
+
</select>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="sort-status" id="sortStatus">
|
|
516
|
+
<button class="btn" id="btnSortCmd" disabled>📋 复制变更指令</button>
|
|
517
|
+
<button class="btn" id="btnMarkSaved" style="display:none" title="把指令交给 AI 落盘后点此清除未保存标记">✓ 已落盘</button>
|
|
518
|
+
</div>
|
|
519
|
+
<div class="bk-wrap" id="bkWrap">
|
|
520
|
+
<span class="cache-badge" id="cacheBadge" style="display:none">⚠️ 缓存模式:cache/prompts.json 读取失败,当前显示上次快照</span>
|
|
521
|
+
<button class="btn" id="btnBkEnable" style="display:none">💾 授权目录备份</button>
|
|
522
|
+
<span class="bk-status" id="bkStatus">本地快照备份中</span>
|
|
523
|
+
<button class="btn" id="btnBkChange" style="display:none">📂 换目录</button>
|
|
524
|
+
<button class="btn" id="btnBkRestore" style="display:none">↩ 恢复备份</button>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="list" id="cardList"></div>
|
|
528
|
+
</main>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<!-- 分组选择浮层 -->
|
|
532
|
+
<div class="gpop" id="gpop"></div>
|
|
533
|
+
|
|
534
|
+
<!-- 模板插值填写框 -->
|
|
535
|
+
<div class="modal-mask" id="fillMask">
|
|
536
|
+
<div class="fill-panel">
|
|
537
|
+
<div class="fp-head">
|
|
538
|
+
<div class="fp-title">填写模板变量</div>
|
|
539
|
+
<span class="fp-count">含 <b id="fpVarCount"></b> 个变量</span>
|
|
540
|
+
</div>
|
|
541
|
+
<div class="fp-preview" id="fpPreview"></div>
|
|
542
|
+
<div class="fp-fields" id="fpFields"></div>
|
|
543
|
+
<div class="fp-live">
|
|
544
|
+
<div class="fp-live-label"><span>复制内容预览</span><span>未填写的变量保留原样</span></div>
|
|
545
|
+
<textarea id="fpLive" readonly></textarea>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="fp-ops">
|
|
548
|
+
<button class="btn" id="fpCancel">取消</button>
|
|
549
|
+
<button class="btn primary" id="fpCopy">⧉ 复制</button>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<!-- 从备份恢复 -->
|
|
555
|
+
<div class="modal-mask" id="bkMask">
|
|
556
|
+
<div class="fill-panel">
|
|
557
|
+
<div class="fp-head">
|
|
558
|
+
<div class="fp-title">从备份恢复</div>
|
|
559
|
+
<span class="fp-count" id="bkMaskNote">backup/ 目录滚动备份 + 本地快照</span>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="bk-list" id="bkList"></div>
|
|
562
|
+
<div class="fp-ops">
|
|
563
|
+
<button class="btn" id="bkCancelBtn">关闭</button>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<!-- 新增/编辑提示词 -->
|
|
569
|
+
<div class="modal-mask" id="itemMask">
|
|
570
|
+
<div class="fill-panel">
|
|
571
|
+
<div class="fp-head">
|
|
572
|
+
<div class="fp-title" id="itemTitle">新增提示词</div>
|
|
573
|
+
<span class="fp-count">落盘 cache/prompts.json · 刷新即见</span>
|
|
574
|
+
</div>
|
|
575
|
+
<div class="fp-fields">
|
|
576
|
+
<div class="fp-field"><label for="itemName">标题</label><input id="itemName" placeholder="如:日报速写" /></div>
|
|
577
|
+
<div class="fp-field"><label for="itemGroup">分组</label><select id="itemGroup"></select></div>
|
|
578
|
+
<div class="fp-field"><label for="itemTags">标签(逗号分隔,最多 3 个)</label><input id="itemTags" placeholder="如:日报,工作" /></div>
|
|
579
|
+
<div class="fp-field"><label for="itemContent">内容(支持 {变量} 插值)</label><textarea id="itemContent" placeholder="提示词正文……"></textarea></div>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="fp-ops">
|
|
582
|
+
<button class="btn" id="itemCancel">取消</button>
|
|
583
|
+
<button class="btn primary" id="itemSave">保存</button>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<!-- 新建/编辑分组 -->
|
|
589
|
+
<div class="modal-mask" id="groupMask">
|
|
590
|
+
<div class="fill-panel" style="width:460px;">
|
|
591
|
+
<div class="fp-head">
|
|
592
|
+
<div class="fp-title" id="groupTitle">新建分组</div>
|
|
593
|
+
<span class="fp-count">落盘 cache/prompts.json · 刷新即见</span>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="fp-fields">
|
|
596
|
+
<div class="fp-field"><label for="groupName">分组名</label><input id="groupName" placeholder="如:灵感" /></div>
|
|
597
|
+
<div class="fp-field"><label for="groupIcon">图标(一个 emoji)</label><input id="groupIcon" placeholder="如:✨" maxlength="4" /></div>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="fp-ops">
|
|
600
|
+
<button class="btn" id="groupCancel">取消</button>
|
|
601
|
+
<button class="btn primary" id="groupSave">保存</button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<!-- 提示词详情 -->
|
|
607
|
+
<div class="modal-mask" id="detailMask">
|
|
608
|
+
<div class="fill-panel detail-panel">
|
|
609
|
+
<div class="fp-head">
|
|
610
|
+
<div class="fp-title" id="detailTitle">详情</div>
|
|
611
|
+
<span class="fp-count" id="detailMeta"></span>
|
|
612
|
+
</div>
|
|
613
|
+
<div class="detail-times" id="detailTimes" style="display:none"></div>
|
|
614
|
+
<div class="detail-content" id="detailContent"></div>
|
|
615
|
+
<div class="detail-vars" id="detailVars" style="display:none"></div>
|
|
616
|
+
<div class="fp-ops">
|
|
617
|
+
<button class="btn" id="detailClose">关闭</button>
|
|
618
|
+
<button class="btn" id="detailEdit">✎ 修改</button>
|
|
619
|
+
<button class="btn primary" id="detailCopy">⧉ 复制</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div class="toast" id="toast"></div>
|
|
625
|
+
|
|
626
|
+
<footer>
|
|
627
|
+
<div class="help-trigger" id="helpTrigger" title="使用帮助(点击展开 / 收起)">
|
|
628
|
+
<span class="ht-icon">?</span><span>使用帮助</span><span class="ht-arrow">∧</span>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="help-panel" id="helpPanel">
|
|
631
|
+
<div class="hp-head">
|
|
632
|
+
<b>锦囊使用指南</b>
|
|
633
|
+
<button class="hp-close" id="helpClose" title="收起">×</button>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="hp-grid">
|
|
636
|
+
<div class="hp-item">
|
|
637
|
+
<div class="hp-t">① 添加与修改</div>
|
|
638
|
+
<div class="hp-d">页面点 <b>「+ 新增提示词」/「👁 详情」/「✎」/「🗑」</b>(授权目录即写即存),或对 AI 说 <b>「把 XX 加到锦囊的写作输出分组」</b>,AI 写入 cache/prompts.json,刷新即见。</div>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="hp-item">
|
|
641
|
+
<div class="hp-t">② 排序与移动</div>
|
|
642
|
+
<div class="hp-d">默认最新添加在前,可切「最近使用 / 使用频率 / 手动顺序」;拖动卡片或点 ▲▼ 自动切回手动、点 ⇄ 移到其它分组,点 <b>📋 复制变更指令</b> 交给 AI 落盘。</div>
|
|
643
|
+
</div>
|
|
644
|
+
<div class="hp-item">
|
|
645
|
+
<div class="hp-t">③ 打开方式</div>
|
|
646
|
+
<div class="hp-d">HTTP 访问读 cache/prompts.json;file:// 直开则回退到本地缓存快照。</div>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="hp-item">
|
|
649
|
+
<div class="hp-t">④ 数据安全</div>
|
|
650
|
+
<div class="hp-d">收藏与排序存浏览器本地(IndexedDB)刷新不丢;每次变更 2 秒内自动备份到 backup/pouch-backup.json(滚动双备份),未授权时降级本地快照(保留 5 份);<b>「📂 换目录」</b> 可随时重配,↩ 一键恢复。</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
</footer>
|
|
655
|
+
|
|
656
|
+
<script>
|
|
657
|
+
/* ============ 锦囊管理台 ============ */
|
|
658
|
+
(function(){
|
|
659
|
+
var state = {
|
|
660
|
+
data: null,
|
|
661
|
+
curGroup: 'all', // 'all' | 'fav' | group.id
|
|
662
|
+
query: '',
|
|
663
|
+
dirty: false,
|
|
664
|
+
sortMode: 'added', // 'added' 最新添加 | 'recent' 最近使用 | 'freq' 使用频率 | 'manual' 手动顺序
|
|
665
|
+
usage: {}, // id -> { lastUsed: ts, useCount: n }(复制时统计,存浏览器本地态)
|
|
666
|
+
moves: [], // 跨组移动待办 [{title, from, to}](随变更指令交给 AI 落盘)
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/* ---------- 数据加载 ---------- */
|
|
670
|
+
var listEl = document.getElementById('cardList');
|
|
671
|
+
var sideNav = document.getElementById('sideNav');
|
|
672
|
+
var curTitle = document.getElementById('curTitle');
|
|
673
|
+
var curTotal = document.getElementById('curTotal');
|
|
674
|
+
var searchInput = document.getElementById('searchInput');
|
|
675
|
+
var searchClear = document.getElementById('searchClear');
|
|
676
|
+
var sortStatus = document.getElementById('sortStatus');
|
|
677
|
+
var sortSel = document.getElementById('sortSel');
|
|
678
|
+
var btnSortCmd = document.getElementById('btnSortCmd');
|
|
679
|
+
var btnMarkSaved = document.getElementById('btnMarkSaved');
|
|
680
|
+
var bkStatus = document.getElementById('bkStatus');
|
|
681
|
+
var btnBkEnable = document.getElementById('btnBkEnable');
|
|
682
|
+
var btnBkChange = document.getElementById('btnBkChange');
|
|
683
|
+
var btnBkRestore = document.getElementById('btnBkRestore');
|
|
684
|
+
var cacheBadge = document.getElementById('cacheBadge');
|
|
685
|
+
|
|
686
|
+
/* ---------- 底部帮助抽屉:点击开合,ESC 收起 ---------- */
|
|
687
|
+
var footerEl = document.querySelector('footer');
|
|
688
|
+
var helpTrigger = document.getElementById('helpTrigger');
|
|
689
|
+
var helpClose = document.getElementById('helpClose');
|
|
690
|
+
if (helpTrigger) helpTrigger.addEventListener('click', function(){ footerEl.classList.toggle('open'); });
|
|
691
|
+
if (helpClose) helpClose.addEventListener('click', function(){ footerEl.classList.remove('open'); });
|
|
692
|
+
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') footerEl.classList.remove('open'); });
|
|
693
|
+
|
|
694
|
+
/* ---------- 内置示例种子(仅兜底:prompts.json 读不到且本地无缓存快照时展示,file:// 直开也能看到示范数据;读到 prompts.json 即以文件为准) ---------- */
|
|
695
|
+
var DEFAULT_POUCH = {
|
|
696
|
+
version: 1,
|
|
697
|
+
groups: [
|
|
698
|
+
{ id: 1762500000000101, name: '写作输出', icon: '✍️' },
|
|
699
|
+
{ id: 1762500000000102, name: '沟通协作', icon: '💬' },
|
|
700
|
+
{ id: 1762500000000103, name: '规划执行', icon: '📌' },
|
|
701
|
+
{ id: 1762500000000104, name: '复盘成长', icon: '🔍' },
|
|
702
|
+
{ id: 1762500000000105, name: '提问艺术', icon: '🎯' }
|
|
703
|
+
],
|
|
704
|
+
prompts: [
|
|
705
|
+
{ id: 1762500000123401, group: 1762500000000101, title: '日报速写', content: '帮我根据以下内容写一份工作日报。今日完成:{今日事项};明日计划:{明日计划};风险阻塞:{风险问题}。要求:简洁、量化优先、分点列出。', tags: ['日报', '汇报'], order: 1, favorite: false, createdAt: '2026-11-01' },
|
|
706
|
+
{ id: 1762500000123402, group: 1762500000000101, title: '周报汇总', content: '帮我写一份本周周报。本周完成:{本周成果};下周计划:{下周安排};需要协调:{协调事项}。要求:突出数据结果,避免流水账。', tags: ['周报', '汇报'], order: 2, favorite: false, createdAt: '2026-11-01' },
|
|
707
|
+
{ id: 1762500000123403, group: 1762500000000101, title: '工作汇报', content: '请根据以下材料起草一份向上级的工作汇报。进展:{工作进展};成果数据:{量化数据};所需支持:{支持需求}。要求:结论先行、语气专业。', tags: ['汇报', '沟通'], order: 3, favorite: false, createdAt: '2026-11-01' },
|
|
708
|
+
{ id: 1762500000123404, group: 1762500000000102, title: '会议纪要', content: '帮我把这份会议记录整理成规范纪要。原始记录:{会议内容}。输出:会议主题、核心结论、待办事项(含负责人与截止时间)。', tags: ['会议', '纪要'], order: 1, favorite: false, createdAt: '2026-11-01' },
|
|
709
|
+
{ id: 1762500000123405, group: 1762500000000102, title: '邮件润色', content: '帮我润色这封工作邮件,使其更专业得体。邮件原文:{邮件内容}。收件人身份:{收件人身份}。', tags: ['邮件', '沟通'], order: 2, favorite: false, createdAt: '2026-11-01' },
|
|
710
|
+
{ id: 1762500000123406, group: 1762500000000102, title: '沟通话术', content: '帮我把这段话改得更委婉得体,用于跨部门沟通。原始表达:{原始表达}。沟通背景:{沟通背景}。', tags: ['沟通', '协作'], order: 3, favorite: false, createdAt: '2026-11-01' },
|
|
711
|
+
{ id: 1762500000123407, group: 1762500000000103, title: '任务拆解', content: '帮我把这个目标拆解成可执行的任务清单。目标:{目标描述};截止时间:{截止日期}。按优先级排序,每项给出验收标准。', tags: ['任务', '拆解'], order: 1, favorite: false, createdAt: '2026-11-01' },
|
|
712
|
+
{ id: 1762500000123408, group: 1762500000000103, title: '计划安排', content: '帮我制定未来 {时间周期} 的工作计划。重点事项:{重点事项};可用时间:{可用工时}。要求:安排到天、留出缓冲。', tags: ['计划', '排期'], order: 2, favorite: false, createdAt: '2026-11-01' },
|
|
713
|
+
{ id: 1762500000123409, group: 1762500000000103, title: '优先级排序', content: '帮我给以下待办事项排序。待办清单:{待办清单}。按重要紧急四象限分类并说明理由。', tags: ['待办', '优先级'], order: 3, favorite: false, createdAt: '2026-11-01' },
|
|
714
|
+
{ id: 1762500000123410, group: 1762500000000104, title: '项目复盘', content: '帮我做一次项目复盘。项目情况:{项目背景};结果数据:{结果数据};遇到的问题:{问题清单}。输出:亮点、不足、改进措施。', tags: ['复盘', '项目'], order: 1, favorite: false, createdAt: '2026-11-01' },
|
|
715
|
+
{ id: 1762500000123411, group: 1762500000000104, title: '阶段总结', content: '帮我把以下工作记录提炼成阶段总结。工作记录:{工作记录}。要求:突出关键成果与量化指标。', tags: ['总结', '成长'], order: 2, favorite: false, createdAt: '2026-11-01' },
|
|
716
|
+
{ id: 1762500000123412, group: 1762500000000104, title: '经验沉淀', content: '帮我把这次经验整理成可复用的方法论。事件经过:{事件描述};我的做法:{处理方式};结果:{最终结果}。输出:步骤化流程 + 注意事项。', tags: ['经验', '方法论'], order: 3, favorite: false, createdAt: '2026-11-01' },
|
|
717
|
+
{ id: 1762500000123413, group: 1762500000000105, title: '需求澄清', content: '帮我把这段模糊需求拆解成可执行的具体要求。原始需求:{需求描述}。输出:目标、约束条件、交付物、验收标准;不确定处附追问清单,不要自行假设。', tags: ['需求', '澄清'], order: 1, favorite: false, createdAt: '2026-11-01' },
|
|
718
|
+
{ id: 1762500000123414, group: 1762500000000105, title: '引导式提问', content: '请按「背景 → 现状 → 目标 → 障碍 → 已试过的办法」引导我逐步提供信息。我要解决的问题:{问题描述}。要求:一次只问一个问题,先别急着给答案。', tags: ['提问', '引导'], order: 2, favorite: false, createdAt: '2026-11-01' },
|
|
719
|
+
{ id: 1762500000123415, group: 1762500000000105, title: '追问清单', content: '针对我的问题先列出 {数量} 个关键追问帮我理清思路,再基于我的回答给方案。我的问题:{问题描述}。追问要求:具体、可回答、按重要性排序。', tags: ['提问', '思考'], order: 3, favorite: false, createdAt: '2026-11-01' }
|
|
720
|
+
]
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
function showErr(msg){
|
|
724
|
+
listEl.innerHTML = '<div class="empty">' + msg + '</div>';
|
|
725
|
+
}
|
|
726
|
+
function visiblePrompts(){
|
|
727
|
+
var ps = state.data.prompts.slice();
|
|
728
|
+
if (state.curGroup === 'fav') ps = ps.filter(function(p){ return p.favorite; });
|
|
729
|
+
else if (state.curGroup !== 'all') ps = ps.filter(function(p){ return p.group === state.curGroup; });
|
|
730
|
+
if (state.query){
|
|
731
|
+
var q = state.query.toLowerCase();
|
|
732
|
+
ps = ps.filter(function(p){
|
|
733
|
+
return p.title.toLowerCase().indexOf(q) > -1
|
|
734
|
+
|| p.content.toLowerCase().indexOf(q) > -1
|
|
735
|
+
|| (p.tags || []).join(' ').toLowerCase().indexOf(q) > -1;
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
ps.sort(sortCmp());
|
|
739
|
+
return ps;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* ---------- 排序比较器(四种模式) ---------- */
|
|
743
|
+
function createdAtOf(p){
|
|
744
|
+
if (p.createdAt){ var t = new Date(p.createdAt).getTime(); if (!isNaN(t)) return t; }
|
|
745
|
+
return 0;
|
|
746
|
+
}
|
|
747
|
+
function fmtStamp(s){
|
|
748
|
+
if (!s) return '';
|
|
749
|
+
var str = String(s).trim();
|
|
750
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str;
|
|
751
|
+
var d = new Date(str.replace(' ', 'T'));
|
|
752
|
+
if (isNaN(d.getTime())) return str;
|
|
753
|
+
function p(n){ return (n < 10 ? '0' : '') + n; }
|
|
754
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
|
|
755
|
+
+ ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
756
|
+
}
|
|
757
|
+
function sortCmp(){
|
|
758
|
+
switch (state.sortMode){
|
|
759
|
+
case 'recent':
|
|
760
|
+
return function(a,b){
|
|
761
|
+
var ua = state.usage[a.id] || { lastUsed: 0 };
|
|
762
|
+
var ub = state.usage[b.id] || { lastUsed: 0 };
|
|
763
|
+
if (ua.lastUsed !== ub.lastUsed) return ub.lastUsed - ua.lastUsed;
|
|
764
|
+
return (a.order||0) - (b.order||0);
|
|
765
|
+
};
|
|
766
|
+
case 'freq':
|
|
767
|
+
return function(a,b){
|
|
768
|
+
var ua = state.usage[a.id] || { useCount: 0 };
|
|
769
|
+
var ub = state.usage[b.id] || { useCount: 0 };
|
|
770
|
+
if (ua.useCount !== ub.useCount) return ub.useCount - ua.useCount;
|
|
771
|
+
return (a.order||0) - (b.order||0);
|
|
772
|
+
};
|
|
773
|
+
case 'manual':
|
|
774
|
+
return function(a,b){ return (a.order||0) - (b.order||0); };
|
|
775
|
+
default: /* 'added':默认最新添加在前 */
|
|
776
|
+
return function(a,b){
|
|
777
|
+
var ta = createdAtOf(a), tb = createdAtOf(b);
|
|
778
|
+
if (ta !== tb) return tb - ta;
|
|
779
|
+
return (a.order||0) - (b.order||0);
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function groupOf(id){
|
|
784
|
+
for (var i = 0; i < state.data.groups.length; i++){
|
|
785
|
+
if (state.data.groups[i].id === id) return state.data.groups[i];
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* ---------- 渲染 ---------- */
|
|
791
|
+
function renderNav(){
|
|
792
|
+
var html = '';
|
|
793
|
+
var all = state.data.prompts.length;
|
|
794
|
+
var favN = state.data.prompts.filter(function(p){ return p.favorite; }).length;
|
|
795
|
+
html += '<div class="nav-item' + (state.curGroup==='all'?' on':'') + '" data-g="all"><span class="icon">🗂️</span>全部<span class="cnt">' + all + '</span></div>';
|
|
796
|
+
html += '<div class="nav-scroll">';
|
|
797
|
+
html += '<div class="nav-item' + (state.curGroup==='fav'?' on':'') + '" data-g="fav"><span class="icon">⭐</span>收藏<span class="cnt">' + favN + '</span></div>';
|
|
798
|
+
state.data.groups.forEach(function(g){
|
|
799
|
+
var n = state.data.prompts.filter(function(p){ return p.group === g.id; }).length;
|
|
800
|
+
html += '<div class="nav-item' + (state.curGroup===g.id?' on':'') + '" data-g="' + g.id + '"><span class="icon">' + g.icon + '</span><span class="g-name">' + escapeHtml(g.name) + '</span><span class="cnt">' + n + '</span><span class="g-ops"><button class="g-edit" title="编辑分组">✎</button><button class="g-del" title="删除分组">🗑</button></span></div>';
|
|
801
|
+
});
|
|
802
|
+
html += '</div>';
|
|
803
|
+
html += '<div class="nav-add" id="navAdd">+ 新建分组</div>';
|
|
804
|
+
html += '<div class="side-tip"><b>锦囊用法</b><br>· 页面直接增删改:+ 新建分组 / + 新增提示词 / ✎ 编辑 / 🗑 删除(授权目录即写即存)<br>· 也可对 AI 说「把 XX 加到写作输出分组」<br>· 顶栏切换排序;⇄ 移组、拖卡片排序自动保存<br>· ⭐ 收藏常用 · 复制次数在卡片右上角</div>';
|
|
805
|
+
sideNav.innerHTML = html;
|
|
806
|
+
sideNav.querySelectorAll('.nav-item').forEach(function(el){
|
|
807
|
+
el.addEventListener('click', function(){
|
|
808
|
+
var gv = el.getAttribute('data-g');
|
|
809
|
+
state.curGroup = (gv === 'all' || gv === 'fav') ? gv : Number(gv);
|
|
810
|
+
renderNav(); render();
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
sideNav.querySelectorAll('.nav-item .g-edit').forEach(function(btn){
|
|
814
|
+
btn.addEventListener('click', function(e){
|
|
815
|
+
e.stopPropagation();
|
|
816
|
+
var gid = Number(btn.closest('.nav-item').getAttribute('data-g'));
|
|
817
|
+
openGroupForm(groupOf(gid));
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
sideNav.querySelectorAll('.nav-item .g-del').forEach(function(btn){
|
|
821
|
+
btn.addEventListener('click', function(e){
|
|
822
|
+
e.stopPropagation();
|
|
823
|
+
var gid = Number(btn.closest('.nav-item').getAttribute('data-g'));
|
|
824
|
+
delGroup(groupOf(gid));
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
var addBtn = sideNav.querySelector('#navAdd');
|
|
828
|
+
if (addBtn) addBtn.addEventListener('click', function(){ openGroupForm(null); });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function render(){
|
|
832
|
+
var ps = visiblePrompts();
|
|
833
|
+
if (state.curGroup === 'all') curTitle.textContent = '全部提示词';
|
|
834
|
+
else if (state.curGroup === 'fav') curTitle.textContent = '⭐ 收藏';
|
|
835
|
+
else { var g = groupOf(state.curGroup); curTitle.textContent = (g ? g.icon + ' ' + g.name : ''); }
|
|
836
|
+
curTotal.textContent = '共 ' + ps.length + ' 条';
|
|
837
|
+
if (!ps.length){
|
|
838
|
+
listEl.innerHTML = '<div class="empty">🕸️ 囊中暂无匹配的妙计</div>';
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
listEl.innerHTML = '';
|
|
842
|
+
ps.forEach(function(p, idx){
|
|
843
|
+
var g = groupOf(p.group);
|
|
844
|
+
var isTpl = hasTemplateVars(p.content);
|
|
845
|
+
var card = document.createElement('div');
|
|
846
|
+
card.className = 'card' + (p.favorite ? ' fav' : '');
|
|
847
|
+
card.draggable = true;
|
|
848
|
+
card.setAttribute('data-id', p.id);
|
|
849
|
+
card.innerHTML =
|
|
850
|
+
'<div class="card-top"><span class="card-title"></span>' + (isTpl ? '<span class="tpl-badge">模板</span>' : '') + '<span class="gbadge">' + (g ? g.icon + ' ' + g.name : '') + '</span><span class="usage-info"></span></div>' +
|
|
851
|
+
'<div class="card-body"></div>' +
|
|
852
|
+
'<div class="card-meta"><button class="card-expand">👁 详情</button><span class="card-time"><svg class="ct-ico" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><circle cx="8" cy="8" r="6.3" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 4.6V8l2.5 1.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg><span class="ct-txt"></span></span></div>' +
|
|
853
|
+
'<div class="card-foot"><span class="tags-wrap"></span>' +
|
|
854
|
+
'<div class="card-ops">' +
|
|
855
|
+
'<button class="op op-move" title="移到其它分组">⇄</button>' +
|
|
856
|
+
'<button class="op op-up" title="上移">▲</button>' +
|
|
857
|
+
'<button class="op op-down" title="下移">▼</button>' +
|
|
858
|
+
'<button class="op op-copy" title="复制提示词">⧉</button>' +
|
|
859
|
+
'<button class="op op-star' + (p.favorite ? ' star-on' : '') + '" title="收藏">★</button>' +
|
|
860
|
+
'<button class="op op-edit" title="编辑">✎</button>' +
|
|
861
|
+
'<button class="op op-del" title="删除">🗑</button>' +
|
|
862
|
+
'</div></div>' +
|
|
863
|
+
'<div class="copy-feedback">✓ 已复制</div>';
|
|
864
|
+
card.querySelector('.card-title').innerHTML = hlHtml(p.title, state.query);
|
|
865
|
+
var uu = state.usage[p.id];
|
|
866
|
+
if (uu && uu.useCount > 0){
|
|
867
|
+
card.querySelector('.usage-info').textContent = '⧉ ' + uu.useCount + ' 次 · ' + relTime(uu.lastUsed);
|
|
868
|
+
}
|
|
869
|
+
var cta = createdAtOf(p);
|
|
870
|
+
card.querySelector('.ct-txt').textContent = cta ? fmtStamp(p.createdAt) : '';
|
|
871
|
+
var qi = state.query ? p.content.toLowerCase().indexOf(state.query.toLowerCase()) : -1;
|
|
872
|
+
card.querySelector('.card-body').innerHTML = hlHtml(p.content, state.query);
|
|
873
|
+
if (qi >= 80){
|
|
874
|
+
var note = document.createElement('span');
|
|
875
|
+
note.className = 'hl-note';
|
|
876
|
+
note.textContent = '正文命中,详见详情';
|
|
877
|
+
card.querySelector('.card-top').appendChild(note);
|
|
878
|
+
}
|
|
879
|
+
var tw = card.querySelector('.tags-wrap');
|
|
880
|
+
(p.tags || []).forEach(function(t){ tw.insertAdjacentHTML('beforeend', '<span class="tag">' + hlHtml(t, state.query) + '</span>'); });
|
|
881
|
+
|
|
882
|
+
/* 详情弹窗 */
|
|
883
|
+
card.querySelector('.card-expand').addEventListener('click', function(){ openDetail(p); });
|
|
884
|
+
/* 复制 */
|
|
885
|
+
card.querySelector('.op-copy').addEventListener('click', function(){ smartCopy(p.content, card); });
|
|
886
|
+
/* 收藏 */
|
|
887
|
+
card.querySelector('.op-star').addEventListener('click', function(){
|
|
888
|
+
p.favorite = !p.favorite;
|
|
889
|
+
persistUserState();
|
|
890
|
+
scheduleBackup();
|
|
891
|
+
renderNav(); render();
|
|
892
|
+
});
|
|
893
|
+
/* 编辑 / 删除 */
|
|
894
|
+
card.querySelector('.op-edit').addEventListener('click', function(e){
|
|
895
|
+
e.stopPropagation();
|
|
896
|
+
openItemForm(p);
|
|
897
|
+
});
|
|
898
|
+
card.querySelector('.op-del').addEventListener('click', function(e){
|
|
899
|
+
e.stopPropagation();
|
|
900
|
+
delItem(p);
|
|
901
|
+
});
|
|
902
|
+
/* 移到其它分组 */
|
|
903
|
+
card.querySelector('.op-move').addEventListener('click', function(e){
|
|
904
|
+
e.stopPropagation();
|
|
905
|
+
openGpop(card.querySelector('.op-move'), p);
|
|
906
|
+
});
|
|
907
|
+
/* 上移/下移 */
|
|
908
|
+
card.querySelector('.op-up').addEventListener('click', function(){ moveItem(p, idx, -1); });
|
|
909
|
+
card.querySelector('.op-down').addEventListener('click', function(){ moveItem(p, idx, 1); });
|
|
910
|
+
|
|
911
|
+
/* 拖拽 */
|
|
912
|
+
card.addEventListener('dragstart', function(e){
|
|
913
|
+
ensureManual();
|
|
914
|
+
state.dragId = p.id;
|
|
915
|
+
card.classList.add('dragging');
|
|
916
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
917
|
+
try{ e.dataTransfer.setData('text/plain', p.id); }catch(err){}
|
|
918
|
+
});
|
|
919
|
+
card.addEventListener('dragend', function(){
|
|
920
|
+
card.classList.remove('dragging');
|
|
921
|
+
document.querySelectorAll('.card.drop-target').forEach(function(c){ c.classList.remove('drop-target'); });
|
|
922
|
+
});
|
|
923
|
+
card.addEventListener('dragover', function(e){
|
|
924
|
+
e.preventDefault();
|
|
925
|
+
if (state.dragId && state.dragId !== p.id){
|
|
926
|
+
document.querySelectorAll('.card.drop-target').forEach(function(c){ c.classList.remove('drop-target'); });
|
|
927
|
+
card.classList.add('drop-target');
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
card.addEventListener('drop', function(e){
|
|
931
|
+
e.preventDefault();
|
|
932
|
+
if (state.dragId && state.dragId !== p.id){
|
|
933
|
+
var from = state.data.prompts.find(function(x){ return x.id === state.dragId; });
|
|
934
|
+
if (from && from.group === p.group){
|
|
935
|
+
var arr = state.data.prompts.filter(function(x){ return x.group === p.group; })
|
|
936
|
+
.sort(function(a,b){ return (a.order||0) - (b.order||0); });
|
|
937
|
+
arr = arr.filter(function(x){ return x.id !== from.id; });
|
|
938
|
+
var pos = arr.indexOf(p);
|
|
939
|
+
arr.splice(pos, 0, from);
|
|
940
|
+
arr.forEach(function(x, i){ x.order = i + 1; });
|
|
941
|
+
markDirty();
|
|
942
|
+
render();
|
|
943
|
+
} else if (from){
|
|
944
|
+
var fb2 = card.querySelector('.copy-feedback');
|
|
945
|
+
if (fb2){
|
|
946
|
+
fb2.textContent = '⇄ 跨组移动请用 ⇄ 按钮';
|
|
947
|
+
fb2.classList.add('show');
|
|
948
|
+
setTimeout(function(){ fb2.classList.remove('show'); }, 1600);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
listEl.appendChild(card);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function moveItem(p, idx, delta){
|
|
959
|
+
ensureManual();
|
|
960
|
+
var arr = visiblePrompts();
|
|
961
|
+
if (idx + delta < 0 || idx + delta >= arr.length) return;
|
|
962
|
+
var t = arr[idx + delta];
|
|
963
|
+
if (p.group !== t.group){
|
|
964
|
+
var fbEl = listEl.querySelector('.card[data-id="' + p.id + '"] .copy-feedback');
|
|
965
|
+
if (fbEl){
|
|
966
|
+
fbEl.textContent = '⇄ 跨组移动请用 ⇄ 按钮';
|
|
967
|
+
fbEl.classList.add('show');
|
|
968
|
+
setTimeout(function(){ fbEl.classList.remove('show'); }, 1600);
|
|
969
|
+
}
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
var gArr = state.data.prompts.filter(function(x){ return x.group === p.group; })
|
|
973
|
+
.sort(function(a,b){ return (a.order||0) - (b.order||0); });
|
|
974
|
+
var i = gArr.indexOf(p);
|
|
975
|
+
var j = i + delta;
|
|
976
|
+
if (j < 0 || j >= gArr.length) return;
|
|
977
|
+
gArr.splice(i, 1); gArr.splice(j, 0, p);
|
|
978
|
+
gArr.forEach(function(x, k){ x.order = k + 1; });
|
|
979
|
+
markDirty();
|
|
980
|
+
render();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function refreshDirtyUI(){
|
|
984
|
+
if (state.dirty){
|
|
985
|
+
sortStatus.classList.add('show');
|
|
986
|
+
btnSortCmd.disabled = false;
|
|
987
|
+
if (btnMarkSaved) btnMarkSaved.style.display = '';
|
|
988
|
+
} else {
|
|
989
|
+
sortStatus.classList.remove('show');
|
|
990
|
+
btnSortCmd.disabled = true;
|
|
991
|
+
if (btnMarkSaved) btnMarkSaved.style.display = 'none';
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function markDirty(){
|
|
995
|
+
state.dirty = true;
|
|
996
|
+
refreshDirtyUI();
|
|
997
|
+
persistUserState();
|
|
998
|
+
scheduleBackup();
|
|
999
|
+
}
|
|
1000
|
+
function syncSortUI(){ if (sortSel) sortSel.value = state.sortMode; }
|
|
1001
|
+
function ensureManual(){
|
|
1002
|
+
if (state.sortMode === 'manual') return;
|
|
1003
|
+
state.sortMode = 'manual';
|
|
1004
|
+
syncSortUI();
|
|
1005
|
+
persistUserState();
|
|
1006
|
+
var old = btnSortCmd.textContent;
|
|
1007
|
+
btnSortCmd.textContent = '✓ 已切回手动顺序';
|
|
1008
|
+
setTimeout(function(){
|
|
1009
|
+
if (state.dirty){ btnSortCmd.textContent = '📋 复制变更指令(未保存)'; }
|
|
1010
|
+
else { btnSortCmd.textContent = old; }
|
|
1011
|
+
}, 2000);
|
|
1012
|
+
}
|
|
1013
|
+
function recordUsage(id, card){
|
|
1014
|
+
if (!id) return;
|
|
1015
|
+
var u = state.usage[id] || (state.usage[id] = { lastUsed: 0, useCount: 0 });
|
|
1016
|
+
u.lastUsed = Date.now();
|
|
1017
|
+
u.useCount += 1;
|
|
1018
|
+
persistUserState();
|
|
1019
|
+
if (card && card.querySelector){
|
|
1020
|
+
var el = card.querySelector('.usage-info');
|
|
1021
|
+
if (el) el.textContent = '⧉ ' + u.useCount + ' 次 · ' + relTime(u.lastUsed);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function relTime(ts){
|
|
1025
|
+
if (!ts) return '';
|
|
1026
|
+
var diff = Date.now() - ts;
|
|
1027
|
+
if (diff < 60000) return '刚刚';
|
|
1028
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前';
|
|
1029
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前';
|
|
1030
|
+
if (diff < 30 * 86400000) return Math.floor(diff / 86400000) + ' 天前';
|
|
1031
|
+
return Math.floor(diff / (30 * 86400000)) + ' 个月前';
|
|
1032
|
+
}
|
|
1033
|
+
/* ---------- 移到其它分组(⇄ 浮层) ---------- */
|
|
1034
|
+
var gpopEl = document.getElementById('gpop');
|
|
1035
|
+
var gpopTarget = null;
|
|
1036
|
+
function openGpop(anchorBtn, p){
|
|
1037
|
+
var html = '<div class="gpop-head">移到分组</div>';
|
|
1038
|
+
state.data.groups.forEach(function(g){
|
|
1039
|
+
var cur = (p.group === g.id);
|
|
1040
|
+
html += '<div class="gpop-item' + (cur ? ' cur' : '') + '"' + (cur ? '' : ' data-g="' + g.id + '"') + '>' + g.icon + ' ' + g.name + (cur ? '(当前)' : '') + '</div>';
|
|
1041
|
+
});
|
|
1042
|
+
gpopEl.innerHTML = html;
|
|
1043
|
+
gpopEl.classList.add('open');
|
|
1044
|
+
gpopTarget = p;
|
|
1045
|
+
var r = anchorBtn.getBoundingClientRect();
|
|
1046
|
+
var gw = gpopEl.offsetWidth;
|
|
1047
|
+
var x = Math.min(Math.max(r.left, 8), window.innerWidth - gw - 12);
|
|
1048
|
+
var y = r.bottom + 8;
|
|
1049
|
+
if (y + gpopEl.offsetHeight > window.innerHeight - 12){ y = Math.max(r.top - gpopEl.offsetHeight - 8, 8); }
|
|
1050
|
+
gpopEl.style.left = x + 'px';
|
|
1051
|
+
gpopEl.style.top = y + 'px';
|
|
1052
|
+
gpopEl.querySelectorAll('.gpop-item[data-g]').forEach(function(el){
|
|
1053
|
+
el.addEventListener('click', function(){ moveToGroup(gpopTarget, el.getAttribute('data-g')); });
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function closeGpop(){
|
|
1057
|
+
gpopEl.classList.remove('open');
|
|
1058
|
+
gpopTarget = null;
|
|
1059
|
+
}
|
|
1060
|
+
function moveToGroup(p, gid){
|
|
1061
|
+
if (!p || p.group === gid){ closeGpop(); return; }
|
|
1062
|
+
var from = groupOf(p.group);
|
|
1063
|
+
var to = groupOf(gid);
|
|
1064
|
+
if (!to){ closeGpop(); return; }
|
|
1065
|
+
var gArr = state.data.prompts.filter(function(x){ return x.group === gid; })
|
|
1066
|
+
.sort(function(a, b){ return (a.order || 0) - (b.order || 0); });
|
|
1067
|
+
var maxOrder = gArr.reduce(function(m, x){ return Math.max(m, x.order || 0); }, 0);
|
|
1068
|
+
p.group = gid;
|
|
1069
|
+
p.order = maxOrder + 1;
|
|
1070
|
+
state.moves.push({ title: p.title, from: from ? from.name : '', to: to.name });
|
|
1071
|
+
closeGpop();
|
|
1072
|
+
markDirty();
|
|
1073
|
+
renderNav(); render();
|
|
1074
|
+
}
|
|
1075
|
+
document.addEventListener('click', function(e){
|
|
1076
|
+
if (gpopEl && gpopEl.classList.contains('open')){
|
|
1077
|
+
var onAnchor = e.target.closest && e.target.closest('.op-move');
|
|
1078
|
+
if (!gpopEl.contains(e.target) && !onAnchor){ closeGpop(); }
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
sortSel.addEventListener('change', function(){
|
|
1082
|
+
state.sortMode = sortSel.value;
|
|
1083
|
+
persistUserState();
|
|
1084
|
+
render();
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
/* ---------- 模板插值({{变量}} / {变量}) ---------- */
|
|
1088
|
+
function escapeHtml(t){ return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
1089
|
+
function escReg(s){ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
1090
|
+
/* 搜索命中高亮:按关键词分段后逐段转义(XSS 安全),命中段包 <mark class="hl"> */
|
|
1091
|
+
function hlHtml(text, q){
|
|
1092
|
+
if (!q) return escapeHtml(text);
|
|
1093
|
+
var parts = String(text).split(new RegExp('(' + escReg(q) + ')', 'gi'));
|
|
1094
|
+
var out = '';
|
|
1095
|
+
for (var i = 0; i < parts.length; i++){
|
|
1096
|
+
out += (i % 2 === 1) ? '<mark class="hl">' + escapeHtml(parts[i]) + '</mark>' : escapeHtml(parts[i]);
|
|
1097
|
+
}
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
var VAR_RE = /\{\{\s*([^{}]+?)\s*\}\}|\{\s*([\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z0-9_·]{1,19})\s*\}/g;
|
|
1101
|
+
function extractVars(text){
|
|
1102
|
+
var vars = []; var m;
|
|
1103
|
+
VAR_RE.lastIndex = 0;
|
|
1104
|
+
while ((m = VAR_RE.exec(text)) !== null){
|
|
1105
|
+
var name = (m[1] || m[2] || '').trim();
|
|
1106
|
+
if (name && vars.indexOf(name) === -1) vars.push(name);
|
|
1107
|
+
}
|
|
1108
|
+
return vars;
|
|
1109
|
+
}
|
|
1110
|
+
function hasTemplateVars(text){ return extractVars(text).length > 0; }
|
|
1111
|
+
function replaceVar(text, name, value){
|
|
1112
|
+
var esc = escReg(name);
|
|
1113
|
+
return text.replace(new RegExp('\\{\\{\\s*' + esc + '\\s*\\}\\}', 'g'), value)
|
|
1114
|
+
.replace(new RegExp('\\{\\s*' + esc + '\\s*\\}', 'g'), value);
|
|
1115
|
+
}
|
|
1116
|
+
function highlightVars(text){
|
|
1117
|
+
return escapeHtml(text).replace(VAR_RE, function(full, d1, s1){
|
|
1118
|
+
return '<span class="var-badge">' + full + '</span>';
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
function smartCopy(text, card){
|
|
1122
|
+
var vars = extractVars(text);
|
|
1123
|
+
if (!vars.length){ copyText(text, card); return; }
|
|
1124
|
+
openFillPanel(text, vars, card);
|
|
1125
|
+
}
|
|
1126
|
+
var fillTemplateText = ''; var fillTargetCard = null;
|
|
1127
|
+
function openFillPanel(text, vars, card){
|
|
1128
|
+
fillTemplateText = text; fillTargetCard = card;
|
|
1129
|
+
document.getElementById('fpVarCount').textContent = vars.length;
|
|
1130
|
+
document.getElementById('fpPreview').innerHTML = highlightVars(text);
|
|
1131
|
+
var html = '';
|
|
1132
|
+
vars.forEach(function(v, i){
|
|
1133
|
+
html += '<div class="fp-field"><label for="fpInput' + i + '">' + escapeHtml('{' + v + '}') + '</label>'
|
|
1134
|
+
+ '<input id="fpInput' + i + '" data-var="' + escapeHtml(v) + '" placeholder="请输入 ' + escapeHtml(v) + ' 的内容" /></div>';
|
|
1135
|
+
});
|
|
1136
|
+
document.getElementById('fpFields').innerHTML = html;
|
|
1137
|
+
document.getElementById('fpLive').value = text;
|
|
1138
|
+
document.getElementById('fillMask').classList.add('open');
|
|
1139
|
+
var inputs = document.querySelectorAll('#fpFields input');
|
|
1140
|
+
inputs.forEach(function(inp, i){
|
|
1141
|
+
inp.addEventListener('input', updateFillPreview);
|
|
1142
|
+
inp.addEventListener('keydown', function(e){ if (e.key === 'Enter'){ e.preventDefault(); handleFillCopy(); } });
|
|
1143
|
+
if (i === 0) setTimeout(function(){ inp.focus(); }, 60);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function updateFillPreview(){
|
|
1147
|
+
var result = fillTemplateText;
|
|
1148
|
+
document.querySelectorAll('#fpFields input').forEach(function(inp){
|
|
1149
|
+
var name = inp.getAttribute('data-var');
|
|
1150
|
+
if (inp.value.trim()) result = replaceVar(result, name, inp.value);
|
|
1151
|
+
});
|
|
1152
|
+
document.getElementById('fpLive').value = result;
|
|
1153
|
+
}
|
|
1154
|
+
function closeFillPanel(){ document.getElementById('fillMask').classList.remove('open'); }
|
|
1155
|
+
function handleFillCopy(){
|
|
1156
|
+
var result = fillTemplateText;
|
|
1157
|
+
document.querySelectorAll('#fpFields input').forEach(function(inp){
|
|
1158
|
+
var name = inp.getAttribute('data-var');
|
|
1159
|
+
if (inp.value.trim()) result = replaceVar(result, name, inp.value);
|
|
1160
|
+
});
|
|
1161
|
+
var card = fillTargetCard;
|
|
1162
|
+
closeFillPanel();
|
|
1163
|
+
copyText(result, card);
|
|
1164
|
+
}
|
|
1165
|
+
document.getElementById('fpCancel').addEventListener('click', closeFillPanel);
|
|
1166
|
+
document.getElementById('fpCopy').addEventListener('click', handleFillCopy);
|
|
1167
|
+
document.getElementById('fillMask').addEventListener('click', function(e){
|
|
1168
|
+
if (e.target === this) closeFillPanel();
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
/* ---------- 复制 ---------- */
|
|
1172
|
+
function copyText(text, card){
|
|
1173
|
+
function done(){
|
|
1174
|
+
var fb = card.querySelector('.copy-feedback');
|
|
1175
|
+
fb.textContent = '✓ 已复制';
|
|
1176
|
+
fb.classList.add('show');
|
|
1177
|
+
setTimeout(function(){ fb.classList.remove('show'); }, 1500);
|
|
1178
|
+
recordUsage(card && card.getAttribute ? card.getAttribute('data-id') : null, card);
|
|
1179
|
+
}
|
|
1180
|
+
function fallback(){
|
|
1181
|
+
var ta = document.createElement('textarea');
|
|
1182
|
+
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
1183
|
+
document.body.appendChild(ta); ta.select();
|
|
1184
|
+
try{ document.execCommand('copy'); }catch(e){}
|
|
1185
|
+
document.body.removeChild(ta); done();
|
|
1186
|
+
}
|
|
1187
|
+
if (navigator.clipboard && navigator.clipboard.writeText){
|
|
1188
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
1189
|
+
} else { fallback(); }
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/* ---------- 页面直写:雪花 id / toast / 写盘 / 表单 ---------- */
|
|
1193
|
+
function idExists(id){
|
|
1194
|
+
return state.data.groups.some(function(g){ return g.id === id; })
|
|
1195
|
+
|| state.data.prompts.some(function(p){ return p.id === id; });
|
|
1196
|
+
}
|
|
1197
|
+
function snowflakeId(){
|
|
1198
|
+
var n;
|
|
1199
|
+
do{
|
|
1200
|
+
n = Number(String(Date.now()) + String(Math.floor(Math.random() * 1000)).padStart(3, '0'));
|
|
1201
|
+
} while (idExists(n));
|
|
1202
|
+
return n;
|
|
1203
|
+
}
|
|
1204
|
+
function nowStamp(){
|
|
1205
|
+
var d = new Date();
|
|
1206
|
+
function p(n){ return (n < 10 ? '0' : '') + n; }
|
|
1207
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
|
|
1208
|
+
+ ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
1209
|
+
}
|
|
1210
|
+
var toastTimer = null;
|
|
1211
|
+
function toast(msg, warn){
|
|
1212
|
+
var el = document.getElementById('toast');
|
|
1213
|
+
if (!el) return;
|
|
1214
|
+
el.textContent = msg;
|
|
1215
|
+
el.className = 'toast show' + (warn ? ' warn' : '');
|
|
1216
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
1217
|
+
toastTimer = setTimeout(function(){ el.className = 'toast'; }, 2800);
|
|
1218
|
+
}
|
|
1219
|
+
function ensureWriteDir(){
|
|
1220
|
+
if (bkDirHandle) return Promise.resolve(true);
|
|
1221
|
+
return maybeAutoAuth().then(function(){ return !!bkDirHandle; });
|
|
1222
|
+
}
|
|
1223
|
+
/* op = { apply: function(最新数据) -> 新数据|null, cmd: 未授权时的 AI 指令 } */
|
|
1224
|
+
function writePromptsJson(op){
|
|
1225
|
+
return ensureWriteDir().then(function(ok){
|
|
1226
|
+
if (!ok){
|
|
1227
|
+
var cmd = typeof op.cmd === 'function' ? op.cmd() : op.cmd;
|
|
1228
|
+
copyText(cmd, { querySelector: function(){ return { classList:{ add:function(){}, remove:function(){} } }; } });
|
|
1229
|
+
toast('页面未授权目录,操作指令已复制 → 粘贴给 AI 落盘', true);
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
return writeBackupNow().then(function(){
|
|
1233
|
+
return readPouch().then(function(text){
|
|
1234
|
+
if (text) return JSON.parse(text);
|
|
1235
|
+
return fetch('cache/prompts.json', { cache: 'no-store' })
|
|
1236
|
+
.then(function(r){ if (!r.ok) throw new Error('nf'); return r.json(); })
|
|
1237
|
+
.catch(function(){
|
|
1238
|
+
return fetch('prompts.json', { cache: 'no-store' }).then(function(r2){ return r2.json(); });
|
|
1239
|
+
})
|
|
1240
|
+
.catch(function(){ return state.data; });
|
|
1241
|
+
});
|
|
1242
|
+
}).then(function(latest){
|
|
1243
|
+
var next = op.apply(latest);
|
|
1244
|
+
if (next === null) return false;
|
|
1245
|
+
return bkWrite('prompts.json', 'cache', JSON.stringify(next, null, 2)).then(function(){
|
|
1246
|
+
state.data = next;
|
|
1247
|
+
applyUserState();
|
|
1248
|
+
state.dirty = false; state.moves = [];
|
|
1249
|
+
persistUserState(); refreshDirtyUI();
|
|
1250
|
+
renderNav(); render();
|
|
1251
|
+
return true;
|
|
1252
|
+
});
|
|
1253
|
+
}).catch(function(){
|
|
1254
|
+
toast('写入 cache/prompts.json 失败,请检查目录授权', true);
|
|
1255
|
+
return false;
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
/* 排序/移组直写:把内存 order/group 覆盖到最新文件(变更 2 秒后自动调用) */
|
|
1260
|
+
function flushData(){
|
|
1261
|
+
if (!state.dirty || !bkDirHandle) return Promise.resolve();
|
|
1262
|
+
return readPouch().then(function(text){
|
|
1263
|
+
if (text) return JSON.parse(text);
|
|
1264
|
+
return fetch('cache/prompts.json', { cache: 'no-store' })
|
|
1265
|
+
.then(function(r){ if (!r.ok) throw new Error('nf'); return r.json(); })
|
|
1266
|
+
.catch(function(){
|
|
1267
|
+
return fetch('prompts.json', { cache: 'no-store' }).then(function(r2){ return r2.json(); });
|
|
1268
|
+
})
|
|
1269
|
+
.catch(function(){ return state.data; });
|
|
1270
|
+
})
|
|
1271
|
+
.then(function(latest){
|
|
1272
|
+
var byId = {};
|
|
1273
|
+
state.data.prompts.forEach(function(p){ byId[p.id] = p; });
|
|
1274
|
+
latest.prompts.forEach(function(p){
|
|
1275
|
+
var m = byId[p.id];
|
|
1276
|
+
if (m){ p.order = m.order; p.group = m.group; }
|
|
1277
|
+
});
|
|
1278
|
+
return bkWrite('prompts.json', 'cache', JSON.stringify(latest, null, 2)).then(function(){
|
|
1279
|
+
state.data = latest;
|
|
1280
|
+
applyUserState();
|
|
1281
|
+
state.dirty = false; state.moves = [];
|
|
1282
|
+
persistUserState(); refreshDirtyUI();
|
|
1283
|
+
renderNav(); render();
|
|
1284
|
+
toast('排序 / 移组已自动保存');
|
|
1285
|
+
});
|
|
1286
|
+
}).catch(function(){});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* ---------- 提示词详情弹窗 ---------- */
|
|
1290
|
+
var detailTarget = null;
|
|
1291
|
+
var detailMask = document.getElementById('detailMask');
|
|
1292
|
+
function fakeCard(id){
|
|
1293
|
+
return {
|
|
1294
|
+
getAttribute: function(k){ return k === 'data-id' ? String(id) : null; },
|
|
1295
|
+
querySelector: function(){ return { classList: { add: function(){}, remove: function(){} } }; }
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function openDetail(p){
|
|
1299
|
+
detailTarget = p;
|
|
1300
|
+
var g = groupOf(p.group);
|
|
1301
|
+
var meta = (g ? escapeHtml(g.icon + ' ' + g.name) : '');
|
|
1302
|
+
if (p.tags && p.tags.length) meta += (meta ? ' · ' : '') + p.tags.map(function(t){ return hlHtml(t, state.query); }).join(' / ');
|
|
1303
|
+
var uu = state.usage[p.id];
|
|
1304
|
+
if (uu && uu.useCount > 0) meta += (meta ? ' · ' : '') + '⧉ ' + uu.useCount + ' 次';
|
|
1305
|
+
document.getElementById('detailTitle').innerHTML = hlHtml(p.title, state.query);
|
|
1306
|
+
document.getElementById('detailMeta').innerHTML = meta;
|
|
1307
|
+
var times = [];
|
|
1308
|
+
times.push('<span>📅 创建 ' + (p.createdAt ? fmtStamp(p.createdAt) : '—') + '</span>');
|
|
1309
|
+
if (p.updatedAt && p.updatedAt !== p.createdAt) times.push('<span>✏ 修改 ' + fmtStamp(p.updatedAt) + '</span>');
|
|
1310
|
+
if (uu && uu.lastUsed) times.push('<span>⧉ 最近使用 ' + relTime(uu.lastUsed) + '</span>');
|
|
1311
|
+
var tEl = document.getElementById('detailTimes');
|
|
1312
|
+
if (times.length){ tEl.style.display = 'flex'; tEl.innerHTML = times.join(''); }
|
|
1313
|
+
else { tEl.style.display = 'none'; }
|
|
1314
|
+
document.getElementById('detailContent').innerHTML = hlHtml(p.content, state.query);
|
|
1315
|
+
var vars = extractVars(p.content);
|
|
1316
|
+
var vEl = document.getElementById('detailVars');
|
|
1317
|
+
if (vars.length){
|
|
1318
|
+
vEl.style.display = 'flex';
|
|
1319
|
+
vEl.innerHTML = '<b>含 ' + vars.length + ' 个插值变量,复制时逐个填写:</b>'
|
|
1320
|
+
+ vars.map(function(v){ return '<span class="var-badge">' + escapeHtml('{' + v + '}') + '</span>'; }).join('');
|
|
1321
|
+
} else { vEl.style.display = 'none'; }
|
|
1322
|
+
detailMask.classList.add('open');
|
|
1323
|
+
}
|
|
1324
|
+
function closeDetail(){ detailMask.classList.remove('open'); detailTarget = null; }
|
|
1325
|
+
document.getElementById('detailClose').addEventListener('click', closeDetail);
|
|
1326
|
+
document.getElementById('detailEdit').addEventListener('click', function(){
|
|
1327
|
+
var p = detailTarget;
|
|
1328
|
+
closeDetail();
|
|
1329
|
+
if (p) openItemForm(p);
|
|
1330
|
+
});
|
|
1331
|
+
document.getElementById('detailCopy').addEventListener('click', function(){
|
|
1332
|
+
if (!detailTarget) return;
|
|
1333
|
+
var t = detailTarget;
|
|
1334
|
+
var hasVars = hasTemplateVars(t.content);
|
|
1335
|
+
closeDetail();
|
|
1336
|
+
smartCopy(t.content, fakeCard(t.id));
|
|
1337
|
+
if (!hasVars) toast('✓ 已复制');
|
|
1338
|
+
});
|
|
1339
|
+
detailMask.addEventListener('click', function(e){ if (e.target === this) closeDetail(); });
|
|
1340
|
+
|
|
1341
|
+
/* ---------- 提示词增删改 ---------- */
|
|
1342
|
+
var itemMask = document.getElementById('itemMask');
|
|
1343
|
+
var itemTitleEl = document.getElementById('itemTitle');
|
|
1344
|
+
var itemName = document.getElementById('itemName');
|
|
1345
|
+
var itemGroupSel = document.getElementById('itemGroup');
|
|
1346
|
+
var itemTags = document.getElementById('itemTags');
|
|
1347
|
+
var itemContent = document.getElementById('itemContent');
|
|
1348
|
+
var editingItemId = null;
|
|
1349
|
+
function fillGroupOptions(){
|
|
1350
|
+
var v = itemGroupSel.value;
|
|
1351
|
+
itemGroupSel.innerHTML = state.data.groups.map(function(g){
|
|
1352
|
+
return '<option value="' + g.id + '">' + escapeHtml(g.icon + ' ' + g.name) + '</option>';
|
|
1353
|
+
}).join('');
|
|
1354
|
+
if (v) itemGroupSel.value = v;
|
|
1355
|
+
}
|
|
1356
|
+
function openItemForm(p){
|
|
1357
|
+
editingItemId = p ? p.id : null;
|
|
1358
|
+
itemTitleEl.textContent = p ? '编辑提示词' : '新增提示词';
|
|
1359
|
+
fillGroupOptions();
|
|
1360
|
+
if (p){
|
|
1361
|
+
itemName.value = p.title;
|
|
1362
|
+
itemGroupSel.value = String(p.group);
|
|
1363
|
+
itemTags.value = (p.tags || []).join(', ');
|
|
1364
|
+
itemContent.value = p.content;
|
|
1365
|
+
} else {
|
|
1366
|
+
itemName.value = ''; itemTags.value = ''; itemContent.value = '';
|
|
1367
|
+
if (state.curGroup !== 'all' && state.curGroup !== 'fav') itemGroupSel.value = String(state.curGroup);
|
|
1368
|
+
}
|
|
1369
|
+
itemMask.classList.add('open');
|
|
1370
|
+
setTimeout(function(){ itemName.focus(); }, 60);
|
|
1371
|
+
}
|
|
1372
|
+
function closeItemForm(){ itemMask.classList.remove('open'); }
|
|
1373
|
+
function saveItem(){
|
|
1374
|
+
var name = itemName.value.trim();
|
|
1375
|
+
if (!name){ toast('标题不能为空', true); return; }
|
|
1376
|
+
var content = itemContent.value.trim();
|
|
1377
|
+
if (!content){ toast('内容不能为空', true); return; }
|
|
1378
|
+
var gid = Number(itemGroupSel.value);
|
|
1379
|
+
var tags = itemTags.value.split(/[,,]/).map(function(s){ return s.trim(); }).filter(Boolean).slice(0, 3);
|
|
1380
|
+
var editId = editingItemId;
|
|
1381
|
+
closeItemForm();
|
|
1382
|
+
writePromptsJson({
|
|
1383
|
+
apply: function(data){
|
|
1384
|
+
var gArr = data.prompts.filter(function(x){ return x.group === gid; });
|
|
1385
|
+
var maxOrder = gArr.reduce(function(m, x){ return Math.max(m, x.order || 0); }, 0);
|
|
1386
|
+
if (editId){
|
|
1387
|
+
var p = data.prompts.find(function(x){ return x.id === editId; });
|
|
1388
|
+
if (!p){ toast('该提示词已不存在,请刷新后重试', true); return null; }
|
|
1389
|
+
if (p.group !== gid){ p.order = maxOrder + 1; }
|
|
1390
|
+
p.title = name; p.group = gid; p.content = content; p.tags = tags; p.updatedAt = nowStamp();
|
|
1391
|
+
} else {
|
|
1392
|
+
data.prompts.push({
|
|
1393
|
+
id: snowflakeId(), group: gid, title: name, content: content,
|
|
1394
|
+
tags: tags, order: maxOrder + 1, favorite: false, createdAt: nowStamp()
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
return data;
|
|
1398
|
+
},
|
|
1399
|
+
cmd: function(){
|
|
1400
|
+
var gname = groupOf(gid) ? groupOf(gid).name : '分组 ' + gid;
|
|
1401
|
+
if (editId){
|
|
1402
|
+
return '请修改锦囊里 id 为 ' + editId + ' 的提示词:\n【新标题】' + name + '\n【新分组】' + gname + '\n【新标签】' + tags.join(', ') + '\n【新内容】\n' + content + '\n同时把 updatedAt 更新为当前时间(格式 YYYY-MM-DD HH:mm:ss,精确到秒)';
|
|
1403
|
+
}
|
|
1404
|
+
return '请把以下提示词加到锦囊「' + gname + '」分组:\n【标题】' + name + '\n【标签】' + tags.join(', ') + '\n【内容】\n' + content;
|
|
1405
|
+
}
|
|
1406
|
+
}).then(function(ok){ if (ok) toast(editId ? '提示词已更新' : '提示词已入囊'); });
|
|
1407
|
+
}
|
|
1408
|
+
function delItem(p){
|
|
1409
|
+
if (!window.confirm('确定删除「' + p.title + '」?删除前会自动备份,可从 ↩ 恢复。')) return;
|
|
1410
|
+
writePromptsJson({
|
|
1411
|
+
apply: function(data){
|
|
1412
|
+
data.prompts = data.prompts.filter(function(x){ return x.id !== p.id; });
|
|
1413
|
+
return data;
|
|
1414
|
+
},
|
|
1415
|
+
cmd: '请删除锦囊里「' + p.title + '」这条'
|
|
1416
|
+
}).then(function(ok){ if (ok) toast('已删除「' + p.title + '」'); });
|
|
1417
|
+
}
|
|
1418
|
+
document.getElementById('itemCancel').addEventListener('click', closeItemForm);
|
|
1419
|
+
document.getElementById('itemSave').addEventListener('click', saveItem);
|
|
1420
|
+
itemName.addEventListener('keydown', function(e){ if (e.key === 'Enter'){ e.preventDefault(); saveItem(); } });
|
|
1421
|
+
itemMask.addEventListener('click', function(e){ if (e.target === this) closeItemForm(); });
|
|
1422
|
+
|
|
1423
|
+
/* ---------- 分组增删改 ---------- */
|
|
1424
|
+
var groupMask = document.getElementById('groupMask');
|
|
1425
|
+
var groupTitleEl = document.getElementById('groupTitle');
|
|
1426
|
+
var groupName = document.getElementById('groupName');
|
|
1427
|
+
var groupIcon = document.getElementById('groupIcon');
|
|
1428
|
+
var editingGroupId = null;
|
|
1429
|
+
function openGroupForm(g){
|
|
1430
|
+
editingGroupId = g ? g.id : null;
|
|
1431
|
+
groupTitleEl.textContent = g ? '编辑分组' : '新建分组';
|
|
1432
|
+
groupName.value = g ? g.name : '';
|
|
1433
|
+
groupIcon.value = g ? g.icon : '✨';
|
|
1434
|
+
groupMask.classList.add('open');
|
|
1435
|
+
setTimeout(function(){ groupName.focus(); }, 60);
|
|
1436
|
+
}
|
|
1437
|
+
function closeGroupForm(){ groupMask.classList.remove('open'); }
|
|
1438
|
+
function saveGroup(){
|
|
1439
|
+
var name = groupName.value.trim();
|
|
1440
|
+
if (!name){ toast('分组名不能为空', true); return; }
|
|
1441
|
+
var icon = (groupIcon.value.trim() || '✨').slice(0, 4);
|
|
1442
|
+
var editId = editingGroupId;
|
|
1443
|
+
if (state.data.groups.some(function(g){ return g.name === name && g.id !== editId; })){
|
|
1444
|
+
toast('已存在同名分组「' + name + '」', true);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
closeGroupForm();
|
|
1448
|
+
writePromptsJson({
|
|
1449
|
+
apply: function(data){
|
|
1450
|
+
if (editId){
|
|
1451
|
+
var g = data.groups.find(function(x){ return x.id === editId; });
|
|
1452
|
+
if (!g){ toast('该分组已不存在,请刷新后重试', true); return null; }
|
|
1453
|
+
g.name = name; g.icon = icon;
|
|
1454
|
+
} else {
|
|
1455
|
+
data.groups.push({ id: snowflakeId(), name: name, icon: icon });
|
|
1456
|
+
}
|
|
1457
|
+
return data;
|
|
1458
|
+
},
|
|
1459
|
+
cmd: editId
|
|
1460
|
+
? ('请把锦囊分组改名为「' + name + '」(icon ' + icon + ')')
|
|
1461
|
+
: ('请帮我在锦囊创建分组「' + name + '」(icon ' + icon + ')')
|
|
1462
|
+
}).then(function(ok){
|
|
1463
|
+
if (ok) toast(editId ? '分组已更新' : '分组「' + name + '」已创建');
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function delGroup(g){
|
|
1467
|
+
var n = state.data.prompts.filter(function(p){ return p.group === g.id; }).length;
|
|
1468
|
+
if (n > 0){ toast('「' + g.name + '」内还有 ' + n + ' 条提示词,请先移出或删除', true); return; }
|
|
1469
|
+
if (!window.confirm('确定删除分组「' + g.name + '」?')) return;
|
|
1470
|
+
if (state.curGroup === g.id) state.curGroup = 'all';
|
|
1471
|
+
writePromptsJson({
|
|
1472
|
+
apply: function(data){
|
|
1473
|
+
data.groups = data.groups.filter(function(x){ return x.id !== g.id; });
|
|
1474
|
+
return data;
|
|
1475
|
+
},
|
|
1476
|
+
cmd: '请删除锦囊分组「' + g.name + '」'
|
|
1477
|
+
}).then(function(ok){ if (ok) toast('分组「' + g.name + '」已删除'); });
|
|
1478
|
+
}
|
|
1479
|
+
document.getElementById('groupCancel').addEventListener('click', closeGroupForm);
|
|
1480
|
+
document.getElementById('groupSave').addEventListener('click', saveGroup);
|
|
1481
|
+
groupName.addEventListener('keydown', function(e){ if (e.key === 'Enter'){ e.preventDefault(); saveGroup(); } });
|
|
1482
|
+
groupMask.addEventListener('click', function(e){ if (e.target === this) closeGroupForm(); });
|
|
1483
|
+
var btnAddItem = document.getElementById('btnAddItem');
|
|
1484
|
+
if (btnAddItem) btnAddItem.addEventListener('click', function(){ openItemForm(null); });
|
|
1485
|
+
|
|
1486
|
+
/* ---------- 排序指令 ---------- */
|
|
1487
|
+
function buildSortCmd(){
|
|
1488
|
+
var lines = ['请帮我调整 prompt-pouch 锦囊(按以下内容落盘 prompts.json):'];
|
|
1489
|
+
var moves = state.moves || [];
|
|
1490
|
+
if (moves.length){
|
|
1491
|
+
lines.push('【分组调整】');
|
|
1492
|
+
moves.forEach(function(m){
|
|
1493
|
+
lines.push('把「' + m.title + '」从「' + m.from + '」移到「' + m.to + '」');
|
|
1494
|
+
});
|
|
1495
|
+
lines.push('');
|
|
1496
|
+
}
|
|
1497
|
+
lines.push('【各组顺序】');
|
|
1498
|
+
state.data.groups.forEach(function(g){
|
|
1499
|
+
var ps = state.data.prompts.filter(function(p){ return p.group === g.id; })
|
|
1500
|
+
.sort(function(a,b){ return (a.order||0) - (b.order||0); });
|
|
1501
|
+
if (!ps.length) return;
|
|
1502
|
+
lines.push('【' + g.name + '】');
|
|
1503
|
+
ps.forEach(function(p, i){ lines.push((i + 1) + '. ' + p.title); });
|
|
1504
|
+
});
|
|
1505
|
+
return lines.join('\n');
|
|
1506
|
+
}
|
|
1507
|
+
btnMarkSaved.addEventListener('click', function(){
|
|
1508
|
+
state.dirty = false;
|
|
1509
|
+
state.moves = [];
|
|
1510
|
+
persistUserState();
|
|
1511
|
+
refreshDirtyUI();
|
|
1512
|
+
var old = btnMarkSaved.textContent;
|
|
1513
|
+
btnMarkSaved.textContent = '✓ 已清除';
|
|
1514
|
+
setTimeout(function(){ btnMarkSaved.textContent = old; }, 1600);
|
|
1515
|
+
});
|
|
1516
|
+
btnSortCmd.addEventListener('click', function(){
|
|
1517
|
+
copyText(buildSortCmd(), {
|
|
1518
|
+
querySelector: function(){ return { classList:{ add:function(){}, remove:function(){} } }; }
|
|
1519
|
+
});
|
|
1520
|
+
var old = btnSortCmd.textContent;
|
|
1521
|
+
btnSortCmd.textContent = '✓ 指令已复制,交给 AI 落盘';
|
|
1522
|
+
setTimeout(function(){
|
|
1523
|
+
btnSortCmd.textContent = old;
|
|
1524
|
+
if (state.dirty){
|
|
1525
|
+
btnSortCmd.textContent = '📋 复制变更指令(未保存)';
|
|
1526
|
+
}
|
|
1527
|
+
}, 2000);
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
/* ---------- 搜索 ---------- */
|
|
1531
|
+
searchInput.addEventListener('input', function(){
|
|
1532
|
+
state.query = searchInput.value.trim();
|
|
1533
|
+
searchClear.style.display = state.query ? '' : 'none';
|
|
1534
|
+
render();
|
|
1535
|
+
});
|
|
1536
|
+
searchClear.addEventListener('click', function(){
|
|
1537
|
+
searchInput.value = ''; state.query = '';
|
|
1538
|
+
searchClear.style.display = 'none';
|
|
1539
|
+
render();
|
|
1540
|
+
});
|
|
1541
|
+
document.addEventListener('keydown', function(e){
|
|
1542
|
+
if (e.key === 'Escape'){
|
|
1543
|
+
closeFillPanel();
|
|
1544
|
+
closeGpop();
|
|
1545
|
+
closeItemForm();
|
|
1546
|
+
closeGroupForm();
|
|
1547
|
+
document.getElementById('bkMask').classList.remove('open');
|
|
1548
|
+
closeDetail();
|
|
1549
|
+
if (searchInput.value){ searchInput.value = ''; state.query = ''; searchClear.style.display = 'none'; render(); }
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
/* ---------- 本地持久化(IndexedDB + localStorage 镜像) ---------- */
|
|
1554
|
+
var localState = null;
|
|
1555
|
+
var db = null;
|
|
1556
|
+
function withTimeout(p, ms, fallback){
|
|
1557
|
+
return new Promise(function(res){
|
|
1558
|
+
var done = false;
|
|
1559
|
+
var t = setTimeout(function(){ if (!done){ done = true; res(fallback); } }, ms);
|
|
1560
|
+
p.then(function(v){ if (!done){ done = true; clearTimeout(t); res(v); } },
|
|
1561
|
+
function(){ if (!done){ done = true; clearTimeout(t); res(fallback); } });
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
function idbOpen(){
|
|
1565
|
+
if (db) return Promise.resolve(db);
|
|
1566
|
+
if (!('indexedDB' in window)) return Promise.reject(new Error('no idb'));
|
|
1567
|
+
var p = new Promise(function(res, rej){
|
|
1568
|
+
var req = indexedDB.open('pouch-state-v1', 1);
|
|
1569
|
+
req.onupgradeneeded = function(){
|
|
1570
|
+
if (!req.result.objectStoreNames.contains('kv')) req.result.createObjectStore('kv');
|
|
1571
|
+
};
|
|
1572
|
+
req.onsuccess = function(){ db = req.result; res(db); };
|
|
1573
|
+
req.onerror = function(){ rej(req.error); };
|
|
1574
|
+
});
|
|
1575
|
+
return withTimeout(p, 2000, null).then(function(d){
|
|
1576
|
+
if (!d) return Promise.reject(new Error('idb timeout'));
|
|
1577
|
+
return d;
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
function idbGet(key){
|
|
1581
|
+
return idbOpen().then(function(){
|
|
1582
|
+
return new Promise(function(res){
|
|
1583
|
+
var rq = db.transaction('kv', 'readonly').objectStore('kv').get(key);
|
|
1584
|
+
rq.onsuccess = function(){ res(rq.result === undefined ? null : rq.result); };
|
|
1585
|
+
rq.onerror = function(){ res(null); };
|
|
1586
|
+
});
|
|
1587
|
+
}).catch(function(){ return null; });
|
|
1588
|
+
}
|
|
1589
|
+
function idbSet(key, val){
|
|
1590
|
+
return idbOpen().then(function(){
|
|
1591
|
+
return new Promise(function(res){
|
|
1592
|
+
var tx = db.transaction('kv', 'readwrite');
|
|
1593
|
+
tx.objectStore('kv').put(val, key);
|
|
1594
|
+
tx.oncomplete = function(){ res(); };
|
|
1595
|
+
tx.onerror = function(){ res(); };
|
|
1596
|
+
});
|
|
1597
|
+
}).catch(function(){});
|
|
1598
|
+
}
|
|
1599
|
+
function snapshotUserState(){
|
|
1600
|
+
var favs = state.data.prompts.filter(function(p){ return p.favorite; }).map(function(p){ return p.id; });
|
|
1601
|
+
var orders = {};
|
|
1602
|
+
state.data.prompts.forEach(function(p){ orders[p.id] = p.order || 0; });
|
|
1603
|
+
var groups = {};
|
|
1604
|
+
state.data.prompts.forEach(function(p){ groups[p.id] = p.group; });
|
|
1605
|
+
return { favs: favs, orders: orders, groups: groups, sortMode: state.sortMode, usage: state.usage, dirty: !!state.dirty, moves: state.moves || [] };
|
|
1606
|
+
}
|
|
1607
|
+
function persistUserState(){
|
|
1608
|
+
if (!state.data) return;
|
|
1609
|
+
localState = snapshotUserState();
|
|
1610
|
+
idbSet('userState', localState);
|
|
1611
|
+
try{ localStorage.setItem('pouch-userstate', JSON.stringify(localState)); }catch(e){}
|
|
1612
|
+
}
|
|
1613
|
+
function applyUserStateFrom(us){
|
|
1614
|
+
if (!us || !state.data) return;
|
|
1615
|
+
// favs 是收藏全量集合:先清后设,恢复/启动都精确还原收藏态
|
|
1616
|
+
state.data.prompts.forEach(function(p){ p.favorite = false; });
|
|
1617
|
+
(us.favs || []).forEach(function(id){
|
|
1618
|
+
var p = state.data.prompts.find(function(x){ return x.id === id; });
|
|
1619
|
+
if (p) p.favorite = true;
|
|
1620
|
+
});
|
|
1621
|
+
var o = us.orders || {};
|
|
1622
|
+
state.data.prompts.forEach(function(p){
|
|
1623
|
+
if (typeof o[p.id] === 'number' && o[p.id] > 0) p.order = o[p.id];
|
|
1624
|
+
});
|
|
1625
|
+
var g = us.groups || {};
|
|
1626
|
+
state.data.prompts.forEach(function(p){
|
|
1627
|
+
if (g[p.id] && g[p.id] !== p.group) p.group = g[p.id];
|
|
1628
|
+
});
|
|
1629
|
+
if (us.sortMode) state.sortMode = us.sortMode;
|
|
1630
|
+
if (us.usage) state.usage = us.usage;
|
|
1631
|
+
if (us.dirty) state.dirty = true;
|
|
1632
|
+
if (us.moves && us.moves.length) state.moves = us.moves;
|
|
1633
|
+
}
|
|
1634
|
+
function applyUserState(){ applyUserStateFrom(localState); }
|
|
1635
|
+
function loadLocalState(){
|
|
1636
|
+
return idbGet('userState').then(function(us){
|
|
1637
|
+
if (us){ localState = us; return; }
|
|
1638
|
+
try{
|
|
1639
|
+
var raw = localStorage.getItem('pouch-userstate');
|
|
1640
|
+
if (raw){ localState = JSON.parse(raw); idbSet('userState', localState); }
|
|
1641
|
+
}catch(e){}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/* ---------- 同目录自动备份(File System Access API) ---------- */
|
|
1646
|
+
var bkDirHandle = null;
|
|
1647
|
+
var bkTimer = null;
|
|
1648
|
+
var lastBackupTs = 0;
|
|
1649
|
+
var pendingAuth = false;
|
|
1650
|
+
var bkDeclined = false;
|
|
1651
|
+
try{ lastBackupTs = parseInt(localStorage.getItem('pouch-last-backup') || '0', 10) || 0; }catch(e){}
|
|
1652
|
+
try{ bkDeclined = localStorage.getItem('pouch-bk-declined') === '1'; }catch(e){}
|
|
1653
|
+
function bkSupported(){ return 'showDirectoryPicker' in window; }
|
|
1654
|
+
function fmtTime(ts){
|
|
1655
|
+
var d = ts instanceof Date ? ts : new Date(ts);
|
|
1656
|
+
function p(n){ return (n < 10 ? '0' : '') + n; }
|
|
1657
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
|
|
1658
|
+
+ ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
1659
|
+
}
|
|
1660
|
+
function currentSnapshot(){
|
|
1661
|
+
return {
|
|
1662
|
+
time: new Date().toISOString(),
|
|
1663
|
+
counts: { groups: state.data.groups.length, prompts: state.data.prompts.length },
|
|
1664
|
+
data: state.data,
|
|
1665
|
+
userState: snapshotUserState()
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
var subDirCache = {};
|
|
1669
|
+
function subDir(dir){
|
|
1670
|
+
if (!dir) return Promise.resolve(bkDirHandle);
|
|
1671
|
+
if (subDirCache[dir]) return Promise.resolve(subDirCache[dir]);
|
|
1672
|
+
return bkDirHandle.getDirectoryHandle(dir, { create: true }).then(function(h){
|
|
1673
|
+
subDirCache[dir] = h;
|
|
1674
|
+
return h;
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/* 数据文件读:cache/prompts.json 优先,读不到回退根目录旧文件(旧目录结构兼容) */
|
|
1678
|
+
function readPouch(){
|
|
1679
|
+
return bkRead('prompts.json', 'cache').then(function(text){
|
|
1680
|
+
if (text !== null) return text;
|
|
1681
|
+
return bkRead('prompts.json');
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
/* 备份文件读:backup/ 优先,读不到回退根目录旧文件(旧目录结构兼容) */
|
|
1685
|
+
function readBk(name){
|
|
1686
|
+
return bkRead(name, 'backup').then(function(text){
|
|
1687
|
+
if (text !== null) return text;
|
|
1688
|
+
return bkRead(name);
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function bkWrite(name, dir, text){
|
|
1692
|
+
return subDir(dir).then(function(h){
|
|
1693
|
+
if (!h) throw new Error('subdir unavailable');
|
|
1694
|
+
return h.getFileHandle(name, { create: true });
|
|
1695
|
+
}).then(function(fh){
|
|
1696
|
+
return fh.createWritable().then(function(w){
|
|
1697
|
+
return w.write(text).then(function(){ return w.close(); });
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function bkRead(name, dir){
|
|
1702
|
+
return subDir(dir).then(function(h){
|
|
1703
|
+
if (!h) throw new Error('subdir unavailable');
|
|
1704
|
+
return h.getFileHandle(name, { create: false });
|
|
1705
|
+
}).then(function(fh){
|
|
1706
|
+
return fh.getFile().then(function(f){ return f.text(); });
|
|
1707
|
+
}).catch(function(){ return null; });
|
|
1708
|
+
}
|
|
1709
|
+
function writeBackupNow(){
|
|
1710
|
+
if (!bkDirHandle) return Promise.resolve(false);
|
|
1711
|
+
var snap = currentSnapshot();
|
|
1712
|
+
return readBk('pouch-backup.json').then(function(prev){
|
|
1713
|
+
var chain = Promise.resolve();
|
|
1714
|
+
if (prev !== null) chain = bkWrite('pouch-backup.prev.json', 'backup', prev);
|
|
1715
|
+
return chain.then(function(){
|
|
1716
|
+
return bkWrite('pouch-backup.json', 'backup', JSON.stringify(snap, null, 2));
|
|
1717
|
+
});
|
|
1718
|
+
}).then(function(){
|
|
1719
|
+
lastBackupTs = Date.now();
|
|
1720
|
+
try{ localStorage.setItem('pouch-last-backup', String(lastBackupTs)); }catch(e){}
|
|
1721
|
+
updateBkUI();
|
|
1722
|
+
return true;
|
|
1723
|
+
}).catch(function(){
|
|
1724
|
+
updateBkUI();
|
|
1725
|
+
return false;
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
function writeSnapNow(){
|
|
1729
|
+
var snap = currentSnapshot();
|
|
1730
|
+
return idbGet('snapRing').then(function(ring){
|
|
1731
|
+
ring = Array.isArray(ring) ? ring : [];
|
|
1732
|
+
ring.push(snap);
|
|
1733
|
+
if (ring.length > 5) ring = ring.slice(ring.length - 5);
|
|
1734
|
+
return idbSet('snapRing', ring);
|
|
1735
|
+
}).then(function(){
|
|
1736
|
+
lastBackupTs = Date.now();
|
|
1737
|
+
try{ localStorage.setItem('pouch-last-backup', String(lastBackupTs)); }catch(e){}
|
|
1738
|
+
updateBkUI();
|
|
1739
|
+
return true;
|
|
1740
|
+
}).catch(function(){
|
|
1741
|
+
updateBkUI();
|
|
1742
|
+
return false;
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
function scheduleBackup(){
|
|
1746
|
+
if (bkTimer) clearTimeout(bkTimer);
|
|
1747
|
+
bkTimer = setTimeout(function(){
|
|
1748
|
+
var chain = Promise.resolve();
|
|
1749
|
+
if (state.dirty) chain = flushData();
|
|
1750
|
+
chain.then(function(){
|
|
1751
|
+
if (bkDirHandle){ writeBackupNow(); return; }
|
|
1752
|
+
maybeAutoAuth().then(function(granted){
|
|
1753
|
+
if (granted) writeBackupNow();
|
|
1754
|
+
else writeSnapNow();
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
}, 2000);
|
|
1758
|
+
}
|
|
1759
|
+
function maybeAutoAuth(){
|
|
1760
|
+
if (bkDirHandle) return Promise.resolve(true);
|
|
1761
|
+
if (!bkSupported() || bkDeclined || pendingAuth) return Promise.resolve(false);
|
|
1762
|
+
if (!state.data) return Promise.resolve(false);
|
|
1763
|
+
var ok = window.confirm('锦囊自动备份\n\n数据一有变更就会自动备份,建议授权写入 prompt-pouch.html 所在目录(首次弹窗选一次,浏览器会记住)。\n\n点「确定」打开目录选择 → 选 prompt-pouch.html 所在文件夹即可;\n点「取消」则改用浏览器本地快照(保留最近 5 份,清浏览器数据会丢)。');
|
|
1764
|
+
if (!ok){
|
|
1765
|
+
bkDeclined = true;
|
|
1766
|
+
try{ localStorage.setItem('pouch-bk-declined', '1'); }catch(e){}
|
|
1767
|
+
return Promise.resolve(false);
|
|
1768
|
+
}
|
|
1769
|
+
pendingAuth = true; updateBkUI();
|
|
1770
|
+
return window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
|
|
1771
|
+
.then(function(h){
|
|
1772
|
+
bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
|
|
1773
|
+
try{ localStorage.removeItem('pouch-bk-declined'); }catch(e){}
|
|
1774
|
+
idbSet('bkHandle', bkDirHandle);
|
|
1775
|
+
updateBkUI();
|
|
1776
|
+
return true;
|
|
1777
|
+
})
|
|
1778
|
+
.catch(function(){ pendingAuth = false; updateBkUI(); return false; });
|
|
1779
|
+
}
|
|
1780
|
+
function updateBkUI(){
|
|
1781
|
+
if (!bkSupported()){
|
|
1782
|
+
btnBkEnable.style.display = 'none';
|
|
1783
|
+
btnBkChange.style.display = 'none';
|
|
1784
|
+
btnBkRestore.style.display = '';
|
|
1785
|
+
bkStatus.textContent = '本地快照备份中(此浏览器不支持目录授权,建议 Chrome / Edge)';
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (pendingAuth){
|
|
1789
|
+
btnBkEnable.style.display = '';
|
|
1790
|
+
btnBkEnable.textContent = '🔑 重新授权备份';
|
|
1791
|
+
btnBkChange.style.display = 'none';
|
|
1792
|
+
btnBkRestore.style.display = '';
|
|
1793
|
+
bkStatus.textContent = '备份已连接 · 需重新授权目录';
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (!bkDirHandle){
|
|
1797
|
+
btnBkEnable.style.display = '';
|
|
1798
|
+
btnBkEnable.textContent = '💾 授权目录备份';
|
|
1799
|
+
btnBkChange.style.display = 'none';
|
|
1800
|
+
btnBkRestore.style.display = '';
|
|
1801
|
+
bkStatus.textContent = '本地快照备份中(保留最近 5 份)· 可授权目录写 backup/pouch-backup.json';
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
btnBkEnable.style.display = '';
|
|
1805
|
+
btnBkEnable.textContent = '💾 立即备份';
|
|
1806
|
+
btnBkChange.style.display = '';
|
|
1807
|
+
btnBkRestore.style.display = '';
|
|
1808
|
+
bkStatus.textContent = '备份已开启' + (lastBackupTs ? ' · 上次 ' + fmtTime(lastBackupTs) : '');
|
|
1809
|
+
}
|
|
1810
|
+
function loadDirHandle(){
|
|
1811
|
+
return idbGet('bkHandle').then(function(h){
|
|
1812
|
+
if (!h) return;
|
|
1813
|
+
bkDirHandle = h;
|
|
1814
|
+
if (typeof bkDirHandle.queryPermission === 'function'){
|
|
1815
|
+
return bkDirHandle.queryPermission({ mode: 'readwrite' }).then(function(st){
|
|
1816
|
+
pendingAuth = (st !== 'granted');
|
|
1817
|
+
}).catch(function(){ pendingAuth = false; });
|
|
1818
|
+
}
|
|
1819
|
+
pendingAuth = false;
|
|
1820
|
+
}).catch(function(){});
|
|
1821
|
+
}
|
|
1822
|
+
btnBkEnable.addEventListener('click', function(){
|
|
1823
|
+
if (bkDirHandle && !pendingAuth){
|
|
1824
|
+
writeBackupNow().then(function(ok){ toast(ok ? '✓ 备份完成 · backup/pouch-backup.json' : '备份失败,请重新授权目录', !ok); });
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (!bkSupported()){
|
|
1828
|
+
alert('当前浏览器不支持「选择目录」授权(Chrome / Edge 可用)。\n本页已自动使用本地快照备份(保留最近 5 份,随浏览器数据走);\n也可随时对 AI 说「导出锦囊数据」生成备份文件备用。');
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (bkDirHandle && pendingAuth){
|
|
1832
|
+
bkDirHandle.requestPermission({ mode: 'readwrite' }).then(function(st){
|
|
1833
|
+
if (st === 'granted'){ pendingAuth = false; updateBkUI(); writeBackupNow(); }
|
|
1834
|
+
else bkStatus.textContent = '未获授权,备份暂停';
|
|
1835
|
+
}).catch(function(){});
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
|
|
1839
|
+
.then(function(h){
|
|
1840
|
+
bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
|
|
1841
|
+
try{ localStorage.removeItem('pouch-bk-declined'); }catch(e){}
|
|
1842
|
+
idbSet('bkHandle', bkDirHandle);
|
|
1843
|
+
updateBkUI();
|
|
1844
|
+
writeBackupNow();
|
|
1845
|
+
})
|
|
1846
|
+
.catch(function(){});
|
|
1847
|
+
});
|
|
1848
|
+
btnBkChange.addEventListener('click', function(){
|
|
1849
|
+
if (!bkSupported()){ updateBkUI(); return; }
|
|
1850
|
+
if (!window.confirm('更换备份目录\n\n建议仍选 prompt-pouch.html 所在目录:页面新增/编辑的分组与提示词、自动备份都写在这个目录,选别处会造成页面与数据文件不一致。\n\n确定继续选择目录?')) return;
|
|
1851
|
+
window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
|
|
1852
|
+
.then(function(h){
|
|
1853
|
+
bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
|
|
1854
|
+
try{ localStorage.removeItem('pouch-bk-declined'); }catch(e){}
|
|
1855
|
+
idbSet('bkHandle', bkDirHandle);
|
|
1856
|
+
updateBkUI();
|
|
1857
|
+
writeBackupNow();
|
|
1858
|
+
})
|
|
1859
|
+
.catch(function(){});
|
|
1860
|
+
});
|
|
1861
|
+
btnBkRestore.addEventListener('click', function(){
|
|
1862
|
+
document.getElementById('bkList').innerHTML = '<div class="bk-empty">读取备份清单…</div>';
|
|
1863
|
+
document.getElementById('bkMaskNote').textContent = bkDirHandle
|
|
1864
|
+
? 'backup/ 目录滚动备份(当前 pouch-backup.json + 上一版 .prev.json)'
|
|
1865
|
+
: '浏览器本地快照(保留最近 5 份,随浏览器数据走)';
|
|
1866
|
+
document.getElementById('bkMask').classList.add('open');
|
|
1867
|
+
if (bkDirHandle){
|
|
1868
|
+
Promise.all([readBk('pouch-backup.json'), readBk('pouch-backup.prev.json')])
|
|
1869
|
+
.then(function(arr){
|
|
1870
|
+
renderBkList([
|
|
1871
|
+
{ name: 'backup/pouch-backup.json · 当前', content: arr[0] },
|
|
1872
|
+
{ name: 'backup/pouch-backup.prev.json · 上一版', content: arr[1] }
|
|
1873
|
+
]);
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
idbGet('snapRing').then(function(ring){
|
|
1878
|
+
ring = Array.isArray(ring) ? ring.slice() : [];
|
|
1879
|
+
ring.reverse();
|
|
1880
|
+
renderBkList(ring.map(function(snap, i){
|
|
1881
|
+
return { name: '本地快照 #' + (ring.length - i), content: JSON.stringify(snap) };
|
|
1882
|
+
}));
|
|
1883
|
+
});
|
|
1884
|
+
});
|
|
1885
|
+
function renderBkList(list){
|
|
1886
|
+
var html = ''; var any = false;
|
|
1887
|
+
list.forEach(function(item, i){
|
|
1888
|
+
if (!item || item.content === null) return;
|
|
1889
|
+
try{
|
|
1890
|
+
var snap = JSON.parse(item.content);
|
|
1891
|
+
any = true;
|
|
1892
|
+
html += '<div class="bk-item">'
|
|
1893
|
+
+ '<div class="bk-item-main"><b>' + item.name + '</b>'
|
|
1894
|
+
+ '<span>备份时间 ' + fmtTime(snap.time) + '</span>'
|
|
1895
|
+
+ '<span>' + snap.counts.groups + ' 分组 / ' + snap.counts.prompts + ' 条提示词</span></div>'
|
|
1896
|
+
+ '<button class="btn bk-restore" data-i="' + i + '">恢复</button>'
|
|
1897
|
+
+ '</div>';
|
|
1898
|
+
}catch(e){}
|
|
1899
|
+
});
|
|
1900
|
+
if (!any) html = '<div class="bk-empty">暂无备份。数据一有变更会自动生成备份:已授权目录时写 pouch-backup.json(滚动保留上一版 .prev.json),未授权时写浏览器本地快照(保留最近 5 份)。</div>';
|
|
1901
|
+
document.getElementById('bkList').innerHTML = html;
|
|
1902
|
+
document.querySelectorAll('.bk-restore').forEach(function(btn){
|
|
1903
|
+
btn.addEventListener('click', function(){
|
|
1904
|
+
doRestore(list[parseInt(btn.getAttribute('data-i'), 10)].content);
|
|
1905
|
+
});
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
function doRestore(content){
|
|
1909
|
+
var snap;
|
|
1910
|
+
try{ snap = JSON.parse(content); }catch(e){ alert('备份文件损坏,无法恢复。'); return; }
|
|
1911
|
+
if (!snap || !snap.data || !Array.isArray(snap.data.groups) || !Array.isArray(snap.data.prompts)){
|
|
1912
|
+
alert('备份结构不完整,无法恢复。'); return;
|
|
1913
|
+
}
|
|
1914
|
+
var ok = window.confirm('将用这份备份覆盖当前数据:\n\n· 备份时间:' + fmtTime(snap.time)
|
|
1915
|
+
+ '\n· 内容:' + snap.counts.groups + ' 分组 / ' + snap.counts.prompts + ' 条提示词\n\n'
|
|
1916
|
+
+ '恢复前会先把当前数据自动另存一份新备份,可随时再恢复回来。\n\n确定恢复?');
|
|
1917
|
+
if (!ok) return;
|
|
1918
|
+
// 先把当前状态固化进备份(目录备份滚入 .prev.json / 快照环记一份),再应用所选备份
|
|
1919
|
+
(bkDirHandle ? writeBackupNow() : writeSnapNow()).then(function(){
|
|
1920
|
+
state.data = snap.data;
|
|
1921
|
+
applyUserStateFrom(snap.userState || {});
|
|
1922
|
+
state.dirty = false; state.moves = [];
|
|
1923
|
+
persistUserState();
|
|
1924
|
+
renderNav(); render();
|
|
1925
|
+
document.getElementById('bkMask').classList.remove('open');
|
|
1926
|
+
if (bkDirHandle){
|
|
1927
|
+
return bkWrite('prompts.json', JSON.stringify(snap.data, null, 2)).then(function(){
|
|
1928
|
+
toast('✓ 已恢复并写入 prompts.json,刷新即见');
|
|
1929
|
+
}).catch(function(){
|
|
1930
|
+
toast('已恢复;写入 prompts.json 失败,请重新授权目录', true);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
markDirty();
|
|
1934
|
+
toast('✓ 已恢复;未授权目录——点「📋 复制变更指令」交给 AI 落盘,或先「💾 授权目录备份」');
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
document.getElementById('bkCancelBtn').addEventListener('click', function(){
|
|
1938
|
+
document.getElementById('bkMask').classList.remove('open');
|
|
1939
|
+
});
|
|
1940
|
+
document.getElementById('bkMask').addEventListener('click', function(e){
|
|
1941
|
+
if (e.target === this) this.classList.remove('open');
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
/* ---------- 启动 ---------- */
|
|
1945
|
+
var bootMode = null;
|
|
1946
|
+
var bootRendered = false;
|
|
1947
|
+
function firstRender(){
|
|
1948
|
+
if (bootRendered || !state.data) return;
|
|
1949
|
+
bootRendered = true;
|
|
1950
|
+
applyUserState();
|
|
1951
|
+
syncSortUI();
|
|
1952
|
+
refreshDirtyUI();
|
|
1953
|
+
renderNav();
|
|
1954
|
+
render();
|
|
1955
|
+
if (bootMode === 'cache') cacheBadge.style.display = '';
|
|
1956
|
+
if (bootMode === 'demo'){
|
|
1957
|
+
cacheBadge.textContent = '📌 示例模式:未读到 cache/prompts.json,当前展示系统自带示例提示词(file:// 直开或数据文件尚未生成)。经 HTTP 打开页面并生成数据文件后即为你的提示词,示例可随时删除。';
|
|
1958
|
+
cacheBadge.style.display = '';
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// 数据优先:fetch 主链不依赖本地存储,页面尽快可见
|
|
1962
|
+
fetch('cache/prompts.json', { cache: 'no-store' })
|
|
1963
|
+
.then(function(r){ if (!r.ok) throw new Error('nf'); return r.json(); })
|
|
1964
|
+
.catch(function(){
|
|
1965
|
+
return fetch('prompts.json', { cache: 'no-store' }).then(function(r2){ return r2.json(); });
|
|
1966
|
+
})
|
|
1967
|
+
.then(function(data){
|
|
1968
|
+
state.data = data;
|
|
1969
|
+
idbSet('lastData', data);
|
|
1970
|
+
bootMode = 'live';
|
|
1971
|
+
firstRender();
|
|
1972
|
+
})
|
|
1973
|
+
.catch(function(){
|
|
1974
|
+
idbGet('lastData').then(function(cached){
|
|
1975
|
+
if (cached){
|
|
1976
|
+
state.data = cached;
|
|
1977
|
+
bootMode = 'cache';
|
|
1978
|
+
firstRender();
|
|
1979
|
+
} else {
|
|
1980
|
+
state.data = DEFAULT_POUCH;
|
|
1981
|
+
bootMode = 'demo';
|
|
1982
|
+
firstRender();
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
// 本地态并行:可用则应用收藏与排序并刷新备份区(不阻塞数据展示)
|
|
1987
|
+
idbOpen()
|
|
1988
|
+
.then(loadLocalState)
|
|
1989
|
+
.then(loadDirHandle)
|
|
1990
|
+
.catch(function(){})
|
|
1991
|
+
.then(function(){
|
|
1992
|
+
applyUserState();
|
|
1993
|
+
syncSortUI();
|
|
1994
|
+
refreshDirtyUI();
|
|
1995
|
+
if (bootRendered){ renderNav(); render(); }
|
|
1996
|
+
updateBkUI();
|
|
1997
|
+
});
|
|
1998
|
+
})();
|
|
1999
|
+
|
|
2000
|
+
/* 主题换肤:四套配色随点随换;记忆上次选择(localStorage,失败静默) */
|
|
2001
|
+
(function(){
|
|
2002
|
+
var THEMES = ['gold','tech','mono','mint'];
|
|
2003
|
+
function init(){
|
|
2004
|
+
var dock = document.getElementById('themeDock');
|
|
2005
|
+
if (!dock) return;
|
|
2006
|
+
function apply(t){
|
|
2007
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
2008
|
+
dock.querySelectorAll('.th-dot').forEach(function(d){
|
|
2009
|
+
d.classList.toggle('on', d.getAttribute('data-theme') === t);
|
|
2010
|
+
});
|
|
2011
|
+
try{ localStorage.setItem('sf-pouch-theme', t); }catch(e){}
|
|
2012
|
+
}
|
|
2013
|
+
var cur = document.documentElement.getAttribute('data-theme') || 'gold';
|
|
2014
|
+
var saved = null;
|
|
2015
|
+
try{ saved = localStorage.getItem('sf-pouch-theme'); }catch(e){}
|
|
2016
|
+
if (saved && THEMES.indexOf(saved) !== -1 && saved !== cur){ apply(saved); }
|
|
2017
|
+
else{
|
|
2018
|
+
dock.querySelectorAll('.th-dot').forEach(function(d){
|
|
2019
|
+
d.classList.toggle('on', d.getAttribute('data-theme') === cur);
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
dock.querySelectorAll('.th-dot').forEach(function(d){
|
|
2023
|
+
d.addEventListener('click', function(){ apply(d.getAttribute('data-theme')); });
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
if (document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', init); }
|
|
2027
|
+
else { init(); }
|
|
2028
|
+
})();
|
|
2029
|
+
</script>
|
|
2030
|
+
|
|
2031
|
+
<div class="theme-dock" id="themeDock">
|
|
2032
|
+
<button class="th-dot" type="button" data-theme="gold" title="gold 暖棕金"></button>
|
|
2033
|
+
<button class="th-dot" type="button" data-theme="tech" title="tech 深蓝科技"></button>
|
|
2034
|
+
<button class="th-dot" type="button" data-theme="mono" title="mono 极简黑白"></button>
|
|
2035
|
+
<button class="th-dot" type="button" data-theme="mint" title="mint 薄荷绿"></button>
|
|
2036
|
+
</div>
|
|
2037
|
+
</body>
|
|
2038
|
+
</html>
|