dsh-lighthouse 0.0.0-stage → 0.3.3

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/client.js ADDED
@@ -0,0 +1,388 @@
1
+ /**
2
+ * dsh-lighthouse · 客户端半 —— 「作品观察区」。
3
+ *
4
+ * 一句话:**给用户一个点开就能看的窗口** —— 每个作品现在怎么样了:
5
+ * 发布了没有(✅ / ⏳ / ❌)、多少星、多少人关注、有没有人留言、收录 PR 排到哪。
6
+ *
7
+ * 它只画,不算:数据全来自 host 半的只读接口 `GET /dsh-lighthouse/status`
8
+ * (同一个数字在面板、工具、卡片里必须一致)。
9
+ *
10
+ * 座位:
11
+ * · `sidebar.panellist` —— 侧栏那个图标(组件收到 `{ size, active }`);
12
+ * · `main`(keyed = 本插件 id)—— 点图标后中间出现的那一屏。
13
+ * 两者的 id 必须一致 —— 侧栏就是靠这个 id 找到要切到哪个 main 面板的。
14
+ *
15
+ * 形态:Plain ModuleLoader bundle(无构建步骤),与官方插件的客户端半同构。
16
+ */
17
+ window.__ModuleLoader__.load({ id: "dsh-lighthouse", factory: (require) => {
18
+ var module = { exports: {} }
19
+ var exports = module.exports
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
21
+
22
+ const react = require("react")
23
+ const h = react.createElement
24
+ const { useCallback, useEffect, useRef, useState } = react
25
+
26
+ const name = "dsh-lighthouse"
27
+ const inject = ["slots"]
28
+ const PANEL_ID = "dsh-lighthouse"
29
+ const ROUTE = "/dsh-lighthouse/status"
30
+
31
+ /* ------------------------------------------------------------ 小工具 */
32
+
33
+ /** 一次性注入样式,重复 apply 不叠加。 */
34
+ function installStyles() {
35
+ const id = "dsh-lighthouse-styles"
36
+ if (document.getElementById(id)) return
37
+ const node = document.createElement("style")
38
+ node.id = id
39
+ node.textContent = `
40
+ .lh-root { padding: 18px 22px 40px; max-width: 900px; margin: 0 auto; color: inherit;
41
+ box-sizing: border-box; height: 100%; overflow-y: auto; }
42
+ .lh-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
43
+ .lh-title { font-size: 18px; font-weight: 650; margin: 0; }
44
+ .lh-sub { font-size: 12px; opacity: .62; }
45
+ .lh-btn { margin-left: auto; font-size: 12px; padding: 4px 10px; border-radius: 7px; cursor: pointer;
46
+ border: 1px solid rgba(128,128,128,.35); background: rgba(128,128,128,.08); color: inherit; }
47
+ .lh-btn:hover { background: rgba(128,128,128,.16); }
48
+ .lh-btn:disabled { opacity: .5; cursor: default; }
49
+ .lh-tiles { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0 18px; }
50
+ .lh-tile { flex: 1 1 96px; min-width: 96px; border: 1px solid rgba(128,128,128,.22); border-radius: 10px; padding: 9px 12px; }
51
+ .lh-tile b { display: block; font-size: 19px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.25; }
52
+ .lh-tile span { font-size: 11px; opacity: .62; }
53
+ .lh-card { border: 1px solid rgba(128,128,128,.22); border-left-width: 3px; border-radius: 10px;
54
+ padding: 11px 14px; margin-bottom: 10px; }
55
+ .lh-card.ok { border-left-color: #3fb950; }
56
+ .lh-card.wait { border-left-color: #d29922; }
57
+ .lh-card.bad { border-left-color: #f85149; }
58
+ .lh-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
59
+ .lh-name { font-size: 13.5px; font-weight: 650; font-family: ui-monospace, Consolas, monospace; }
60
+ .lh-zh { font-size: 11.5px; opacity: .72; }
61
+ .lh-link { color: inherit; text-decoration: none; border-bottom: 1px dashed rgba(128,128,128,.55); }
62
+ .lh-link:hover { color: #58a6ff; border-bottom-color: #58a6ff; }
63
+ .lh-tag { font-size: 10.5px; opacity: .58; border: 1px solid rgba(128,128,128,.3); border-radius: 999px; padding: 0 7px; }
64
+ .lh-line { font-size: 12.5px; margin-top: 4px; display: flex; gap: 12px; flex-wrap: wrap; }
65
+ .lh-k { opacity: .58; }
66
+ .lh-num { font-variant-numeric: tabular-nums; font-weight: 600; }
67
+ .lh-state { font-size: 12px; }
68
+ .lh-dim { opacity: .55; }
69
+ .lh-err { color: #f85149; font-size: 12.5px; margin: 8px 0; }
70
+ .lh-empty { opacity: .6; font-size: 13px; padding: 24px 0; text-align: center; }
71
+ .lh-fx { position: fixed; inset: 0; width: 100vw; height: 100vh;
72
+ pointer-events: none; z-index: 9999; }
73
+ `
74
+ document.head.appendChild(node)
75
+ }
76
+
77
+ /** 小时 → 人话。 */
78
+ function hoursText(hours) {
79
+ if (!Number.isFinite(hours)) return "?"
80
+ if (hours < 1) return Math.round(hours * 60) + " 分钟"
81
+ if (hours < 48) return hours.toFixed(1) + " 小时"
82
+ return (hours / 24).toFixed(1) + " 天"
83
+ }
84
+
85
+ /** ISO 时间 → MM-DD HH:mm。 */
86
+ function shortTime(iso) {
87
+ if (typeof iso !== "string" || iso === "") return "—"
88
+ const d = new Date(iso)
89
+ if (Number.isNaN(d.getTime())) return "—"
90
+ const p = (n) => String(n).padStart(2, "0")
91
+ return p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes())
92
+ }
93
+
94
+ /* --------------------------------------------------------------- 取数 */
95
+
96
+ /**
97
+ * 拉一次状态。只读接口,失败就把原因原样显示出来(不编数字)。
98
+ */
99
+ function useStatus() {
100
+ const [state, setState] = useState({ loading: true, report: null, error: null, at: 0 })
101
+ const load = useCallback(function () {
102
+ setState(function (prev) { return { ...prev, loading: true } })
103
+ fetch(ROUTE, { credentials: "same-origin", cache: "no-store" })
104
+ .then(function (res) { return res.json() })
105
+ .then(function (json) {
106
+ if (json && json.ok === true) {
107
+ setState({ loading: false, report: json.report, error: null, at: Date.now() })
108
+ } else {
109
+ const why = (json && json.errors ? json.errors.join(";") : (json && json.error) || "读不到")
110
+ setState({ loading: false, report: null, error: why, at: Date.now() })
111
+ }
112
+ })
113
+ .catch(function (err) {
114
+ setState({ loading: false, report: null, error: String((err && err.message) || err), at: Date.now() })
115
+ })
116
+ }, [])
117
+ useEffect(function () { load() }, [load])
118
+ return { ...state, reload: load }
119
+ }
120
+
121
+ /* --------------------------------------------------------------- 组件 */
122
+
123
+ /** 侧栏图标:一座灯塔。 */
124
+ function PanelIcon() {
125
+ return h("svg", {
126
+ width: 16, height: 16, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true",
127
+ },
128
+ h("path", { d: "M7.1 1.2h1.8v1.5l1.1.6.7 2.9H5.3l.7-2.9 1.1-.6V1.2z" }),
129
+ h("path", { d: "M1.6 4.3 4 5.5l-.5 1.1L1 5.5l.6-1.2zm12.8 0 .6 1.2-2.5 1.1-.5-1.1 2.4-1.2z" }),
130
+ h("path", { d: "M5.1 7.4h5.8l1.3 7.4H3.8l1.3-7.4z" }),
131
+ h("path", { d: "M6.2 9.2h3.6v1.1H6.2V9.2z", fill: "rgba(0,0,0,.35)" })
132
+ )
133
+ }
134
+
135
+ /** 一个数字格。 */
136
+ function Tile(props) {
137
+ return h("div", { className: "lh-tile" },
138
+ h("b", null, String(props.value)),
139
+ h("span", null, props.label)
140
+ )
141
+ }
142
+
143
+ /**
144
+ * 一个作品一张卡。**状态用图标说话**:
145
+ * 已发布 ✅;收录/PR 还在路上 ⏳;读不到 ❌(并写出原因)。
146
+ */
147
+ function WorkCard(props) {
148
+ const item = props.item
149
+ const work = item.work || {}
150
+ const local = item.local || {}
151
+ const gh = item.github || {}
152
+ const market = item.market || {}
153
+ const released = gh.exists === true
154
+ const listed = market.listed === true
155
+ const pr = gh.pr
156
+ const cls = listed ? "lh-card ok" : (released ? "lh-card wait" : "lh-card bad")
157
+
158
+ const stateLine = []
159
+ stateLine.push(h("span", { key: "rel", className: "lh-state" },
160
+ released ? "✅ 已发布" : (gh.exists === false ? "❌ 仓库不存在" : "❌ 读不到仓库")))
161
+ stateLine.push(h("span", { key: "mk", className: "lh-state" },
162
+ listed
163
+ ? "✅ 市场已收录" + (market.downloads !== undefined ? "(下载 " + market.downloads + ")" : "")
164
+ : (market.available === true ? "⏳ 收录中" : "❌ 市场读不到")))
165
+ if (pr) {
166
+ const waited = (Date.now() - Date.parse(pr.createdAt)) / 3600000
167
+ stateLine.push(h("span", { key: "pr", className: "lh-state" },
168
+ pr.mergedAt
169
+ ? "✅ PR #" + pr.number + " 已合并"
170
+ : (pr.state === "open" ? "⏳ PR #" + pr.number + " 等 " + hoursText(waited) : "· PR #" + pr.number + " " + pr.state)))
171
+ } else {
172
+ stateLine.push(h("span", { key: "nopr", className: "lh-state lh-dim" }, "· 没有收录 PR"))
173
+ }
174
+
175
+ // 中文名(名册 nameZh 优先)+ 直达链接(GitHub;有市场条目页就再给一个)
176
+ const zh = (work && work.nameZh) || (local && local.displayName) || ""
177
+ const link = (item.links && item.links.github) || null
178
+ const marketLink = (item.links && item.links.market) || null
179
+
180
+ return h("div", { className: cls },
181
+ h("div", { className: "lh-row" },
182
+ link
183
+ ? h("a", { className: "lh-name lh-link", href: link, target: "_blank", rel: "noreferrer" }, work.name || "?")
184
+ : h("span", { className: "lh-name" }, work.name || "?"),
185
+ zh ? h("span", { className: "lh-zh" }, zh) : null,
186
+ work.category ? h("span", { className: "lh-tag" }, work.category) : null,
187
+ marketLink
188
+ ? h("a", { className: "lh-zh lh-link", href: marketLink, target: "_blank", rel: "noreferrer", title: "市场条目页" }, "市场 →")
189
+ : null,
190
+ h("span", { className: "lh-sub", style: { marginLeft: "auto" } },
191
+ "本地 " + (local.available === true ? (local.version || "?") + " · " + (local.files || 0) + " 文件" : "读不到"))
192
+ ),
193
+ h("div", { className: "lh-line" },
194
+ h("span", null, h("span", { className: "lh-k" }, "⭐ 星 "), h("span", { className: "lh-num" }, String(gh.stars || 0))),
195
+ h("span", null, h("span", { className: "lh-k" }, "👁 关注 "), h("span", { className: "lh-num" }, String(gh.watchers || 0))),
196
+ h("span", null, h("span", { className: "lh-k" }, "💬 留言 "), h("span", { className: "lh-num" }, String(gh.openIssues || 0))),
197
+ h("span", null, h("span", { className: "lh-k" }, "最后推送 "), h("span", null, shortTime(gh.pushedAt)))
198
+ ),
199
+ h("div", { className: "lh-line" }, stateLine)
200
+ )
201
+ }
202
+
203
+ /** 观察区主体。 */
204
+ function WatchPanel() {
205
+ const { loading, report, error, at, reload } = useStatus()
206
+ const s = (report && report.summary) || null
207
+
208
+ /* ---------------- 刷新粒子特效:跟重启坞同款的粒子海浪 ---------------- */
209
+ const [fxOn, setFxOn] = useState(false)
210
+ const fxEndRef = useRef(null)
211
+ const fxCanvasRef = useRef(null)
212
+
213
+ function playFx(after) {
214
+ fxEndRef.current = typeof after === "function" ? after : null
215
+ setFxOn(true)
216
+ }
217
+
218
+ // 调试入口:不用点刷新也能放一次特效
219
+ useEffect(function () {
220
+ const trigger = function () { playFx() }
221
+ window.__dshLighthouseFx = trigger
222
+ return function () {
223
+ if (window.__dshLighthouseFx === trigger) delete window.__dshLighthouseFx
224
+ }
225
+ }, [])
226
+
227
+ useEffect(function () {
228
+ if (!fxOn) return undefined
229
+ const canvas = fxCanvasRef.current
230
+ if (!canvas) { setFxOn(false); return undefined }
231
+ const dpr = Math.min(window.devicePixelRatio || 1, 2)
232
+ const W = window.innerWidth
233
+ const H = window.innerHeight
234
+ canvas.width = Math.round(W * dpr)
235
+ canvas.height = Math.round(H * dpr)
236
+ const ctx = canvas.getContext("2d")
237
+ if (!ctx) { setFxOn(false); return undefined }
238
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
239
+
240
+ const DURATION = 2000
241
+ const TAU = Math.PI * 2
242
+ const diag = Math.hypot(W, H)
243
+ const dirX = W / diag
244
+ const dirY = H / diag
245
+ const STEP = 22
246
+ const KEEP = 0.22
247
+ const parts = []
248
+ for (let y = -STEP; y < H + STEP; y += STEP) {
249
+ for (let x = -STEP; x < W + STEP; x += STEP) {
250
+ if (Math.random() > KEEP) continue
251
+ parts.push({
252
+ x: x + (Math.random() - 0.5) * STEP * 0.9,
253
+ y: y + (Math.random() - 0.5) * STEP * 0.9,
254
+ s: (x * dirX + y * dirY) / diag,
255
+ jitter: 0.55 + Math.random() * 0.45,
256
+ hue: 194 + Math.random() * 42,
257
+ })
258
+ }
259
+ }
260
+
261
+ let raf = 0
262
+ let finished = false
263
+ const begin = performance.now()
264
+
265
+ const frame = function (now) {
266
+ if (finished) return
267
+ const t = Math.min(1, (now - begin) / DURATION)
268
+ if (t >= 1) {
269
+ finished = true
270
+ ctx.clearRect(0, 0, W, H)
271
+ const cb = fxEndRef.current
272
+ fxEndRef.current = null
273
+ setFxOn(false)
274
+ if (typeof cb === "function") cb()
275
+ return
276
+ }
277
+ ctx.clearRect(0, 0, W, H)
278
+ ctx.globalCompositeOperation = "lighter"
279
+ const travel = t * 3
280
+ for (const q of parts) {
281
+ const phase = q.s * TAU * 2.2 - travel * TAU * 0.55
282
+ const wave =
283
+ Math.sin(phase) * 0.62 +
284
+ Math.sin(phase * 1.93 + 1.7) * 0.26 +
285
+ Math.sin(phase * 0.47 + 3.1) * 0.12
286
+ if (wave <= 0) continue
287
+ const lum = Math.pow(wave, 1.9) * q.jitter
288
+ if (lum <= 0.02) continue
289
+ const depth = 0.75 + q.s * 0.55
290
+ const size = (1.1 + lum * 6.4) * depth
291
+ const light = 58 + lum * 38
292
+ const arm = size * (1.7 + lum * 1.7)
293
+ const thick = Math.max(1.0, size * 0.46)
294
+ if (lum > 0.35) {
295
+ ctx.globalAlpha = Math.min(0.34, lum * 0.3)
296
+ ctx.fillStyle = "hsl(" + q.hue + " 100% 78%)"
297
+ ctx.beginPath()
298
+ ctx.arc(q.x, q.y, arm * 0.85, 0, TAU)
299
+ ctx.fill()
300
+ }
301
+ ctx.globalAlpha = Math.min(1, lum * 1.75)
302
+ ctx.fillStyle = "hsl(" + q.hue + " 96% " + light + "%)"
303
+ ctx.fillRect(q.x - arm, q.y - thick / 2, arm * 2, thick)
304
+ ctx.fillRect(q.x - thick / 2, q.y - arm, thick, arm * 2)
305
+ ctx.beginPath()
306
+ ctx.arc(q.x, q.y, thick * 1.15, 0, TAU)
307
+ ctx.fill()
308
+ }
309
+ ctx.globalAlpha = 1
310
+ ctx.globalCompositeOperation = "source-over"
311
+ raf = requestAnimationFrame(frame)
312
+ }
313
+ raf = requestAnimationFrame(frame)
314
+ return function () {
315
+ finished = true
316
+ cancelAnimationFrame(raf)
317
+ }
318
+ }, [fxOn])
319
+
320
+ const body = []
321
+ if (error !== null) {
322
+ body.push(h("div", { className: "lh-err", key: "err" }, "读不到:" + error))
323
+ }
324
+ if (s !== null) {
325
+ body.push(h("div", { className: "lh-tiles", key: "tiles" },
326
+ h(Tile, { key: "w", value: s.works, label: "作品" }),
327
+ h(Tile, { key: "p", value: s.published, label: "已发布" }),
328
+ h(Tile, { key: "l", value: s.listed, label: "市场已收录" }),
329
+ h(Tile, { key: "d", value: s.totalDownloads, label: "总下载" }),
330
+ h(Tile, { key: "pr", value: s.openPrs, label: "待合 PR" })
331
+ ))
332
+ const list = Array.isArray(report.works) ? report.works : []
333
+ for (const item of list) body.push(h(WorkCard, { key: item.work && item.work.name, item }))
334
+ if (list.length === 0) body.push(h("div", { className: "lh-empty", key: "empty" }, "名册里还没有作品"))
335
+ }
336
+ if (report === null && error === null && loading === true) {
337
+ body.push(h("div", { className: "lh-empty", key: "loading" }, "读取中…"))
338
+ }
339
+
340
+ return h("div", { className: "lh-root" },
341
+ fxOn ? h("canvas", { className: "lh-fx", ref: fxCanvasRef }) : null,
342
+ h("div", { className: "lh-head" },
343
+ h("h1", { className: "lh-title" }, "🚢 作品观察区"),
344
+ h("span", { className: "lh-sub" },
345
+ at ? "更新于 " + shortTime(new Date(at).toISOString())
346
+ : (report && report.generatedAt ? "数据 " + shortTime(report.generatedAt) : "")),
347
+ h("button", {
348
+ className: "lh-btn",
349
+ onClick: function () { if (loading === true) return; playFx(reload) },
350
+ disabled: loading === true,
351
+ type: "button",
352
+ },
353
+ loading === true ? "刷新中…" : "刷新")
354
+ ),
355
+ h("div", { className: "lh-sub", style: { marginTop: "6px" } },
356
+ report && report.market
357
+ ? "市场登记表:" + (report.market.available === true
358
+ ? (report.market.count || "?") + " 条(更新于 " + (report.market.updated || "?") + ")"
359
+ : "读不到(" + (report.market.reason || "?") + ")")
360
+ : "全部数字来自 GitHub API 与本机市场接口,不经模型生成。"),
361
+ body
362
+ )
363
+ }
364
+
365
+ /* --------------------------------------------------------------- 挂载 */
366
+
367
+ function apply(ctx) {
368
+ installStyles()
369
+ // 侧栏图标:id 就是 main 面板的 key,两者必须一致
370
+ ctx.slots.inject("sidebar.panellist", function () {
371
+ return ctx.slots.register({
372
+ name: "sidebar.panellist",
373
+ id: PANEL_ID,
374
+ order: 60,
375
+ label: function () { return "作品观察区" },
376
+ }, PanelIcon)
377
+ })
378
+ // 中间那一屏
379
+ ctx.slots.inject("main", function () {
380
+ return ctx.slots.register({ name: "main", key: PANEL_ID }, WatchPanel)
381
+ })
382
+ }
383
+
384
+ exports.name = name
385
+ exports.inject = inject
386
+ exports.apply = apply
387
+ return module.exports
388
+ }})
@@ -0,0 +1,4 @@
1
+ # dsh-lighthouse bundle patch: 把插件行插进 profile 名单。
2
+ - insert:
3
+ - id: plugin-lighthouse
4
+ name: 'dsh-lighthouse'