dsh-pet-dom 2.0.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/lib/console.js ADDED
@@ -0,0 +1,422 @@
1
+ // ============================================================================
2
+ // dsh-pet-dom —— lib/console.js 桌宠控制台(UI 层,纯 DOM,不了解宠物)
3
+ // ============================================================================
4
+ // 形态:一个可拖动面板(右上角)。标题栏 × = 完全隐藏(双击宠物唤回);
5
+ // 双击标题栏 = 折叠/展开面板本体。
6
+ // 图标:全部内联 SVG(无 emoji)。
7
+ // 主题:自动跟随 DSH 页面白天/黑夜(body 背景亮度 <128 深色;每 2s 复查)。
8
+ // 设计:玻璃拟态中性面板,accent(主题色)只出现在选中态/开关/当前项;
9
+ // 分段模式选择器、自绘滑动开关、键帽弱化成中性 chip。
10
+ // 回调交给 mascot:{ onMode(mode), onHighlight(enable, color), onTheme(color) }
11
+ // ============================================================================
12
+
13
+ const THEMES = [
14
+ { name: '霓虹紫', color: '#8b5cf6' },
15
+ { name: '薄荷绿', color: '#10b981' },
16
+ { name: '蜜桃粉', color: '#f472b6' },
17
+ { name: '金黄', color: '#f59e0b' },
18
+ { name: '冰蓝', color: '#38bdf8' },
19
+ { name: '珍珠白', color: '#f8fafc' }
20
+ ]
21
+
22
+ const STORE_KEY = 'petDomConsole_v1'
23
+
24
+ const ICON = {
25
+ paw: '<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="14.6" r="3.4"/><circle cx="5" cy="11.6" r="2.2"/><circle cx="8.6" cy="7" r="2.2"/><circle cx="15.4" cy="7" r="2.2"/><circle cx="19" cy="11.6" r="2.2"/></svg>',
26
+ chev: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>',
27
+ x: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>',
28
+ pad: '<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 6.5h8A5.5 5.5 0 0 1 21.5 12v3.6a3.6 3.6 0 0 1-6.5 2.2L13.6 16h-3.2l-1.4 1.8a3.6 3.6 0 0 1-6.5-2.2V12A5.5 5.5 0 0 1 8 6.5zM8.2 9v1.8H6.4v1.8h1.8v1.8H10v-1.8h1.8v-1.8H10V9H8.2zm7 1a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2zm2.6 2.6a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2z"/></svg>',
29
+ sun: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/><path d="M12 2.8v2.2M12 19v2.2M2.8 12H5M19 12h2.2M5.5 5.5l1.6 1.6M16.9 16.9l1.6 1.6M18.5 5.5l-1.6 1.6M7.1 16.9L5.5 18.5"/></svg>'
30
+ }
31
+
32
+ function loadState() {
33
+ try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {} } catch (e) { return {} }
34
+ }
35
+ function saveState(st) {
36
+ try { localStorage.setItem(STORE_KEY, JSON.stringify(st)) } catch (e) { /* 忽略 */ }
37
+ }
38
+ function detectDark() {
39
+ try {
40
+ var m = (getComputedStyle(document.body).backgroundColor.match(/\d+/g) || []).map(Number)
41
+ if (m.length >= 3) return (m[0] * 0.299 + m[1] * 0.587 + m[2] * 0.114) < 128
42
+ } catch (e) {}
43
+ try { return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) } catch (e) { return false }
44
+ }
45
+
46
+ // ---------- 样式表(面板级 CSS 变量;data-ui 切换明暗) ----------
47
+ var CSS = `
48
+ .petcon-root{position:fixed;z-index:2147483000;box-sizing:border-box;width:300px;color:var(--fg);
49
+ background:var(--bg);border:1px solid var(--line);border-radius:14px;box-shadow:0 10px 32px var(--shadow);
50
+ backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);overflow:hidden;
51
+ font:12px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
52
+ .petcon-root[data-ui="dark"]{--bg:rgba(23,24,32,.78);--fg:#eceef4;--fg2:rgba(236,238,244,.55);
53
+ --line:rgba(255,255,255,.09);--head:rgba(255,255,255,.045);--chip:rgba(255,255,255,.07);
54
+ --chipHover:rgba(255,255,255,.12);--kbdBg:rgba(255,255,255,.09);--kbdBd:rgba(255,255,255,.16);
55
+ --swBg:rgba(255,255,255,.18);--swBd:rgba(255,255,255,.3);
56
+ --shadow:rgba(0,0,0,.5);--accentSoft:rgba(139,92,246,.32)}
57
+ .petcon-root[data-ui="light"]{--bg:rgba(255,255,255,.86);--fg:#23262f;--fg2:rgba(35,38,47,.55);
58
+ --line:rgba(35,38,47,.1);--head:rgba(35,38,47,.035);--chip:rgba(35,38,47,.06);
59
+ --chipHover:rgba(35,38,47,.11);--kbdBg:rgba(35,38,47,.055);--kbdBd:rgba(35,38,47,.14);
60
+ --swBg:#c6cad4;--swBd:#aab0bf;
61
+ --shadow:rgba(20,22,30,.16);--accentSoft:rgba(139,92,246,.14)}
62
+ .petcon-head{display:flex;align-items:center;gap:6px;padding:10px 12px 9px;cursor:move;
63
+ background:var(--head);user-select:none;border-bottom:1px solid var(--line)}
64
+ .petcon-head .t{flex:1;font-weight:600;font-size:12.5px;display:flex;align-items:center;gap:6px}
65
+ .petcon-head .ic{color:var(--accent)}
66
+ .petcon-hbtn{border:0;background:transparent;color:var(--fg2);border-radius:7px;padding:4px;cursor:pointer;
67
+ display:inline-flex;align-items:center;transition:background .15s}
68
+ .petcon-hbtn:hover{background:var(--chipHover);color:var(--fg)}
69
+ .petcon-capbtn{position:fixed;z-index:2147483000;right:10px;top:8px;width:28px;height:28px;border-radius:50%;
70
+ color:var(--fg);background:var(--bg);border:1px solid var(--line);cursor:pointer;user-select:none;
71
+ display:none;align-items:center;justify-content:center;box-shadow:0 2px 10px var(--shadow);
72
+ backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);transition:transform .15s,border-color .15s}
73
+ .petcon-capbtn:hover{transform:scale(1.12);border-color:var(--accent)}
74
+ .petcon-capbtn .i{color:var(--accent);display:inline-flex}
75
+ .petcon-body{padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px;
76
+ max-height:calc(100vh - 110px);overflow-y:auto;overscroll-behavior:contain}
77
+ .petcon-seg{display:flex;position:relative;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--head)}
78
+ .petcon-seg button{flex:1;border:0;background:transparent;color:var(--fg2);padding:6px 4px;font-size:11.5px;
79
+ cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;transition:background .15s,color .15s}
80
+ .petcon-seg button:hover{background:var(--chip)}
81
+ .petcon-seg button.on{background:var(--accentSoft);color:var(--fg)}
82
+ .petcon-seg button.on svg{color:var(--accent)}
83
+ .petcon-keys{display:none;flex-direction:column;gap:6px;padding:2px 1px}
84
+ .petcon-keys .row{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--fg2);flex-wrap:wrap}
85
+ .petcon-kbd{display:inline-block;min-width:17px;text-align:center;padding:1px 5px;border-radius:5px;
86
+ border:1px solid var(--kbdBd);border-bottom-width:2px;background:var(--kbdBg);color:var(--fg);
87
+ font:10.5px/16px ui-monospace,Consolas,monospace}
88
+ .petcon-sw{display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 2px;border-radius:8px;transition:background .15s}
89
+ .petcon-sw:hover{background:var(--chip)}
90
+ .petcon-sw .track{flex:0 0 auto;width:30px;height:18px;border-radius:9px;background:var(--swBg);
91
+ border:1px solid var(--swBd);position:relative;transition:background .18s}
92
+ .petcon-sw .track i{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;
93
+ background:var(--fg2);transition:left .18s,background .18s}
94
+ .petcon-sw.on .track{background:var(--swOn);border-color:var(--swOn)}
95
+ .petcon-sw.on .track i{left:14px;background:#fff}
96
+ .petcon-theme{display:flex;align-items:center;gap:7px}
97
+ .petcon-theme .lbl{flex:0 0 40px;color:var(--fg2)}
98
+ .petcon-dot{width:17px;height:17px;border-radius:50%;cursor:pointer;box-sizing:border-box;
99
+ border:1.5px solid var(--kbdBd);transition:transform .12s,box-shadow .12s}
100
+ .petcon-dot:hover{transform:scale(1.18)}
101
+ .petcon-dot.on{transform:scale(1.18);box-shadow:0 0 0 2px var(--bg),0 0 0 4px currentColor}
102
+ .petcon-custom{background:conic-gradient(from 0deg,#f43f5e,#f59e0b,#10b981,#38bdf8,#8b5cf6,#f43f5e);
103
+ display:inline-flex;align-items:center;justify-content:center;padding:0;overflow:hidden}
104
+ .petcon-custom i{width:9px;height:9px;border-radius:50%;background:currentColor;border:1px solid rgba(0,0,0,.15)}
105
+ @media (max-width:480px){.petcon-root{width:min(280px,calc(100vw - 20px))}}
106
+ `
107
+ var styleEl = document.createElement('style')
108
+ styleEl.textContent = CSS
109
+ document.head.appendChild(styleEl)
110
+
111
+ export function createPetConsole(handlers) {
112
+ handlers = handlers || {}
113
+ var st = Object.assign({ open: true, x: null, y: null, mode: 'default', collapsed: false, highlight: false, themeIdx: 0, customColor: null, dedupGap: 90, highlightAlpha: 0.2 }, loadState())
114
+ // 旧默认值迁移:存量 localStorage 里"旧默认 3px/50%"→ 新默认 90px/20%(用户已手动调的保留)
115
+ if (st.dedupGap === 3) st.dedupGap = 90
116
+ if (st.highlightAlpha === 0.5) st.highlightAlpha = 0.2
117
+ // 旧数据迁移:hidden(完全隐藏)→ collapsed(收起为可见小钮)
118
+ if (st.hidden && !st.collapsed) { st.collapsed = true }
119
+ delete st.hidden
120
+ var uiDark = detectDark()
121
+
122
+ // ---------- 结构 ----------
123
+ var root = document.createElement('div')
124
+ root.className = 'petcon-root'
125
+ root.setAttribute('data-ui', uiDark ? 'dark' : 'light')
126
+
127
+ var head = document.createElement('div')
128
+ head.className = 'petcon-head'
129
+ head.innerHTML =
130
+ '<span class="t"><span class="ic">' + ICON.paw + '</span>桌宠控制台</span>' +
131
+ '<button class="petcon-hbtn" data-a="fold" title="收起为悬浮小按钮">' + ICON.chev + '</button>'
132
+ var btnFold = head.querySelector('[data-a=fold]')
133
+
134
+ var body = document.createElement('div')
135
+ body.className = 'petcon-body'
136
+ body.innerHTML =
137
+ '<div class="petcon-seg">' +
138
+ '<button data-m="default">' + ICON.sun + ' 默认(自动)</button>' +
139
+ '<button data-m="game">' + ICON.pad + ' 游戏(键盘控制)</button>' +
140
+ '</div>' +
141
+ '<div class="petcon-keys">' +
142
+ '<div class="row"><span class="petcon-kbd">&#8592;</span><span class="petcon-kbd">A</span>左移</div>' +
143
+ '<div class="row"><span class="petcon-kbd">&#8594;</span><span class="petcon-kbd">D</span>右移</div>' +
144
+ '<div class="row"><span class="petcon-kbd">空格</span><span class="petcon-kbd">&#8593;</span><span class="petcon-kbd">W</span>跳跃(空中可连跳)</div>' +
145
+ '<div class="row"><span class="petcon-kbd">&#8595;</span><span class="petcon-kbd">S</span>快速下落(站在表面时=摔落)</div>' +
146
+ '<div class="row"><span class="petcon-kbd">H</span>胡桃摇</div>' +
147
+ '</div>' +
148
+ '<div class="petcon-sw"><span class="track"><i></i></span><span>高亮可跳跃边缘(上界)</span></div>' +
149
+ '<div class="petcon-sw petcon-sw2"><span class="track"><i></i></span><span>禁用 ↑↓</span></div>' +
150
+ '<div class="petcon-gap" style="display:flex;align-items:center;gap:8px;padding:4px 2px">' +
151
+ '<span style="flex:0 0 40px;color:var(--fg2)">表面合并</span>' +
152
+ '<input type="range" min="0" max="200" step="1" style="flex:1;accent-color:var(--accent)">' +
153
+ '<span class="val" style="flex:0 0 40px;text-align:right;color:var(--fg2)">90px</span>' +
154
+ '</div>' +
155
+ '<div class="petcon-alpha" style="display:flex;align-items:center;gap:8px;padding:4px 2px">' +
156
+ '<span style="flex:0 0 40px;color:var(--fg2)">高亮透明</span>' +
157
+ '<input type="range" min="5" max="100" step="5" style="flex:1;accent-color:var(--accent)">' +
158
+ '<span class="val" style="flex:0 0 40px;text-align:right;color:var(--fg2)">20%</span>' +
159
+ '</div>' +
160
+ '<div class="petcon-theme"><span class="lbl">主题</span></div>'
161
+
162
+ var segDefault = body.querySelector('[data-m=default]')
163
+ var segGame = body.querySelector('[data-m=game]')
164
+ var keysBox = body.querySelector('.petcon-keys')
165
+ var sw = body.querySelector('.petcon-sw')
166
+ var sw2 = body.querySelector('.petcon-sw2')
167
+ var gapRange = body.querySelector('.petcon-gap input')
168
+ var gapVal = body.querySelector('.petcon-gap .val')
169
+ var alphaRange = body.querySelector('.petcon-alpha input')
170
+ var alphaVal = body.querySelector('.petcon-alpha .val')
171
+ var themeRow = body.querySelector('.petcon-theme')
172
+
173
+ // 主题色点(6 预设)+ 自定义(彩虹环圆点,点击弹系统取色器;UI 与色点一致)
174
+ var dots = []
175
+ THEMES.forEach(function (t, i) {
176
+ var d = document.createElement('span')
177
+ d.className = 'petcon-dot' + (i === st.themeIdx ? ' on' : '')
178
+ d.style.background = t.color
179
+ d.style.color = t.color // currentColor → 选中环用自身颜色(它是高亮色预览)
180
+ d.title = t.name
181
+ d.addEventListener('click', function () { setTheme(i) })
182
+ themeRow.appendChild(d); dots.push(d)
183
+ })
184
+ // 自定义圆点:彩虹环 + 中心 fg 点;点击 → 隐藏的原生取色器
185
+ var CUSTOM_IDX = 6
186
+ var customDot = document.createElement('span')
187
+ customDot.className = 'petcon-dot petcon-custom' + (st.themeIdx === CUSTOM_IDX ? ' on' : '')
188
+ customDot.title = '自定义颜色…'
189
+ customDot.innerHTML = '<i></i>'
190
+ var colorInput = document.createElement('input')
191
+ colorInput.type = 'color'
192
+ colorInput.value = st.customColor || '#8b5cf6'
193
+ colorInput.style.cssText = 'position:absolute;width:0;height:0;border:0;padding:0;visibility:hidden;'
194
+ colorInput.addEventListener('change', function () {
195
+ st.customColor = colorInput.value
196
+ setTheme(CUSTOM_IDX)
197
+ })
198
+ customDot.addEventListener('click', function (e) {
199
+ e.stopPropagation()
200
+ colorInput.value = st.customColor || '#8b5cf6'
201
+ colorInput.click()
202
+ })
203
+ themeRow.appendChild(customDot)
204
+ themeRow.appendChild(colorInput)
205
+ dots.push(customDot)
206
+
207
+ // 当前主题色(预设 0-5 / 自定义 6)
208
+ function themeColor() {
209
+ return st.themeIdx === CUSTOM_IDX ? (st.customColor || THEMES[0].color) : THEMES[st.themeIdx].color
210
+ }
211
+
212
+ // 收起态悬浮钮(可见的恢复入口;点击展开面板)
213
+ var capBtn = document.createElement('button')
214
+ capBtn.className = 'petcon-capbtn'
215
+ capBtn.innerHTML = '<span class="i">' + ICON.paw + '</span>'
216
+ capBtn.title = '展开桌宠控制台'
217
+ document.body.appendChild(capBtn)
218
+
219
+ // 面板本体
220
+ document.body.appendChild(root)
221
+ root.appendChild(head); root.appendChild(body)
222
+
223
+ function accent() { return themeColor() } // 面板 UI accent = 当前主题色(与高亮一致)
224
+
225
+ // ---------- 状态 → UI ----------
226
+ function paint() {
227
+ root.setAttribute('data-ui', uiDark ? 'dark' : 'light')
228
+ // 安全 accent:用户色/主题色太浅(>200 亮度)→ 压深到银灰档;面板 UI 一律用它
229
+ var ac = swOnColor(accent())
230
+ root.style.setProperty('--accent', ac)
231
+ root.style.setProperty('--accentSoft', hexToRgba(ac, uiDark ? 0.32 : 0.15))
232
+ root.style.setProperty('--swOn', ac)
233
+ // capBtn 是 body 直接子元素(吃不到 root 的变量)—— 显式补齐,否则收起后透明无形
234
+ capBtn.style.setProperty('--accent', ac)
235
+ capBtn.style.setProperty('--bg', uiDark ? 'rgba(23,24,32,.9)' : 'rgba(255,255,255,.95)')
236
+ capBtn.style.setProperty('--fg', uiDark ? '#eceef4' : '#23262f')
237
+ capBtn.style.setProperty('--line', uiDark ? 'rgba(255,255,255,.16)' : 'rgba(35,38,47,.18)')
238
+ capBtn.style.setProperty('--shadow', uiDark ? 'rgba(0,0,0,.5)' : 'rgba(20,22,30,.18)')
239
+ }
240
+ // 混黑/混白(开关等小控件用;不依赖 mascot)
241
+ function shadeFor(hex, amt) {
242
+ var n = parseInt(hex.slice(1), 16) || 0
243
+ var r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
244
+ var t = amt > 0 ? 0 : 255, a = Math.abs(amt)
245
+ r = Math.round(r + (t - r) * a); g = Math.round(g + (t - g) * a); b = Math.round(b + (t - b) * a)
246
+ return 'rgb(' + r + ',' + g + ',' + b + ')'
247
+ }
248
+ // 安全色:亮度 >200(珍珠白/淡黄等浅色)→ 深派生(银灰色会);普通颜色原样。
249
+ // 对用户自定义任意颜色同样生效(预设 0-5 / 自定义 6 都走这里)。
250
+ function swOnColor(hex) {
251
+ var n = parseInt(hex.slice(1), 16) || 0
252
+ var lum = 0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)
253
+ return lum > 200 ? shadeFor(hex, 0.45) : hex
254
+ }
255
+ function hexToRgba(hex, alpha) {
256
+ var n = parseInt(hex.slice(1), 16) || 0
257
+ return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + alpha + ')'
258
+ }
259
+
260
+ function apply() {
261
+ // 收起 ⇄ 展开:锚点同源(st.x/st.y),收起钮留在面板原位置;展开面板恢复同位置
262
+ capBtn.style.display = st.collapsed ? 'flex' : 'none'
263
+ root.style.display = st.collapsed ? 'none' : 'block'
264
+ var px = st.x !== null ? st.x : (window.innerWidth - (st.collapsed ? 46 : root.offsetWidth + 14))
265
+ var py = st.y !== null ? st.y : 10
266
+ if (st.collapsed) {
267
+ capBtn.style.left = px + 'px'; capBtn.style.top = py + 'px'
268
+ capBtn.style.right = ''
269
+ } else {
270
+ root.style.left = px + 'px'; root.style.top = py + 'px'
271
+ save()
272
+ }
273
+ }
274
+
275
+ function refresh() {
276
+ var active = st.mode === 'game'
277
+ segDefault.classList.toggle('on', !active)
278
+ segGame.classList.toggle('on', active)
279
+ keysBox.style.display = active ? 'flex' : 'none'
280
+ sw.classList.toggle('on', st.highlight)
281
+ sw2.classList.toggle('on', st.disableUpDown)
282
+ gapRange.value = st.dedupGap
283
+ gapVal.textContent = st.dedupGap + 'px'
284
+ alphaRange.value = Math.round(st.highlightAlpha * 100)
285
+ alphaVal.textContent = Math.round(st.highlightAlpha * 100) + '%'
286
+ // 色点选中态:预设用自身色环;自定义圆点用当前值色环
287
+ dots.forEach(function (d, i) {
288
+ d.classList.toggle('on', i === st.themeIdx)
289
+ })
290
+ customDot.style.color = themeColor()
291
+ paint()
292
+ apply()
293
+ }
294
+
295
+ function save() {
296
+ st.x = parseFloat(root.style.left) || null
297
+ st.y = parseFloat(root.style.top) || null
298
+ saveState(st)
299
+ }
300
+ function persist() { saveState(st); refresh() }
301
+
302
+ // ---------- 交互 ----------
303
+ btnFold.addEventListener('click', function (e) { e.stopPropagation(); st.collapsed = true; refresh() })
304
+ capBtn.addEventListener('click', function () { st.collapsed = false; refresh() })
305
+ segDefault.addEventListener('click', function () { setMode('default') })
306
+ segGame.addEventListener('click', function () { setMode('game') })
307
+ sw.addEventListener('click', function () {
308
+ st.highlight = !st.highlight
309
+ if (handlers.onHighlight) handlers.onHighlight(st.highlight, accent())
310
+ persist()
311
+ })
312
+ sw2.addEventListener('click', function () {
313
+ st.disableUpDown = !st.disableUpDown
314
+ if (handlers.onDisableUpDown) handlers.onDisableUpDown(st.disableUpDown)
315
+ persist()
316
+ })
317
+ // 表面合并滑块:值显示/保存即时;重扫回调节流 120ms(拖动高频不卡);change 兜底立即同步
318
+ var gapTimer = null
319
+ gapRange.addEventListener('input', function () {
320
+ st.dedupGap = parseInt(gapRange.value, 10) || 0
321
+ gapVal.textContent = st.dedupGap + 'px'
322
+ saveState(st)
323
+ if (gapTimer) clearTimeout(gapTimer)
324
+ gapTimer = setTimeout(function () {
325
+ gapTimer = null
326
+ if (handlers.onDedupGap) handlers.onDedupGap(st.dedupGap)
327
+ }, 120)
328
+ })
329
+ gapRange.addEventListener('change', function () {
330
+ st.dedupGap = parseInt(gapRange.value, 10) || 0
331
+ saveState(st)
332
+ if (gapTimer) { clearTimeout(gapTimer); gapTimer = null }
333
+ if (handlers.onDedupGap) handlers.onDedupGap(st.dedupGap)
334
+ })
335
+ // 高亮透明度滑块:同上(节流 120ms;默认 0.5)
336
+ var alphaTimer = null
337
+ alphaRange.addEventListener('input', function () {
338
+ st.highlightAlpha = (parseInt(alphaRange.value, 10) || 50) / 100
339
+ alphaVal.textContent = Math.round(st.highlightAlpha * 100) + '%'
340
+ saveState(st)
341
+ if (alphaTimer) clearTimeout(alphaTimer)
342
+ alphaTimer = setTimeout(function () {
343
+ alphaTimer = null
344
+ if (handlers.onHighlightAlpha) handlers.onHighlightAlpha(st.highlightAlpha)
345
+ }, 120)
346
+ })
347
+ alphaRange.addEventListener('change', function () {
348
+ st.highlightAlpha = (parseInt(alphaRange.value, 10) || 50) / 100
349
+ saveState(st)
350
+ if (alphaTimer) { clearTimeout(alphaTimer); alphaTimer = null }
351
+ if (handlers.onHighlightAlpha) handlers.onHighlightAlpha(st.highlightAlpha)
352
+ })
353
+ head.addEventListener('dblclick', function (e) {
354
+ if (e.target.closest('button')) return
355
+ st.collapsed = !st.collapsed; refresh()
356
+ })
357
+
358
+ // 拖动(标题栏 / 收起钮;共用 pointermove/up,位置锚点共享 st.x/st.y)
359
+ var drag = null
360
+ var btnDrag = null
361
+ head.addEventListener('pointerdown', function (e) {
362
+ if (e.target.closest('button')) return
363
+ drag = { dx: e.clientX - root.offsetLeft, dy: e.clientY - root.offsetTop }
364
+ e.preventDefault()
365
+ })
366
+ capBtn.addEventListener('pointerdown', function (e) {
367
+ btnDrag = { dx: e.clientX - capBtn.offsetLeft, dy: e.clientY - capBtn.offsetTop }
368
+ e.preventDefault()
369
+ })
370
+ window.addEventListener('pointermove', function (e) {
371
+ if (drag) {
372
+ root.style.left = Math.max(6, Math.min(window.innerWidth - root.offsetWidth - 6, e.clientX - drag.dx)) + 'px'
373
+ root.style.top = Math.max(6, Math.min(window.innerHeight - 44, e.clientY - drag.dy)) + 'px'
374
+ } else if (btnDrag) {
375
+ capBtn.style.left = Math.max(6, Math.min(window.innerWidth - 34, e.clientX - btnDrag.dx)) + 'px'
376
+ capBtn.style.top = Math.max(6, Math.min(window.innerHeight - 34, e.clientY - btnDrag.dy)) + 'px'
377
+ }
378
+ })
379
+ window.addEventListener('pointerup', function () {
380
+ if (drag) { drag = null; save(); apply() }
381
+ if (btnDrag) {
382
+ btnDrag = null
383
+ // 收起钮拖到哪 → 展开面板就到哪(锚点共享)
384
+ st.x = parseFloat(capBtn.style.left) || null
385
+ st.y = parseFloat(capBtn.style.top) || null
386
+ saveState(st)
387
+ }
388
+ })
389
+
390
+ // ---------- 主题跟随(2s 复查) ----------
391
+ function watchTheme() {
392
+ var d = detectDark()
393
+ if (d !== uiDark) { uiDark = d; paint(); refresh() }
394
+ else paint()
395
+ }
396
+
397
+ function setMode(m) {
398
+ st.mode = m; persist()
399
+ if (handlers.onMode) handlers.onMode(m)
400
+ }
401
+ function setTheme(i) {
402
+ st.themeIdx = i
403
+ var ac = themeColor() // 预设色 / 自定义色(i=6 时)
404
+ persist()
405
+ if (handlers.onTheme) handlers.onTheme(ac)
406
+ if (st.highlight && handlers.onHighlight) handlers.onHighlight(true, ac)
407
+ }
408
+ paint(); refresh()
409
+ // 初始化状态同步给 mascot:mode(曾漏:localStorage 恢复'game'只改了 UI,宠物实际还在默认模式)
410
+ if (handlers.onMode) handlers.onMode(st.mode)
411
+ if (handlers.onHighlight) handlers.onHighlight(st.highlight, accent())
412
+ if (handlers.onTheme) handlers.onTheme(accent())
413
+ if (handlers.onDedupGap) handlers.onDedupGap(st.dedupGap)
414
+ if (handlers.onHighlightAlpha) handlers.onHighlightAlpha(st.highlightAlpha)
415
+ setInterval(watchTheme, 2000)
416
+
417
+ return {
418
+ show: function () { st.collapsed = false; refresh() },
419
+ getMode: function () { return st.mode },
420
+ getState: function () { return Object.assign({}, st) }
421
+ }
422
+ }
package/lib/index.js ADDED
@@ -0,0 +1,147 @@
1
+ // ============================================================================
2
+ // dsh-pet-dom —— 宿主侧插件本体(cordis bundle 服务)
3
+ // ============================================================================
4
+ // 职责:
5
+ // 1. 注册 HTTP 路由:/dsh-pet-dom/miku.gif(精灵图)、/dsh-pet-dom/mascot.js、
6
+ // /dsh-pet-dom/brain.js(客户端 ESM 模块)
7
+ // 2. 通过 ctx.webServer.tapIndex 把 <script type="module" src="/dsh-pet-dom/mascot.js">
8
+ // 注入 DSH Web 页面的 </body>,脚本运行后创建桌宠。
9
+ // 桌宠的全部行为都在 lib/mascot.js(客户端)与 lib/brain.js(规划大脑,纯函数)
10
+ // 里,由原 uniapp Pet.vue 纯浏览器移植。
11
+ // ============================================================================
12
+
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ // Package root: lib/index.js -> package root. 保持 bundle 可搬迁(本地 link 或 node_modules 均可)。
18
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
19
+
20
+ const SPRITE_FILE = path.join(PACKAGE_ROOT, 'assets', 'miku.gif')
21
+ const MASCOT_FILE = path.join(PACKAGE_ROOT, 'lib', 'mascot.js')
22
+ const BRAIN_FILE = path.join(PACKAGE_ROOT, 'lib', 'brain.js')
23
+ const CONSOLE_FILE = path.join(PACKAGE_ROOT, 'lib', 'console.js')
24
+
25
+ // 客户端脚本内容(每次请求现读,与 brain/console 一致 —— 开发期改代码刷新即生效,无需重启 dsh web)
26
+ const name = 'dsh-pet-dom'
27
+ const inject = ['webServer']
28
+
29
+ function apply(ctx) {
30
+ let spriteBytes = null
31
+ const disposers = []
32
+
33
+ function loadSprite() {
34
+ if (spriteBytes) return spriteBytes
35
+ spriteBytes = fs.readFileSync(SPRITE_FILE)
36
+ return spriteBytes
37
+ }
38
+
39
+ const JSON_HEADERS = {
40
+ 'Content-Type': 'application/json; charset=utf-8',
41
+ 'Access-Control-Allow-Origin': '*',
42
+ 'Cache-Control': 'no-store',
43
+ }
44
+
45
+ // 精灵图
46
+ disposers.push(ctx.webServer.register({
47
+ kind: 'exact',
48
+ path: '/dsh-pet-dom/miku.gif',
49
+ handler: (req, res) => {
50
+ try {
51
+ const bytes = loadSprite()
52
+ res.writeHead(200, {
53
+ 'Content-Type': 'image/gif',
54
+ 'Cache-Control': 'no-store',
55
+ 'Content-Length': String(bytes.length),
56
+ })
57
+ res.end(bytes)
58
+ } catch (err) {
59
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
60
+ res.end('sprite unavailable: ' + String((err && err.message) || err))
61
+ }
62
+ },
63
+ }))
64
+
65
+ // 客户端桌宠脚本(ESM:import brain.js / console.js;每次请求现读,开发期改代码刷新即生效)
66
+ disposers.push(ctx.webServer.register({
67
+ kind: 'exact',
68
+ path: '/dsh-pet-dom/mascot.js',
69
+ handler: (req, res) => {
70
+ try {
71
+ const src = fs.readFileSync(MASCOT_FILE, 'utf8')
72
+ res.writeHead(200, {
73
+ 'Content-Type': 'application/javascript; charset=utf-8',
74
+ 'Cache-Control': 'no-store',
75
+ })
76
+ res.end(src)
77
+ } catch (err) {
78
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
79
+ res.end('mascot script unavailable: ' + String((err && err.message) || err))
80
+ }
81
+ },
82
+ }))
83
+
84
+ // 规划大脑模块(mascot.js 的 ESM 依赖;文件小,按请求现读,便于热改)
85
+ disposers.push(ctx.webServer.register({
86
+ kind: 'exact',
87
+ path: '/dsh-pet-dom/brain.js',
88
+ handler: (req, res) => {
89
+ try {
90
+ const src = fs.readFileSync(BRAIN_FILE, 'utf8')
91
+ res.writeHead(200, {
92
+ 'Content-Type': 'application/javascript; charset=utf-8',
93
+ 'Cache-Control': 'no-store',
94
+ })
95
+ res.end(src)
96
+ } catch (err) {
97
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
98
+ res.end('brain script unavailable: ' + String((err && err.message) || err))
99
+ }
100
+ },
101
+ }))
102
+
103
+ // 控制台模块(mascot.js 的 ESM 依赖;按请求现读)
104
+ disposers.push(ctx.webServer.register({
105
+ kind: 'exact',
106
+ path: '/dsh-pet-dom/console.js',
107
+ handler: (req, res) => {
108
+ try {
109
+ const src = fs.readFileSync(CONSOLE_FILE, 'utf8')
110
+ res.writeHead(200, {
111
+ 'Content-Type': 'application/javascript; charset=utf-8',
112
+ 'Cache-Control': 'no-store',
113
+ })
114
+ res.end(src)
115
+ } catch (err) {
116
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
117
+ res.end('console script unavailable: ' + String((err && err.message) || err))
118
+ }
119
+ },
120
+ }))
121
+
122
+ // 健康检查(可选,便于验证插件已挂载)
123
+ disposers.push(ctx.webServer.register({
124
+ kind: 'exact',
125
+ path: '/dsh-pet-dom/health.json',
126
+ handler: (req, res) => {
127
+ res.writeHead(200, JSON_HEADERS)
128
+ res.end(JSON.stringify({ ok: true, name, version: '2.0.0' }))
129
+ },
130
+ }))
131
+
132
+ // 把桌宠脚本注入 DSH Web 页面 </body>(ESM 模块:mascot.js 内部 import brain.js)
133
+ disposers.push(ctx.webServer.tapIndex((html) => {
134
+ if (html.indexOf('/dsh-pet-dom/mascot.js') !== -1) return html
135
+ const tag = '<script type="module" src="/dsh-pet-dom/mascot.js"></script>'
136
+ if (html.indexOf('</body>') !== -1) return html.replace('</body>', tag + '</body>')
137
+ return html + tag
138
+ }))
139
+
140
+ ctx.effect(() => () => {
141
+ for (const d of disposers) {
142
+ try { d() } catch (err) {}
143
+ }
144
+ })
145
+ }
146
+
147
+ export { name, inject, apply }