source-code-mgmt 1.1.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 +260 -0
- package/lib/client.js +997 -0
- package/lib/index.js +1601 -0
- package/package.json +22 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* source-code-mgmt — browser half (client.js).
|
|
3
|
+
*
|
|
4
|
+
* Classic-script plugin bundle (no build step): registers into the sidebar
|
|
5
|
+
* footer via the official `sidebar.footer.action` list slot with a negative
|
|
6
|
+
* order so the trigger renders at the bottom of the left rail (above other
|
|
7
|
+
* footer entries). Clicking the trigger opens a "源代码管理" panel with:
|
|
8
|
+
*
|
|
9
|
+
* 1. 环境检查 — git / gh presence & version
|
|
10
|
+
* 2. SSH — key presence, ed25519 generation, github.com ssh config, test
|
|
11
|
+
* 3. 代码 — pick a folder, show repo status, push (auto-.gitignore >100MB),
|
|
12
|
+
* or create a new private cloud repo & push
|
|
13
|
+
*
|
|
14
|
+
* React is a platform module (`require("react")`). Everything is plain
|
|
15
|
+
* createElement — this file runs verbatim as a classic script.
|
|
16
|
+
*/
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: "source-code-mgmt",
|
|
19
|
+
factory: (require) => {
|
|
20
|
+
var module = { exports: {} };
|
|
21
|
+
var exports = module.exports;
|
|
22
|
+
|
|
23
|
+
const PLUGIN_ID = "source-code-mgmt";
|
|
24
|
+
const API = "/api/source-code-mgmt";
|
|
25
|
+
|
|
26
|
+
let React = null;
|
|
27
|
+
try { React = require("react"); } catch {}
|
|
28
|
+
let ReactDOM = null;
|
|
29
|
+
try { ReactDOM = require("react-dom"); } catch {}
|
|
30
|
+
|
|
31
|
+
const { useState, useEffect, useRef, useCallback } = React;
|
|
32
|
+
|
|
33
|
+
// theme tokens (dark/light aware)
|
|
34
|
+
const T = {
|
|
35
|
+
border: "var(--dsw-alias-border-l2, rgba(128,128,128,.35))",
|
|
36
|
+
label: "var(--dsw-alias-label-primary, inherit)",
|
|
37
|
+
secondary: "var(--dsw-alias-label-secondary, rgba(128,128,128,.8))",
|
|
38
|
+
layer1: "var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08))",
|
|
39
|
+
layer2: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,.14))",
|
|
40
|
+
hover: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12))",
|
|
41
|
+
active: "var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.18))",
|
|
42
|
+
success: "var(--dsw-alias-state-success-primary, #22c55e)",
|
|
43
|
+
danger: "var(--dsw-alias-text-danger, #ef4444)",
|
|
44
|
+
warn: "var(--dsw-alias-state-warn-primary, #f59e0b)",
|
|
45
|
+
brand: "var(--dsw-alias-brand-primary, #4f8cff)",
|
|
46
|
+
mask: "var(--dsw-alias-bg-mask-1, rgba(0,0,0,.5))",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ---------- fetch helpers ----------
|
|
50
|
+
async function jget(path) {
|
|
51
|
+
const res = await fetch(API + path);
|
|
52
|
+
if (!res.ok) throw new Error("HTTP " + String(res.status));
|
|
53
|
+
return await res.json();
|
|
54
|
+
}
|
|
55
|
+
async function jpost(path, body) {
|
|
56
|
+
const res = await fetch(API + path, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify(body || {}),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) throw new Error("HTTP " + String(res.status));
|
|
62
|
+
return await res.json();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------- preload cache ----------
|
|
66
|
+
// DSH 打开(插件激活)时就预先检测并缓存,点开「代码管理」面板直接秒显,
|
|
67
|
+
// 不再每次点击重新加载。
|
|
68
|
+
const cache = {
|
|
69
|
+
env: null,
|
|
70
|
+
ssh: null,
|
|
71
|
+
defDir: null,
|
|
72
|
+
repo: null,
|
|
73
|
+
workspaces: [],
|
|
74
|
+
customDirs: [],
|
|
75
|
+
ready: false,
|
|
76
|
+
};
|
|
77
|
+
let preloadPromise = null;
|
|
78
|
+
|
|
79
|
+
/** 预取 env / ssh / default-dir / repo / workspaces,结果写入 cache。可并发安全。 */
|
|
80
|
+
function preload() {
|
|
81
|
+
if (!preloadPromise) {
|
|
82
|
+
preloadPromise = (async () => {
|
|
83
|
+
try {
|
|
84
|
+
const [env, ssh, def, ws] = await Promise.all([
|
|
85
|
+
jget("/env").catch(() => null),
|
|
86
|
+
jget("/ssh").catch(() => null),
|
|
87
|
+
jget("/default-dir").catch(() => null),
|
|
88
|
+
jget("/workspaces").catch(() => null),
|
|
89
|
+
]);
|
|
90
|
+
cache.env = env;
|
|
91
|
+
cache.ssh = ssh;
|
|
92
|
+
if (ws && Array.isArray(ws.workspaces)) {
|
|
93
|
+
cache.workspaces = ws.workspaces;
|
|
94
|
+
}
|
|
95
|
+
if (ws && Array.isArray(ws.customDirs)) {
|
|
96
|
+
cache.customDirs = ws.customDirs;
|
|
97
|
+
}
|
|
98
|
+
if (def && def.dir) {
|
|
99
|
+
cache.defDir = def.dir;
|
|
100
|
+
cache.repo = await jget("/repo?dir=" + encodeURIComponent(def.dir)).catch(() => null);
|
|
101
|
+
}
|
|
102
|
+
cache.ready = true;
|
|
103
|
+
} catch {
|
|
104
|
+
/* 保持部分缓存 */
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
}
|
|
108
|
+
return preloadPromise;
|
|
109
|
+
}
|
|
110
|
+
/** 忽略缓存强制重新拉取某个资源并更新 cache。 */
|
|
111
|
+
async function refreshCache(key) {
|
|
112
|
+
if (key === "env") cache.env = await jget("/env").catch(() => cache.env);
|
|
113
|
+
else if (key === "ssh") cache.ssh = await jget("/ssh").catch(() => cache.ssh);
|
|
114
|
+
else if (key === "workspaces") {
|
|
115
|
+
const w = await jget("/workspaces").catch(() => null);
|
|
116
|
+
if (w && Array.isArray(w.workspaces)) cache.workspaces = w.workspaces;
|
|
117
|
+
if (w && Array.isArray(w.customDirs)) cache.customDirs = w.customDirs;
|
|
118
|
+
}
|
|
119
|
+
else if (key === "repo") {
|
|
120
|
+
cache.repo = await jget("/repo?dir=" + encodeURIComponent(cache.defDir || "")).catch(() => cache.repo);
|
|
121
|
+
}
|
|
122
|
+
return cache[key];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------- small presentational bits ----------
|
|
126
|
+
const h = React.createElement;
|
|
127
|
+
|
|
128
|
+
function Dot({ color }) {
|
|
129
|
+
return h("span", { style: { color, marginRight: 4 } }, "●");
|
|
130
|
+
}
|
|
131
|
+
function Field({ label, value, children }) {
|
|
132
|
+
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
|
|
133
|
+
h("span", { style: { color: T.secondary, fontSize: 13, width: 84, flexShrink: 0 } }, label),
|
|
134
|
+
children ?? h("span", { style: { color: T.label, fontSize: 13, fontWeight: 500 } }, value ?? "")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function Btn({ label, onClick, tone, disabled, wide, noBg }) {
|
|
138
|
+
const bg =
|
|
139
|
+
noBg ? "transparent"
|
|
140
|
+
: tone === "primary" ? T.brand
|
|
141
|
+
: "transparent";
|
|
142
|
+
const color =
|
|
143
|
+
tone === "primary" ? (noBg ? T.brand : "#fff")
|
|
144
|
+
: tone === "danger" ? T.danger
|
|
145
|
+
: tone === "success" ? T.success
|
|
146
|
+
: T.label;
|
|
147
|
+
return h("button", {
|
|
148
|
+
type: "button",
|
|
149
|
+
disabled: !!disabled,
|
|
150
|
+
onClick,
|
|
151
|
+
style: {
|
|
152
|
+
appearance: "none", font: "inherit", cursor: disabled ? "not-allowed" : "pointer",
|
|
153
|
+
border: "1px solid " + (tone === "danger" ? T.danger : T.border),
|
|
154
|
+
borderRadius: 8, padding: "5px 14px", fontSize: 13, lineHeight: 1.5,
|
|
155
|
+
background: bg,
|
|
156
|
+
color,
|
|
157
|
+
opacity: disabled ? 0.5 : 1,
|
|
158
|
+
flex: wide ? "1" : undefined,
|
|
159
|
+
},
|
|
160
|
+
}, label);
|
|
161
|
+
}
|
|
162
|
+
function Box({ title, children }) {
|
|
163
|
+
return h("div", { style: { border: "1px solid " + T.border, borderRadius: 12, padding: 14 } },
|
|
164
|
+
h("div", { style: { fontSize: 13, fontWeight: 600, color: T.label, marginBottom: 10 } }, title),
|
|
165
|
+
children
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function pre(code) {
|
|
169
|
+
return h("pre", { style: { whiteSpace: "pre-wrap", fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.6, color: T.secondary, margin: 0 } }, code);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------- section 1: 环境检查 ----------
|
|
173
|
+
function EnvSection() {
|
|
174
|
+
const [env, setEnv] = useState(() => cache.env);
|
|
175
|
+
const [err, setErr] = useState(null);
|
|
176
|
+
const load = useCallback(async () => {
|
|
177
|
+
setErr(null);
|
|
178
|
+
// 先读缓存(DSH 打开时已预取),再后台刷新保持最新
|
|
179
|
+
if (cache.env) setEnv(cache.env);
|
|
180
|
+
try { setEnv(await refreshCache("env")); }
|
|
181
|
+
catch (e) { setErr(String(e)); }
|
|
182
|
+
}, []);
|
|
183
|
+
useEffect(() => { void preload().then(load); }, [load]);
|
|
184
|
+
|
|
185
|
+
return h(Box, { title: "① 环境检查" },
|
|
186
|
+
h(Field, { label: "操作系统" }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
187
|
+
env ? h(React.Fragment, null,
|
|
188
|
+
h(Field, { label: "Git", value: env.git.installed ? "✅ " + env.git.version : "❌ 未安装" }),
|
|
189
|
+
h(Field, { label: "GitHub CLI", value: env.gh.installed ? "✅ " + env.gh.version : "❌ 未安装" }),
|
|
190
|
+
h(Field, { label: "SSH", value: env.ssh && env.ssh.installed ? "✅ 已找到" : "❌ 未找到" })
|
|
191
|
+
) : h(Field, { label: "检测中", value: err ? "失败: " + err : "…" }),
|
|
192
|
+
(!env || !env.git.installed || !env.gh.installed) ? h("div", { style: { marginTop: 8 } },
|
|
193
|
+
h("span", { style: { color: T.warn, fontSize: 12 } },
|
|
194
|
+
"未安装工具请先安装 Git for Windows 和 GitHub CLI,然后重启。" + (err ? "(" + err + ")" : ""))
|
|
195
|
+
) : h(Btn, { label: "重新检查", onClick: load, tone: "ghost" })
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------- section 2: SSH ----------
|
|
200
|
+
function SshSection({ provider, setProvider }) {
|
|
201
|
+
const [ssh, setSsh] = useState(() => cache.ssh);
|
|
202
|
+
const [busy, setBusy] = useState(false);
|
|
203
|
+
const [msg, setMsg] = useState(null);
|
|
204
|
+
const [err, setErr] = useState(null);
|
|
205
|
+
// 平台状态由 ScmPanel 共享(②里选择,③跟随)
|
|
206
|
+
const prov = provider || "github";
|
|
207
|
+
const setProv = setProvider || (() => {});
|
|
208
|
+
|
|
209
|
+
const load = useCallback(async () => {
|
|
210
|
+
setErr(null);
|
|
211
|
+
if (cache.ssh) setSsh(cache.ssh);
|
|
212
|
+
try { setSsh(await refreshCache("ssh")); }
|
|
213
|
+
catch (e) { setErr(String(e)); }
|
|
214
|
+
}, []);
|
|
215
|
+
useEffect(() => { void preload().then(load); }, [load]);
|
|
216
|
+
|
|
217
|
+
const act = useCallback(async (path, label, payload) => {
|
|
218
|
+
setBusy(true); setMsg(null); setErr(null);
|
|
219
|
+
try {
|
|
220
|
+
const r = await jpost(path, payload);
|
|
221
|
+
if (r.alreadyExists || r.alreadyConfigured) setMsg(label + ":已存在,无需重复操作");
|
|
222
|
+
else if (r.ok) setMsg(label + "成功");
|
|
223
|
+
else setErr(r.error || label + "失败");
|
|
224
|
+
void load();
|
|
225
|
+
} catch (e) { setErr(label + "失败:" + e); }
|
|
226
|
+
finally { setBusy(false); }
|
|
227
|
+
}, [load]);
|
|
228
|
+
|
|
229
|
+
// 依据所选平台判断各自的 SSH config 是否已配置
|
|
230
|
+
const providerConfigured = prov === "gitee"
|
|
231
|
+
? !!(ssh && ssh.sshGiteeConfigured)
|
|
232
|
+
: !!(ssh && ssh.sshGitHubConfigured);
|
|
233
|
+
const providerHost = prov === "gitee" ? "gitee.com" : "github.com";
|
|
234
|
+
|
|
235
|
+
return h(Box, { title: "② SSH 密钥与连接" },
|
|
236
|
+
// 平台选择(默认 GitHub),③代码管理跟随
|
|
237
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 10 } },
|
|
238
|
+
h("span", { style: { color: T.secondary, fontSize: 13, width: 84, flexShrink: 0 } }, "平台"),
|
|
239
|
+
h("select", {
|
|
240
|
+
value: prov,
|
|
241
|
+
onChange: (e) => setProv(e.target.value),
|
|
242
|
+
title: "选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换",
|
|
243
|
+
style: { font: "inherit", fontSize: 13, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
244
|
+
},
|
|
245
|
+
h("option", { value: "github" }, "GitHub(默认)"),
|
|
246
|
+
h("option", { value: "gitee" }, "Gitee"),
|
|
247
|
+
)
|
|
248
|
+
),
|
|
249
|
+
// key status
|
|
250
|
+
h(Field, { label: "密钥" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
251
|
+
ssh ? (ssh.hasKey ? "✅ id_ed25519 已生成" : "⚠️ 未生成") : "…")
|
|
252
|
+
),
|
|
253
|
+
h(Field, { label: "GH 登录" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
254
|
+
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || "已登录") : "⚠️ 未登录") : "…")
|
|
255
|
+
),
|
|
256
|
+
h(Field, { label: "SSH 配置" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
257
|
+
ssh ? (providerConfigured ? "✅ " + providerHost + " 已配置(443)" : "⚠️ 未配置/限制 22 端口需配置") : "…")
|
|
258
|
+
),
|
|
259
|
+
|
|
260
|
+
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 } },
|
|
261
|
+
h(Btn, { label: "生成密钥", onClick: () => act("/gen-key", "生成密钥"), disabled: busy }),
|
|
262
|
+
h(Btn, { label: "配置 SSH(config)", onClick: () => act("/write-config", "配置 SSH", { provider: prov }), disabled: busy }),
|
|
263
|
+
h(Btn, { label: "测试连接", onClick: async () => {
|
|
264
|
+
setBusy(true); setErr(null); setMsg(null);
|
|
265
|
+
try {
|
|
266
|
+
const r = await jpost("/ssh-test", { provider: prov });
|
|
267
|
+
if (r.connected) setMsg("SSH 连接成功(" + (prov === "gitee" ? "Gitee" : "GitHub") + "):" + (r.account ? "Hi " + r.account : "已认证"));
|
|
268
|
+
else setErr("连接失败,请确认密钥已上传到 " + (prov === "gitee" ? "Gitee" : "GitHub") + " 或已登录 gh");
|
|
269
|
+
void load();
|
|
270
|
+
} catch (e) { setErr("测试失败:" + e); }
|
|
271
|
+
finally { setBusy(false); }
|
|
272
|
+
}, tone: "primary", noBg: true, disabled: busy }),
|
|
273
|
+
),
|
|
274
|
+
ssh && ssh.pubContent ? h("div", { style: { marginTop: 10 } },
|
|
275
|
+
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, "公钥(复制上传到 " + (prov === "gitee" ? "Gitee → 设置 → SSH 公钥" : "GitHub → Settings → SSH keys") + ",或运行 gh auth login 自动上传):"),
|
|
276
|
+
h("code", { style: { display: "block", whiteSpace: "pre-wrap", wordBreak: "break-all", fontSize: 11, lineHeight: 1.5, color: T.secondary, background: T.layer1, padding: 10, borderRadius: 8 } }, ssh.pubContent)
|
|
277
|
+
) : null,
|
|
278
|
+
msg ? h("div", { style: { marginTop: 10, color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
279
|
+
err ? h("div", { style: { marginTop: 10, color: T.danger, fontSize: 13 } }, "❌ " + err) : null,
|
|
280
|
+
(!ssh || ssh.gitHubNotConfigured) ? null : null
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------- section 3: 代码管理 ----------
|
|
285
|
+
function RepoSection({ provider }) {
|
|
286
|
+
// 平台(来自②,默认 GitHub),③随其切换检测/创建等逻辑
|
|
287
|
+
const prov = provider || "github";
|
|
288
|
+
const [dir, setDir] = useState(() => cache.defDir ?? "");
|
|
289
|
+
const [dirDraft, setDirDraft] = useState(() => cache.defDir ?? "");
|
|
290
|
+
const [repo, setRepo] = useState(() => cache.repo);
|
|
291
|
+
const [busy, setBusy] = useState(false);
|
|
292
|
+
const [result, setResult] = useState(null);
|
|
293
|
+
const [msg, setMsg] = useState(null);
|
|
294
|
+
const [err, setErr] = useState(null);
|
|
295
|
+
const [visibility, setVisibility] = useState("private");
|
|
296
|
+
const [workspaces, setWorkspaces] = useState(() => cache.workspaces || []);
|
|
297
|
+
// Gitee 令牌相关(仅在 prov==='gitee' 时显示/使用)
|
|
298
|
+
const [giteeToken, setGiteeToken] = useState("");
|
|
299
|
+
const [giteeConfigured, setGiteeConfigured] = useState(null); // null=未加载, true/false
|
|
300
|
+
const [giteeOwnerName, setGiteeOwnerName] = useState("");
|
|
301
|
+
const [giteeTokenMsg, setGiteeTokenMsg] = useState(null);
|
|
302
|
+
const [giteeTokenErr, setGiteeTokenErr] = useState(null);
|
|
303
|
+
const [giteeTokenBusy, setGiteeTokenBusy] = useState(false);
|
|
304
|
+
// 自定义目录(用户手动选择的非 DSH 工作区,持久化于插件本地),用于给下拉项加 X 删除
|
|
305
|
+
const [customDirs, setCustomDirs] = useState(() => cache.customDirs || []);
|
|
306
|
+
// 详情弹窗:'changes'(改动文件列表)| 'sync'(同步差异)| null(关闭)
|
|
307
|
+
const [detail, setDetail] = useState(null);
|
|
308
|
+
// 仓库名称强制 = 文件夹名(不允许手动填写)
|
|
309
|
+
const repoName = (repo && repo.defaultRepoName) || "";
|
|
310
|
+
// 目录选择器状态
|
|
311
|
+
const [pickOpen, setPickOpen] = useState(false);
|
|
312
|
+
const [pickPath, setPickPath] = useState("");
|
|
313
|
+
const [pickErr, setPickErr] = useState(null);
|
|
314
|
+
const [picking, setPicking] = useState(false);
|
|
315
|
+
|
|
316
|
+
// 平台 ref:让各 useCallback(空依赖)能读取最新 provider,避免大量依赖改写
|
|
317
|
+
const provRef = useRef(prov);
|
|
318
|
+
provRef.current = prov;
|
|
319
|
+
|
|
320
|
+
const loadRepo = useCallback(async (d, keepRepo) => {
|
|
321
|
+
setErr(null);
|
|
322
|
+
if (!keepRepo) setRepo(null);
|
|
323
|
+
try {
|
|
324
|
+
const p = provRef.current || "github";
|
|
325
|
+
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p));
|
|
326
|
+
setRepo(r);
|
|
327
|
+
// 默认选中当前工作区:/repo 在未传 dir 时返回当前工作区路径,
|
|
328
|
+
// 把返回的实际目录同步到输入框和当前选中目录。
|
|
329
|
+
if (r && r.dir) {
|
|
330
|
+
setDirDraft(r.dir);
|
|
331
|
+
setDir(r.dir);
|
|
332
|
+
cache.defDir = r.dir;
|
|
333
|
+
}
|
|
334
|
+
// 缓存最新 repo 状态
|
|
335
|
+
cache.repo = r;
|
|
336
|
+
// 仓库已存在且能读到实际可见性时,让下拉框默认选中实际状态,
|
|
337
|
+
// 这样「修改仓库状态」按钮只在用户主动切换时出现。
|
|
338
|
+
if (r && r.repoExists === true && (r.visibility === "public" || r.visibility === "private")) {
|
|
339
|
+
setVisibility(r.visibility);
|
|
340
|
+
}
|
|
341
|
+
if (r && !r.isGitRepo) setErr(r.error || "该目录不是 git 仓库");
|
|
342
|
+
} catch (e) { setErr(String(e)); }
|
|
343
|
+
}, []);
|
|
344
|
+
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
// 首次加载:先看缓存(DSH 打开时已预取当前工作区),秒显;
|
|
347
|
+
// 无缓存则 preload(取默认工作区)后加载。
|
|
348
|
+
if (cache.repo) {
|
|
349
|
+
setRepo(cache.repo);
|
|
350
|
+
if (cache.defDir) { setDir(cache.defDir); setDirDraft(cache.defDir); }
|
|
351
|
+
}
|
|
352
|
+
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
353
|
+
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
354
|
+
void preload().then(() => {
|
|
355
|
+
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
356
|
+
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
357
|
+
// 默认工作区:优先 defDir,否则第一个工作区
|
|
358
|
+
const d = cache.defDir
|
|
359
|
+
|| (cache.workspaces && cache.workspaces[0])
|
|
360
|
+
|| dir || "";
|
|
361
|
+
if (d) { setDir(d); setDirDraft(d); loadRepo(d); }
|
|
362
|
+
});
|
|
363
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
// ---- Gitee 令牌:查询/保存/清除(仅 gitee 模式使用)----
|
|
367
|
+
const loadGiteeTokenStatus = useCallback(async () => {
|
|
368
|
+
try {
|
|
369
|
+
const r = await jget("/gitee-token");
|
|
370
|
+
setGiteeConfigured(!!r.configured);
|
|
371
|
+
setGiteeOwnerName(r.owner || "");
|
|
372
|
+
setGiteeTokenMsg(null);
|
|
373
|
+
setGiteeTokenErr(null);
|
|
374
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("读取令牌状态失败:" + e); }
|
|
375
|
+
}, []);
|
|
376
|
+
|
|
377
|
+
const saveGiteeToken = useCallback(async () => {
|
|
378
|
+
setGiteeTokenBusy(true); setGiteeTokenErr(null); setGiteeTokenMsg(null);
|
|
379
|
+
try {
|
|
380
|
+
const r = await jpost("/gitee-token", { token: giteeToken });
|
|
381
|
+
if (r.ok) {
|
|
382
|
+
setGiteeConfigured(true);
|
|
383
|
+
setGiteeOwnerName(r.owner || "");
|
|
384
|
+
setGiteeToken("");
|
|
385
|
+
setGiteeTokenMsg("已保存 Gitee 令牌(账号:" + (r.owner || "?") + ")");
|
|
386
|
+
// 令牌就绪后刷新仓库,让同名检测等按 Gitee 生效
|
|
387
|
+
loadRepo(dir || cache.defDir || "", true);
|
|
388
|
+
} else {
|
|
389
|
+
setGiteeConfigured(false);
|
|
390
|
+
setGiteeTokenErr(r.error || "保存令牌失败");
|
|
391
|
+
}
|
|
392
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("保存令牌失败:" + e); }
|
|
393
|
+
finally { setGiteeTokenBusy(false); }
|
|
394
|
+
}, [giteeToken, dir, loadRepo]);
|
|
395
|
+
|
|
396
|
+
const clearGiteeToken = useCallback(async () => {
|
|
397
|
+
setGiteeTokenBusy(true); setGiteeTokenErr(null); setGiteeTokenMsg(null);
|
|
398
|
+
try {
|
|
399
|
+
const r = await jpost("/gitee-token", { clear: true });
|
|
400
|
+
setGiteeConfigured(false);
|
|
401
|
+
setGiteeOwnerName("");
|
|
402
|
+
setGiteeToken("");
|
|
403
|
+
setGiteeTokenMsg(r.ok ? "已清除 Gitee 令牌" : "清除失败");
|
|
404
|
+
loadRepo(dir || cache.defDir || "", true);
|
|
405
|
+
} catch (e) { setGiteeTokenErr("清除令牌失败:" + e); }
|
|
406
|
+
finally { setGiteeTokenBusy(false); }
|
|
407
|
+
}, [dir, loadRepo]);
|
|
408
|
+
|
|
409
|
+
// 平台切换:gitee 时读取令牌状态;无论切到哪都刷新仓库(跟随平台检测/显示)
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (prov === "gitee") void loadGiteeTokenStatus();
|
|
412
|
+
if (dir || cache.defDir) loadRepo(dir || cache.defDir || "", true);
|
|
413
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
414
|
+
}, [prov]);
|
|
415
|
+
|
|
416
|
+
const push = useCallback(async () => {
|
|
417
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
418
|
+
try {
|
|
419
|
+
const r = await jpost("/push", { dir: dir || undefined });
|
|
420
|
+
setResult(r);
|
|
421
|
+
if (r.ok) setMsg("已推送");
|
|
422
|
+
else setErr(r.error || r.pushError || "推送失败");
|
|
423
|
+
void loadRepo(dir, true);
|
|
424
|
+
} catch (e) { setErr("推送失败:" + e); }
|
|
425
|
+
finally { setBusy(false); }
|
|
426
|
+
}, [dir, loadRepo]);
|
|
427
|
+
|
|
428
|
+
const pull = useCallback(async () => {
|
|
429
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
430
|
+
try {
|
|
431
|
+
const r = await jpost("/pull", { dir: dir || undefined });
|
|
432
|
+
setResult(r);
|
|
433
|
+
if (r.ok) setMsg(r.upToDate ? "已是最新,无需拉取" : "拉取成功,已更新到最新");
|
|
434
|
+
else setErr(r.error || "拉取失败");
|
|
435
|
+
void loadRepo(dir, true);
|
|
436
|
+
} catch (e) { setErr("拉取失败:" + e); }
|
|
437
|
+
finally { setBusy(false); }
|
|
438
|
+
}, [dir, loadRepo]);
|
|
439
|
+
|
|
440
|
+
const mergePush = useCallback(async () => {
|
|
441
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
442
|
+
try {
|
|
443
|
+
const r = await jpost("/merge-push", { dir: dir || undefined });
|
|
444
|
+
setResult(r);
|
|
445
|
+
if (r.ok) setMsg("已拉取远程更新并推送更改");
|
|
446
|
+
else setErr(r.error || "合并推送失败");
|
|
447
|
+
void loadRepo(dir, true);
|
|
448
|
+
} catch (e) { setErr("合并推送失败:" + e); }
|
|
449
|
+
finally { setBusy(false); }
|
|
450
|
+
}, [dir, loadRepo]);
|
|
451
|
+
|
|
452
|
+
const forcePush = useCallback(async () => {
|
|
453
|
+
if (!window.confirm("强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?")) return;
|
|
454
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
455
|
+
try {
|
|
456
|
+
const r = await jpost("/force-push", { dir: dir || undefined });
|
|
457
|
+
setResult(r);
|
|
458
|
+
if (r.ok) setMsg("已强制推送,远程已更新为本地状态");
|
|
459
|
+
else setErr(r.error || "强制推送失败");
|
|
460
|
+
void loadRepo(dir, true);
|
|
461
|
+
} catch (e) { setErr("强制推送失败:" + e); }
|
|
462
|
+
finally { setBusy(false); }
|
|
463
|
+
}, [dir, loadRepo]);
|
|
464
|
+
|
|
465
|
+
const forcePull = useCallback(async () => {
|
|
466
|
+
if (!window.confirm("强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?")) return;
|
|
467
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
468
|
+
try {
|
|
469
|
+
const r = await jpost("/force-pull", { dir: dir || undefined });
|
|
470
|
+
setResult(r);
|
|
471
|
+
if (r.ok) setMsg("已强制拉取远程更新");
|
|
472
|
+
else setErr(r.error || "强制拉取失败");
|
|
473
|
+
void loadRepo(dir, true);
|
|
474
|
+
} catch (e) { setErr("强制拉取失败:" + e); }
|
|
475
|
+
finally { setBusy(false); }
|
|
476
|
+
}, [dir, loadRepo]);
|
|
477
|
+
|
|
478
|
+
const changeVisibility = useCallback(async () => {
|
|
479
|
+
const name = (repo && repo.defaultRepoName) || "";
|
|
480
|
+
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
481
|
+
const target = visibility === "public" ? "public" : "private";
|
|
482
|
+
if (!window.confirm("确定把仓库 " + name + " 改为「" + (target === "public" ? "公开" : "私有") + "」?修改可见性可能影响 Star、关注者等。")) return;
|
|
483
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
484
|
+
try {
|
|
485
|
+
const r = await jpost("/set-visibility", { dir: dir || undefined, name, visibility: target, provider: provRef.current || "github" });
|
|
486
|
+
setResult(r);
|
|
487
|
+
if (r.ok) setMsg("已把仓库设置为「" + (target === "public" ? "公开" : "私有") + "」");
|
|
488
|
+
else setErr(r.error || "修改可见性失败");
|
|
489
|
+
void loadRepo(dir, true);
|
|
490
|
+
} catch (e) { setErr("修改可见性失败:" + e); }
|
|
491
|
+
finally { setBusy(false); }
|
|
492
|
+
}, [dir, repo, visibility, loadRepo]);
|
|
493
|
+
|
|
494
|
+
const create = useCallback(async () => {
|
|
495
|
+
const name = (repo && repo.defaultRepoName) || "";
|
|
496
|
+
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
497
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
498
|
+
try {
|
|
499
|
+
const r = await jpost("/create", { dir: dir || undefined, name, visibility, provider: provRef.current || "github" });
|
|
500
|
+
setResult(r);
|
|
501
|
+
if (r.ok) setMsg("仓库已创建并推送:" + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
502
|
+
else setErr(r.error || "创建失败");
|
|
503
|
+
void loadRepo(dir, true);
|
|
504
|
+
} catch (e) { setErr("创建失败:" + e); }
|
|
505
|
+
finally { setBusy(false); }
|
|
506
|
+
}, [dir, repo, visibility, loadRepo]);
|
|
507
|
+
|
|
508
|
+
// 打开目录选择器,预填当前目录
|
|
509
|
+
const openPicker = useCallback(() => {
|
|
510
|
+
setPickPath(dir || cache.defDir || "");
|
|
511
|
+
setPickErr(null);
|
|
512
|
+
setPickOpen(true);
|
|
513
|
+
}, [dir]);
|
|
514
|
+
|
|
515
|
+
// 调用宿主原生文件夹选择对话框,回填路径
|
|
516
|
+
const browseDir = useCallback(async () => {
|
|
517
|
+
setPicking(true); setPickErr(null);
|
|
518
|
+
try {
|
|
519
|
+
const r = await jpost("/pick-dir", { initial: pickPath || cache.defDir || undefined });
|
|
520
|
+
if (r.ok && r.dir) setPickPath(r.dir);
|
|
521
|
+
else setPickErr(r.error || "未选择目录");
|
|
522
|
+
} catch (e) { setPickErr("选择失败:" + e); }
|
|
523
|
+
finally { setPicking(false); }
|
|
524
|
+
}, [pickPath]);
|
|
525
|
+
|
|
526
|
+
// 确认:持久化加入列表 + 加载
|
|
527
|
+
const confirmPick = useCallback(async () => {
|
|
528
|
+
const path = String(pickPath || "").trim();
|
|
529
|
+
if (!path) { setPickErr("请输入或选择目录路径"); return; }
|
|
530
|
+
setPicking(true); setPickErr(null);
|
|
531
|
+
try {
|
|
532
|
+
const r = await jpost("/add-workspace", { dir: path });
|
|
533
|
+
if (r.ok) {
|
|
534
|
+
if (Array.isArray(r.workspaces)) setWorkspaces(r.workspaces);
|
|
535
|
+
if (Array.isArray(r.customDirs)) setCustomDirs(r.customDirs);
|
|
536
|
+
setDir(r.dir); setDirDraft(r.dir);
|
|
537
|
+
cache.defDir = r.dir;
|
|
538
|
+
// 切换到新目录,清空上一个目录的操作结果/提示。
|
|
539
|
+
setResult(null); setMsg(null); setErr(null);
|
|
540
|
+
setPickOpen(false);
|
|
541
|
+
loadRepo(r.dir);
|
|
542
|
+
} else {
|
|
543
|
+
setPickErr(r.error || "添加目录失败");
|
|
544
|
+
}
|
|
545
|
+
} catch (e) { setPickErr("添加目录失败:" + e); }
|
|
546
|
+
finally { setPicking(false); }
|
|
547
|
+
}, [pickPath, loadRepo]);
|
|
548
|
+
|
|
549
|
+
const cancelPick = useCallback(() => {
|
|
550
|
+
setPickOpen(false); setPickErr(null); setPickPath("");
|
|
551
|
+
}, []);
|
|
552
|
+
|
|
553
|
+
// 删除下拉中的自定义目录记录(只删记录,不删实际文件夹)
|
|
554
|
+
const removeWorkspace = useCallback(async (path) => {
|
|
555
|
+
setBusy(true); setErr(null);
|
|
556
|
+
try {
|
|
557
|
+
const r = await jpost("/remove-workspace", { dir: path });
|
|
558
|
+
if (r.ok) {
|
|
559
|
+
if (Array.isArray(r.workspaces)) setWorkspaces(r.workspaces);
|
|
560
|
+
if (Array.isArray(r.customDirs)) setCustomDirs(r.customDirs);
|
|
561
|
+
cache.workspaces = r.workspaces || cache.workspaces;
|
|
562
|
+
cache.customDirs = r.customDirs || cache.customDirs;
|
|
563
|
+
// 如果删除的是当前选中目录,清空选中并刷新为默认工作区
|
|
564
|
+
if (dir === path) {
|
|
565
|
+
const next = (r.workspaces && r.workspaces[0]) || "";
|
|
566
|
+
setDir(next); setDirDraft(next);
|
|
567
|
+
if (next) loadRepo(next); else setRepo(null);
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
setErr(r.error || "删除失败");
|
|
571
|
+
}
|
|
572
|
+
} catch (e) { setErr("删除失败:" + e); }
|
|
573
|
+
finally { setBusy(false); }
|
|
574
|
+
}, [dir, loadRepo]);
|
|
575
|
+
|
|
576
|
+
// 强制对齐:本地完全重置为远程分支状态(丢弃本地差异)
|
|
577
|
+
const align = useCallback(async () => {
|
|
578
|
+
if (!window.confirm("强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?")) return;
|
|
579
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
580
|
+
try {
|
|
581
|
+
const r = await jpost("/align", { dir: dir || undefined });
|
|
582
|
+
setResult(r);
|
|
583
|
+
if (r.ok) setMsg("已强制对齐到远程分支");
|
|
584
|
+
else setErr(r.error || "强制对齐失败");
|
|
585
|
+
void loadRepo(dir);
|
|
586
|
+
} catch (e) { setErr("强制对齐失败:" + e); }
|
|
587
|
+
finally { setBusy(false); }
|
|
588
|
+
}, [dir, loadRepo]);
|
|
589
|
+
|
|
590
|
+
// 只创建 git 仓库(不拉取不推送),由用户自行决定下一步
|
|
591
|
+
const initGit = useCallback(async () => {
|
|
592
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
593
|
+
try {
|
|
594
|
+
const r = await jpost("/init-git", { dir: dir || undefined });
|
|
595
|
+
setResult(r);
|
|
596
|
+
if (r.ok) setMsg(r.alreadyRepo ? "已是 git 仓库" : "已创建 git 仓库,请自行拉取或推送");
|
|
597
|
+
else setErr(r.error || "创建 git 失败");
|
|
598
|
+
void loadRepo(dir);
|
|
599
|
+
} catch (e) { setErr("创建 git 失败:" + e); }
|
|
600
|
+
finally { setBusy(false); }
|
|
601
|
+
}, [dir, loadRepo]);
|
|
602
|
+
|
|
603
|
+
return h(Box, { title: "③ 代码管理" },
|
|
604
|
+
// 平台提示 + Gitee 令牌(仅 gitee 模式)
|
|
605
|
+
h("div", { style: { marginBottom: 10, fontSize: 12, color: T.secondary, lineHeight: 1.6 } },
|
|
606
|
+
"当前平台:<" + (prov === "gitee" ? "Gitee" : "GitHub") + ">(在 ② SSH 里切换,③ 的同名检测 / 新建仓库 / 可见性会跟随)。"),
|
|
607
|
+
prov === "gitee" ? h("div", { style: { border: "1px solid " + T.border, borderRadius: 10, padding: 10, marginBottom: 10, background: T.layer1 } },
|
|
608
|
+
h("div", { style: { fontSize: 12, fontWeight: 600, color: T.label, marginBottom: 6 } },
|
|
609
|
+
"Gitee 私人令牌(OpenAPI,需 projects 权限)"),
|
|
610
|
+
giteeConfigured === true
|
|
611
|
+
? h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
612
|
+
h("span", { style: { color: T.success, fontSize: 12 } }, "✅ 已配置" + (giteeOwnerName ? "(账号 " + giteeOwnerName + ")" : "")),
|
|
613
|
+
h(Btn, { label: "清除令牌", onClick: clearGiteeToken, disabled: giteeTokenBusy, noBg: true, tone: "danger" })
|
|
614
|
+
)
|
|
615
|
+
: h("div", null,
|
|
616
|
+
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
617
|
+
h("input", {
|
|
618
|
+
type: "password", value: giteeToken, disabled: giteeTokenBusy,
|
|
619
|
+
placeholder: "粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)",
|
|
620
|
+
onChange: (e) => setGiteeToken(e.target.value),
|
|
621
|
+
onKeyDown: (e) => { if (e.key === "Enter") void saveGiteeToken(); },
|
|
622
|
+
style: { flex: 1, minWidth: 200, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,.14))", color: T.label, outline: "none" },
|
|
623
|
+
}),
|
|
624
|
+
h(Btn, { label: "保存令牌", onClick: saveGiteeToken, tone: "primary", noBg: true, disabled: giteeTokenBusy || !String(giteeToken || "").trim() }),
|
|
625
|
+
),
|
|
626
|
+
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 6, lineHeight: 1.6 } },
|
|
627
|
+
"令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。")
|
|
628
|
+
),
|
|
629
|
+
giteeTokenMsg ? h("div", { style: { marginTop: 6, color: T.success, fontSize: 12 } }, "✅ " + giteeTokenMsg) : null,
|
|
630
|
+
giteeTokenErr ? h("div", { style: { marginTop: 6, color: T.danger, fontSize: 12 } }, "❌ " + giteeTokenErr) : null
|
|
631
|
+
) : null,
|
|
632
|
+
h("div", { style: { display: "flex", gap: 8, marginBottom: 10, alignItems: "center" } },
|
|
633
|
+
h("span", { style: { color: T.secondary, fontSize: 13, whiteSpace: "nowrap" } }, "选择工作区"),
|
|
634
|
+
h("select", {
|
|
635
|
+
value: dir,
|
|
636
|
+
disabled: workspaces.length === 0,
|
|
637
|
+
onChange: (e) => {
|
|
638
|
+
const v = e.target.value;
|
|
639
|
+
if (!v) return;
|
|
640
|
+
// 切换工作区时清空上一个工作区的操作结果/提示。
|
|
641
|
+
setResult(null); setMsg(null); setErr(null);
|
|
642
|
+
setDir(v);
|
|
643
|
+
setDirDraft(v);
|
|
644
|
+
loadRepo(v);
|
|
645
|
+
},
|
|
646
|
+
title: "选择 DSH 已登记的工作区文件夹",
|
|
647
|
+
style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
648
|
+
},
|
|
649
|
+
workspaces.length === 0 ? h("option", { key: "__empty", value: "" }, "(暂无可选工作区)")
|
|
650
|
+
: h("option", { key: "__none", value: "" }, "— 请选择 —"),
|
|
651
|
+
workspaces.map((w) =>
|
|
652
|
+
h("option", { key: w, value: w }, String(w).split(/[\\/]/).filter(Boolean).pop() || w)
|
|
653
|
+
)
|
|
654
|
+
),
|
|
655
|
+
h(Btn, { label: "选择目录", onClick: openPicker, disabled: picking, noBg: true, tone: "primary" }),
|
|
656
|
+
),
|
|
657
|
+
dir && customDirs && customDirs.includes(dir) ? h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10, alignItems: "center" } },
|
|
658
|
+
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, "自定义目录:"),
|
|
659
|
+
h("span", { key: dir, style: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", border: "1px solid " + T.border, borderRadius: 12, fontSize: 12, background: T.layer1, color: T.label, cursor: "pointer" }, title: dir, onClick: () => { setResult(null); setMsg(null); setErr(null); setDir(dir); setDirDraft(dir); loadRepo(dir); } },
|
|
660
|
+
String(dir).split(/[\\/]/).filter(Boolean).pop() || dir,
|
|
661
|
+
h("span", {
|
|
662
|
+
role: "button", "aria-label": "删除该目录记录",
|
|
663
|
+
title: "删除该目录记录(不删除实际文件夹)",
|
|
664
|
+
onClick: (e) => { e.stopPropagation(); void removeWorkspace(dir); },
|
|
665
|
+
style: { color: T.danger, fontWeight: 700, padding: "0 2px", cursor: "pointer" },
|
|
666
|
+
}, "✕")
|
|
667
|
+
)
|
|
668
|
+
) : null,
|
|
669
|
+
repo && repo.isGitRepo ? h("div", { style: { marginBottom: 10 } },
|
|
670
|
+
h(Field, { label: "目录", value: repo.dir }),
|
|
671
|
+
h(Field, { label: "分支", value: repo.branch }),
|
|
672
|
+
h(Field, { label: "远程", value: repo.remoteUrl || "(无)" }),
|
|
673
|
+
h(Field, { label: "改动" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
674
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + " 个文件" : "无"),
|
|
675
|
+
repo.changedFiles && repo.changedFiles.length > 0 ? h("button", {
|
|
676
|
+
type: "button",
|
|
677
|
+
onClick: () => setDetail("changes"),
|
|
678
|
+
title: "查看改动的文件列表",
|
|
679
|
+
style: changeBtnStyle(),
|
|
680
|
+
}, "查看")
|
|
681
|
+
: null
|
|
682
|
+
)),
|
|
683
|
+
h(Field, { label: "同步" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
684
|
+
h("span", { style: { color: T.label, fontSize: 13 } },
|
|
685
|
+
(repo.ahead > 0 ? "本地领先 " + repo.ahead + " 提交" : "") +
|
|
686
|
+
(repo.ahead > 0 && repo.behind > 0 ? "、落后 " + repo.behind + " 提交" : (repo.behind > 0 ? "落后 " + repo.behind + " 提交" : "")) +
|
|
687
|
+
((repo.ahead === 0 && repo.behind === 0) ? "与远程一致" : "")
|
|
688
|
+
),
|
|
689
|
+
(repo.ahead > 0 || repo.behind > 0) ? h("button", {
|
|
690
|
+
type: "button",
|
|
691
|
+
onClick: () => setDetail("sync"),
|
|
692
|
+
title: "查看本地与远程的提交差异",
|
|
693
|
+
style: changeBtnStyle(),
|
|
694
|
+
}, "查看")
|
|
695
|
+
: null
|
|
696
|
+
)),
|
|
697
|
+
repo.ignoredLarge && repo.ignoredLarge.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
698
|
+
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
699
|
+
"⚠️ 以下 " + repo.ignoredLarge.length + " 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):"),
|
|
700
|
+
h("div", { style: { maxHeight: 120, overflow: "auto", background: T.layer1, borderRadius: 8, padding: 8 } },
|
|
701
|
+
repo.ignoredLarge.slice(0, 30).map((f, i) =>
|
|
702
|
+
h("div", { key: i, style: { fontSize: 11, color: T.secondary, lineHeight: 1.5 } },
|
|
703
|
+
(f.kind === "dir" ? "📁 " + f.path + "/(整个文件夹)" : "📄 " + f.path) +
|
|
704
|
+
" — " + fmtMB(f.bytes))
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
) : null,
|
|
708
|
+
) : err ? h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 8 } }, err) : null,
|
|
709
|
+
|
|
710
|
+
(() => {
|
|
711
|
+
// 状态机:根据本地是否有更改、远程是否有更新,动态显示按钮。
|
|
712
|
+
const hasRemote = !!(repo && repo.hasRemote);
|
|
713
|
+
const localChanges = hasRemote && ((repo.dirty && repo.dirtyCount > 0) || (repo.ahead > 0));
|
|
714
|
+
const remoteUpdates = hasRemote && (repo.behind > 0);
|
|
715
|
+
const isBlocked = busy || !dir;
|
|
716
|
+
const btns = [];
|
|
717
|
+
// 非 git 仓库但远程存在同名仓库 -> 只「创建 Git」(由用户决定后续拉取/推送)
|
|
718
|
+
if (repo && !repo.isGitRepo && repo.repoExists === true) {
|
|
719
|
+
btns.push(h(Btn, { key: "init", label: "创建 Git", onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: "仅初始化 git 仓库,不拉取不推送,由你决定下一步" }));
|
|
720
|
+
} else if (hasRemote) {
|
|
721
|
+
if (localChanges && remoteUpdates) {
|
|
722
|
+
// 本地和远程都有更新 -> 三按钮:合并推送 / 强制推送 / 强制拉取
|
|
723
|
+
btns.push(h(Btn, { key: "mp", label: "拉取更新并推送更改", onClick: mergePush, tone: "primary", noBg: true, disabled: isBlocked }));
|
|
724
|
+
btns.push(h(Btn, { key: "fp", label: "强制推送", onClick: forcePush, disabled: isBlocked, title: "用本地版本覆盖远程(远程非本地更改被丢弃)" }));
|
|
725
|
+
btns.push(h(Btn, { key: "fpl", label: "强制拉取", onClick: forcePull, disabled: isBlocked, title: "强制并入远程更新" }));
|
|
726
|
+
} else if (localChanges) {
|
|
727
|
+
// 只有本地有更改 -> 只显示推送(正常 push)
|
|
728
|
+
btns.push(h(Btn, { key: "push", label: "推送更改", onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
729
|
+
} else if (remoteUpdates) {
|
|
730
|
+
// 只有远程有更新 -> 只显示拉取(正常 pull)
|
|
731
|
+
btns.push(h(Btn, { key: "pull", label: "拉取更新", onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
732
|
+
} else {
|
|
733
|
+
// 完全同步 -> 无按钮,显示已是最新
|
|
734
|
+
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, "✓ 已是最新"));
|
|
735
|
+
}
|
|
736
|
+
// 有远程时总是提供「强制对齐」作为兜底(本地完全重置为远程状态)
|
|
737
|
+
if (repo && repo.isGitRepo) {
|
|
738
|
+
btns.push(h(Btn, { key: "align", label: "强制对齐", onClick: align, disabled: isBlocked, title: "本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况" }));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
742
|
+
btns,
|
|
743
|
+
h(Btn, { label: "刷新状态", onClick: () => { setResult(null); setMsg(null); setErr(null); loadRepo(dir); }, disabled: busy })
|
|
744
|
+
);
|
|
745
|
+
})(),
|
|
746
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
747
|
+
h("input", {
|
|
748
|
+
type: "text", value: repoName, readOnly: true,
|
|
749
|
+
title: "仓库名称自动取文件夹名(不可修改)",
|
|
750
|
+
placeholder: "(未加载文件夹)",
|
|
751
|
+
style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "not-allowed", opacity: 0.85 },
|
|
752
|
+
}),
|
|
753
|
+
h("select", {
|
|
754
|
+
value: visibility,
|
|
755
|
+
onChange: (e) => setVisibility(e.target.value),
|
|
756
|
+
title: "仓库可见性:私有 / 公开" + (repo && repo.repoExists === true ? "(修改当前仓库的可见性)" : ""),
|
|
757
|
+
style: { font: "inherit", fontSize: 12, padding: "6px 8px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
758
|
+
},
|
|
759
|
+
h("option", { value: "private" }, "私有"),
|
|
760
|
+
h("option", { value: "public" }, "公开")
|
|
761
|
+
),
|
|
762
|
+
repo && repo.repoExists === true ? (
|
|
763
|
+
// 仓库已存在:根据所选值与当前实际可见性决定按钮 / 提示
|
|
764
|
+
repo.visibility === visibility ? h(Btn, {
|
|
765
|
+
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
766
|
+
disabled: true,
|
|
767
|
+
}) : h(Btn, {
|
|
768
|
+
key: "setvis", label: "修改仓库状态", onClick: changeVisibility, tone: "success",
|
|
769
|
+
disabled: busy || !repoName,
|
|
770
|
+
})
|
|
771
|
+
) : h(Btn, {
|
|
772
|
+
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
773
|
+
disabled: busy || !repoName,
|
|
774
|
+
})
|
|
775
|
+
),
|
|
776
|
+
repo && repo.repoExists === true ? h("div", { style: { marginTop: 8, fontSize: 12 } },
|
|
777
|
+
repo.visibility
|
|
778
|
+
? (repo.visibility === visibility
|
|
779
|
+
? h("span", { style: { color: T.success } }, "✓ 仓库已是「" + (repo.visibility === "public" ? "公开" : "私有") + "」状态,如需修改请调整左侧可见性选择。")
|
|
780
|
+
: h("span", { style: { color: T.warn } }, "将把仓库从「" + (repo.visibility === "public" ? "公开" : "私有") + "」改为「" + (visibility === "public" ? "公开" : "私有") + "」,点击「修改仓库状态」执行。"))
|
|
781
|
+
: h("span", { style: { color: T.secondary } }, "⚠️ 同名仓库已经创建(无法读取当前可见性,可能未" + (prov === "gitee" ? "配置 Gitee 令牌" : "登录 gh") + ")")
|
|
782
|
+
) : repo && repo.repoExists === false ? h("div", { style: { marginTop: 8, color: T.success, fontSize: 12 } },
|
|
783
|
+
"✓ 同名仓库 " + repoName + " 不存在,可在 " + (prov === "gitee" ? "Gitee" : "GitHub") + "「新建仓库并推送」创建(可选私有/公开)。"
|
|
784
|
+
) : null,
|
|
785
|
+
|
|
786
|
+
result ? h("div", { style: { marginTop: 12, borderTop: "1px solid " + T.border, paddingTop: 10 } },
|
|
787
|
+
msg ? h("div", { style: { color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
788
|
+
result.skipped && result.skipped.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
789
|
+
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
790
|
+
"已忽略未上传(" + result.skipped.length + " 项 >100MB):"),
|
|
791
|
+
result.skipped.map((s, i) => h("div", { key: i, style: { fontSize: 11, color: T.warn, lineHeight: 1.5 } }, "· " + s.path + " — " + s.reason)))
|
|
792
|
+
: result.ok ? h("div", { style: { color: T.success, fontSize: 12, marginTop: 6 } },
|
|
793
|
+
"已提交" + (result.commitHash ? " " + result.commitHash : "") + (result.pushed ? " 并推送" : "(推送省略)"))
|
|
794
|
+
: err ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + err)
|
|
795
|
+
: result.pushError ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + result.pushError)
|
|
796
|
+
: null
|
|
797
|
+
) : err ? h("div", { style: { marginTop: 10, color: T.danger, fontSize: 13 } }, "❌ " + err) : null,
|
|
798
|
+
detail ? ReactDOM.createPortal(
|
|
799
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
800
|
+
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: () => setDetail(null) }),
|
|
801
|
+
h("div", { style: { position: "relative", zIndex: 1, width: 520, maxWidth: "calc(100vw - 48px)", maxHeight: "calc(100vh - 100px)", display: "flex", flexDirection: "column", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": "查看详情" },
|
|
802
|
+
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 10 } },
|
|
803
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, flex: 1 } },
|
|
804
|
+
detail === "changes" ? "改动文件" : "与远程同步差异"),
|
|
805
|
+
h("button", { type: "button", style: closeBtnStyle(), "aria-label": "关闭", onClick: () => setDetail(null) }, "✕")
|
|
806
|
+
),
|
|
807
|
+
h("div", { style: { flex: 1, overflow: "auto" } },
|
|
808
|
+
detail === "changes" ? (
|
|
809
|
+
repo && repo.changedFiles && repo.changedFiles.length > 0
|
|
810
|
+
? repo.changedFiles.map((f, i) => h("div", { key: i, style: { display: "flex", gap: 8, alignItems: "baseline", fontSize: 13, lineHeight: 1.8, borderBottom: "1px solid " + T.border, padding: "3px 2px" } },
|
|
811
|
+
h("span", { style: { color: typeColor(f.type), fontSize: 11, flexShrink: 0, width: 52 } }, typeLabel(f.type)),
|
|
812
|
+
h("span", { style: { color: T.label, wordBreak: "break-all", flex: 1 } }, f.path)
|
|
813
|
+
))
|
|
814
|
+
: h("div", { style: { color: T.secondary, fontSize: 13 } }, "当前没有改动。")
|
|
815
|
+
) : (
|
|
816
|
+
h("div", null,
|
|
817
|
+
repo && (repo.ahead > 0 || repo.behind > 0) ? h("div", null,
|
|
818
|
+
repo.behind > 0 ? h("div", { style: { marginBottom: 10 } },
|
|
819
|
+
h("div", { style: { color: T.warn, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地落后 " + repo.behind + " 个提交(远程有而本地没有):"),
|
|
820
|
+
repo.behindCommits && repo.behindCommits.length > 0
|
|
821
|
+
? repo.behindCommits.map((c, i) => h("div", { key: i, style: { fontSize: 12, color: T.secondary, lineHeight: 1.7, fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)" } }, c))
|
|
822
|
+
: null
|
|
823
|
+
) : null,
|
|
824
|
+
repo.ahead > 0 ? h("div", null,
|
|
825
|
+
h("div", { style: { color: T.brand, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地领先 " + repo.ahead + " 个提交(本地有而远程没有):"),
|
|
826
|
+
repo.aheadCommits && repo.aheadCommits.length > 0
|
|
827
|
+
? repo.aheadCommits.map((c, i) => h("div", { key: i, style: { fontSize: 12, color: T.secondary, lineHeight: 1.7, fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)" } }, c))
|
|
828
|
+
: null
|
|
829
|
+
) : null
|
|
830
|
+
) : h("div", { style: { color: T.success, fontSize: 13 } }, "✓ 已与远程同步,无差异。")
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
),
|
|
836
|
+
document.body
|
|
837
|
+
) : null,
|
|
838
|
+
pickOpen ? ReactDOM.createPortal(
|
|
839
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
840
|
+
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: cancelPick }),
|
|
841
|
+
h("div", { style: { position: "relative", zIndex: 1, width: 480, maxWidth: "calc(100vw - 48px)", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": "选择目录" },
|
|
842
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, "选择代码目录"),
|
|
843
|
+
h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
844
|
+
"可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。"),
|
|
845
|
+
h("input", {
|
|
846
|
+
type: "text", value: pickPath, disabled: picking,
|
|
847
|
+
placeholder: "C:\\Users\\你的用户名\\项目目录",
|
|
848
|
+
onChange: (e) => setPickPath(e.target.value),
|
|
849
|
+
onKeyDown: (e) => { if (e.key === "Enter") void confirmPick(); },
|
|
850
|
+
style: { width: "100%", boxSizing: "border-box", font: "inherit", fontSize: 13, padding: "8px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none" },
|
|
851
|
+
}),
|
|
852
|
+
pickErr ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + pickErr) : null,
|
|
853
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" } },
|
|
854
|
+
h(Btn, { label: "浏览…", onClick: browseDir, disabled: picking, noBg: true }),
|
|
855
|
+
h(Btn, { label: "取消", onClick: cancelPick, disabled: picking }),
|
|
856
|
+
h(Btn, { label: "确定", onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
),
|
|
860
|
+
document.body
|
|
861
|
+
) : null
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function fmtMB(bytes) {
|
|
866
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Inline "查看" button style (small, quiet). */
|
|
870
|
+
function changeBtnStyle() {
|
|
871
|
+
return {
|
|
872
|
+
appearance: "none", font: "inherit", cursor: "pointer",
|
|
873
|
+
border: "1px solid " + T.border, borderRadius: 6,
|
|
874
|
+
padding: "1px 8px", fontSize: 12, lineHeight: 1.6,
|
|
875
|
+
background: "transparent", color: T.brand,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** Chinese label for a changed-file status type. */
|
|
880
|
+
function typeLabel(t) {
|
|
881
|
+
return { untracked: "新增", added: "新增", deleted: "删除", renamed: "重命名", modified: "修改" }[t] || "修改";
|
|
882
|
+
}
|
|
883
|
+
/** Color for a changed-file status type. */
|
|
884
|
+
function typeColor(t) {
|
|
885
|
+
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[t] || T.label;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ---------- the main panel ----------
|
|
889
|
+
function ScmPanel({ onClose }) {
|
|
890
|
+
// 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
|
|
891
|
+
const [provider, setProvider] = useState("github");
|
|
892
|
+
return h("div", { style: panelStyle(T), role: "dialog", "aria-modal": "true", "aria-label": "源代码管理" },
|
|
893
|
+
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
894
|
+
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
|
|
895
|
+
h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
|
|
896
|
+
),
|
|
897
|
+
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
898
|
+
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
|
|
899
|
+
h(EnvSection, null),
|
|
900
|
+
h("div", { style: { height: 12 } }),
|
|
901
|
+
h(SshSection, { provider, setProvider }),
|
|
902
|
+
h("div", { style: { height: 12 } }),
|
|
903
|
+
h(RepoSection, { provider })
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function panelStyle() {
|
|
908
|
+
return {
|
|
909
|
+
position: "relative", zIndex: 1, display: "flex", flexDirection: "column", gap: 4,
|
|
910
|
+
width: 620, maxWidth: "calc(100vw - 48px)",
|
|
911
|
+
maxHeight: "calc(100vh - 48px)", overflow: "auto",
|
|
912
|
+
boxSizing: "border-box", padding: 24, borderRadius: 20,
|
|
913
|
+
background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border,
|
|
914
|
+
color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function closeBtnStyle() {
|
|
918
|
+
return { appearance: "none", border: "none", background: "transparent", color: T.secondary, cursor: "pointer", fontSize: 18, lineHeight: 1, padding: "2px 6px", borderRadius: 6 };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function overlayStyle() {
|
|
922
|
+
return { position: "fixed", inset: 0, zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" };
|
|
923
|
+
}
|
|
924
|
+
function maskStyle() {
|
|
925
|
+
return { position: "absolute", inset: 0, background: T.mask, backdropFilter: "var(--dsw-mask-blur, blur(4px))" };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ---------- the sidebar footer trigger ----------
|
|
929
|
+
function ScmTrigger({ wide }) {
|
|
930
|
+
const [open, setOpen] = useState(false);
|
|
931
|
+
|
|
932
|
+
const icon = h("svg", {
|
|
933
|
+
viewBox: "0 0 16 16", width: wide ? 16 : 18, height: wide ? 16 : 18,
|
|
934
|
+
fill: "none", stroke: "currentColor", strokeWidth: 1.5,
|
|
935
|
+
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
936
|
+
},
|
|
937
|
+
// a simplified "repo / branch" glyph: a small box with a branch
|
|
938
|
+
h("rect", { x: "1.5", y: "2.5", width: "9", height: "11", rx: "1.5" }),
|
|
939
|
+
h("path", { d: "M14 6.5v3.5a2 2 0 0 1-2 2H5.5" })
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const wideStyle = {
|
|
943
|
+
display: "inline-flex", alignItems: "center", gap: 6,
|
|
944
|
+
height: 30, padding: "0 12px", border: "none", borderRadius: 15,
|
|
945
|
+
background: "transparent", color: T.secondary, cursor: "pointer",
|
|
946
|
+
fontSize: 13, lineHeight: 1,
|
|
947
|
+
};
|
|
948
|
+
const railStyle = {
|
|
949
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
950
|
+
width: 36, height: 36, border: "none", borderRadius: "50%", padding: 0,
|
|
951
|
+
background: "transparent", color: T.secondary, cursor: "pointer",
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
return h(React.Fragment, null,
|
|
955
|
+
h("button", {
|
|
956
|
+
type: "button",
|
|
957
|
+
style: wide ? wideStyle : railStyle,
|
|
958
|
+
title: "源代码管理",
|
|
959
|
+
"aria-label": "源代码管理",
|
|
960
|
+
onClick: () => setOpen(true),
|
|
961
|
+
onMouseEnter: (e) => { e.currentTarget.style.background = T.hover; e.currentTarget.style.color = T.label; },
|
|
962
|
+
onMouseLeave: (e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.secondary; },
|
|
963
|
+
},
|
|
964
|
+
icon,
|
|
965
|
+
wide ? h("span", { style: { fontSize: 13 } }, "代码管理") : null
|
|
966
|
+
),
|
|
967
|
+
open ? ReactDOM.createPortal(
|
|
968
|
+
h("div", { style: overlayStyle(), role: "presentation" },
|
|
969
|
+
h("div", { style: maskStyle(), "aria-hidden": "true", onClick: () => setOpen(false) }),
|
|
970
|
+
h(ScmPanel, { onClose: () => setOpen(false) })
|
|
971
|
+
),
|
|
972
|
+
document.body
|
|
973
|
+
) : null
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ---------- cordis plugin body ----------
|
|
978
|
+
const inject = ["slots"];
|
|
979
|
+
|
|
980
|
+
function apply(ctx) {
|
|
981
|
+
// DSH 打开(插件激活)时就预取环境/SSH/默认工作区/仓库状态,
|
|
982
|
+
// 点开「代码管理」面板时直接使用缓存,无需重新加载。
|
|
983
|
+
void preload();
|
|
984
|
+
if (!ctx.slots || typeof ctx.slots.inject !== "function") return;
|
|
985
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
986
|
+
name: "sidebar.footer.action",
|
|
987
|
+
id: PLUGIN_ID,
|
|
988
|
+
order: -100,
|
|
989
|
+
}, ScmTrigger));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
exports.name = PLUGIN_ID;
|
|
993
|
+
exports.inject = inject;
|
|
994
|
+
exports.apply = apply;
|
|
995
|
+
return module.exports;
|
|
996
|
+
}
|
|
997
|
+
});
|