@mrrisega/dsh-remote 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/LICENSE +81 -0
- package/README.md +103 -0
- package/clients/dsh-remote/dsh-bridge.mjs +658 -0
- package/clients/dsh-remote/package.json +11 -0
- package/clients/dsh-remote/src/lifecycle.mjs +3 -0
- package/clients/dsh-remote/test/bridge-gzip.test.mjs +215 -0
- package/clients/dsh-remote/test/lifecycle.test.mjs +7 -0
- package/clients/dsh-remote/test/tunnel-auth.test.mjs +111 -0
- package/docs/self-hosting.md +109 -0
- package/dsh-setup.mjs +616 -0
- package/package.json +37 -0
- package/packages/dsh-remote-ui/README.md +81 -0
- package/packages/dsh-remote-ui/lib/client.js +1173 -0
- package/packages/dsh-remote-ui/lib/index.js +733 -0
- package/packages/dsh-remote-ui/package.json +26 -0
- package/packages/dsh-remote-ui/test/account-info.test.mjs +92 -0
- package/packages/dsh-remote-ui/test/account-switch.test.mjs +80 -0
- package/packages/dsh-remote-ui/test/feedback-proxy.test.mjs +154 -0
- package/packages/dsh-remote-ui/test/register-proxy.test.mjs +52 -0
- package/packages/dsh-remote-ui/test/settings-entry.test.mjs +282 -0
- package/packages/dsh-remote-ui/test/sms-captcha-ui.test.mjs +123 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
// dsh-remote-ui — browser half(手写 bundle,无需构建)
|
|
2
|
+
//
|
|
3
|
+
// 格式遵循 dsh 浏览器插件约定(双半插件,bundle 手写无构建):
|
|
4
|
+
// window.__ModuleLoader__.load({ id: <包名>, factory: (require) => {...} })
|
|
5
|
+
// factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
|
|
6
|
+
//
|
|
7
|
+
// 功能:
|
|
8
|
+
// - settings.section:设置页「远程控制」栏目(位于「Agent 预设」下方,官方扩展点)
|
|
9
|
+
// - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
|
|
10
|
+
// · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
|
|
11
|
+
// · 登录要求图形验证码;注册要求两次密码 + 图形验证码
|
|
12
|
+
// · 远程地址展示 + bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
|
|
13
|
+
// - 首次安装引导:设置页栏目旁小红点(localStorage dsh-remote-seen-dot 控制)
|
|
14
|
+
// - shell.overlay:满意度弹窗(安装体验约 10 分钟后弹出)
|
|
15
|
+
// 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: "dsh-remote-ui",
|
|
18
|
+
factory: (require) => {
|
|
19
|
+
var module = { exports: {} };
|
|
20
|
+
var exports = module.exports;
|
|
21
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
22
|
+
|
|
23
|
+
var react = require("react");
|
|
24
|
+
var h = react.createElement;
|
|
25
|
+
var useState = react.useState;
|
|
26
|
+
var useEffect = react.useEffect;
|
|
27
|
+
var useCallback = react.useCallback;
|
|
28
|
+
var useSyncExternalStore = react.useSyncExternalStore;
|
|
29
|
+
|
|
30
|
+
// ── 样式(浅色高对比,data-plugin 便于 HMR 清理) ───────────────────────
|
|
31
|
+
// 面板主体内嵌于 DSH Web「设置」页的 settings.section 栏目(官方扩展点),
|
|
32
|
+
// 不再使用侧边栏入口与全窗浮动层,故不再需要 .dru-entry/.dru-backdrop/.dru-panel。
|
|
33
|
+
var styleEl = document.createElement("style");
|
|
34
|
+
styleEl.setAttribute("data-plugin", "dsh-remote-ui");
|
|
35
|
+
styleEl.textContent = [
|
|
36
|
+
// 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
|
|
37
|
+
".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px}",
|
|
38
|
+
".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
|
|
39
|
+
".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
|
|
40
|
+
".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
|
|
41
|
+
".dru-settings-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,#8c959f);margin-top:2px}",
|
|
42
|
+
".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
|
|
43
|
+
// 首次安装引导小红点(挂在设置页「远程控制」导航栏目右上角)
|
|
44
|
+
".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,#fff);pointer-events:none;z-index:1}",
|
|
45
|
+
".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
|
|
46
|
+
".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
|
|
47
|
+
".dru-url{background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;word-break:break-all;display:flex;align-items:center;justify-content:space-between;gap:8px;color:#1f2328}",
|
|
48
|
+
".dru-url button{flex:none;border:1px solid #d0d7de;background:#ffffff;color:#0969da;border-radius:6px;padding:3px 8px;font-size:12px;cursor:pointer}",
|
|
49
|
+
".dru-url button:hover{background:#f6f8fa}",
|
|
50
|
+
".dru-field{margin-bottom:11px}",
|
|
51
|
+
".dru-field > label{display:block;font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
|
|
52
|
+
".dru-input{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
|
|
53
|
+
".dru-input:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
|
|
54
|
+
".dru-actions{display:flex;gap:8px;flex-wrap:wrap}",
|
|
55
|
+
".dru-btn{padding:8px 14px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit}",
|
|
56
|
+
".dru-btn:disabled{opacity:.55;cursor:default}",
|
|
57
|
+
".dru-btn-primary{background:#0969da;color:#ffffff;border-color:#0969da}",
|
|
58
|
+
".dru-btn-primary:hover:not(:disabled){background:#0860bd}",
|
|
59
|
+
".dru-btn-ghost{background:#ffffff;color:#1f2328;border-color:#d0d7de}",
|
|
60
|
+
".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
|
|
61
|
+
".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
|
|
62
|
+
".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
|
|
63
|
+
".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
|
|
64
|
+
".dru-tab{flex:1;padding:7px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none}",
|
|
65
|
+
".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
|
|
66
|
+
".dru-captcha{display:flex;align-items:stretch;gap:8px}",
|
|
67
|
+
".dru-captcha .dru-input{flex:1;min-width:0}",
|
|
68
|
+
".dru-captcha-box{width:118px;height:42px;flex:none;border-radius:8px;border:1px solid #d0d7de;cursor:pointer;background:#f6f8fa;display:flex;align-items:center;justify-content:center;font-size:12px;color:#57606a;overflow:hidden}",
|
|
69
|
+
".dru-captcha-box svg{display:block;width:100%;height:100%}",
|
|
70
|
+
".dru-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}",
|
|
71
|
+
".dru-avatar{width:40px;height:40px;border-radius:50%;background:#0969da;color:#ffffff;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:700;flex-shrink:0}",
|
|
72
|
+
".dru-user .mail{font-size:14px;font-weight:600;color:#1f2328;word-break:break-all}",
|
|
73
|
+
".dru-user .plan{font-size:12px;color:#57606a;margin-top:2px}",
|
|
74
|
+
".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color:#1f2328}",
|
|
75
|
+
".dru-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}",
|
|
76
|
+
".dru-dot-on{background:#1a7f37;box-shadow:0 0 6px rgba(26,127,55,.6)}",
|
|
77
|
+
".dru-dot-off{background:#8c959f}",
|
|
78
|
+
".dru-meta{font-size:12px;color:#57606a;margin-top:6px;word-break:break-all}",
|
|
79
|
+
".dru-msg{font-size:12.5px;min-height:18px;margin-top:8px}",
|
|
80
|
+
".dru-msg-ok{color:#1a7f37}",
|
|
81
|
+
".dru-msg-err{color:#cf222e}",
|
|
82
|
+
".dru-msg-warn{color:#9a6700}",
|
|
83
|
+
".dru-hint{font-size:12px;color:#57606a;line-height:1.6}",
|
|
84
|
+
// ── 用户反馈模块 ──
|
|
85
|
+
".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
|
|
86
|
+
".dru-fb-tab{flex:1;padding:6px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
|
|
87
|
+
".dru-fb-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
|
|
88
|
+
".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
|
|
89
|
+
".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit;resize:vertical;min-height:64px}",
|
|
90
|
+
".dru-fb-textarea:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
|
|
91
|
+
".dru-fb-item{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:10px 12px;margin-bottom:8px}",
|
|
92
|
+
".dru-fb-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap}",
|
|
93
|
+
".dru-fb-badge{font-size:11px;border-radius:999px;padding:1px 8px;flex:none}",
|
|
94
|
+
".dru-fb-badge-open{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
|
|
95
|
+
".dru-fb-badge-processing{color:#0969da;background:#ddf4ff;border:1px solid #b6e3ff}",
|
|
96
|
+
".dru-fb-badge-done{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
|
|
97
|
+
".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de}",
|
|
98
|
+
".dru-fb-item-title{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px}",
|
|
99
|
+
".dru-fb-item-content{font-size:12.5px;color:#57606a;white-space:pre-wrap;word-break:break-word;margin:4px 0}",
|
|
100
|
+
".dru-fb-item-time{font-size:11px;color:#8c959f}",
|
|
101
|
+
".dru-fb-reply{border-top:1px dashed #eaeef2;margin-top:8px;padding-top:8px}",
|
|
102
|
+
".dru-fb-reply-row{display:flex;gap:6px;align-items:flex-start;margin-bottom:6px}",
|
|
103
|
+
".dru-fb-reply-who{font-size:12px;font-weight:600;color:#0969da;flex:none;width:76px}",
|
|
104
|
+
".dru-fb-reply-who.user{color:#57606a}",
|
|
105
|
+
".dru-fb-reply-text{font-size:12.5px;color:#1f2328;white-space:pre-wrap;word-break:break-word;flex:1}",
|
|
106
|
+
".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;font-size:12.5px;font-family:inherit;resize:vertical;min-height:44px}",
|
|
107
|
+
".dru-fb-empty{font-size:12.5px;color:#8c959f;text-align:center;padding:14px 0}",
|
|
108
|
+
// ── 满意度弹窗(10 分钟体验后) ──
|
|
109
|
+
".dru-popup{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}",
|
|
110
|
+
".dru-popup-card{width:min(400px,calc(100vw - 48px));background:#ffffff;color:#1f2328;border:1px solid #d0d7de;border-radius:12px;box-shadow:0 24px 64px rgba(0,0,0,.45);font-size:14px;line-height:1.5;font-family:var(--dsw-font-family,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif);overflow:hidden}",
|
|
111
|
+
".dru-popup-body{padding:22px 22px 16px;text-align:center}",
|
|
112
|
+
".dru-popup-icon{font-size:34px;margin-bottom:8px}",
|
|
113
|
+
".dru-popup-title{font-size:16px;font-weight:700;color:#1f2328;margin-bottom:6px}",
|
|
114
|
+
".dru-popup-sub{font-size:12.5px;color:#57606a;margin-bottom:16px}",
|
|
115
|
+
".dru-popup-rate{display:flex;gap:10px;justify-content:center;margin-bottom:14px}",
|
|
116
|
+
".dru-popup-rate button{flex:1;max-width:96px;border:1px solid #d0d7de;background:#f6f8fa;border-radius:10px;padding:12px 6px;font-size:20px;cursor:pointer;font-family:inherit}",
|
|
117
|
+
".dru-popup-rate button:hover{border-color:#0969da;background:#ddf4ff}",
|
|
118
|
+
".dru-popup-rate button.sel{border-color:#0969da;background:#ddf4ff;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
|
|
119
|
+
".dru-popup-rate button .lbl{display:block;font-size:11px;color:#57606a;margin-top:4px;font-weight:600}",
|
|
120
|
+
".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13px;font-family:inherit;resize:vertical;min-height:56px;text-align:left}",
|
|
121
|
+
".dru-popup-invite{background:#f6f8fa;border:1px dashed #d0d7de;border-radius:8px;padding:12px;font-size:13px;color:#57606a;text-align:center;margin-bottom:12px}",
|
|
122
|
+
".dru-popup-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
|
|
123
|
+
".dru-popup-foot{display:flex;justify-content:space-between;padding:10px 22px;border-top:1px solid #eaeef2;font-size:12px;color:#8c959f}",
|
|
124
|
+
".dru-popup-foot button{border:none;background:none;color:#57606a;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 6px}",
|
|
125
|
+
".dru-popup-foot button:hover{color:#0969da}",
|
|
126
|
+
".dru-popup .dru-msg{text-align:left}",
|
|
127
|
+
].join("\n");
|
|
128
|
+
document.head.appendChild(styleEl);
|
|
129
|
+
|
|
130
|
+
// ── 首次安装引导小红点(设置页「远程控制」栏目,localStorage 控制) ────
|
|
131
|
+
// 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
|
|
132
|
+
// 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
|
|
133
|
+
// 设置页由官方 shell 渲染(class 含 navCell 的导航按钮 + 栏目 label),
|
|
134
|
+
// 注入采用 DOM 兜底:MutationObserver 监听设置页打开,把红点挂到栏目按钮右上角。
|
|
135
|
+
var DOT_SEEN_KEY = "dsh-remote-seen-dot";
|
|
136
|
+
function dotSeen() {
|
|
137
|
+
try { return localStorage.getItem(DOT_SEEN_KEY) === "1"; } catch (e) { return true; }
|
|
138
|
+
}
|
|
139
|
+
function dotMarkSeen() {
|
|
140
|
+
try { localStorage.setItem(DOT_SEEN_KEY, "1"); } catch (e) {}
|
|
141
|
+
try {
|
|
142
|
+
var dot = document.querySelector(".dru-reddot");
|
|
143
|
+
if (dot && dot.parentNode) dot.parentNode.removeChild(dot);
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
if (window.__dshRemoteDotObs) {
|
|
146
|
+
try { window.__dshRemoteDotObs.disconnect(); } catch (e) {}
|
|
147
|
+
window.__dshRemoteDotObs = null;
|
|
148
|
+
}
|
|
149
|
+
window.__dshRemoteDotWatch = false;
|
|
150
|
+
}
|
|
151
|
+
/** 给单个设置页导航栏目按钮挂红点(幂等)。 */
|
|
152
|
+
function dotInject(cell) {
|
|
153
|
+
try {
|
|
154
|
+
if (cell.querySelector(".dru-reddot")) return;
|
|
155
|
+
cell.style.position = "relative";
|
|
156
|
+
var dot = document.createElement("span");
|
|
157
|
+
dot.className = "dru-reddot";
|
|
158
|
+
dot.setAttribute("aria-hidden", "true");
|
|
159
|
+
cell.appendChild(dot);
|
|
160
|
+
cell.addEventListener("click", function onDotCellClick() {
|
|
161
|
+
dotMarkSeen();
|
|
162
|
+
cell.removeEventListener("click", onDotCellClick);
|
|
163
|
+
});
|
|
164
|
+
} catch (e) {}
|
|
165
|
+
}
|
|
166
|
+
/** 扫描设置页导航,找到「远程控制」栏目按钮后注入红点。 */
|
|
167
|
+
function dotScan() {
|
|
168
|
+
if (dotSeen()) return;
|
|
169
|
+
var cells = document.querySelectorAll("button");
|
|
170
|
+
for (var i = 0; i < cells.length; i++) {
|
|
171
|
+
var c = cells[i];
|
|
172
|
+
// 语义化匹配:设置页导航按钮(navCell)+ 栏目名「远程控制」。
|
|
173
|
+
// class 为 shell 的 hashed 类名,取子串匹配避免依赖具体 hash;
|
|
174
|
+
// 若未来 hash 变化导致匹配失败,仅红点不显示,栏目本身不受影响。
|
|
175
|
+
if (String(c.className || "").indexOf("navCell") === -1) continue;
|
|
176
|
+
if ((c.textContent || "").indexOf("远程控制") === -1) continue;
|
|
177
|
+
dotInject(c);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** 开始监听设置页打开(首次安装期间有效;点击后自动停止)。 */
|
|
181
|
+
function dotWatch() {
|
|
182
|
+
if (dotSeen()) return;
|
|
183
|
+
if (window.__dshRemoteDotWatch) return;
|
|
184
|
+
window.__dshRemoteDotWatch = true;
|
|
185
|
+
try {
|
|
186
|
+
var obs = new MutationObserver(function () { dotScan(); });
|
|
187
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
188
|
+
window.__dshRemoteDotObs = obs;
|
|
189
|
+
} catch (e) {}
|
|
190
|
+
dotScan();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── 宿主 API ────────────────────────────────────────────────────────────
|
|
194
|
+
function api(path, options) {
|
|
195
|
+
return fetch(path, options).then(function (res) {
|
|
196
|
+
return res.text().then(function (text) {
|
|
197
|
+
var body = null;
|
|
198
|
+
try { body = JSON.parse(text); } catch (e) { body = null; }
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
var err = (body && (body.error || (body.body && body.body.error && (body.body.error.message || body.body.error)))) || ("HTTP " + res.status);
|
|
201
|
+
var e = new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
202
|
+
e.status = res.status; e.body = body;
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
205
|
+
return body;
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
var post = function (path, data) {
|
|
210
|
+
return api(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(data || {}) });
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ── 用户反馈模块:本地状态(thread 令牌 / 弹窗节流) ───────────────────
|
|
214
|
+
var FB_THREADS_KEY = "dsh-feedback-threads";
|
|
215
|
+
var FB_POPUP_KEY = "dsh-feedback-popup";
|
|
216
|
+
var FB_POPUP_DELAY_MS = 10 * 60 * 1000; // 安装/体验后约 10 分钟弹窗
|
|
217
|
+
var FB_POPUP_LATER_MS = 24 * 3600 * 1000; // “稍后再说”延迟 1 天
|
|
218
|
+
var FB_CATEGORIES = [
|
|
219
|
+
{ value: "feature", label: "功能类" },
|
|
220
|
+
{ value: "bug", label: "Bug 类" },
|
|
221
|
+
{ value: "requirement", label: "需求类" },
|
|
222
|
+
{ value: "other", label: "其他" },
|
|
223
|
+
];
|
|
224
|
+
var FB_STATUS_LABEL = { open: "待处理", processing: "处理中", done: "已解决" };
|
|
225
|
+
|
|
226
|
+
function fbLoadThreads() {
|
|
227
|
+
try { return JSON.parse(localStorage.getItem(FB_THREADS_KEY) || "[]"); } catch (e) { return []; }
|
|
228
|
+
}
|
|
229
|
+
function fbSaveThreads(list) {
|
|
230
|
+
try { localStorage.setItem(FB_THREADS_KEY, JSON.stringify(list.slice(-20))); } catch (e) {}
|
|
231
|
+
}
|
|
232
|
+
function fbRememberThread(id, token) {
|
|
233
|
+
var list = fbLoadThreads().filter(function (t) { return t.id !== id; });
|
|
234
|
+
list.push({ id: id, token: token, at: Date.now() });
|
|
235
|
+
fbSaveThreads(list);
|
|
236
|
+
}
|
|
237
|
+
/** 清除本地保存的用户反馈线程凭据(thread_token 只存于此,见 FB_THREADS_KEY)。
|
|
238
|
+
* 退出登录 / 切换连接账号时调用:反馈历史保留在服务端(按账号校验),
|
|
239
|
+
* 账号身份变化后本机不再持有旧线程的访问凭据。 */
|
|
240
|
+
function fbClearThreads() {
|
|
241
|
+
try { localStorage.removeItem(FB_THREADS_KEY); } catch (e) {}
|
|
242
|
+
}
|
|
243
|
+
function fbPopState() {
|
|
244
|
+
try { return JSON.parse(localStorage.getItem(FB_POPUP_KEY) || "null"); } catch (e) { return null; }
|
|
245
|
+
}
|
|
246
|
+
function fbSavePopState(s) {
|
|
247
|
+
try { localStorage.setItem(FB_POPUP_KEY, JSON.stringify(s)); } catch (e) {}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 同源反馈代理调用(浏览器 → /dsh-remote/feedback/* → 独立反馈服务)。 */
|
|
251
|
+
function fbApi(path, options) {
|
|
252
|
+
var opts = Object.assign({}, options || {});
|
|
253
|
+
var headers = Object.assign({}, opts.headers || {});
|
|
254
|
+
if (opts.token) headers.authorization = "Bearer " + opts.token;
|
|
255
|
+
if (opts.body !== undefined) headers["content-type"] = "application/json";
|
|
256
|
+
return api("/dsh-remote/feedback/api" + path, Object.assign({}, opts, { headers: headers }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function fbStatusBadge(status) {
|
|
260
|
+
return h("span", { className: "dru-fb-badge dru-fb-badge-" + (status || "open") },
|
|
261
|
+
FB_STATUS_LABEL[status] || status || "待处理");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 提交表单用验证码(服务端要求匿名首条必填)。
|
|
265
|
+
* 状态机:idle → loading → ready | error;区域永远可见,失败可点击重试。 */
|
|
266
|
+
function FbCaptcha(props) {
|
|
267
|
+
var st = props.state || "idle";
|
|
268
|
+
var ready = st === "ready" && props.cap && props.cap.id;
|
|
269
|
+
var box;
|
|
270
|
+
if (ready) {
|
|
271
|
+
box = h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: props.onRefresh,
|
|
272
|
+
dangerouslySetInnerHTML: props.cap.svg && props.cap.svg.indexOf("<svg") === 0 ? { __html: props.cap.svg } : void 0 },
|
|
273
|
+
props.cap.svg && props.cap.svg.indexOf("<svg") !== 0 ? props.cap.svg : null);
|
|
274
|
+
} else if (st === "loading") {
|
|
275
|
+
box = h("div", { className: "dru-captcha-box" }, h("span", { className: "dru-hint" }, "验证码加载中…"));
|
|
276
|
+
} else if (st === "error") {
|
|
277
|
+
box = h("div", { className: "dru-captcha-box", title: "点击重试", onClick: props.onRefresh },
|
|
278
|
+
h("span", { className: "dru-hint", style: { color: "#cf222e" } }, "加载失败,点击重试"));
|
|
279
|
+
} else {
|
|
280
|
+
box = h("div", { className: "dru-captcha-box", title: "点击加载验证码", onClick: props.onRefresh },
|
|
281
|
+
h("span", { className: "dru-hint" }, "点击加载验证码"));
|
|
282
|
+
}
|
|
283
|
+
return h("div", { className: "dru-captcha", style: { marginBottom: 10 } },
|
|
284
|
+
h("input", { className: "dru-input", value: props.answer, placeholder: "图中 4 位数字", autoComplete: "off", inputMode: "numeric", maxLength: 4, disabled: !ready, onChange: function (e) { props.setAnswer(e.target.value); } }),
|
|
285
|
+
box
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 面板内的“用户反馈”卡片(提交 + 我的反馈 双 tab)。 */
|
|
290
|
+
function FeedbackCard(props) {
|
|
291
|
+
var cfgArr = useState(null); var cfg = cfgArr[0]; var setCfg = cfgArr[1];
|
|
292
|
+
var fbAuthArr = useState("anonymous"); var fbAuth = fbAuthArr[0]; var setFbAuth = fbAuthArr[1];
|
|
293
|
+
var tabArr = useState("submit"); var tab = tabArr[0]; var setTab = tabArr[1];
|
|
294
|
+
var catArr = useState("bug"); var cat = catArr[0]; var setCat = catArr[1];
|
|
295
|
+
var titleArr = useState(""); var fbTitle = titleArr[0]; var setFbTitle = titleArr[1];
|
|
296
|
+
var contentArr = useState(""); var fbContent = contentArr[0]; var setFbContent = contentArr[1];
|
|
297
|
+
var contactArr = useState(""); var contact = contactArr[0]; var setContact = contactArr[1];
|
|
298
|
+
var capArr = useState(null); var cap = capArr[0]; var setCap = capArr[1];
|
|
299
|
+
var capStateArr = useState("idle"); var capState = capStateArr[0]; var setCapState = capStateArr[1];
|
|
300
|
+
var capTxtArr = useState(""); var capTxt = capTxtArr[0]; var setCapTxt = capTxtArr[1];
|
|
301
|
+
var threadsArr = useState(fbLoadThreads()); var threads = threadsArr[0]; var setThreads = threadsArr[1];
|
|
302
|
+
var openIdArr = useState(""); var openId = openIdArr[0]; var setOpenId = openIdArr[1];
|
|
303
|
+
var threadDataArr = useState(null); var threadData = threadDataArr[0]; var setThreadData = threadDataArr[1];
|
|
304
|
+
var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
|
|
305
|
+
var msgArr = useState(null); var fbMsg = msgArr[0]; var setFbMsg = msgArr[1];
|
|
306
|
+
|
|
307
|
+
function setMsg(kind, text) { setFbMsg({ kind: kind, text: text }); }
|
|
308
|
+
|
|
309
|
+
var loadCfg = useCallback(function () {
|
|
310
|
+
api("/dsh-remote/feedback-config").then(function (b) {
|
|
311
|
+
setCfg(b);
|
|
312
|
+
// 登录态(节点半自动附加 JWT)免验证码;匿名才需要图形验证码
|
|
313
|
+
setFbAuth(b.auth === "account" ? "account" : "anonymous");
|
|
314
|
+
if (b.auth !== "account") loadCaptcha();
|
|
315
|
+
if (!b.reachable) setMsg("warn", "反馈服务未连接,请检查网络或 feedback_url 配置");
|
|
316
|
+
}).catch(function (e) { setCfg({ reachable: false }); setMsg("warn", "反馈服务未连接:" + e.message); });
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
function loadCaptcha() {
|
|
320
|
+
setCapState("loading");
|
|
321
|
+
setCapTxt("");
|
|
322
|
+
fbApi("/feedback/captcha").then(function (b) {
|
|
323
|
+
var cid = b.captcha_id || b.id; if (cid) { setCap({ id: cid, svg: b.svg || "" }); setCapState("ready"); }
|
|
324
|
+
else { setCap(null); setCapState("error"); }
|
|
325
|
+
}).catch(function (e) {
|
|
326
|
+
setCap(null); setCapState("error");
|
|
327
|
+
setMsg("err", "验证码加载失败:" + e.message);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function loadThread(id) {
|
|
332
|
+
var t = threads.filter(function (x) { return x.id === id; })[0];
|
|
333
|
+
if (!t) return;
|
|
334
|
+
setBusy("thread:" + id);
|
|
335
|
+
fbApi("/feedback/" + id, { token: t.token }).then(function (b) {
|
|
336
|
+
setThreadData(b.feedback);
|
|
337
|
+
}).catch(function (e) {
|
|
338
|
+
setMsg("err", "加载反馈详情失败:" + e.message);
|
|
339
|
+
}).finally(function () { setBusy(""); });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
useEffect(function () { loadCfg(); }, [loadCfg]);
|
|
343
|
+
|
|
344
|
+
var doSubmit = function () {
|
|
345
|
+
if (!fbContent.trim()) { setMsg("err", "请填写反馈内容"); return; }
|
|
346
|
+
if (fbContent.length > 2000) { setMsg("err", "内容不能超过 2000 字"); return; }
|
|
347
|
+
var accountAuth = fbAuth === "account";
|
|
348
|
+
if (!accountAuth) {
|
|
349
|
+
if (capState !== "ready" || !cap || !cap.id) {
|
|
350
|
+
loadCaptcha();
|
|
351
|
+
setMsg("err", "请先输入验证码(图中 4 位数字,点图片可刷新)");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!capTxt.trim()) { setMsg("err", "请输入验证码(图中 4 位数字)"); return; }
|
|
355
|
+
}
|
|
356
|
+
setBusy("submit");
|
|
357
|
+
var payload = {
|
|
358
|
+
kind: "feedback",
|
|
359
|
+
category: cat,
|
|
360
|
+
title: fbTitle.trim(),
|
|
361
|
+
content: fbContent.trim(),
|
|
362
|
+
contact: contact.trim() || (cfg && cfg.phone ? cfg.phone : ""),
|
|
363
|
+
};
|
|
364
|
+
if (!accountAuth) {
|
|
365
|
+
payload.captcha_id = cap.id;
|
|
366
|
+
payload.captcha_answer = capTxt.trim();
|
|
367
|
+
}
|
|
368
|
+
fbApi("/feedback", { method: "POST", body: JSON.stringify(payload) })
|
|
369
|
+
.then(function (b) {
|
|
370
|
+
fbRememberThread(b.feedback.id, b.thread_token);
|
|
371
|
+
setThreads(fbLoadThreads());
|
|
372
|
+
setFbTitle(""); setFbContent(""); setContact(""); setCap(null); setCapState("idle"); setCapTxt("");
|
|
373
|
+
setMsg("ok", "✅ 反馈已提交,可在「我的反馈」查看回复");
|
|
374
|
+
setTab("mine");
|
|
375
|
+
})
|
|
376
|
+
.catch(function (e) {
|
|
377
|
+
var errBody = e.body && e.body.error;
|
|
378
|
+
if (errBody && errBody.code === "captcha_invalid") { setCapTxt(""); loadCaptcha(); setMsg("err", "验证码错误或已过期,请重新输入"); }
|
|
379
|
+
else if (errBody && errBody.code === "rate_limited") setMsg("err", "今天提交次数已达上限,请明天再试");
|
|
380
|
+
else setMsg("err", "提交失败:" + (errBody && errBody.message ? errBody.message : e.message));
|
|
381
|
+
})
|
|
382
|
+
.finally(function () { setBusy(""); });
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
var doReply = function (id) {
|
|
386
|
+
var input = document.getElementById("dru-fb-reply-" + id);
|
|
387
|
+
var text = input ? input.value.trim() : "";
|
|
388
|
+
if (!text) { setMsg("err", "请填写回复内容"); return; }
|
|
389
|
+
var t = threads.filter(function (x) { return x.id === id; })[0];
|
|
390
|
+
if (!t) return;
|
|
391
|
+
setBusy("reply:" + id);
|
|
392
|
+
fbApi("/feedback/" + id + "/replies", { method: "POST", body: JSON.stringify({ content: text }), token: t.token })
|
|
393
|
+
.then(function () {
|
|
394
|
+
if (input) input.value = "";
|
|
395
|
+
loadThread(id);
|
|
396
|
+
setMsg("ok", "✅ 已回复");
|
|
397
|
+
})
|
|
398
|
+
.catch(function (e) {
|
|
399
|
+
var errBody = e.body && e.body.error;
|
|
400
|
+
if (errBody && errBody.code === "rate_limited") setMsg("err", "今天回复次数已达上限,请明天再试");
|
|
401
|
+
else setMsg("err", "回复失败:" + (errBody && errBody.message ? errBody.message : e.message));
|
|
402
|
+
})
|
|
403
|
+
.finally(function () { setBusy(""); });
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
var openThread = function (id) {
|
|
407
|
+
setOpenId(id === openId ? "" : id);
|
|
408
|
+
setThreadData(null);
|
|
409
|
+
if (id !== openId) loadThread(id);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return h("div", { className: "dru-card" },
|
|
413
|
+
h("h3", null, "💬 用户反馈"),
|
|
414
|
+
h("div", { className: "dru-fb-tabs" },
|
|
415
|
+
h("div", { className: "dru-fb-tab" + (tab === "submit" ? " active" : ""), onClick: function () { setTab("submit"); setFbMsg(null); } }, "提交反馈"),
|
|
416
|
+
h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () { setTab("mine"); setFbMsg(null); setThreads(fbLoadThreads()); } }, "我的反馈" + (threads.length ? "(" + threads.length + ")" : ""))
|
|
417
|
+
),
|
|
418
|
+
tab === "submit"
|
|
419
|
+
? h("div", null,
|
|
420
|
+
h("div", { className: "dru-field" },
|
|
421
|
+
h("label", null, "类别"),
|
|
422
|
+
h("select", { className: "dru-fb-select", value: cat, onChange: function (e) { setCat(e.target.value); } },
|
|
423
|
+
FB_CATEGORIES.map(function (c) { return h("option", { key: c.value, value: c.value }, c.label); }))
|
|
424
|
+
),
|
|
425
|
+
h("div", { className: "dru-field" },
|
|
426
|
+
h("label", null, "标题(可选)"),
|
|
427
|
+
h("input", { className: "dru-input", type: "text", value: fbTitle, maxLength: 120, placeholder: "一句话描述", onChange: function (e) { setFbTitle(e.target.value); } })
|
|
428
|
+
),
|
|
429
|
+
h("div", { className: "dru-field" },
|
|
430
|
+
h("label", null, "内容"),
|
|
431
|
+
h("textarea", { className: "dru-fb-textarea", value: fbContent, maxLength: 2000, placeholder: "请描述遇到的问题或功能想法…", onChange: function (e) { setFbContent(e.target.value); } })
|
|
432
|
+
),
|
|
433
|
+
h("div", { className: "dru-field" },
|
|
434
|
+
h("label", null, "联系方式(可选)"),
|
|
435
|
+
h("input", { className: "dru-input", type: "text", value: contact, maxLength: 120, placeholder: cfg && cfg.phone ? "已登录手机号:" + cfg.phone : "邮箱/手机号,便于我们跟进", onChange: function (e) { setContact(e.target.value); } })
|
|
436
|
+
),
|
|
437
|
+
fbAuth === "account"
|
|
438
|
+
? h("div", { className: "dru-hint", style: { marginBottom: 10 } }, "已登录账号,提交反馈无需验证码。")
|
|
439
|
+
: FbCaptcha({ state: capState, cap: cap, answer: capTxt, setAnswer: setCapTxt, onRefresh: loadCaptcha }),
|
|
440
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "提交反馈"),
|
|
441
|
+
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
442
|
+
)
|
|
443
|
+
: h("div", null,
|
|
444
|
+
threads.length === 0
|
|
445
|
+
? h("div", { className: "dru-fb-empty" }, "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : ""))
|
|
446
|
+
: threads.slice().reverse().map(function (t) {
|
|
447
|
+
var open = openId === t.id;
|
|
448
|
+
var td = open ? threadData : null;
|
|
449
|
+
return h("div", { key: t.id, className: "dru-fb-item" },
|
|
450
|
+
h("div", { className: "dru-fb-item-head" },
|
|
451
|
+
fbStatusBadge(td ? td.status : "open"),
|
|
452
|
+
h("span", { className: "dru-fb-cat" }, (function () {
|
|
453
|
+
var c = FB_CATEGORIES.filter(function (x) { return x.value === (td ? td.category : ""); })[0];
|
|
454
|
+
return c ? c.label : (td ? td.category : "反馈");
|
|
455
|
+
})()),
|
|
456
|
+
h("span", { className: "dru-fb-item-title" }, td ? (td.title || "反馈") : "反馈 #" + t.id.slice(-6)),
|
|
457
|
+
h("span", { className: "dru-fb-item-time" }, td ? new Date(td.created_at).toLocaleString() : "")
|
|
458
|
+
),
|
|
459
|
+
open && td
|
|
460
|
+
? h("div", null,
|
|
461
|
+
h("div", { className: "dru-fb-item-content" }, td.content),
|
|
462
|
+
td.replies.map(function (r) {
|
|
463
|
+
return h("div", { key: r.id, className: "dru-fb-reply" },
|
|
464
|
+
h("div", { className: "dru-fb-reply-row" },
|
|
465
|
+
h("span", { className: "dru-fb-reply-who" + (r.author === "user" ? " user" : "") }, r.author === "admin" ? "管理员回复" : "我"),
|
|
466
|
+
h("span", { className: "dru-fb-reply-text" }, r.content)
|
|
467
|
+
),
|
|
468
|
+
h("div", { className: "dru-fb-item-time" }, new Date(r.created_at).toLocaleString())
|
|
469
|
+
);
|
|
470
|
+
}),
|
|
471
|
+
h("div", { className: "dru-fb-reply" },
|
|
472
|
+
h("textarea", { id: "dru-fb-reply-" + t.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
|
|
473
|
+
h("div", { style: { textAlign: "right", marginTop: 6 } },
|
|
474
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(t.id); } }, busy === "reply:" + t.id ? "发送中…" : "回复")
|
|
475
|
+
)
|
|
476
|
+
),
|
|
477
|
+
h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
|
|
478
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(t.id); } }, "↻ 刷新"),
|
|
479
|
+
" ",
|
|
480
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
: h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
|
|
484
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(t.id); } }, open ? "收起" : "查看 / 回复"))
|
|
485
|
+
);
|
|
486
|
+
}),
|
|
487
|
+
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── 满意度弹窗(安装体验约 10 分钟后弹出) ──────────────────────────────
|
|
493
|
+
|
|
494
|
+
/** 模块级弹窗开关(与面板独立)。 */
|
|
495
|
+
var popupOpen = false;
|
|
496
|
+
var popupListeners = new Set();
|
|
497
|
+
function setPopupOpen(v) { popupOpen = !!v; popupListeners.forEach(function (l) { l(); }); }
|
|
498
|
+
function subscribePopup(cb) { popupListeners.add(cb); return function () { popupListeners.delete(cb); }; }
|
|
499
|
+
function usePopupOpen() { return useSyncExternalStore(subscribePopup, function () { return popupOpen; }); }
|
|
500
|
+
|
|
501
|
+
/** 首次安装时间戳(首次观察到 bridge 运行时记下,便于“先体验 10 分钟”)。 */
|
|
502
|
+
var fbFirstSeenAt = null;
|
|
503
|
+
function fbEnsureFirstSeen() {
|
|
504
|
+
var s = fbPopState();
|
|
505
|
+
if (s && s.state === "pending" && s.firstSeen) { fbFirstSeenAt = s.firstSeen; return; }
|
|
506
|
+
if (fbFirstSeenAt) return;
|
|
507
|
+
fbFirstSeenAt = Date.now();
|
|
508
|
+
fbSavePopState({ state: "pending", firstSeen: fbFirstSeenAt, nextAt: s && s.nextAt ? s.nextAt : 0 });
|
|
509
|
+
}
|
|
510
|
+
/** 弹窗触发检查:到点且未完成/未推迟。 */
|
|
511
|
+
function fbMaybeOpenPopup() {
|
|
512
|
+
var s = fbPopState();
|
|
513
|
+
if (!s || s.state !== "pending" || popupOpen) return;
|
|
514
|
+
if (!fbFirstSeenAt) return;
|
|
515
|
+
if (Date.now() - fbFirstSeenAt < FB_POPUP_DELAY_MS) return;
|
|
516
|
+
if (s.nextAt && Date.now() < s.nextAt) return;
|
|
517
|
+
setPopupOpen(true);
|
|
518
|
+
}
|
|
519
|
+
function fbPopupLater() {
|
|
520
|
+
fbSavePopState({ state: "pending", firstSeen: fbFirstSeenAt || Date.now(), nextAt: Date.now() + FB_POPUP_LATER_MS });
|
|
521
|
+
setPopupOpen(false);
|
|
522
|
+
}
|
|
523
|
+
function fbPopupDone() {
|
|
524
|
+
fbSavePopState({ state: "done", firstSeen: fbFirstSeenAt || Date.now(), nextAt: 0 });
|
|
525
|
+
setPopupOpen(false);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function FeedbackPopup() {
|
|
529
|
+
var open = usePopupOpen();
|
|
530
|
+
var stepArr = useState("rate"); var step = stepArr[0]; var setStep = stepArr[1];
|
|
531
|
+
var ratingArr = useState(null); var rating = ratingArr[0]; var setRating = ratingArr[1];
|
|
532
|
+
var noteArr = useState(""); var note = noteArr[0]; var setNote = noteArr[1];
|
|
533
|
+
var recommendArr = useState(null); var recommend = recommendArr[0]; var setRecommend = recommendArr[1];
|
|
534
|
+
var capArr = useState(null); var cap = capArr[0]; var setCap = capArr[1];
|
|
535
|
+
var capStateArr = useState("idle"); var capState = capStateArr[0]; var setCapState = capStateArr[1];
|
|
536
|
+
var capTxtArr = useState(""); var capTxt = capTxtArr[0]; var setCapTxt = capTxtArr[1];
|
|
537
|
+
var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
|
|
538
|
+
var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
|
|
539
|
+
var fbAuthArr = useState("anonymous"); var fbAuth = fbAuthArr[0]; var setFbAuth = fbAuthArr[1];
|
|
540
|
+
// 邀请链接(已登录用户推荐成功后展示)
|
|
541
|
+
var inviteArr = useState(null); var popupInvite = inviteArr[0]; var setPopupInvite = inviteArr[1];
|
|
542
|
+
var inviteCopiedArr = useState(false); var inviteCopied = inviteCopiedArr[0]; var setInviteCopied = inviteCopiedArr[1];
|
|
543
|
+
|
|
544
|
+
function loadPopupCaptcha() {
|
|
545
|
+
setCapState("loading");
|
|
546
|
+
setCapTxt("");
|
|
547
|
+
fbApi("/feedback/captcha").then(function (b) {
|
|
548
|
+
var cid = b.captcha_id || b.id; if (cid) { setCap({ id: cid, svg: b.svg || "" }); setCapState("ready"); }
|
|
549
|
+
else { setCap(null); setCapState("error"); }
|
|
550
|
+
}).catch(function () {
|
|
551
|
+
setCap(null); setCapState("error");
|
|
552
|
+
setMessage({ kind: "err", text: "验证码加载失败,可点击验证码区域重试" });
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** 预取邀请信息(已登录且有邀请码时生成专属链接)。 */
|
|
557
|
+
function loadPopupInvite() {
|
|
558
|
+
if (popupInvite) return;
|
|
559
|
+
api("/dsh-remote/account").then(function (b) {
|
|
560
|
+
var acct = (b && b.ok && b.account) ? b.account : null;
|
|
561
|
+
if (!acct || !acct.invite_code) { setPopupInvite({ code: "" }); return; }
|
|
562
|
+
api("/dsh-remote/remote-url").then(function (u) {
|
|
563
|
+
var base = (u && u.remoteUrl) || "https://n.risegao.cn:13443/app/";
|
|
564
|
+
setPopupInvite({ code: acct.invite_code, link: base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(acct.invite_code) });
|
|
565
|
+
}).catch(function () { setPopupInvite({ code: acct.invite_code }); });
|
|
566
|
+
}).catch(function () { setPopupInvite({ code: "" }); });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
useEffect(function () {
|
|
570
|
+
if (!open) return;
|
|
571
|
+
// 登录态(节点半自动附加 JWT)免验证码;匿名才需要图形验证码
|
|
572
|
+
api("/dsh-remote/feedback-config").then(function (b) {
|
|
573
|
+
setFbAuth(b.auth === "account" ? "account" : "anonymous");
|
|
574
|
+
if (b.auth === "account") { setCap(null); setCapState("idle"); }
|
|
575
|
+
}).catch(function () { setFbAuth("anonymous"); });
|
|
576
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
577
|
+
}, [open]);
|
|
578
|
+
|
|
579
|
+
useEffect(function () {
|
|
580
|
+
if (open && step === "rate" && capState === "idle" && fbAuth !== "account") loadPopupCaptcha();
|
|
581
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
582
|
+
}, [open, step, capState, fbAuth]);
|
|
583
|
+
|
|
584
|
+
if (!open) return null;
|
|
585
|
+
|
|
586
|
+
var doSubmit = function () {
|
|
587
|
+
if (rating === null) { setMessage({ kind: "err", text: "请先选择满意度" }); return; }
|
|
588
|
+
if (step === "rate") { setStep("recommend"); return; }
|
|
589
|
+
var accountAuth = fbAuth === "account";
|
|
590
|
+
if (!accountAuth) {
|
|
591
|
+
if (capState !== "ready" || !cap || !cap.id) { loadPopupCaptcha(); setMessage({ kind: "err", text: "请先输入验证码" }); return; }
|
|
592
|
+
if (!capTxt.trim()) { setMessage({ kind: "err", text: "请输入验证码(图中 4 位数字)" }); return; }
|
|
593
|
+
}
|
|
594
|
+
setBusy("submit");
|
|
595
|
+
var payload = {
|
|
596
|
+
kind: "rating",
|
|
597
|
+
category: "satisfaction",
|
|
598
|
+
content: note.trim() || (rating >= 4 ? "对远程控制功能满意" : "对远程控制功能不太满意"),
|
|
599
|
+
rating: rating,
|
|
600
|
+
recommend: recommend === true ? 1 : 0,
|
|
601
|
+
};
|
|
602
|
+
if (!accountAuth) {
|
|
603
|
+
payload.captcha_id = cap.id;
|
|
604
|
+
payload.captcha_answer = capTxt.trim();
|
|
605
|
+
}
|
|
606
|
+
fbApi("/feedback", { method: "POST", body: JSON.stringify(payload) })
|
|
607
|
+
.then(function (b) {
|
|
608
|
+
fbRememberThread(b.feedback.id, b.thread_token);
|
|
609
|
+
fbPopupDone();
|
|
610
|
+
})
|
|
611
|
+
.catch(function (e) {
|
|
612
|
+
var errBody = e.body && e.body.error;
|
|
613
|
+
if (errBody && errBody.code === "captcha_invalid") { setStep("rate"); setCapTxt(""); loadPopupCaptcha(); setMessage({ kind: "err", text: "验证码错误或已过期,请重新输入" }); }
|
|
614
|
+
else if (errBody && errBody.code === "rate_limited") { fbPopupLater(); setMessage({ kind: "err", text: "今日提交已达上限,明天再来吧" }); }
|
|
615
|
+
else setMessage({ kind: "err", text: "提交失败:" + ((errBody && errBody.message) || e.message) + "(可稍后再试)" });
|
|
616
|
+
})
|
|
617
|
+
.finally(function () { setBusy(""); });
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
return h("div", { className: "dru-popup", onMouseDown: function (e) { if (e.target === e.currentTarget) fbPopupLater(); } },
|
|
621
|
+
h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "用户反馈" },
|
|
622
|
+
h("div", { className: "dru-popup-body" },
|
|
623
|
+
step === "rate"
|
|
624
|
+
? h("div", null,
|
|
625
|
+
h("div", { className: "dru-popup-icon" }, "😊"),
|
|
626
|
+
h("div", { className: "dru-popup-title" }, "您对远程控制功能满意吗?"),
|
|
627
|
+
h("div", { className: "dru-popup-sub" }, "用了 10 分钟,说说真实感受吧(1 分钟搞定)"),
|
|
628
|
+
h("div", { className: "dru-popup-rate" },
|
|
629
|
+
[ [5, "😄", "很满意"], [3, "😐", "一般"], [1, "😞", "不满意"] ].map(function (r) {
|
|
630
|
+
return h("button", { key: r[0], type: "button", className: rating === r[0] ? "sel" : "", onClick: function () { setRating(r[0]); setMessage(null); } },
|
|
631
|
+
h("span", null, r[1]), h("span", { className: "lbl" }, r[2]));
|
|
632
|
+
})
|
|
633
|
+
),
|
|
634
|
+
h("textarea", { className: "dru-popup-textarea", value: note, maxLength: 2000, placeholder: "说说想法或遇到的问题(可选)…", onChange: function (e) { setNote(e.target.value); } }),
|
|
635
|
+
fbAuth === "account"
|
|
636
|
+
? h("div", { className: "dru-hint", style: { marginTop: 10 } }, "已登录账号,提交无需验证码。")
|
|
637
|
+
: FbCaptcha({ state: capState, cap: cap, answer: capTxt, setAnswer: setCapTxt, onRefresh: loadPopupCaptcha }),
|
|
638
|
+
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
|
|
639
|
+
h("div", { className: "dru-popup-actions" },
|
|
640
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: doSubmit }, "下一步")
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
: h("div", null,
|
|
644
|
+
h("div", { className: "dru-popup-icon" }, "🤝"),
|
|
645
|
+
h("div", { className: "dru-popup-title" }, "愿意推荐给朋友吗?"),
|
|
646
|
+
h("div", { className: "dru-popup-sub" }, "邀请好友一起使用,双方都能获得更好的体验"),
|
|
647
|
+
recommend === true
|
|
648
|
+
? h("div", { className: "dru-popup-invite" },
|
|
649
|
+
h("div", null, "🎉 感谢推荐!"),
|
|
650
|
+
popupInvite && popupInvite.code && popupInvite.link
|
|
651
|
+
? h("div", null,
|
|
652
|
+
h("div", { className: "dru-url", style: { marginTop: 10, textAlign: "left" } },
|
|
653
|
+
h("span", null, popupInvite.link),
|
|
654
|
+
h("button", { type: "button", onClick: function () {
|
|
655
|
+
try { navigator.clipboard.writeText(popupInvite.link).then(function () { setInviteCopied(true); setTimeout(function () { setInviteCopied(false); }, 1500); }); } catch (e) {}
|
|
656
|
+
} }, inviteCopied ? "已复制" : "复制邀请链接")
|
|
657
|
+
),
|
|
658
|
+
h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给好友,注册时自动带上你的邀请码。")
|
|
659
|
+
)
|
|
660
|
+
: h("div", { style: { marginTop: 6, fontSize: 12 } },
|
|
661
|
+
popupInvite && popupInvite.code
|
|
662
|
+
? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎯 邀请好友赚会员」中复制邀请链接。"
|
|
663
|
+
: "登录手机号账号后,在设置面板 →「🎯 邀请好友赚会员」中获取专属邀请链接。")
|
|
664
|
+
)
|
|
665
|
+
: h("div", null,
|
|
666
|
+
h("div", { className: "dru-popup-actions" },
|
|
667
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setRecommend(true); loadPopupInvite(); } }, "愿意推荐"),
|
|
668
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: function () { setRecommend(false); } }, "暂时不了")
|
|
669
|
+
),
|
|
670
|
+
h("div", { className: "dru-hint", style: { marginTop: 10 } }, "推荐成功可获得专属邀请链接")
|
|
671
|
+
),
|
|
672
|
+
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
|
|
673
|
+
recommend !== null && h("div", { className: "dru-popup-actions" },
|
|
674
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "完成")
|
|
675
|
+
)
|
|
676
|
+
)
|
|
677
|
+
),
|
|
678
|
+
h("div", { className: "dru-popup-foot" },
|
|
679
|
+
h("button", { type: "button", onClick: fbPopupLater }, "稍后再说"),
|
|
680
|
+
h("button", { type: "button", onClick: fbPopupDone }, "不再提示")
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── 图标 ────────────────────────────────────────────────────────────────
|
|
687
|
+
// 通用「远程控制」图标(🖥 风格 emoji,与面板内既有 emoji 图标体系一致):
|
|
688
|
+
// 栏目导航 label 与栏目头部均使用它。原侧边栏入口(电源图标按钮)已随入口迁移移除。
|
|
689
|
+
|
|
690
|
+
// ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
|
|
691
|
+
function RemoteControlSection(props) {
|
|
692
|
+
|
|
693
|
+
var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
|
|
694
|
+
var modeArr = useState("saas"); var mode = modeArr[0]; var setMode = modeArr[1]; // saas | local
|
|
695
|
+
var viewArr = useState("home"); var view = viewArr[0]; var setView = viewArr[1]; // home | feedback | invite
|
|
696
|
+
var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
|
|
697
|
+
var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
|
|
698
|
+
var copiedArr = useState(false); var copied = copiedArr[0]; var setCopied = copiedArr[1];
|
|
699
|
+
|
|
700
|
+
// 我的信息(个人中心)数据
|
|
701
|
+
var acctArr = useState(null); var account = acctArr[0]; var setAccount = acctArr[1];
|
|
702
|
+
var quotaArr = useState(null); var quota = quotaArr[0]; var setQuota = quotaArr[1];
|
|
703
|
+
var inviteArr = useState(null); var inviteData = inviteArr[0]; var setInviteData = inviteArr[1];
|
|
704
|
+
var pubArr = useState(null); var pub = pubArr[0]; var setPub = pubArr[1];
|
|
705
|
+
|
|
706
|
+
// 登录表单字段
|
|
707
|
+
var phoneArr = useState(""); var phone = phoneArr[0]; var setPhone = phoneArr[1];
|
|
708
|
+
var passArr = useState(""); var pass = passArr[0]; var setPass = passArr[1];
|
|
709
|
+
var lcapArr = useState(null); var lcap = lcapArr[0]; var setLcap = lcapArr[1]; // {id, svg}
|
|
710
|
+
var lcapTxtArr = useState(""); var lcapTxt = lcapTxtArr[0]; var setLcapTxt = lcapTxtArr[1];
|
|
711
|
+
// 注册表单字段
|
|
712
|
+
var rphoneArr = useState(""); var rphone = rphoneArr[0]; var setRphone = rphoneArr[1];
|
|
713
|
+
var rpassArr = useState(""); var rpass = rpassArr[0]; var setRpass = rpassArr[1];
|
|
714
|
+
var rpass2Arr = useState(""); var rpass2 = rpass2Arr[0]; var setRpass2 = rpass2Arr[1];
|
|
715
|
+
var rsmsArr = useState(""); var rsms = rsmsArr[0]; var setRsms = rsmsArr[1];
|
|
716
|
+
var rsmsBtnArr = useState("获取验证码"); var rsmsBtn = rsmsBtnArr[0]; var setRsmsBtn = rsmsBtnArr[1];
|
|
717
|
+
var rcapArr = useState(null); var rcap = rcapArr[0]; var setRcap = rcapArr[1];
|
|
718
|
+
var rcapTxtArr = useState(""); var rcapTxt = rcapTxtArr[0]; var setRcapTxt = rcapTxtArr[1];
|
|
719
|
+
var rInviteArr = useState(""); var rInvite = rInviteArr[0]; var setRInvite = rInviteArr[1];
|
|
720
|
+
// 注册 tab(登录/注册)
|
|
721
|
+
var authTabArr = useState("login"); var authTab = authTabArr[0]; var setAuthTab = authTabArr[1];
|
|
722
|
+
|
|
723
|
+
// 自建服务表单字段
|
|
724
|
+
var shArr = useState(""); var selfHost = shArr[0]; var setSelfHost = shArr[1];
|
|
725
|
+
var lkArr = useState(""); var localKey = lkArr[0]; var setLocalKey = lkArr[1];
|
|
726
|
+
|
|
727
|
+
var refresh = useCallback(function () {
|
|
728
|
+
setBusy("status");
|
|
729
|
+
api("/dsh-remote/status").then(function (body) {
|
|
730
|
+
setSt(body);
|
|
731
|
+
setMode(body && body.config && body.config.mode === "local" ? "local" : "saas");
|
|
732
|
+
setMessage(null);
|
|
733
|
+
}).catch(function (e) { setMessage({ kind: "err", text: "读取状态失败: " + e.message }); })
|
|
734
|
+
.finally(function () { setBusy(""); });
|
|
735
|
+
}, []);
|
|
736
|
+
|
|
737
|
+
function loadCaptcha(kind) {
|
|
738
|
+
var setCap = kind === "login" ? setLcap : setRcap;
|
|
739
|
+
setCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
|
|
740
|
+
fetch("/dsh-remote/captcha").then(function (r) {
|
|
741
|
+
return r.text().then(function (text) {
|
|
742
|
+
if (!r.ok) throw new Error("http " + r.status);
|
|
743
|
+
var body = null; try { body = JSON.parse(text); } catch (e) { body = null; }
|
|
744
|
+
if (body && body.captcha_id) setCap({ id: body.captcha_id, svg: body.svg || "" });
|
|
745
|
+
else if (body && body.svg) setCap({ id: null, svg: body.svg });
|
|
746
|
+
else setCap({ id: null, svg: null });
|
|
747
|
+
});
|
|
748
|
+
}).catch(function () { setCap({ id: null, svg: null }); });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
var loggedIn = !!(st && st.config && (st.config.phone || st.config.hasLocalKey));
|
|
752
|
+
var serviceRunning = !!(st && st.service && st.service.running);
|
|
753
|
+
var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
|
|
754
|
+
|
|
755
|
+
// 已登录(SaaS)→ 拉我的信息/配额/公共配置
|
|
756
|
+
useEffect(function () {
|
|
757
|
+
if (!(st && st.config && st.config.phone)) return;
|
|
758
|
+
api("/dsh-remote/account").then(function (b) { if (b && b.ok) setAccount(b.account); }).catch(function () {});
|
|
759
|
+
api("/dsh-remote/quota").then(function (b) { if (b && b.ok) setQuota(b.quota); }).catch(function () {});
|
|
760
|
+
api("/dsh-remote/remote-url").then(function (b) { if (b && b.ok && b.publicConfig) setPub(b.publicConfig); }).catch(function () {});
|
|
761
|
+
}, [st && st.config && st.config.phone]);
|
|
762
|
+
|
|
763
|
+
useEffect(function () { refresh(); }, [refresh]);
|
|
764
|
+
useEffect(function () {
|
|
765
|
+
if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !lcap) loadCaptcha("login");
|
|
766
|
+
}, [st, loggedIn, mode, authTab, lcap]);
|
|
767
|
+
|
|
768
|
+
function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
|
|
769
|
+
|
|
770
|
+
// ---------- 登录(密码/短信) ----------
|
|
771
|
+
var doLogin = function () {
|
|
772
|
+
if (!phone.trim() || !pass) { setMsg("err", "请填写手机号与密码"); return; }
|
|
773
|
+
if (!lcap || !lcap.id || !lcapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); if (!lcap) loadCaptcha("login"); return; }
|
|
774
|
+
var prevPhone = (st && st.config && st.config.phone) || "";
|
|
775
|
+
setBusy("login");
|
|
776
|
+
post("/dsh-remote/login", { phone: phone.trim(), password: pass, captcha_id: lcap.id, captcha_answer: lcapTxt.trim() })
|
|
777
|
+
.then(function (body) {
|
|
778
|
+
if (body.ok || body.status === 200) {
|
|
779
|
+
return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
|
|
780
|
+
// 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
|
|
781
|
+
if (prevPhone !== phone.trim()) fbClearThreads();
|
|
782
|
+
setSt(cfg); setPass(""); setLcapTxt(""); setLcap(null);
|
|
783
|
+
setMsg("ok", "✅ 登录成功,账号已保存");
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
var relayBody = body.body || {};
|
|
787
|
+
var errText = (relayBody.error && relayBody.error.message) || body.error || ("登录失败(" + (body.status || "?") + ")");
|
|
788
|
+
setMsg("err", String(errText));
|
|
789
|
+
setLcapTxt(""); setLcap(null); loadCaptcha("login");
|
|
790
|
+
})
|
|
791
|
+
.catch(function (e) { setMsg("err", "登录失败: " + e.message); })
|
|
792
|
+
.finally(function () { setBusy(""); });
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
var sendRegSms = function () {
|
|
796
|
+
if (!/^1\d{10}$/.test(rphone.trim())) { setMsg("err", "请输入正确的手机号"); return; }
|
|
797
|
+
if (rcap && (!rcap.id || !rcapTxt.trim())) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); return; }
|
|
798
|
+
setBusy("sms");
|
|
799
|
+
post("/dsh-remote/sms-code", { phone: rphone.trim(), ...(rcap ? { captcha_id: rcap.id, captcha_answer: rcapTxt.trim() } : {}) })
|
|
800
|
+
.then(function (body) {
|
|
801
|
+
var testCode = body && (body.test_code || (body.body && body.body.test_code));
|
|
802
|
+
setRcap(null); setRcapTxt("");
|
|
803
|
+
if (testCode) setMsg("ok", "验证码已发送(测试码: " + testCode + ")");
|
|
804
|
+
else setMsg("ok", "验证码已发送");
|
|
805
|
+
var s = 60; setRsmsBtn(s + "s");
|
|
806
|
+
var t = setInterval(function () { s--; if (s <= 0) { clearInterval(t); setRsmsBtn("获取验证码"); } else setRsmsBtn(s + "s"); }, 1000);
|
|
807
|
+
})
|
|
808
|
+
.catch(function (e) {
|
|
809
|
+
var relayError = e.body && e.body.body && e.body.body.error;
|
|
810
|
+
if (relayError && relayError.code === "captcha_invalid") {
|
|
811
|
+
setRcapTxt(""); loadCaptcha("register");
|
|
812
|
+
setMsg("err", "发送次数较多,请输入图中验证码后重试");
|
|
813
|
+
} else setMsg("err", "发送失败: " + e.message);
|
|
814
|
+
})
|
|
815
|
+
.finally(function () { setBusy(""); });
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
var doRegister = function () {
|
|
819
|
+
if (!/^1\d{10}$/.test(rphone.trim())) { setMsg("err", "请输入正确的手机号"); return; }
|
|
820
|
+
if (!rsms.trim()) { setMsg("err", "请填写短信验证码"); return; }
|
|
821
|
+
if (rpass.length < 8) { setMsg("err", "密码至少 8 位"); return; }
|
|
822
|
+
if (rpass !== rpass2) { setMsg("err", "两次输入的密码不一致"); return; }
|
|
823
|
+
setBusy("register");
|
|
824
|
+
var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass };
|
|
825
|
+
var invite = rInvite.trim().toUpperCase();
|
|
826
|
+
if (invite) regPayload.invite_code = invite;
|
|
827
|
+
if (rcap) { regPayload.captcha_id = rcap.id; regPayload.captcha_answer = rcapTxt.trim(); }
|
|
828
|
+
post("/dsh-remote/register", regPayload)
|
|
829
|
+
.then(function (body) {
|
|
830
|
+
if (body.ok || body.status === 201 || (body.body && body.body.token)) {
|
|
831
|
+
return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
|
|
832
|
+
setSt(cfg);
|
|
833
|
+
setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
|
|
834
|
+
setMsg("ok", "✅ 注册成功,已自动登录");
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
var b = body.body || {};
|
|
838
|
+
setMsg("err", (b.error && b.error.message) || "注册失败(" + (body.status || "?") + ")");
|
|
839
|
+
})
|
|
840
|
+
.catch(function (e) { setMsg("err", "注册失败: " + e.message); })
|
|
841
|
+
.finally(function () { setBusy(""); });
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
var doLogout = function (clearForm) {
|
|
845
|
+
setBusy("logout");
|
|
846
|
+
post("/dsh-remote/logout").then(function (body) {
|
|
847
|
+
setSt(body); setAccount(null); setQuota(null);
|
|
848
|
+
// 退出登录:清除本机保存的用户反馈线程凭据(thread_token 见 FB_THREADS_KEY),
|
|
849
|
+
// 反馈历史保留在服务端(按账号校验),账号身份变化后本机不再可见旧线程。
|
|
850
|
+
fbClearThreads();
|
|
851
|
+
if (clearForm) { setPhone(""); setPass(""); setLcapTxt(""); setLcap(null); setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRsmsBtn("获取验证码"); setRcapTxt(""); setRcap(null); }
|
|
852
|
+
setMessage(null);
|
|
853
|
+
}).catch(function (e) { setMsg("err", "退出失败: " + e.message); })
|
|
854
|
+
.finally(function () { setBusy(""); });
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
var toggleBridge = function (start) {
|
|
858
|
+
setBusy(start ? "start" : "stop");
|
|
859
|
+
post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
|
|
860
|
+
.then(function (body) {
|
|
861
|
+
setSt(body);
|
|
862
|
+
setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅ bridge 已启动" + (body.pid ? " (pid=" + body.pid + ")" : "") : "bridge 已停止") : (body.detail || body.status || "操作失败"));
|
|
863
|
+
})
|
|
864
|
+
.catch(function (e) { setMsg("err", (start ? "启动" : "停止") + "失败: " + e.message); })
|
|
865
|
+
.finally(function () { setBusy(""); });
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
var copyUrl = function () {
|
|
869
|
+
if (!st) return;
|
|
870
|
+
try { navigator.clipboard.writeText(st.remoteUrl).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
var saveLocal = function () {
|
|
874
|
+
if (!selfHost.trim() || !localKey.trim()) { setMsg("err", "请填写服务器地址与访问密钥"); return; }
|
|
875
|
+
setBusy("local");
|
|
876
|
+
post("/dsh-remote/config", { mode: "local", selfHostUrl: selfHost.trim(), localKey: localKey.trim() })
|
|
877
|
+
.then(function (body) {
|
|
878
|
+
// 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
|
|
879
|
+
fbClearThreads();
|
|
880
|
+
setSt(body); setSelfHost(""); setLocalKey("");
|
|
881
|
+
setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
|
|
882
|
+
})
|
|
883
|
+
.catch(function (e) { setMsg("err", "保存失败: " + e.message); })
|
|
884
|
+
.finally(function () { setBusy(""); });
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
var loadInvite = function () {
|
|
888
|
+
setBusy("invite");
|
|
889
|
+
api("/dsh-remote/remote-url").then(function (b) {
|
|
890
|
+
var pubBody = (b && b.publicConfig) || {};
|
|
891
|
+
setPub(pubBody);
|
|
892
|
+
}).catch(function () {});
|
|
893
|
+
api("/dsh-remote/invite-records").then(function (b) {
|
|
894
|
+
if (b && b.ok) setInviteData({ records: b.records || [], rewards: b.rewards || [] });
|
|
895
|
+
else setInviteData({ records: [], rewards: [] });
|
|
896
|
+
}).catch(function () { setInviteData({ records: [], rewards: [] }); })
|
|
897
|
+
.finally(function () { setBusy(""); });
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
var field = function (label, inputEl) {
|
|
901
|
+
return h("div", { className: "dru-field" }, h("label", null, label), inputEl);
|
|
902
|
+
};
|
|
903
|
+
var input = function (attrs) { return h("input", Object.assign({ className: "dru-input", type: "text" }, attrs)); };
|
|
904
|
+
var card = function (title, children) { return h("div", { className: "dru-card" }, title ? h("h3", null, title) : null, children); };
|
|
905
|
+
|
|
906
|
+
// ---------- 我的信息(个人中心) ----------
|
|
907
|
+
function fmtDate(ts) {
|
|
908
|
+
if (!ts) return "—";
|
|
909
|
+
try { var d = new Date(Number(ts)); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } catch (e) { return "—"; }
|
|
910
|
+
}
|
|
911
|
+
/** 推广/升级页地址(/app/promo),取不到时返回空(按钮隐藏)。 */
|
|
912
|
+
function promoUrl() {
|
|
913
|
+
var base = (pub && pub.app_url) || (st && st.remoteUrl) || "";
|
|
914
|
+
if (!base) return "";
|
|
915
|
+
return base.replace(/\/+$/, "") + "/promo";
|
|
916
|
+
}
|
|
917
|
+
function renderAccount() {
|
|
918
|
+
var a = account;
|
|
919
|
+
var plan = a ? a.plan : "free";
|
|
920
|
+
var source = a ? (a.plan_source || "plan") : "plan";
|
|
921
|
+
var isMember = plan === "pro" || plan === "pro_max";
|
|
922
|
+
var endsAt = a && (a.plan_ends_at || a.trial_expires_at) ? Number(a.plan_ends_at || a.trial_expires_at) : 0;
|
|
923
|
+
var quotaPct = quota && quota.limit_enabled ? quota.percent : null;
|
|
924
|
+
var promo = promoUrl();
|
|
925
|
+
var planText;
|
|
926
|
+
if (!isMember) planText = "免费额度: 带宽 ≈1Mbps" + (quotaPct !== null ? " · 本月流量已用 " + quotaPct + "%" : " · 本月流量限额 1GB");
|
|
927
|
+
else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
|
|
928
|
+
else if (endsAt) planText = plan === "pro_max" ? "Pro Max 会员 · 到期 " + fmtDate(endsAt) : "PRO 会员 · 到期 " + fmtDate(endsAt);
|
|
929
|
+
else planText = plan === "pro_max" ? "Pro Max 长期会员" : "PRO 长期会员";
|
|
930
|
+
return h("div", null,
|
|
931
|
+
h("div", { className: "dru-user" },
|
|
932
|
+
h("div", { className: "dru-avatar" }, (st.config.phone || "D").charAt(0).toUpperCase()),
|
|
933
|
+
h("div", null,
|
|
934
|
+
h("div", { className: "mail" }, st.config.phone),
|
|
935
|
+
h("div", { className: "plan" }, plan === "pro_max" ? "Pro Max 会员" : plan === "pro" ? "PRO 会员" : "免费用户")
|
|
936
|
+
)
|
|
937
|
+
),
|
|
938
|
+
// 套餐状态(免费额度 / 会员到期日)
|
|
939
|
+
h("div", { className: "dru-status-line", style: { marginTop: 10 } },
|
|
940
|
+
h("span", { className: "dru-dot " + (isMember ? "dru-dot-on" : "dru-dot-off") }),
|
|
941
|
+
h("span", null, planText)
|
|
942
|
+
),
|
|
943
|
+
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
944
|
+
promo && h("a", { className: "dru-btn dru-btn-primary", style: { textDecoration: "none", display: "inline-flex", alignItems: "center" }, href: promo, target: "_blank", rel: "noopener" },
|
|
945
|
+
!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员"),
|
|
946
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
|
|
947
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
|
|
948
|
+
h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
|
|
949
|
+
),
|
|
950
|
+
endsAt && isMember ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
951
|
+
"到期后如需继续使用会员权益,请在到期前续费。" + (promo ? "升级/续费入口在推广页。" : "")
|
|
952
|
+
) : null
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ---------- 邀请视图 ----------
|
|
957
|
+
function renderInvite() {
|
|
958
|
+
var rule = (pub && pub.invite_rule) || { n: 3, days: 15 };
|
|
959
|
+
var code = (account && account.invite_code) || "";
|
|
960
|
+
var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://n.risegao.cn:13443/app/";
|
|
961
|
+
var link = base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code);
|
|
962
|
+
var copyInvite = function () {
|
|
963
|
+
try { navigator.clipboard.writeText(link).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
|
|
964
|
+
};
|
|
965
|
+
return h("div", null,
|
|
966
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
|
|
967
|
+
card("🎯 邀请好友赚会员", [
|
|
968
|
+
h("div", { className: "dru-hint", style: { marginBottom: 8 } },
|
|
969
|
+
"每邀请 " + rule.n + " 位好友安装并注册,你获得 " + rule.days + " 天 PRO 会员(好友完成设备安装后计入有效邀请)。"),
|
|
970
|
+
code ? h("div", null,
|
|
971
|
+
h("div", { className: "dru-hint" }, "我的邀请码: " + code),
|
|
972
|
+
h("div", { className: "dru-url" },
|
|
973
|
+
h("span", null, link),
|
|
974
|
+
h("button", { type: "button", onClick: copyInvite }, copied ? "已复制" : "复制链接")
|
|
975
|
+
),
|
|
976
|
+
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
977
|
+
"把链接发给好友,或把邀请码「" + code + "」告诉他们,注册时填写即可。")
|
|
978
|
+
) : h("div", null,
|
|
979
|
+
h("div", { className: "dru-hint" }, "需要手机号账号登录后才能生成专属邀请链接。"),
|
|
980
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%", marginTop: 10 }, disabled: busy !== "", onClick: function () {
|
|
981
|
+
setBusy("invite-refresh");
|
|
982
|
+
api("/dsh-remote/account").then(function (b) {
|
|
983
|
+
if (b && b.ok && b.account && b.account.invite_code) { setAccount(b.account); setMsg("ok", "邀请码已生成"); }
|
|
984
|
+
else setMsg("err", "获取邀请码失败,请稍后重试或先登录手机号账号");
|
|
985
|
+
}).catch(function (e) { setMsg("err", "获取邀请码失败:" + e.message); })
|
|
986
|
+
.finally(function () { setBusy(""); });
|
|
987
|
+
} }, busy === "invite-refresh" ? "生成中…" : "获取邀请码")
|
|
988
|
+
),
|
|
989
|
+
h("div", { className: "dru-hint", style: { marginTop: 10 } }, "有效邀请记录(登录可见):")
|
|
990
|
+
]),
|
|
991
|
+
code ? card("我的邀请记录",
|
|
992
|
+
(inviteData && inviteData.records && inviteData.records.length)
|
|
993
|
+
? h("div", { className: "dru-status-line" }, "已邀请 " + inviteData.records.length + " 人(有效绑定), 获得奖励 " + ((inviteData.rewards || []).length) + " 次")
|
|
994
|
+
: h("div", { className: "dru-hint" }, "暂无有效邀请记录。邀请好友安装并注册,完成设备绑定后即计入。")
|
|
995
|
+
) : null
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// ---------- 反馈视图(独立页,带返回) ----------
|
|
1000
|
+
function renderFeedback() {
|
|
1001
|
+
return h("div", null,
|
|
1002
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
|
|
1003
|
+
h(FeedbackCard, null)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ---------- 主视图 ----------
|
|
1008
|
+
function renderHome() {
|
|
1009
|
+
var isLocal = mode === "local";
|
|
1010
|
+
return h("div", null,
|
|
1011
|
+
// 连接模式主 Tab(云端服务 / 自建服务,二选一)
|
|
1012
|
+
h("div", { className: "dru-tabs", style: { marginTop: 4 } },
|
|
1013
|
+
h("div", { className: "dru-tab" + (!isLocal ? " active" : ""), onClick: function () { setMode("saas"); setMessage(null); } }, "☁️ 云端服务"),
|
|
1014
|
+
h("div", { className: "dru-tab" + (isLocal ? " active" : ""), onClick: function () { setMode("local"); setMessage(null); } }, "🖥 自建服务")
|
|
1015
|
+
),
|
|
1016
|
+
// 云端 tab:远程控制地址 + 账号(手机号登录,官方托管)
|
|
1017
|
+
!isLocal ? h("div", null,
|
|
1018
|
+
card("📱 远程控制地址",
|
|
1019
|
+
h("div", { className: "dru-url" },
|
|
1020
|
+
h("span", null, st ? st.remoteUrl : "加载中…"),
|
|
1021
|
+
h("button", { type: "button", onClick: copyUrl }, copied ? "已复制" : "复制")
|
|
1022
|
+
),
|
|
1023
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1024
|
+
st && st.relayReachable === false ? "⚠ 云端服务不可达(地址可能不是最新)" : "手机浏览器打开此地址登录后即可远程控制本机。")
|
|
1025
|
+
),
|
|
1026
|
+
card("🔑 账号",
|
|
1027
|
+
st === null
|
|
1028
|
+
? h("div", { className: "dru-hint" }, "正在读取远控状态…")
|
|
1029
|
+
: loggedIn && st.config.phone
|
|
1030
|
+
? renderAccount()
|
|
1031
|
+
: h("div", null,
|
|
1032
|
+
h("div", { className: "dru-tabs" },
|
|
1033
|
+
h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setMessage(null); } }, "登录"),
|
|
1034
|
+
h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setMessage(null); } }, "注册")
|
|
1035
|
+
),
|
|
1036
|
+
authTab === "login"
|
|
1037
|
+
? h("div", null,
|
|
1038
|
+
field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
|
|
1039
|
+
field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
|
|
1040
|
+
field("验证码", h("div", { className: "dru-captcha" },
|
|
1041
|
+
input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
|
|
1042
|
+
h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("login"); }, dangerouslySetInnerHTML: lcap && lcap.svg && lcap.svg.indexOf("<svg") === 0 ? { __html: lcap.svg } : void 0 },
|
|
1043
|
+
lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
|
|
1044
|
+
)),
|
|
1045
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录")
|
|
1046
|
+
)
|
|
1047
|
+
: h("div", null,
|
|
1048
|
+
field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
|
|
1049
|
+
field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
|
|
1050
|
+
field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
|
|
1051
|
+
field("邀请码(选填)", input({ value: rInvite, placeholder: "好友的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
|
|
1052
|
+
rcap && field("图形验证码", h("div", { className: "dru-captcha" },
|
|
1053
|
+
input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
|
|
1054
|
+
h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
|
|
1055
|
+
rcap.svg && rcap.svg.indexOf("<svg") !== 0 ? rcap.svg : null)
|
|
1056
|
+
)),
|
|
1057
|
+
field("短信验证码", h("div", { className: "dru-captcha" },
|
|
1058
|
+
input({ value: rsms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRsms(e.target.value); } }),
|
|
1059
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendRegSms }, rsmsBtn)
|
|
1060
|
+
)),
|
|
1061
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doRegister }, busy === "register" ? "注册中…" : "注册")
|
|
1062
|
+
),
|
|
1063
|
+
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
|
|
1064
|
+
)
|
|
1065
|
+
)
|
|
1066
|
+
) : card("🔌 自建服务",
|
|
1067
|
+
st && st.config && st.config.mode === "local"
|
|
1068
|
+
? h("div", null,
|
|
1069
|
+
h("div", { className: "dru-status-line" },
|
|
1070
|
+
h("span", { className: "dru-dot dru-dot-on" }),
|
|
1071
|
+
h("span", null, "已连接 " + (st.config.selfHostUrl || "自建服务"))
|
|
1072
|
+
),
|
|
1073
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1074
|
+
"当前使用自建服务(本地认证,无需手机号账号)。切换到云端:在上方「云端服务」标签用手机号账号登录即可。")
|
|
1075
|
+
)
|
|
1076
|
+
: h("div", null,
|
|
1077
|
+
field("服务器地址", input({ value: selfHost, placeholder: "my.example.com:13443", onChange: function (e) { setSelfHost(e.target.value); } })),
|
|
1078
|
+
field("访问密钥", input({ type: "password", value: localKey, placeholder: "自建服务的访问密钥", autoComplete: "off", onChange: function (e) { setLocalKey(e.target.value); } })),
|
|
1079
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: saveLocal }, busy === "local" ? "保存中…" : "切换到自建服务"),
|
|
1080
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
1081
|
+
"服务器地址由你自行部署决定:填写你自建的 dsh-remote 服务地址与访问密钥即可(不是上面的云端地址)。部署方法见 README「自建部署」一节;切回云端随时可登录恢复。")
|
|
1082
|
+
)
|
|
1083
|
+
),
|
|
1084
|
+
// Bridge 状态
|
|
1085
|
+
card("🖥 Bridge 服务",
|
|
1086
|
+
h("div", { className: "dru-status-line" },
|
|
1087
|
+
h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
|
|
1088
|
+
h("span", null, st ? (serviceRunning ? "运行中" : "已停止") : "查询中…"),
|
|
1089
|
+
launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
|
|
1090
|
+
),
|
|
1091
|
+
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
1092
|
+
!serviceRunning
|
|
1093
|
+
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")
|
|
1094
|
+
: h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止 bridge")
|
|
1095
|
+
),
|
|
1096
|
+
h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
|
|
1097
|
+
h("div", { className: "dru-meta" }, st ? (st.service && st.service.plistExists ? "自启动服务已安装" : "自启动服务未安装(启动时自动创建)") : "")
|
|
1098
|
+
),
|
|
1099
|
+
// 关于 dsh-remote(开源项目说明卡片)
|
|
1100
|
+
card("📖 关于 dsh-remote", [
|
|
1101
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "① 为什么推荐用 SaaS:不用自己买服务器、不用折腾部署,装好客户端就能用,最省心。"),
|
|
1102
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "② 会员费去向:付的是网络带宽/服务器成本,也是给开发者的合理支持,让项目持续维护。"),
|
|
1103
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "③ 也可以自建:项目完全开源,有服务器可自行部署,流量走自己的服务器,闭环自控。"),
|
|
1104
|
+
h("div", { className: "dru-hint" }, "④ 一句话总结:简单省心用 SaaS,技术玩家可自建。")
|
|
1105
|
+
]),
|
|
1106
|
+
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程控制" },
|
|
1111
|
+
h("div", { className: "dru-settings-head" },
|
|
1112
|
+
h("span", { className: "dru-settings-icon" }, "🖥"),
|
|
1113
|
+
h("div", null,
|
|
1114
|
+
h("h2", { className: "dru-settings-title" }, "远程控制"),
|
|
1115
|
+
h("div", { className: "dru-settings-sub" }, "通过手机远程控制本机 dsh web")
|
|
1116
|
+
)
|
|
1117
|
+
),
|
|
1118
|
+
h("div", { className: "dru-settings-body" },
|
|
1119
|
+
view === "feedback" ? renderFeedback() : view === "invite" ? renderInvite() : renderHome()
|
|
1120
|
+
)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ── 插件入口 ─────────────────────────────────────────────────────────────
|
|
1125
|
+
var inject = ["slots"];
|
|
1126
|
+
function apply(ctx) {
|
|
1127
|
+
// 面板入口迁移:从侧边栏(sidebar.footer.action)移入「设置」页官方扩展点
|
|
1128
|
+
// settings.section(列表槽,由 ui-settings-general 在 sidebar.settings 下声明)。
|
|
1129
|
+
// order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(🖥 通用远程控制图标)。
|
|
1130
|
+
ctx.slots.inject("settings.section", function () {
|
|
1131
|
+
return ctx.slots.register({
|
|
1132
|
+
name: "settings.section",
|
|
1133
|
+
id: "dsh-remote",
|
|
1134
|
+
order: 30,
|
|
1135
|
+
label: function () { return "🖥 远程控制"; }
|
|
1136
|
+
}, RemoteControlSection);
|
|
1137
|
+
});
|
|
1138
|
+
ctx.slots.inject("shell.overlay", function () {
|
|
1139
|
+
return ctx.slots.register({ name: "shell.overlay", id: "dsh-feedback-popup", order: 90 }, FeedbackPopup);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// 首次安装引导小红点(localStorage dsh-remote-seen-dot;点击后不再显示)
|
|
1143
|
+
dotWatch();
|
|
1144
|
+
|
|
1145
|
+
// 满意度弹窗调度:首次观察到 bridge 运行(即“安装完成并体验”)后约 10 分钟弹出;
|
|
1146
|
+
// 每 60 秒复查一次,避免 dsh web 启动晚于到点时间。
|
|
1147
|
+
api("/dsh-remote/status").then(function (body) {
|
|
1148
|
+
if (body && body.service && body.service.running) {
|
|
1149
|
+
fbEnsureFirstSeen();
|
|
1150
|
+
fbMaybeOpenPopup();
|
|
1151
|
+
}
|
|
1152
|
+
}).catch(function () {});
|
|
1153
|
+
var popupTimer = setInterval(function () {
|
|
1154
|
+
var s = fbPopState();
|
|
1155
|
+
if (!s || s.state !== "pending") { clearInterval(popupTimer); return; }
|
|
1156
|
+
if (!fbFirstSeenAt) {
|
|
1157
|
+
api("/dsh-remote/status").then(function (body) {
|
|
1158
|
+
if (body && body.service && body.service.running) {
|
|
1159
|
+
fbEnsureFirstSeen();
|
|
1160
|
+
fbMaybeOpenPopup();
|
|
1161
|
+
}
|
|
1162
|
+
}).catch(function () {});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
fbMaybeOpenPopup();
|
|
1166
|
+
}, 60 * 1000);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
exports.inject = inject;
|
|
1170
|
+
exports.apply = apply;
|
|
1171
|
+
return module.exports;
|
|
1172
|
+
},
|
|
1173
|
+
});
|