@weibaohui/dsh-code-poem 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/client/bundle-mc.js +350 -0
- package/client/index.js +336 -0
- package/cordis.patch.yml +11 -0
- package/data/entries.json +1 -0
- package/package.json +63 -0
- package/src/index.js +214 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dsh-code-poem — Client half
|
|
5
|
+
*
|
|
6
|
+
* 代码如诗:右下角一枚"诗笺"卷轴按钮,点开宣纸卡片,随机展示一条
|
|
7
|
+
* 古诗词 / 成语 / 龙文鞭影典故。
|
|
8
|
+
* - ❤ 收藏(localStorage 持久化,⭐ 打开收藏夹)
|
|
9
|
+
* - 🎲 换一条(可按 诗/词/文/成语/典故 过滤,近期不重复)
|
|
10
|
+
* - 「释义 · 故事」展开区:白话释义 / 古注 / 出处 / 故事 / 注释 / 赏析
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const STYLE = `
|
|
14
|
+
@keyframes cp-pop{0%{transform:scale(.92) translateY(8px);opacity:0}100%{transform:scale(1) translateY(0);opacity:1}}
|
|
15
|
+
@keyframes cp-flip{0%{transform:rotateY(90deg);opacity:0}100%{transform:rotateY(0);opacity:1}}
|
|
16
|
+
.cp-root{position:fixed;right:26px;bottom:20px;z-index:1200;display:flex;flex-direction:column;align-items:flex-end;gap:10px;
|
|
17
|
+
font:13px/1.75 "Kaiti SC","STKaiti","KaiTi","FangSong","Songti SC","SimSun",serif;user-select:none}
|
|
18
|
+
/* 卷轴按钮 */
|
|
19
|
+
.cp-toggle{width:46px;height:46px;border-radius:50%;border:1.5px solid #b5543b;cursor:pointer;
|
|
20
|
+
background:linear-gradient(145deg,#f8f2e2,#efe6cf);color:#8c3226;font-size:22px;line-height:1;
|
|
21
|
+
box-shadow:0 6px 18px rgba(0,0,0,.45),inset 0 0 0 3px rgba(140,50,38,.12);display:flex;align-items:center;justify-content:center}
|
|
22
|
+
.cp-toggle:hover{transform:translateY(-2px);box-shadow:0 9px 22px rgba(0,0,0,.5),inset 0 0 0 3px rgba(140,50,38,.22)}
|
|
23
|
+
/* 诗笺卡片:宣纸 */
|
|
24
|
+
.cp-card{position:relative;width:352px;max-height:min(76vh,640px);display:flex;flex-direction:column;overflow:hidden;
|
|
25
|
+
background:linear-gradient(160deg,#faf5e8 0%,#f4ecd8 55%,#efe4cb 100%);color:#33291f;border-radius:6px;
|
|
26
|
+
border:1px solid #c9b98f;box-shadow:0 14px 44px rgba(0,0,0,.5),inset 0 0 0 4px rgba(181,84,59,.07),inset 0 0 0 5px rgba(120,98,60,.18);
|
|
27
|
+
animation:cp-pop .22s ease}
|
|
28
|
+
.cp-card::before{content:"";position:absolute;inset:6px;border:1px solid rgba(140,50,38,.18);border-radius:4px;pointer-events:none}
|
|
29
|
+
/* 顶部:印章 + 标题 */
|
|
30
|
+
.cp-head{display:flex;align-items:center;gap:10px;padding:12px 14px 6px 16px}
|
|
31
|
+
.cp-seal{flex-shrink:0;width:34px;height:34px;background:linear-gradient(150deg,#a4342a,#8c2a22);color:#f6ead2;border-radius:5px;
|
|
32
|
+
display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;letter-spacing:0;
|
|
33
|
+
box-shadow:0 2px 6px rgba(140,42,34,.45);transform:rotate(-3deg);writing-mode:vertical-rl;line-height:1.1}
|
|
34
|
+
.cp-htext{flex:1;min-width:0}
|
|
35
|
+
.cp-title{font-size:19px;font-weight:700;color:#2f2418;letter-spacing:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
36
|
+
.cp-sub{font-size:11px;color:#8a7455;letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
37
|
+
.cp-close{flex-shrink:0;width:24px;height:24px;border:none;background:transparent;color:#a08c66;font-size:15px;cursor:pointer;border-radius:6px}
|
|
38
|
+
.cp-close:hover{background:rgba(140,50,38,.1);color:#8c3226}
|
|
39
|
+
/* 正文 */
|
|
40
|
+
.cp-body{flex:1;overflow-y:auto;padding:8px 20px 10px;scrollbar-width:thin;scrollbar-color:#c9b98f transparent}
|
|
41
|
+
.cp-front{white-space:pre-wrap;text-align:center;font-size:15.5px;line-height:2.05;color:#33291f;letter-spacing:1.5px;
|
|
42
|
+
padding:6px 0 10px;animation:cp-flip .3s ease}
|
|
43
|
+
.cp-front.cp-left{text-align:left;font-size:14px;line-height:1.95;letter-spacing:.5px}
|
|
44
|
+
.cp-digest{position:relative;font-size:12.5px;color:#6f5c3f;line-height:1.8;border-top:1px dashed #cbbb92;padding-top:8px;margin-top:2px;
|
|
45
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
46
|
+
.cp-digest.cp-open{-webkit-line-clamp:unset}
|
|
47
|
+
/* 展开区 */
|
|
48
|
+
.cp-more{margin-top:10px;border-top:1px solid #cbbb92;padding-top:8px;animation:cp-pop .2s ease}
|
|
49
|
+
.cp-sec{margin-bottom:10px}
|
|
50
|
+
.cp-sec h4{margin:0 0 4px;font-size:12px;color:#8c3226;letter-spacing:2px;font-weight:700}
|
|
51
|
+
.cp-sec h4::before{content:"◆ ";font-size:9px;vertical-align:2px}
|
|
52
|
+
.cp-sec p{margin:0;font-size:12.5px;line-height:1.85;color:#4a3d2c;white-space:pre-wrap}
|
|
53
|
+
/* 展开按钮 */
|
|
54
|
+
.cp-toggle-more{width:100%;border:none;background:transparent;color:#8c3226;font-size:12px;letter-spacing:2px;cursor:pointer;
|
|
55
|
+
padding:7px 0 2px;font-family:inherit}
|
|
56
|
+
.cp-toggle-more:hover{color:#5e1f16}
|
|
57
|
+
/* 底部工具栏 */
|
|
58
|
+
.cp-foot{display:flex;align-items:center;gap:4px;padding:8px 12px;border-top:1px solid #d5c69d;background:rgba(233,221,192,.55)}
|
|
59
|
+
.cp-btn{border:1px solid transparent;background:transparent;color:#5f4d33;font-size:14px;cursor:pointer;border-radius:8px;
|
|
60
|
+
padding:4px 8px;line-height:1;font-family:inherit}
|
|
61
|
+
.cp-btn:hover{background:rgba(140,50,38,.09)}
|
|
62
|
+
.cp-btn.cp-loved{color:#b5362a}
|
|
63
|
+
.cp-chips{flex:1;display:flex;gap:2px;justify-content:flex-end;flex-wrap:wrap}
|
|
64
|
+
.cp-chip{border:1px solid #c9b98f;background:transparent;color:#7a6748;font-size:10.5px;border-radius:9px;padding:2px 7px;
|
|
65
|
+
cursor:pointer;font-family:inherit;letter-spacing:1px}
|
|
66
|
+
.cp-chip.cp-on{background:#8c3226;border-color:#8c3226;color:#f6ead2}
|
|
67
|
+
/* 收藏夹面板 */
|
|
68
|
+
.cp-favs{flex:1;overflow-y:auto;padding:4px 12px 10px;scrollbar-width:thin;scrollbar-color:#c9b98f transparent}
|
|
69
|
+
.cp-favrow{display:flex;align-items:center;gap:8px;padding:6px 4px;border-bottom:1px dashed #d5c69d;cursor:pointer}
|
|
70
|
+
.cp-favrow:hover{background:rgba(140,50,38,.06)}
|
|
71
|
+
.cp-favrow .cp-fk{flex-shrink:0;font-size:10px;color:#a4342a;border:1px solid rgba(164,52,42,.5);border-radius:4px;padding:1px 5px}
|
|
72
|
+
.cp-favrow .cp-ft{flex:1;font-size:13.5px;color:#33291f;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
73
|
+
.cp-favrow .cp-fd{flex-shrink:0;color:#b0a184;font-size:11px}
|
|
74
|
+
.cp-favempty{text-align:center;color:#a08c66;font-size:12.5px;padding:26px 0}
|
|
75
|
+
.cp-hint{font-size:10.5px;color:#a8946e;text-align:center;padding:0 0 6px;letter-spacing:1px}
|
|
76
|
+
.cp-origin{display:flex;align-items:center;gap:6px;padding:5px 16px 0;font-size:11px;color:#8c3226;letter-spacing:1.5px}
|
|
77
|
+
.cp-origin::before{content:"";width:5px;height:5px;background:#a4342a;border-radius:1px;transform:rotate(45deg);flex-shrink:0}
|
|
78
|
+
.cp-card.cp-auto{animation:cp-pop .3s ease}
|
|
79
|
+
`
|
|
80
|
+
|
|
81
|
+
const KINDS = ['诗', '词', '文', '成语', '典故']
|
|
82
|
+
const SEAL = { '诗': '诗', '词': '词', '文': '文', '成语': '语', '典故': '典' }
|
|
83
|
+
const LS_FAVS = 'cp-favs'
|
|
84
|
+
const LS_KIND = 'cp-kind'
|
|
85
|
+
const LS_RECENT = 'cp-recent'
|
|
86
|
+
const LS_AUTO = 'cp-auto'
|
|
87
|
+
|
|
88
|
+
/** 会话事件 → 诗笺缘起语。turn 类必触发;user_msg 按概率。 */
|
|
89
|
+
const ORIGINS = {
|
|
90
|
+
turn_end: '功行圆满,且读一句——',
|
|
91
|
+
tool_error: '路遇荆棘,诗以解之——',
|
|
92
|
+
turn_abort: '收势小憩,翻页片刻——',
|
|
93
|
+
user_msg: '道友起意,赠笺一张——',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const favStore = {
|
|
97
|
+
list() { try { return JSON.parse(localStorage.getItem(LS_FAVS) || '[]') } catch { return [] } },
|
|
98
|
+
save(list) { try { localStorage.setItem(LS_FAVS, JSON.stringify(list)) } catch {} },
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const recentStore = {
|
|
102
|
+
list() { try { return JSON.parse(localStorage.getItem(LS_RECENT) || '[]') } catch { return [] } },
|
|
103
|
+
push(id) {
|
|
104
|
+
const list = this.list().filter((x) => x !== id)
|
|
105
|
+
list.unshift(id)
|
|
106
|
+
try { localStorage.setItem(LS_RECENT, JSON.stringify(list.slice(0, 10))) } catch {}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
inject: ['slots'],
|
|
112
|
+
|
|
113
|
+
apply(ctx) {
|
|
114
|
+
const slots = ctx.get('slots')
|
|
115
|
+
if (slots === undefined) return
|
|
116
|
+
const React = require('react')
|
|
117
|
+
const { useState, useEffect, useCallback } = React
|
|
118
|
+
|
|
119
|
+
const api = (p) => fetch(`/dsh-code-poem/api/${p}`).then((r) => r.json())
|
|
120
|
+
|
|
121
|
+
function App() {
|
|
122
|
+
const [open, setOpen] = useState(false)
|
|
123
|
+
const [entry, setEntry] = useState(null)
|
|
124
|
+
const [kind, setKind] = useState(() => localStorage.getItem(LS_KIND) || 'all')
|
|
125
|
+
const [loved, setLoved] = useState(false)
|
|
126
|
+
const [favs, setFavs] = useState(() => favStore.list())
|
|
127
|
+
const [showFavs, setShowFavs] = useState(false)
|
|
128
|
+
const [expanded, setExpanded] = useState(false)
|
|
129
|
+
const [auto, setAuto] = useState(() => localStorage.getItem(LS_AUTO) !== 'off')
|
|
130
|
+
const [origin, setOrigin] = useState(null) // 本次弹出的缘起(null=手动)
|
|
131
|
+
const lastFeedId = React.useRef(0)
|
|
132
|
+
const lastAutoAt = React.useRef(0)
|
|
133
|
+
const autoRef = React.useRef(auto)
|
|
134
|
+
autoRef.current = auto
|
|
135
|
+
const kindRef = React.useRef(kind)
|
|
136
|
+
kindRef.current = kind
|
|
137
|
+
|
|
138
|
+
// 本地诗笺池:每 5 分钟或收到回合开始事件时批量拉 30 条,
|
|
139
|
+
// 事件触发/手动换一条都从池里取,几乎不再打 API
|
|
140
|
+
const poolRef = React.useRef([])
|
|
141
|
+
const refreshBundle = useCallback(async (k) => {
|
|
142
|
+
try {
|
|
143
|
+
const exclude = recentStore.list().join(',')
|
|
144
|
+
const r = await api(`bundle?kind=${encodeURIComponent(k || 'all')}&n=30&exclude=${encodeURIComponent(exclude)}`)
|
|
145
|
+
if (r && r.entries && r.entries.length) {
|
|
146
|
+
poolRef.current = r.entries
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
return false
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
153
|
+
const draw = useCallback(async (k) => {
|
|
154
|
+
if (!poolRef.current.length) await refreshBundle(k || kindRef.current)
|
|
155
|
+
const e = poolRef.current.shift()
|
|
156
|
+
if (e) {
|
|
157
|
+
setEntry(e)
|
|
158
|
+
setExpanded(false)
|
|
159
|
+
recentStore.push(e.id)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
try { // 池子空且批量拉取失败时的兜底单抽
|
|
163
|
+
const exclude = recentStore.list().join(',')
|
|
164
|
+
const r = await api(`draw?kind=${encodeURIComponent(k || 'all')}&exclude=${encodeURIComponent(exclude)}`)
|
|
165
|
+
if (r && r.entry) {
|
|
166
|
+
setEntry(r.entry)
|
|
167
|
+
setExpanded(false)
|
|
168
|
+
recentStore.push(r.entry.id)
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
171
|
+
}, [refreshBundle])
|
|
172
|
+
|
|
173
|
+
useEffect(() => { if (open && !entry) draw(kind) }, [open])
|
|
174
|
+
|
|
175
|
+
// ── 事件联动:轮询会话事件流,随机自动浮现诗笺 ──
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
const react = (ev) => {
|
|
178
|
+
if (!autoRef.current) return
|
|
179
|
+
let why = null
|
|
180
|
+
if (ev.kind === 'turn_end' || ev.kind === 'tool_error' || ev.kind === 'turn_abort') why = ev.kind
|
|
181
|
+
else if (ev.kind === 'user_msg' && Math.random() < 0.25) why = ev.kind
|
|
182
|
+
if (!why) return
|
|
183
|
+
const now = Date.now()
|
|
184
|
+
if (now - lastAutoAt.current < 90000) return // 90 秒内不重复打扰
|
|
185
|
+
lastAutoAt.current = now
|
|
186
|
+
setOrigin(ORIGINS[why])
|
|
187
|
+
setExpanded(false)
|
|
188
|
+
setOpen(true)
|
|
189
|
+
draw(kindRef.current)
|
|
190
|
+
}
|
|
191
|
+
const tick = () => {
|
|
192
|
+
api(`feed?after=${lastFeedId.current}`).then((r) => {
|
|
193
|
+
for (const ev of r.events || []) {
|
|
194
|
+
lastFeedId.current = Math.max(lastFeedId.current, ev.id)
|
|
195
|
+
if (ev.kind === 'turn_start') refreshBundle(kindRef.current) // 回合开始 → 顺手换一批
|
|
196
|
+
react(ev)
|
|
197
|
+
}
|
|
198
|
+
}).catch(() => {})
|
|
199
|
+
}
|
|
200
|
+
refreshBundle(kindRef.current) // 启动先取一批
|
|
201
|
+
const iv = setInterval(tick, 15000) // 事件流 15 秒一轮(响应很小)
|
|
202
|
+
const bv = setInterval(() => refreshBundle(kindRef.current), 300000) // 5 分钟换一批
|
|
203
|
+
return () => { clearInterval(iv); clearInterval(bv) }
|
|
204
|
+
}, [draw, refreshBundle])
|
|
205
|
+
|
|
206
|
+
// 自动浮现的卡片:14 秒无操作自动收起(展开或手动交互后不收)
|
|
207
|
+
React.useEffect(() => {
|
|
208
|
+
if (!open || !origin) return
|
|
209
|
+
const t = setTimeout(() => { if (!expanded) setOpen(false) }, 14000)
|
|
210
|
+
return () => clearTimeout(t)
|
|
211
|
+
}, [open, origin, expanded, entry])
|
|
212
|
+
|
|
213
|
+
const autoOff = () => { setOrigin(null) } // 用户触摸卡片 → 视为手动模式,不再自动收
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
setLoved(!!entry && favs.some((f) => f.id === entry.id))
|
|
217
|
+
}, [entry, favs])
|
|
218
|
+
|
|
219
|
+
const toggleFav = () => {
|
|
220
|
+
if (!entry) return
|
|
221
|
+
setFavs((prev) => {
|
|
222
|
+
const has = prev.some((f) => f.id === entry.id)
|
|
223
|
+
const next = has
|
|
224
|
+
? prev.filter((f) => f.id !== entry.id)
|
|
225
|
+
: [{ id: entry.id, title: entry.title, kind: entry.kind, front: entry.front, digest: entry.digest }, ...prev]
|
|
226
|
+
favStore.save(next)
|
|
227
|
+
return next
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const openFav = async (f) => {
|
|
232
|
+
try {
|
|
233
|
+
const r = await api(`entry?id=${encodeURIComponent(f.id)}`)
|
|
234
|
+
if (r && r.entry) { setEntry(r.entry); setExpanded(true); setShowFavs(false) }
|
|
235
|
+
else { setEntry({ id: f.id, kind: f.kind, title: f.title, sub: '(收藏快照)', author: '', front: f.front, digest: f.digest, sections: [] }) ; setExpanded(false); setShowFavs(false) }
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pickKind = (k) => {
|
|
240
|
+
setKind(k)
|
|
241
|
+
try { localStorage.setItem(LS_KIND, k) } catch {}
|
|
242
|
+
poolRef.current = [] // 类别变了,旧池子作废
|
|
243
|
+
draw(k)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const frontCls = entry && (entry.kind === '文' || entry.kind === '成语') ? 'cp-front cp-left' : 'cp-front'
|
|
247
|
+
const sections = (entry && entry.sections) || []
|
|
248
|
+
|
|
249
|
+
return React.createElement('div', { className: 'cp-root' },
|
|
250
|
+
open && React.createElement('div', {
|
|
251
|
+
className: 'cp-card' + (origin ? ' cp-auto' : ''),
|
|
252
|
+
onMouseDown: autoOff,
|
|
253
|
+
},
|
|
254
|
+
React.createElement('div', { className: 'cp-head' },
|
|
255
|
+
React.createElement('div', { className: 'cp-seal' }, entry ? (SEAL[entry.kind] || '诗') : '诗'),
|
|
256
|
+
React.createElement('div', { className: 'cp-htext' },
|
|
257
|
+
React.createElement('div', { className: 'cp-title' }, entry ? entry.title : '诗笺'),
|
|
258
|
+
React.createElement('div', { className: 'cp-sub' },
|
|
259
|
+
entry ? [entry.sub, entry.author].filter(Boolean).join(' · ') : '代码如诗')),
|
|
260
|
+
React.createElement('button', { className: 'cp-close', title: '收起', onClick: () => setOpen(false) }, '✕')),
|
|
261
|
+
origin && React.createElement('div', { className: 'cp-origin' }, ORIGINS[origin] || '缘起'),
|
|
262
|
+
showFavs
|
|
263
|
+
? React.createElement('div', { className: 'cp-favs' },
|
|
264
|
+
favs.length === 0 && React.createElement('div', { className: 'cp-favempty' }, '还没有收藏,遇到喜欢的句子点个 ❤ 吧'),
|
|
265
|
+
favs.map((f) => React.createElement('div', {
|
|
266
|
+
key: f.id, className: 'cp-favrow',
|
|
267
|
+
onClick: () => openFav(f), title: f.digest || '',
|
|
268
|
+
},
|
|
269
|
+
React.createElement('span', { className: 'cp-fk' }, SEAL[f.kind] || f.kind),
|
|
270
|
+
React.createElement('span', { className: 'cp-ft' }, f.title),
|
|
271
|
+
React.createElement('span', {
|
|
272
|
+
className: 'cp-fd', title: '取消收藏',
|
|
273
|
+
onClick: (ev) => {
|
|
274
|
+
ev.stopPropagation()
|
|
275
|
+
const next = favs.filter((x) => x.id !== f.id)
|
|
276
|
+
favStore.save(next); setFavs(next)
|
|
277
|
+
},
|
|
278
|
+
}, '✖'))))
|
|
279
|
+
: React.createElement('div', { className: 'cp-body' },
|
|
280
|
+
entry && React.createElement('div', { key: entry.id, className: 'cp-front ' + frontCls.split(' ').slice(1).join(' ') || 'cp-front' },
|
|
281
|
+
entry.front),
|
|
282
|
+
entry && entry.digest && React.createElement('div', {
|
|
283
|
+
className: 'cp-digest' + (expanded ? ' cp-open' : ''),
|
|
284
|
+
onClick: () => setExpanded(true), title: '点击展开',
|
|
285
|
+
}, entry.digest),
|
|
286
|
+
expanded && React.createElement('div', { className: 'cp-more', key: 'more' },
|
|
287
|
+
...(sections.length
|
|
288
|
+
? sections.map((s, i) => React.createElement('div', { className: 'cp-sec', key: i },
|
|
289
|
+
React.createElement('h4', null, s.h),
|
|
290
|
+
React.createElement('p', null, s.t)))
|
|
291
|
+
: [React.createElement('div', { className: 'cp-favempty', key: 'empty' }, '这条没有更多释义了')])),
|
|
292
|
+
!expanded && React.createElement('button', {
|
|
293
|
+
className: 'cp-toggle-more',
|
|
294
|
+
onClick: () => setExpanded(true),
|
|
295
|
+
}, sections.length ? '—— 展开 释义 · 故事 ▾ ——' : '—— 展开 全文 ▾ ——')),
|
|
296
|
+
React.createElement('div', { className: 'cp-foot' },
|
|
297
|
+
React.createElement('button', {
|
|
298
|
+
className: 'cp-btn' + (loved ? ' cp-loved' : ''),
|
|
299
|
+
title: loved ? '取消收藏' : '收藏', onClick: toggleFav,
|
|
300
|
+
}, loved ? '❤️' : '🤍'),
|
|
301
|
+
React.createElement('button', { className: 'cp-btn', title: '换一条', onClick: () => draw(kind) }, '🎲'),
|
|
302
|
+
React.createElement('button', {
|
|
303
|
+
className: 'cp-btn', title: '收藏夹',
|
|
304
|
+
onClick: () => setShowFavs((v) => !v),
|
|
305
|
+
}, showFavs ? '📖' : `⭐${favs.length || ''}`),
|
|
306
|
+
React.createElement('button', {
|
|
307
|
+
className: 'cp-btn', title: auto ? '事件联动:开(回合结束/报错时自动浮现)' : '事件联动:关',
|
|
308
|
+
onClick: () => {
|
|
309
|
+
const next = !auto
|
|
310
|
+
setAuto(next)
|
|
311
|
+
try { localStorage.setItem(LS_AUTO, next ? 'on' : 'off') } catch {}
|
|
312
|
+
},
|
|
313
|
+
}, auto ? '🔔' : '🔕'),
|
|
314
|
+
React.createElement('div', { className: 'cp-chips' },
|
|
315
|
+
React.createElement('button', {
|
|
316
|
+
className: 'cp-chip' + (kind === 'all' ? ' cp-on' : ''),
|
|
317
|
+
onClick: () => pickKind('all'),
|
|
318
|
+
}, '随机'),
|
|
319
|
+
KINDS.map((k) => React.createElement('button', {
|
|
320
|
+
key: k, className: 'cp-chip' + (kind === k ? ' cp-on' : ''),
|
|
321
|
+
onClick: () => pickKind(k),
|
|
322
|
+
}, k))))),
|
|
323
|
+
React.createElement('button', {
|
|
324
|
+
className: 'cp-toggle', title: '代码如诗:随机一条诗词/成语/典故',
|
|
325
|
+
onClick: () => setOpen((v) => !v),
|
|
326
|
+
}, '📜'),
|
|
327
|
+
open && !origin && React.createElement('div', { className: 'cp-hint' }, '诗词 · 成语 · 典故'),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
slots.inject('sidebar.footer.action', () => slots.register(
|
|
332
|
+
{ name: 'sidebar.footer.action', id: '@weibaohui/dsh-code-poem', order: 98 },
|
|
333
|
+
() => React.createElement(App, null)
|
|
334
|
+
))
|
|
335
|
+
},
|
|
336
|
+
}
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# dsh-code-poem bundle patch: contributes the plugin row into the
|
|
2
|
+
# host composition when this package is installed via `dsh plugin add`.
|
|
3
|
+
#
|
|
4
|
+
# HOST-PLANE: reads the 诗笺 dataset (古诗词/成语/龙文鞭影典故) bundled in
|
|
5
|
+
# data/entries.json and serves its HTTP API over a webServer route; the
|
|
6
|
+
# client half registers a sidebar footer action rendering the floating
|
|
7
|
+
# 诗笺 card overlay with favorite (❤) support.
|
|
8
|
+
|
|
9
|
+
- insert:
|
|
10
|
+
- id: dsh-code-poem
|
|
11
|
+
name: '@weibaohui/dsh-code-poem'
|