dsh-pick 0.0.0-stage → 0.2.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 +142 -2
- package/bin/dsh-pick.js +155 -0
- package/data/catalog.json +1 -0
- package/lib/classify.js +56 -0
- package/lib/engine.js +409 -0
- package/lib/metrics.js +105 -0
- package/lib/words.js +71 -0
- package/package.json +23 -4
- package/plugin/client.js +119 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +109 -0
- package/web/panel.html +272 -0
- package/web/panel.js +428 -0
package/plugin/client.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* global window, document */
|
|
2
|
+
// plugin/client.js — dsh-pick 的设置页
|
|
3
|
+
//
|
|
4
|
+
// 手写单文件 bundle,没有构建链:由 dsh web 客户端的 ModuleLoader 注入
|
|
5
|
+
// (写法照 dsh-model / dsh-vps-manager)。挂在「设置 → dsh-pick」。
|
|
6
|
+
//
|
|
7
|
+
// 界面本身就是一个 iframe,指向宿主路由 /api-dsh-pick/panel —— 面板那套 UI
|
|
8
|
+
// 是独立调试过的(dsh-pick serve 直接能开),这里原样复用,不再用 React 重写一遍。
|
|
9
|
+
// 硬约束:界面崩了不能影响 dsh,注册一律包在 try/catch 里。
|
|
10
|
+
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: 'dsh-pick',
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
const module = { exports: {} }
|
|
15
|
+
const React = require('react')
|
|
16
|
+
const h = React.createElement
|
|
17
|
+
const { useEffect, useRef, useState } = React
|
|
18
|
+
|
|
19
|
+
// 语言:跟着页面(dsh 只有中、英两种)
|
|
20
|
+
const isZh = () => {
|
|
21
|
+
const l = (document.documentElement.lang || navigator.language || 'zh').toLowerCase()
|
|
22
|
+
return l.startsWith('zh')
|
|
23
|
+
}
|
|
24
|
+
const L = (zh, en) => (isZh() ? zh : en)
|
|
25
|
+
|
|
26
|
+
// 用 dsh 自己的颜色变量,跟随明暗主题
|
|
27
|
+
const C = {
|
|
28
|
+
text: 'var(--dsw-alias-label-primary, inherit)',
|
|
29
|
+
sub: 'var(--dsw-alias-label-secondary, #888)',
|
|
30
|
+
faint: 'var(--dsw-alias-label-tertiary, #999)',
|
|
31
|
+
border: 'var(--dsw-alias-border-l3, rgba(127,127,127,.25))',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PickPanel() {
|
|
35
|
+
const frame = useRef(null)
|
|
36
|
+
const [meta, setMeta] = useState(null)
|
|
37
|
+
const [h, setH] = useState(720)
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let alive = true
|
|
41
|
+
fetch('/api-dsh-pick/meta', { credentials: 'same-origin' })
|
|
42
|
+
.then(r => r.json())
|
|
43
|
+
.then(j => { if (alive) setMeta(j) })
|
|
44
|
+
.catch(() => {})
|
|
45
|
+
return () => { alive = false }
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
// iframe 里改不了高度(跨文档),用一个粗策略:内容高差太多就跟着长
|
|
50
|
+
const t = setInterval(() => {
|
|
51
|
+
try {
|
|
52
|
+
const doc = frame.current && frame.current.contentDocument
|
|
53
|
+
if (doc && doc.body) {
|
|
54
|
+
const need = Math.max(720, doc.body.scrollHeight + 40)
|
|
55
|
+
if (Math.abs(need - h) > 40) setH(need)
|
|
56
|
+
}
|
|
57
|
+
} catch { /* 跨域就不管,保持默认高度 */ }
|
|
58
|
+
}, 1500)
|
|
59
|
+
return () => clearInterval(t)
|
|
60
|
+
}, [h])
|
|
61
|
+
|
|
62
|
+
const tip = !meta
|
|
63
|
+
? L('正在读取目录…', 'Loading catalog…')
|
|
64
|
+
: meta.total
|
|
65
|
+
? L(
|
|
66
|
+
`目录 ${meta.total.toLocaleString()} 个插件 · 去噪指标 ${meta.deepCount} 个 · 数据截止 ${meta.anchor}`,
|
|
67
|
+
`${meta.total.toLocaleString()} plugins · ${meta.deepCount} with denoised metrics · as of ${meta.anchor}`)
|
|
68
|
+
: L('目录未生成:在 dsh-pick 目录跑一次 dsh-pick build', 'Catalog not built yet: run `dsh-pick build`')
|
|
69
|
+
|
|
70
|
+
return h('div', { style: { color: C.text } },
|
|
71
|
+
h('div', {
|
|
72
|
+
style: {
|
|
73
|
+
fontSize: 12, color: C.faint, margin: '2px 0 10px', lineHeight: 1.6,
|
|
74
|
+
},
|
|
75
|
+
}, tip),
|
|
76
|
+
|
|
77
|
+
h('iframe', {
|
|
78
|
+
ref: frame,
|
|
79
|
+
src: '/api-dsh-pick/panel',
|
|
80
|
+
title: 'dsh-pick',
|
|
81
|
+
style: {
|
|
82
|
+
width: '100%',
|
|
83
|
+
height: h + 'px',
|
|
84
|
+
border: '1px solid ' + C.border,
|
|
85
|
+
borderRadius: '10px',
|
|
86
|
+
background: 'transparent',
|
|
87
|
+
display: 'block',
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
h('div', {
|
|
92
|
+
style: { fontSize: 11, color: C.faint, marginTop: 8, lineHeight: 1.7 },
|
|
93
|
+
},
|
|
94
|
+
L(
|
|
95
|
+
'排名按周下载;日常基线已扣掉版本发布日与次日的拉取虚高,比原始下载量更接近真实使用量。' +
|
|
96
|
+
'下载量含 CI / 镜像 / 爬虫流量,不等于用户数。',
|
|
97
|
+
'Ranks are weekly downloads; daily baseline strips the publish-day spike. ' +
|
|
98
|
+
'Download counts include CI/mirror traffic, not real users.'),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const injectSlots = ['slots']
|
|
104
|
+
function apply(ctx) {
|
|
105
|
+
try {
|
|
106
|
+
ctx.slots.inject('settings.section', () =>
|
|
107
|
+
ctx.slots.register(
|
|
108
|
+
{ name: 'settings.section', id: 'dsh-pick', order: 30, label: () => 'dsh-pick' },
|
|
109
|
+
PickPanel,
|
|
110
|
+
))
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn('[dsh-pick] 设置页注册失败', error)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { name: 'dsh-pick', inject: injectSlots, apply }
|
|
117
|
+
return module.exports
|
|
118
|
+
},
|
|
119
|
+
})
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// plugin/index.js — dsh-pick 的 DSH 插件(宿主端)
|
|
2
|
+
//
|
|
3
|
+
// 只做一件事:把「选插件」面板挂到 dsh 自己的 webServer 上,让 dsh 设置页里能看,
|
|
4
|
+
// 顺便给面板提供数据接口。数据全部来自本包 data/catalog.json,不外传。
|
|
5
|
+
//
|
|
6
|
+
// 路由(都走 dsh 的登录校验,所以先过 dsh 自己的鉴权):
|
|
7
|
+
// GET /api-dsh-pick/panel 面板 HTML
|
|
8
|
+
// GET /api-dsh-pick/panel.js 面板逻辑
|
|
9
|
+
// GET /api-dsh-pick/words.js 搜索词典
|
|
10
|
+
// GET /api-dsh-pick/catalog.json 目录(含排名、链接、去噪指标)
|
|
11
|
+
// GET /api-dsh-pick/meta 目录概要(更新时间、包数)
|
|
12
|
+
//
|
|
13
|
+
// 写法照 dsh-model(同机参考)与 dsh-vps-manager(同作者)的 plugin/index.js。
|
|
14
|
+
|
|
15
|
+
import { readFile } from 'node:fs/promises'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
|
|
19
|
+
/** 本包根目录:插件就是完整的 dsh-pick 包 */
|
|
20
|
+
const PKG_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
21
|
+
|
|
22
|
+
const TYPES = {
|
|
23
|
+
'.html': 'text/html; charset=utf-8',
|
|
24
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 只放行这几个固定文件,别把 bin/ 也暴露出去 */
|
|
29
|
+
const FILES = {
|
|
30
|
+
'/panel': 'web/panel.html',
|
|
31
|
+
'/panel.js': 'web/panel.js',
|
|
32
|
+
'/words.js': 'lib/words.js',
|
|
33
|
+
'/catalog.json': 'data/catalog.json',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readPkgJson() {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(await readFile(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
39
|
+
} catch {
|
|
40
|
+
return {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 目录概要:给面板顶部显示「更新时间 / 包数」用 */
|
|
45
|
+
async function catalogMeta() {
|
|
46
|
+
try {
|
|
47
|
+
const cat = JSON.parse(await readFile(join(PKG_ROOT, 'data', 'catalog.json'), 'utf8'))
|
|
48
|
+
const packages = cat.packages || {}
|
|
49
|
+
return {
|
|
50
|
+
asOf: cat.asOf || null,
|
|
51
|
+
anchor: cat.anchor || null,
|
|
52
|
+
total: cat.total || Object.keys(packages).length,
|
|
53
|
+
deepCount: Object.values(packages).filter(p => p && p.deep).length,
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return { asOf: null, anchor: null, total: 0, deepCount: 0 }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const name = 'dsh-pick'
|
|
61
|
+
export const inject = []
|
|
62
|
+
|
|
63
|
+
export function apply(ctx) {
|
|
64
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
65
|
+
const ws = webCtx.webServer
|
|
66
|
+
if (!ws || typeof ws.register !== 'function') return
|
|
67
|
+
|
|
68
|
+
const track = d => d
|
|
69
|
+
|
|
70
|
+
// 面板静态资源
|
|
71
|
+
for (const [route, rel] of Object.entries(FILES)) {
|
|
72
|
+
track(ws.register({
|
|
73
|
+
kind: 'exact',
|
|
74
|
+
path: '/api-dsh-pick' + route,
|
|
75
|
+
handler: async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const buf = await readFile(join(PKG_ROOT, rel))
|
|
78
|
+
const ext = rel.slice(rel.lastIndexOf('.'))
|
|
79
|
+
res.writeHead(200, {
|
|
80
|
+
'content-type': TYPES[ext] || 'text/plain; charset=utf-8',
|
|
81
|
+
'cache-control': 'no-store',
|
|
82
|
+
})
|
|
83
|
+
res.end(buf)
|
|
84
|
+
} catch (e) {
|
|
85
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' })
|
|
86
|
+
res.end(JSON.stringify({
|
|
87
|
+
error: { code: 'not_built', message: `缺少 ${rel},先跑一次 dsh-pick build` },
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 目录概要
|
|
95
|
+
track(ws.register({
|
|
96
|
+
kind: 'exact',
|
|
97
|
+
path: '/api-dsh-pick/meta',
|
|
98
|
+
handler: async (req, res) => {
|
|
99
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
100
|
+
res.end(JSON.stringify(await catalogMeta()))
|
|
101
|
+
},
|
|
102
|
+
}))
|
|
103
|
+
|
|
104
|
+
// 面板是内嵌在 dsh 设置页里的 iframe,同源即可,不需要额外令牌:
|
|
105
|
+
// 这些路由全都先过 dsh 的登录校验(挂在 dsh 自己的 webServer 上)。
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default { name, inject, apply, __meta: { pkgRoot: PKG_ROOT, version: () => readPkgJson().then(p => p.version || '') } }
|
package/web/panel.html
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%232f6bff'/%3E%3Cpath d='M8 20l5-6 4 4 7-9' stroke='white' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
|
|
7
|
+
<title>dsh-pick · DSH 插件选型助手</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root{
|
|
10
|
+
--bg:#f5f6f8; --panel:#fff; --line:#e6e8ec; --soft:#f1f3f6;
|
|
11
|
+
--text:#1a1e26; --t2:#58606e; --t3:#8b95a4;
|
|
12
|
+
--accent:#2f6bff; --accent-bg:#eaf0ff;
|
|
13
|
+
--ok:#0f9d58; --ok-bg:#e7f6ee;
|
|
14
|
+
--warn:#d97a06; --warn-bg:#fdf3e3;
|
|
15
|
+
--bad:#c94040; --bad-bg:#fdecec;
|
|
16
|
+
}
|
|
17
|
+
*{box-sizing:border-box}
|
|
18
|
+
body{
|
|
19
|
+
margin:0;background:var(--bg);color:var(--text);
|
|
20
|
+
font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
|
|
21
|
+
-webkit-font-smoothing:antialiased;font-size:14px;line-height:1.55;padding:0 20px 60px;
|
|
22
|
+
}
|
|
23
|
+
.wrap{max-width:1180px;margin:0 auto}
|
|
24
|
+
a{color:var(--accent);text-decoration:none}
|
|
25
|
+
|
|
26
|
+
/* ---------- 顶栏 ---------- */
|
|
27
|
+
.bar{position:sticky;top:0;z-index:40;background:rgba(245,246,248,.92);backdrop-filter:blur(8px);
|
|
28
|
+
padding:16px 0 12px;border-bottom:1px solid var(--line);margin-bottom:22px}
|
|
29
|
+
.bar .top{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
|
30
|
+
.logo{width:38px;height:38px;border-radius:10px;background:var(--accent);color:#fff;
|
|
31
|
+
display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px;flex:none}
|
|
32
|
+
h1{margin:0;font-size:19px;font-weight:650;letter-spacing:-.2px}
|
|
33
|
+
.tagline{font-size:12px;color:var(--t3);margin-top:2px}
|
|
34
|
+
.meta{margin-left:auto;font-size:11.5px;color:var(--t3);text-align:right;line-height:1.7}
|
|
35
|
+
.meta b{color:var(--t2);font-weight:600;font-variant-numeric:tabular-nums}
|
|
36
|
+
.tabs{display:flex;gap:2px;margin-top:14px;background:var(--soft);border-radius:9px;padding:3px;width:max-content}
|
|
37
|
+
.tabs button{appearance:none;border:none;background:transparent;font-family:inherit;font-size:12.5px;
|
|
38
|
+
font-weight:600;color:var(--t3);padding:7px 16px;border-radius:6px;cursor:pointer}
|
|
39
|
+
.tabs button.on{background:#fff;color:var(--text);box-shadow:0 1px 2px rgba(16,24,40,.09)}
|
|
40
|
+
|
|
41
|
+
/* ---------- 选插件 ---------- */
|
|
42
|
+
.ask{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px;margin-bottom:18px}
|
|
43
|
+
.ask label{display:block;font-size:12.5px;color:var(--t3);margin-bottom:10px}
|
|
44
|
+
.ask .row{display:flex;gap:10px}
|
|
45
|
+
.ask input{
|
|
46
|
+
flex:1;appearance:none;border:1px solid var(--line);border-radius:10px;padding:13px 16px;
|
|
47
|
+
font-size:14.5px;font-family:inherit;background:#fcfcfd;color:var(--text)
|
|
48
|
+
}
|
|
49
|
+
.ask input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(47,107,255,.12);background:#fff}
|
|
50
|
+
.btn{appearance:none;border:1px solid var(--accent);background:var(--accent);color:#fff;font-family:inherit;
|
|
51
|
+
font-size:13.5px;font-weight:600;padding:0 22px;border-radius:10px;cursor:pointer;white-space:nowrap}
|
|
52
|
+
.btn:hover{filter:brightness(1.07)}
|
|
53
|
+
.btn.ghost{background:#fff;color:var(--accent)}
|
|
54
|
+
.btn.ghost:hover{background:var(--accent-bg);filter:none}
|
|
55
|
+
.btn:disabled{opacity:.5;cursor:default}
|
|
56
|
+
.cand .ok{font-size:9.5px;background:var(--ok-bg);color:#0b7a45;border-radius:20px;padding:1px 6px;font-weight:700;flex:none}
|
|
57
|
+
.cand .nook{font-size:9.5px;background:#f0f2f5;color:var(--t3);border-radius:20px;padding:1px 6px;flex:none}
|
|
58
|
+
.cand .rk{font-size:10.5px;color:var(--t3);margin:-2px 0 5px;font-variant-numeric:tabular-nums}
|
|
59
|
+
.cand .rk b{color:var(--t2);font-weight:700}
|
|
60
|
+
.cand .d{font-size:11.5px;color:var(--t2);margin:5px 0 8px;line-height:1.5;
|
|
61
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
62
|
+
.links{display:flex;gap:6px;margin-top:9px;padding-top:9px;border-top:1px dashed var(--line)}
|
|
63
|
+
.lk{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;color:var(--t2);text-decoration:none;
|
|
64
|
+
border:1px solid var(--line);border-radius:6px;padding:2px 8px;background:#fff;transition:.15s}
|
|
65
|
+
.lk:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
66
|
+
.lk svg{flex:none}
|
|
67
|
+
.examples{display:flex;gap:8px;flex-wrap:wrap;margin-top:13px}
|
|
68
|
+
.examples span{font-size:11.5px;color:var(--t3);align-self:center}
|
|
69
|
+
.chip{background:var(--soft);color:var(--t2);border:1px solid transparent;border-radius:20px;
|
|
70
|
+
padding:5px 12px;font-size:12px;cursor:pointer;font-family:inherit}
|
|
71
|
+
.chip:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
72
|
+
|
|
73
|
+
.section{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:19px 20px;margin-bottom:16px}
|
|
74
|
+
.section h2{margin:0 0 3px;font-size:14.5px;font-weight:650;display:flex;align-items:center;gap:9px}
|
|
75
|
+
.section h2 .n{background:var(--accent);color:#fff;width:19px;height:19px;border-radius:6px;
|
|
76
|
+
font-size:11px;display:inline-flex;align-items:center;justify-content:center;font-weight:700;flex:none}
|
|
77
|
+
.section .cap{margin:0 0 15px;font-size:12px;color:var(--t3)}
|
|
78
|
+
|
|
79
|
+
/* ---------- 候选卡 ---------- */
|
|
80
|
+
.cands{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:11px}
|
|
81
|
+
.cand{border:1px solid var(--line);border-radius:11px;padding:13px 14px;background:#fcfcfd;
|
|
82
|
+
display:flex;gap:11px;cursor:pointer;transition:border-color .15s,background .15s;position:relative}
|
|
83
|
+
.cand:hover{border-color:#c6d4f5}
|
|
84
|
+
.cand.on{border-color:var(--accent);background:var(--accent-bg)}
|
|
85
|
+
.cand .box{width:17px;height:17px;border:1.5px solid #b9c2ce;border-radius:5px;flex:none;margin-top:2px;
|
|
86
|
+
display:flex;align-items:center;justify-content:center;font-size:11px;color:#fff;background:#fff}
|
|
87
|
+
.cand.on .box{background:var(--accent);border-color:var(--accent)}
|
|
88
|
+
.cand .body{min-width:0;flex:1}
|
|
89
|
+
.cand .nm{font-size:13.5px;font-weight:650;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
|
90
|
+
.cand .nm .v{font-size:10.5px;color:var(--t3);font-weight:500}
|
|
91
|
+
.cand .rel{font-size:10px;background:var(--accent-bg);color:var(--accent);border-radius:20px;
|
|
92
|
+
padding:1px 7px;font-weight:700;margin-left:auto;flex:none}
|
|
93
|
+
.cand .d{font-size:11.5px;color:var(--t2);margin:5px 0 8px;line-height:1.5;
|
|
94
|
+
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
95
|
+
.cand .kv{display:flex;gap:13px;font-size:11px;color:var(--t3);flex-wrap:wrap}
|
|
96
|
+
.cand .kv b{color:var(--t2);font-weight:650;font-variant-numeric:tabular-nums}
|
|
97
|
+
.empty{color:var(--t3);font-size:13px;text-align:center;padding:26px 0}
|
|
98
|
+
|
|
99
|
+
/* ---------- 对比表 ---------- */
|
|
100
|
+
.tblwrap{overflow-x:auto;margin:0 -4px}
|
|
101
|
+
table.cmp{width:100%;border-collapse:collapse;font-size:12.5px;min-width:640px}
|
|
102
|
+
table.cmp th,table.cmp td{padding:10px 11px;text-align:right;border-bottom:1px solid var(--soft);white-space:nowrap}
|
|
103
|
+
table.cmp th:first-child,table.cmp td:first-child{text-align:left;color:var(--t3);font-size:11.5px;font-weight:600}
|
|
104
|
+
table.cmp thead th{color:var(--text);font-weight:650;font-size:12.5px}
|
|
105
|
+
table.cmp thead th i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px}
|
|
106
|
+
table.cmp tbody tr:last-child td{border-bottom:none}
|
|
107
|
+
td.best{color:var(--ok);font-weight:700}
|
|
108
|
+
td.best::after{content:' ★';font-size:9px}
|
|
109
|
+
.miss{color:#b9c2ce}
|
|
110
|
+
.num{font-variant-numeric:tabular-nums}
|
|
111
|
+
|
|
112
|
+
/* ---------- 结论 ---------- */
|
|
113
|
+
.verdict{border-radius:11px;padding:14px 16px;margin-bottom:11px;border:1px solid}
|
|
114
|
+
.verdict.win{background:var(--ok-bg);border-color:#bfe6d1}
|
|
115
|
+
.verdict.alt{background:var(--warn-bg);border-color:#f2ddb6}
|
|
116
|
+
.verdict.no{background:var(--bad-bg);border-color:#f2cccc}
|
|
117
|
+
.verdict h4{margin:0 0 6px;font-size:13px;display:flex;align-items:center;gap:7px}
|
|
118
|
+
.verdict.win h4{color:#0b7a45}.verdict.alt h4{color:#a8620a}.verdict.no h4{color:#a52f2f}
|
|
119
|
+
.verdict p{margin:0;font-size:12.5px;color:var(--t2);line-height:1.7}
|
|
120
|
+
.verdict p b{color:var(--text);font-weight:650}
|
|
121
|
+
.rule{font-size:11.5px;color:var(--t3);margin-top:12px;padding-top:11px;border-top:1px dashed var(--line);line-height:1.8}
|
|
122
|
+
|
|
123
|
+
/* ---------- 排行榜 ---------- */
|
|
124
|
+
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
|
|
125
|
+
.filters select{appearance:none;border:1px solid var(--line);border-radius:8px;padding:7px 11px;
|
|
126
|
+
font-family:inherit;font-size:12.5px;background:#fff;color:var(--t2);cursor:pointer}
|
|
127
|
+
table.rank{width:100%;border-collapse:collapse;font-size:12.5px}
|
|
128
|
+
table.rank th{text-align:left;font-size:11px;color:var(--t3);font-weight:600;padding:0 10px 9px;border-bottom:1px solid var(--line)}
|
|
129
|
+
table.rank td{padding:9px 10px;border-bottom:1px solid var(--soft);color:var(--t2);font-variant-numeric:tabular-nums}
|
|
130
|
+
table.rank tbody tr:hover td{background:#fafbfc}
|
|
131
|
+
table.rank td.nm{color:var(--text);font-weight:650;font-variant-numeric:normal}
|
|
132
|
+
td.r,th.r{text-align:right}
|
|
133
|
+
td.up{color:var(--ok);font-weight:600}
|
|
134
|
+
td.down{color:var(--bad);font-weight:600}
|
|
135
|
+
.catpill{font-size:10.5px;background:var(--soft);color:var(--t2);border-radius:20px;padding:2px 8px;white-space:nowrap}
|
|
136
|
+
|
|
137
|
+
/* ---------- 监控 ---------- */
|
|
138
|
+
.mon{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--soft);flex-wrap:wrap}
|
|
139
|
+
.mon:last-child{border-bottom:none}
|
|
140
|
+
.mon .nm{font-size:13px;font-weight:650;flex:1;min-width:150px}
|
|
141
|
+
.mon .mrank{font-size:10.5px;color:var(--t3);font-variant-numeric:tabular-nums}
|
|
142
|
+
.mon .mrank b{color:var(--t2);font-weight:700}
|
|
143
|
+
.mon .mlinks{display:flex;gap:5px}
|
|
144
|
+
.mon .x{appearance:none;border:none;background:none;color:var(--t3);cursor:pointer;font-size:16px;padding:0 4px}
|
|
145
|
+
.mon .x:hover{color:var(--bad)}
|
|
146
|
+
|
|
147
|
+
.foot{font-size:11.5px;color:var(--t3);line-height:1.9;margin-top:6px}
|
|
148
|
+
.foot b{color:var(--t2);font-weight:650}
|
|
149
|
+
.spark{display:inline-flex;align-items:flex-end;gap:1px;height:16px;vertical-align:middle}
|
|
150
|
+
.spark i{width:3px;background:#b9c2ce;border-radius:1px;display:block}
|
|
151
|
+
.hidden{display:none!important}
|
|
152
|
+
@media(max-width:720px){
|
|
153
|
+
body{padding:0 12px 48px}
|
|
154
|
+
.meta{margin-left:0;text-align:left;width:100%}
|
|
155
|
+
.ask{padding:17px}
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
<body>
|
|
160
|
+
<div class="wrap">
|
|
161
|
+
|
|
162
|
+
<div class="bar">
|
|
163
|
+
<div class="top">
|
|
164
|
+
<div class="logo">Pick</div>
|
|
165
|
+
<div>
|
|
166
|
+
<h1>DSH 插件选型助手</h1>
|
|
167
|
+
<div class="tagline">说需求 → 找候选 → 多维比较 → 出结论。下载量是证据,判断归你。</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="meta" id="meta">正在加载目录…</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="tabs" id="tabs">
|
|
172
|
+
<button data-tab="pick" class="on">选插件</button>
|
|
173
|
+
<button data-tab="rank">排行榜</button>
|
|
174
|
+
<button data-tab="watch">我的监控 <span id="watchCount"></span></button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- ============ Tab 1:选插件 ============ -->
|
|
179
|
+
<div id="tab-pick">
|
|
180
|
+
<div class="ask">
|
|
181
|
+
<label>你想解决什么问题?</label>
|
|
182
|
+
<div class="row">
|
|
183
|
+
<input id="need" placeholder="例如:我想装个数据库 / 我想让 DSH 能连微信 / 记忆能力" autocomplete="off">
|
|
184
|
+
<button class="btn" id="btnFind">找候选</button>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="examples">
|
|
187
|
+
<span>试试:</span>
|
|
188
|
+
<button class="chip">数据库</button>
|
|
189
|
+
<button class="chip">微信通知</button>
|
|
190
|
+
<button class="chip">记忆</button>
|
|
191
|
+
<button class="chip">终端美化</button>
|
|
192
|
+
<button class="chip">代码审查</button>
|
|
193
|
+
<button class="chip">费用统计</button>
|
|
194
|
+
<button class="chip">远程服务器</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="section hidden" id="secCand">
|
|
199
|
+
<h2><span class="n">2</span>候选 <span style="font-size:11.5px;color:var(--t3);font-weight:500" id="candCount"></span></h2>
|
|
200
|
+
<p class="cap">点选 2~6 个进行比较。排序按「相关性 × 下载量」,下载量只做微弱加权,避免发版本最勤的挤掉真正相关的。</p>
|
|
201
|
+
<div class="cands" id="cands"></div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="section hidden" id="secCmp">
|
|
205
|
+
<h2><span class="n">3</span>比较</h2>
|
|
206
|
+
<p class="cap">★ 为该指标当前最优。<b>日常基线</b>已扣掉版本发布日与次日的拉取虚高,比原始下载量更接近真实使用量。</p>
|
|
207
|
+
<div class="tblwrap">
|
|
208
|
+
<table class="cmp"><thead id="cmpHead"></thead><tbody id="cmpBody"></tbody></table>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="rule" id="cmpNote"></div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="section hidden" id="secVerdict">
|
|
214
|
+
<h2><span class="n">4</span>结论</h2>
|
|
215
|
+
<p class="cap">以下判断由本地规则给出,依据就是上面这张表里的数字。装上 DSH 插件版后,这一步由 AI 生成,能结合你的具体场景解释取舍。</p>
|
|
216
|
+
<div id="verdicts"></div>
|
|
217
|
+
<div style="display:flex;gap:9px;margin-top:14px;flex-wrap:wrap">
|
|
218
|
+
<button class="btn ghost" id="btnExport">导出结论(网页)</button>
|
|
219
|
+
<button class="btn ghost" id="btnWatch">加入我的监控</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div class="section">
|
|
224
|
+
<h2>排行榜</h2>
|
|
225
|
+
<p class="cap">换一种方式发现:按去噪后的指标看整个生态。</p>
|
|
226
|
+
<div class="filters" id="rankFilters">
|
|
227
|
+
<button class="chip on" data-m="base">日常基线</button>
|
|
228
|
+
<button class="chip" data-m="w">周下载</button>
|
|
229
|
+
<button class="chip" data-m="growth">增速最快</button>
|
|
230
|
+
<button class="chip" data-m="s">健康度</button>
|
|
231
|
+
<select id="rankCat"></select>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="tblwrap">
|
|
234
|
+
<table class="rank">
|
|
235
|
+
<thead><tr>
|
|
236
|
+
<th style="width:44px">#</th><th>插件</th><th>分类</th>
|
|
237
|
+
<th class="r">基线</th><th class="r">周下载</th><th class="r">环比</th>
|
|
238
|
+
<th class="r">版本节奏</th><th class="r">最近更新</th><th>链接</th>
|
|
239
|
+
</tr></thead>
|
|
240
|
+
<tbody id="rankBody"></tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- ============ Tab 2:监控 ============ -->
|
|
247
|
+
<div id="tab-watch" class="hidden">
|
|
248
|
+
<div class="section">
|
|
249
|
+
<h2>我的监控</h2>
|
|
250
|
+
<p class="cap">存在浏览器本地(localStorage),不上传。数据取自目录,目录每天重建一次。</p>
|
|
251
|
+
<div id="watchList"></div>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="section">
|
|
254
|
+
<h2>对比图</h2>
|
|
255
|
+
<p class="cap">对数刻度,否则头部插件会把其余压成一条线。</p>
|
|
256
|
+
<div id="watchChart"></div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="foot" id="foot"></div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<script type="module">
|
|
264
|
+
// 同一个面板两种宿主:独立预览加载 /web/panel.js,插件内嵌加载 /api-dsh-pick/panel.js
|
|
265
|
+
const inPlugin = location.pathname.indexOf('/api-dsh-pick') === 0
|
|
266
|
+
import(inPlugin ? '/api-dsh-pick/panel.js' : '/web/panel.js').catch(e => {
|
|
267
|
+
document.body.insertAdjacentHTML('beforeend',
|
|
268
|
+
'<pre style="color:#c00;padding:20px">面板加载失败:' + e.message + '</pre>')
|
|
269
|
+
})
|
|
270
|
+
</script>
|
|
271
|
+
</body>
|
|
272
|
+
</html>
|