source-code-mgmt 1.8.0 → 1.10.1
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.en.md +219 -0
- package/README.md +47 -79
- package/cordis.patch.yml +15 -0
- package/lib/client.js +421 -259
- package/lib/index.js +82 -66
- package/package.json +6 -3
package/lib/client.js
CHANGED
|
@@ -59,13 +59,14 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// ---------- fetch helpers ----------
|
|
62
|
+
// 每个请求都带上当前语言(?lang=zh|en),host 端据此返回对应语言的错误/结果消息。
|
|
62
63
|
async function jget(path) {
|
|
63
|
-
const res = await fetch(API + path);
|
|
64
|
+
const res = await fetch(API + path + (path.indexOf("?") >= 0 ? "&" : "?") + "lang=" + currentLang());
|
|
64
65
|
if (!res.ok) throw new Error("HTTP " + String(res.status));
|
|
65
66
|
return await res.json();
|
|
66
67
|
}
|
|
67
68
|
async function jpost(path, body) {
|
|
68
|
-
const res = await fetch(API + path, {
|
|
69
|
+
const res = await fetch(API + path + (path.indexOf("?") >= 0 ? "&" : "?") + "lang=" + currentLang(), {
|
|
69
70
|
method: "POST",
|
|
70
71
|
headers: { "content-type": "application/json" },
|
|
71
72
|
body: JSON.stringify(body || {}),
|
|
@@ -177,6 +178,44 @@ window.__ModuleLoader__.load({
|
|
|
177
178
|
// ---------- small presentational bits ----------
|
|
178
179
|
const h = React.createElement;
|
|
179
180
|
|
|
181
|
+
// ---------- i18n: follows DSH’s language setting (Settings → General → Language), live ----------
|
|
182
|
+
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装":"Install"," 执行成功":" executed successfully","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control panel\".","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel"};
|
|
183
|
+
let scmLang = (() => { try { return String(navigator.language || "zh").toLowerCase().split("-")[0] === "en" ? "en" : "zh" } catch { return "zh" } })();
|
|
184
|
+
let scmLocaleFace = null; // ctx.locale (the DSH LocaleRuntime), set in apply()
|
|
185
|
+
let scmSessionList = null; // ctx.get('sessions').list (ObservableSnapshot), set in apply() — 用于判断「无会话空态」
|
|
186
|
+
function currentLang() { try { if (scmLocaleFace && typeof scmLocaleFace.getLocale === "function") return scmLocaleFace.getLocale().active } catch {} return scmLang }
|
|
187
|
+
function t(key, params) {
|
|
188
|
+
let s = key;
|
|
189
|
+
try { if (currentLang() === "en") s = EN_DICT[key] ?? key } catch {}
|
|
190
|
+
if (params && params.length) { let i = 0; s = String(s).replace(/\$\{\}/g, () => String(params[i++] ?? "")) }
|
|
191
|
+
return s
|
|
192
|
+
}
|
|
193
|
+
/** Re-render the subtree when DSH switches language (subscribe to the LocaleRuntime).
|
|
194
|
+
* 依赖 scmLocaleFace:若 client-locale 晚于本插件激活、捕获发生在组件挂载之后,
|
|
195
|
+
* effect 会重跑并补上订阅;订阅后立即重渲染一次,晚捕获时立刻用上当前语言。 */
|
|
196
|
+
function useLocale() {
|
|
197
|
+
const [, force] = useState(0);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!scmLocaleFace || typeof scmLocaleFace.subscribe !== "function") return;
|
|
200
|
+
const off = scmLocaleFace.subscribe(() => force((n) => n + 1));
|
|
201
|
+
force((n) => n + 1);
|
|
202
|
+
return off;
|
|
203
|
+
}, [scmLocaleFace]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Re-render when the session list changes(新增/切换/清空会话)。用于空态常驻按钮判断:
|
|
207
|
+
* 读到 current === undefined 即「当前没有会话」。 */
|
|
208
|
+
function useSession() {
|
|
209
|
+
const [, force] = useState(0);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!scmSessionList || typeof scmSessionList.subscribe !== "function") return;
|
|
212
|
+
const off = scmSessionList.subscribe(() => force((n) => n + 1));
|
|
213
|
+
force((n) => n + 1);
|
|
214
|
+
return off;
|
|
215
|
+
}, [scmSessionList]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
180
219
|
function Field({ label, value, children }) {
|
|
181
220
|
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
|
|
182
221
|
h("span", { style: { color: T.secondary, fontSize: 13, width: 84, flexShrink: 0 } }, label),
|
|
@@ -227,8 +266,8 @@ window.__ModuleLoader__.load({
|
|
|
227
266
|
canCollapse ? h("span", { style: { flex: 1 } }) : null,
|
|
228
267
|
canCollapse ? h("button", {
|
|
229
268
|
type: "button",
|
|
230
|
-
title: collapsed ? "展开" : "折叠",
|
|
231
|
-
"aria-label": collapsed ? "展开该部分" : "折叠该部分",
|
|
269
|
+
title: collapsed ? t("展开") : t("折叠"),
|
|
270
|
+
"aria-label": collapsed ? t("展开该部分") : t("折叠该部分"),
|
|
232
271
|
onClick: () => setCollapsed(!collapsed),
|
|
233
272
|
style: collapseBtnStyle(),
|
|
234
273
|
}, collapsed ? "▸" : "▾") : null
|
|
@@ -292,16 +331,16 @@ window.__ModuleLoader__.load({
|
|
|
292
331
|
const toolRow = (tool, label, installed, version) => {
|
|
293
332
|
const hint = INSTALL_HINTS[tool] || ""
|
|
294
333
|
const status = installed
|
|
295
|
-
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || "已安装"))
|
|
334
|
+
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || t("已安装")))
|
|
296
335
|
: h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
297
|
-
h("span", { style: { color: T.danger, fontSize: 13 } }, "❌ 未安装"),
|
|
298
|
-
h("button", { type: "button", title: "复制安装命令", disabled: installing !== null, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); void copyText(hint); } }, "复制安装命令"),
|
|
299
|
-
h("button", { type: "button", title: "一键安装(会自动选包管理器)", disabled: installing !== null, style: { ...changeBtnStyle(), color: T.brand, fontWeight: 600 }, onClick: (e) => { e.stopPropagation(); void doInstall(tool); } }, installing === tool ? "安装中…" : "安装")
|
|
336
|
+
h("span", { style: { color: T.danger, fontSize: 13 } }, t("❌ 未安装")),
|
|
337
|
+
h("button", { type: "button", title: t("复制安装命令"), disabled: installing !== null, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); void copyText(hint); } }, t("复制安装命令")),
|
|
338
|
+
h("button", { type: "button", title: t("一键安装(会自动选包管理器)"), disabled: installing !== null, style: { ...changeBtnStyle(), color: T.brand, fontWeight: 600 }, onClick: (e) => { e.stopPropagation(); void doInstall(tool); } }, installing === tool ? t("安装中…") : t("安装"))
|
|
300
339
|
)
|
|
301
340
|
const feedback = installResult && installResult.tool === tool
|
|
302
341
|
? (installResult.ok
|
|
303
|
-
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || "已安装") + " 执行成功" + (installResult.needElevation ? "(如未生效需以管理员身份重试)" : ""))
|
|
304
|
-
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || "安装失败") + (installResult.command ? ":" + installResult.command : "")))
|
|
342
|
+
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || t("已安装")) + t(" 执行成功") + (installResult.needElevation ? t("(如未生效需以管理员身份重试)") : ""))
|
|
343
|
+
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || t("安装失败")) + (installResult.command ? ":" + installResult.command : "")))
|
|
305
344
|
: null
|
|
306
345
|
return h("div", { key: tool, style: { marginTop: 4 } },
|
|
307
346
|
h(Field, { label }, status),
|
|
@@ -309,16 +348,16 @@ window.__ModuleLoader__.load({
|
|
|
309
348
|
)
|
|
310
349
|
}
|
|
311
350
|
|
|
312
|
-
return h(Box, { title: "① 环境检查", badge: allPresent ? "✅ 均存在" : null, defaultCollapsed: !!allPresent },
|
|
313
|
-
h(Field, { label: "操作系统" }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
351
|
+
return h(Box, { title: t("① 环境检查"), badge: allPresent ? t("✅ 均存在") : null, defaultCollapsed: !!allPresent },
|
|
352
|
+
h(Field, { label: t("操作系统") }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
314
353
|
env ? h(React.Fragment, null,
|
|
315
354
|
toolRow("git", "Git", env.git.installed, env.git.version),
|
|
316
355
|
toolRow("gh", "GitHub CLI", env.gh.installed, env.gh.version),
|
|
317
|
-
toolRow("ssh", "SSH", !!(env.ssh && env.ssh.installed), env.ssh && env.ssh.installed ? "已找到" : null)
|
|
318
|
-
) : h(Field, { label: "检测中", value: err ? "失败: " + err : "…" }),
|
|
356
|
+
toolRow("ssh", "SSH", !!(env.ssh && env.ssh.installed), env.ssh && env.ssh.installed ? t("已找到") : null)
|
|
357
|
+
) : h(Field, { label: t("检测中"), value: err ? t("失败: ") + err : "…" }),
|
|
319
358
|
(!env || !env.git.installed || !env.gh.installed || !(env.ssh && env.ssh.installed)) ? h("div", { style: { marginTop: 8, fontSize: 12, color: T.secondary, lineHeight: 1.6 } },
|
|
320
|
-
"未找到的工具可用上方「安装」按钮一键安装(" + (winOs ? "Windows 用 winget / 内置功能" : "用系统包管理器") + ",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。"
|
|
321
|
-
) : h(Btn, { label: "重新检查", onClick: load, tone: "ghost" })
|
|
359
|
+
t("未找到的工具可用上方「安装」按钮一键安装(") + (winOs ? t("Windows 用 winget / 内置功能") : t("用系统包管理器")) + t(",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。")
|
|
360
|
+
) : h(Btn, { label: t("重新检查"), onClick: load, tone: "ghost" })
|
|
322
361
|
);
|
|
323
362
|
}
|
|
324
363
|
|
|
@@ -346,11 +385,11 @@ window.__ModuleLoader__.load({
|
|
|
346
385
|
setBusy(true); setMsg(null); setErr(null);
|
|
347
386
|
try {
|
|
348
387
|
const r = await jpost(path, payload);
|
|
349
|
-
if (r.alreadyExists || r.alreadyConfigured) setMsg(label + ":已存在,无需重复操作");
|
|
350
|
-
else if (r.ok) setMsg(label + "成功");
|
|
351
|
-
else setErr(r.error || label + "失败");
|
|
388
|
+
if (r.alreadyExists || r.alreadyConfigured) setMsg(label + t(":已存在,无需重复操作"));
|
|
389
|
+
else if (r.ok) setMsg(label + t("成功"));
|
|
390
|
+
else setErr(r.error || label + t("失败"));
|
|
352
391
|
void load();
|
|
353
|
-
} catch (e) { setErr(label + "失败:" + e); }
|
|
392
|
+
} catch (e) { setErr(label + t("失败:") + e); }
|
|
354
393
|
finally { setBusy(false); }
|
|
355
394
|
}, [load]);
|
|
356
395
|
|
|
@@ -362,52 +401,52 @@ window.__ModuleLoader__.load({
|
|
|
362
401
|
|
|
363
402
|
// 标题行的平台切换下拉:折叠时也可见、可交互;切换后自动展开(要做配置)。
|
|
364
403
|
const titleExtra = h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, flexShrink: 0 } },
|
|
365
|
-
h("span", { style: { fontSize: 12, color: T.secondary } }, "平台"),
|
|
404
|
+
h("span", { style: { fontSize: 12, color: T.secondary } }, t("平台")),
|
|
366
405
|
h("select", {
|
|
367
406
|
value: prov,
|
|
368
407
|
onChange: (e) => { setProv(e.target.value); setCollapsed(false); },
|
|
369
|
-
title: "选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换",
|
|
408
|
+
title: t("选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换"),
|
|
370
409
|
style: { font: "inherit", fontSize: 12, padding: "3px 6px", border: "1px solid " + T.border, borderRadius: 6, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
371
410
|
},
|
|
372
|
-
h("option", { value: "github" }, "GitHub(默认)"),
|
|
411
|
+
h("option", { value: "github" }, t("GitHub(默认)")),
|
|
373
412
|
h("option", { value: "gitee" }, "Gitee"),
|
|
374
413
|
)
|
|
375
414
|
);
|
|
376
415
|
|
|
377
416
|
return h(Box, {
|
|
378
|
-
title: "② SSH 密钥与连接",
|
|
417
|
+
title: t("② SSH 密钥与连接"),
|
|
379
418
|
defaultCollapsed: true,
|
|
380
419
|
collapsed,
|
|
381
420
|
onToggle: (v) => setCollapsed(v),
|
|
382
421
|
titleExtra,
|
|
383
422
|
},
|
|
384
423
|
// key status(显示实际检测到的密钥名,可能不是 id_ed25519)
|
|
385
|
-
h(Field, { label: "密钥" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
386
|
-
ssh ? (ssh.hasKey ? "✅ " + (ssh.keyBase || "id_ed25519") + " 已生成" : "⚠️ 未生成") : "…")
|
|
424
|
+
h(Field, { label: t("密钥") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
425
|
+
ssh ? (ssh.hasKey ? "✅ " + (ssh.keyBase || "id_ed25519") + t(" 已生成") : t("⚠️ 未生成")) : "…")
|
|
387
426
|
),
|
|
388
|
-
h(Field, { label: "GH 登录" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
389
|
-
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || "已登录") : "⚠️ 未登录") : "…")
|
|
427
|
+
h(Field, { label: t("GH 登录") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
428
|
+
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || t("已登录")) : t("⚠️ 未登录")) : "…")
|
|
390
429
|
),
|
|
391
|
-
h(Field, { label: "SSH 配置" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
392
|
-
ssh ? (providerConfigured ? "✅ " + providerHost + " 已配置(443)" : "⚠️ 未配置/限制 22 端口需配置") : "…")
|
|
430
|
+
h(Field, { label: t("SSH 配置") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
431
|
+
ssh ? (providerConfigured ? "✅ " + providerHost + t(" 已配置(443)") : t("⚠️ 未配置/限制 22 端口需配置")) : "…")
|
|
393
432
|
),
|
|
394
433
|
|
|
395
434
|
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 } },
|
|
396
|
-
h(Btn, { label: "生成密钥", onClick: () => act("/gen-key", "生成密钥"), disabled: busy }),
|
|
397
|
-
h(Btn, { label: "配置 SSH(config)", onClick: () => act("/write-config", "配置 SSH", { provider: prov }), disabled: busy }),
|
|
398
|
-
h(Btn, { label: "测试连接", onClick: async () => {
|
|
435
|
+
h(Btn, { label: t("生成密钥"), onClick: () => act("/gen-key", t("生成密钥")), disabled: busy }),
|
|
436
|
+
h(Btn, { label: t("配置 SSH(config)"), onClick: () => act("/write-config", t("配置 SSH"), { provider: prov }), disabled: busy }),
|
|
437
|
+
h(Btn, { label: t("测试连接"), onClick: async () => {
|
|
399
438
|
setBusy(true); setErr(null); setMsg(null);
|
|
400
439
|
try {
|
|
401
440
|
const r = await jpost("/ssh-test", { provider: prov });
|
|
402
|
-
if (r.connected) setMsg("SSH 连接成功(" + (prov === "gitee" ? "Gitee" : "GitHub") + "):" + (r.account ? "Hi " + r.account : "已认证"));
|
|
403
|
-
else setErr("连接失败,请确认密钥已上传到 " + (prov === "gitee" ? "Gitee" : "GitHub") + " 或已登录 gh");
|
|
441
|
+
if (r.connected) setMsg(t("SSH 连接成功(") + (prov === "gitee" ? "Gitee" : "GitHub") + "):" + (r.account ? "Hi " + r.account : t("已认证")));
|
|
442
|
+
else setErr(t("连接失败,请确认密钥已上传到 ") + (prov === "gitee" ? "Gitee" : "GitHub") + t(" 或已登录 gh"));
|
|
404
443
|
void load();
|
|
405
|
-
} catch (e) { setErr("测试失败:" + e); }
|
|
444
|
+
} catch (e) { setErr(t("测试失败:") + e); }
|
|
406
445
|
finally { setBusy(false); }
|
|
407
446
|
}, tone: "primary", noBg: true, disabled: busy }),
|
|
408
447
|
),
|
|
409
448
|
ssh && ssh.pubContent ? h("div", { style: { marginTop: 10 } },
|
|
410
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, "公钥(复制上传到 " + (prov === "gitee" ? "Gitee → 设置 → SSH 公钥" : "GitHub → Settings → SSH keys") + ",或运行 gh auth login 自动上传):"),
|
|
449
|
+
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("公钥(复制上传到 ") + (prov === "gitee" ? t("Gitee → 设置 → SSH 公钥") : "GitHub → Settings → SSH keys") + t(",或运行 gh auth login 自动上传):")),
|
|
411
450
|
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)
|
|
412
451
|
) : null,
|
|
413
452
|
msg ? h("div", { style: { marginTop: 10, color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
@@ -539,7 +578,7 @@ window.__ModuleLoader__.load({
|
|
|
539
578
|
// 先清掉上次残留的缓存(文件集刚可能变化),再并发拉取。
|
|
540
579
|
clearDiffCache(r && r.dir);
|
|
541
580
|
if (!hasBetterSidebar) void prefetchDiffs(r && r.dir, r && r.changedFiles, 3);
|
|
542
|
-
if (r && !r.isGitRepo) setErr(r.error || "该目录不是 git 仓库");
|
|
581
|
+
if (r && !r.isGitRepo) setErr(r.error || t("该目录不是 git 仓库"));
|
|
543
582
|
} catch (e) { setErr(String(e)); }
|
|
544
583
|
finally {
|
|
545
584
|
// 只有本次是最新调用时才收起「刷新中…」提示,避免旧请求失败把提示提前关掉。
|
|
@@ -575,7 +614,7 @@ window.__ModuleLoader__.load({
|
|
|
575
614
|
setGiteeOwnerName(r.owner || "");
|
|
576
615
|
setGiteeTokenMsg(null);
|
|
577
616
|
setGiteeTokenErr(null);
|
|
578
|
-
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("读取令牌状态失败:" + e); }
|
|
617
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr(t("读取令牌状态失败:") + e); }
|
|
579
618
|
}, []);
|
|
580
619
|
|
|
581
620
|
const saveGiteeToken = useCallback(async () => {
|
|
@@ -586,14 +625,14 @@ window.__ModuleLoader__.load({
|
|
|
586
625
|
setGiteeConfigured(true);
|
|
587
626
|
setGiteeOwnerName(r.owner || "");
|
|
588
627
|
setGiteeToken("");
|
|
589
|
-
setGiteeTokenMsg("已保存 Gitee 令牌(账号:" + (r.owner || "?") + ")");
|
|
628
|
+
setGiteeTokenMsg(t("已保存 Gitee 令牌(账号:") + (r.owner || "?") + ")");
|
|
590
629
|
// 令牌就绪后刷新仓库,让同名检测等按 Gitee 生效
|
|
591
630
|
loadRepo(dir || cache.defDir || "", true, true);
|
|
592
631
|
} else {
|
|
593
632
|
setGiteeConfigured(false);
|
|
594
|
-
setGiteeTokenErr(r.error || "保存令牌失败");
|
|
633
|
+
setGiteeTokenErr(r.error || t("保存令牌失败"));
|
|
595
634
|
}
|
|
596
|
-
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("保存令牌失败:" + e); }
|
|
635
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr(t("保存令牌失败:") + e); }
|
|
597
636
|
finally { setGiteeTokenBusy(false); }
|
|
598
637
|
}, [giteeToken, dir, loadRepo]);
|
|
599
638
|
|
|
@@ -604,9 +643,9 @@ window.__ModuleLoader__.load({
|
|
|
604
643
|
setGiteeConfigured(false);
|
|
605
644
|
setGiteeOwnerName("");
|
|
606
645
|
setGiteeToken("");
|
|
607
|
-
setGiteeTokenMsg(r.ok ? "已清除 Gitee 令牌" : "清除失败");
|
|
646
|
+
setGiteeTokenMsg(r.ok ? t("已清除 Gitee 令牌") : t("清除失败"));
|
|
608
647
|
loadRepo(dir || cache.defDir || "", true, true);
|
|
609
|
-
} catch (e) { setGiteeTokenErr("清除令牌失败:" + e); }
|
|
648
|
+
} catch (e) { setGiteeTokenErr(t("清除令牌失败:") + e); }
|
|
610
649
|
finally { setGiteeTokenBusy(false); }
|
|
611
650
|
}, [dir, loadRepo]);
|
|
612
651
|
|
|
@@ -628,10 +667,10 @@ window.__ModuleLoader__.load({
|
|
|
628
667
|
try {
|
|
629
668
|
const r = await jpost("/push", { dir: dir || undefined });
|
|
630
669
|
setResult(r);
|
|
631
|
-
if (r.ok) setMsg("已推送");
|
|
632
|
-
else setErr(r.error || r.pushError || "推送失败");
|
|
670
|
+
if (r.ok) setMsg(t("已推送"));
|
|
671
|
+
else setErr(r.error || r.pushError || t("推送失败"));
|
|
633
672
|
void loadRepo(dir, true, true);
|
|
634
|
-
} catch (e) { setErr("推送失败:" + e); }
|
|
673
|
+
} catch (e) { setErr(t("推送失败:") + e); }
|
|
635
674
|
finally { setBusy(false); }
|
|
636
675
|
}, [dir, loadRepo]);
|
|
637
676
|
|
|
@@ -644,10 +683,10 @@ window.__ModuleLoader__.load({
|
|
|
644
683
|
try {
|
|
645
684
|
const r = await jpost("/align", { dir: dir || undefined });
|
|
646
685
|
setResult(r);
|
|
647
|
-
if (r.ok) setMsg("已拉取远程更新(本地已对齐到远程最新状态)");
|
|
648
|
-
else setErr(r.error || "拉取失败");
|
|
686
|
+
if (r.ok) setMsg(t("已拉取远程更新(本地已对齐到远程最新状态)"));
|
|
687
|
+
else setErr(r.error || t("拉取失败"));
|
|
649
688
|
void loadRepo(dir, true, true);
|
|
650
|
-
} catch (e) { setErr("拉取失败:" + e); }
|
|
689
|
+
} catch (e) { setErr(t("拉取失败:") + e); }
|
|
651
690
|
finally { setBusy(false); }
|
|
652
691
|
}, [dir, loadRepo]);
|
|
653
692
|
|
|
@@ -656,66 +695,66 @@ window.__ModuleLoader__.load({
|
|
|
656
695
|
try {
|
|
657
696
|
const r = await jpost("/merge-push", { dir: dir || undefined });
|
|
658
697
|
setResult(r);
|
|
659
|
-
if (r.ok) setMsg("已拉取远程更新并推送更改");
|
|
660
|
-
else setErr(r.error || "合并推送失败");
|
|
698
|
+
if (r.ok) setMsg(t("已拉取远程更新并推送更改"));
|
|
699
|
+
else setErr(r.error || t("合并推送失败"));
|
|
661
700
|
void loadRepo(dir, true, true);
|
|
662
|
-
} catch (e) { setErr("合并推送失败:" + e); }
|
|
701
|
+
} catch (e) { setErr(t("合并推送失败:") + e); }
|
|
663
702
|
finally { setBusy(false); }
|
|
664
703
|
}, [dir, loadRepo]);
|
|
665
704
|
|
|
666
705
|
const forcePush = useCallback(async () => {
|
|
667
|
-
if (!window.confirm("强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?")) return;
|
|
706
|
+
if (!window.confirm(t("强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?"))) return;
|
|
668
707
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
669
708
|
try {
|
|
670
709
|
const r = await jpost("/force-push", { dir: dir || undefined });
|
|
671
710
|
setResult(r);
|
|
672
|
-
if (r.ok) setMsg("已强制推送,远程已更新为本地状态");
|
|
673
|
-
else setErr(r.error || "强制推送失败");
|
|
711
|
+
if (r.ok) setMsg(t("已强制推送,远程已更新为本地状态"));
|
|
712
|
+
else setErr(r.error || t("强制推送失败"));
|
|
674
713
|
void loadRepo(dir, true, true);
|
|
675
|
-
} catch (e) { setErr("强制推送失败:" + e); }
|
|
714
|
+
} catch (e) { setErr(t("强制推送失败:") + e); }
|
|
676
715
|
finally { setBusy(false); }
|
|
677
716
|
}, [dir, loadRepo]);
|
|
678
717
|
|
|
679
718
|
const forcePull = useCallback(async () => {
|
|
680
|
-
if (!window.confirm("强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?")) return;
|
|
719
|
+
if (!window.confirm(t("强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?"))) return;
|
|
681
720
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
682
721
|
try {
|
|
683
722
|
const r = await jpost("/force-pull", { dir: dir || undefined });
|
|
684
723
|
setResult(r);
|
|
685
|
-
if (r.ok) setMsg("已强制拉取远程更新");
|
|
686
|
-
else setErr(r.error || "强制拉取失败");
|
|
724
|
+
if (r.ok) setMsg(t("已强制拉取远程更新"));
|
|
725
|
+
else setErr(r.error || t("强制拉取失败"));
|
|
687
726
|
void loadRepo(dir, true, true);
|
|
688
|
-
} catch (e) { setErr("强制拉取失败:" + e); }
|
|
727
|
+
} catch (e) { setErr(t("强制拉取失败:") + e); }
|
|
689
728
|
finally { setBusy(false); }
|
|
690
729
|
}, [dir, loadRepo]);
|
|
691
730
|
|
|
692
731
|
const changeVisibility = useCallback(async () => {
|
|
693
732
|
const name = (repo && repo.defaultRepoName) || "";
|
|
694
|
-
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
733
|
+
if (!name) { setErr(t("请先加载一个有效的文件夹")); return; }
|
|
695
734
|
const target = visibility === "public" ? "public" : "private";
|
|
696
|
-
if (!window.confirm("确定把仓库 " + name + " 改为「" + (target === "public" ? "公开" : "私有") + "」?修改可见性可能影响 Star、关注者等。")) return;
|
|
735
|
+
if (!window.confirm(t("确定把仓库 ") + name + t(" 改为「") + (target === "public" ? t("公开") : t("私有")) + t("」?修改可见性可能影响 Star、关注者等。"))) return;
|
|
697
736
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
698
737
|
try {
|
|
699
738
|
const r = await jpost("/set-visibility", { dir: dir || undefined, name, visibility: target, provider: provRef.current || "github" });
|
|
700
739
|
setResult(r);
|
|
701
|
-
if (r.ok) setMsg("已把仓库设置为「" + (target === "public" ? "公开" : "私有") + "」");
|
|
702
|
-
else setErr(r.error || "修改可见性失败");
|
|
740
|
+
if (r.ok) setMsg(t("已把仓库设置为「") + (target === "public" ? t("公开") : t("私有")) + "」");
|
|
741
|
+
else setErr(r.error || t("修改可见性失败"));
|
|
703
742
|
void loadRepo(dir, true, true);
|
|
704
|
-
} catch (e) { setErr("修改可见性失败:" + e); }
|
|
743
|
+
} catch (e) { setErr(t("修改可见性失败:") + e); }
|
|
705
744
|
finally { setBusy(false); }
|
|
706
745
|
}, [dir, repo, visibility, loadRepo]);
|
|
707
746
|
|
|
708
747
|
const create = useCallback(async () => {
|
|
709
748
|
const name = (repo && repo.defaultRepoName) || "";
|
|
710
|
-
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
749
|
+
if (!name) { setErr(t("请先加载一个有效的文件夹")); return; }
|
|
711
750
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
712
751
|
try {
|
|
713
752
|
const r = await jpost("/create", { dir: dir || undefined, name, visibility, provider: provRef.current || "github" });
|
|
714
753
|
setResult(r);
|
|
715
|
-
if (r.ok) setMsg("仓库已创建并推送:" + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
716
|
-
else setErr(r.error || "创建失败");
|
|
754
|
+
if (r.ok) setMsg(t("仓库已创建并推送:") + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
755
|
+
else setErr(r.error || t("创建失败"));
|
|
717
756
|
void loadRepo(dir, true, true);
|
|
718
|
-
} catch (e) { setErr("创建失败:" + e); }
|
|
757
|
+
} catch (e) { setErr(t("创建失败:") + e); }
|
|
719
758
|
finally { setBusy(false); }
|
|
720
759
|
}, [dir, repo, visibility, loadRepo]);
|
|
721
760
|
|
|
@@ -732,15 +771,15 @@ window.__ModuleLoader__.load({
|
|
|
732
771
|
try {
|
|
733
772
|
const r = await jpost("/pick-dir", { initial: pickPath || cache.defDir || undefined });
|
|
734
773
|
if (r.ok && r.dir) setPickPath(r.dir);
|
|
735
|
-
else setPickErr(r.error || "未选择目录");
|
|
736
|
-
} catch (e) { setPickErr("选择失败:" + e); }
|
|
774
|
+
else setPickErr(r.error || t("未选择目录"));
|
|
775
|
+
} catch (e) { setPickErr(t("选择失败:") + e); }
|
|
737
776
|
finally { setPicking(false); }
|
|
738
777
|
}, [pickPath]);
|
|
739
778
|
|
|
740
779
|
// 确认:持久化加入列表 + 加载
|
|
741
780
|
const confirmPick = useCallback(async () => {
|
|
742
781
|
const path = String(pickPath || "").trim();
|
|
743
|
-
if (!path) { setPickErr("请输入或选择目录路径"); return; }
|
|
782
|
+
if (!path) { setPickErr(t("请输入或选择目录路径")); return; }
|
|
744
783
|
setPicking(true); setPickErr(null);
|
|
745
784
|
try {
|
|
746
785
|
const r = await jpost("/add-workspace", { dir: path });
|
|
@@ -754,9 +793,9 @@ window.__ModuleLoader__.load({
|
|
|
754
793
|
setPickOpen(false);
|
|
755
794
|
loadRepo(r.dir, true, true);
|
|
756
795
|
} else {
|
|
757
|
-
setPickErr(r.error || "添加目录失败");
|
|
796
|
+
setPickErr(r.error || t("添加目录失败"));
|
|
758
797
|
}
|
|
759
|
-
} catch (e) { setPickErr("添加目录失败:" + e); }
|
|
798
|
+
} catch (e) { setPickErr(t("添加目录失败:") + e); }
|
|
760
799
|
finally { setPicking(false); }
|
|
761
800
|
}, [pickPath, loadRepo]);
|
|
762
801
|
|
|
@@ -781,23 +820,23 @@ window.__ModuleLoader__.load({
|
|
|
781
820
|
if (next) loadRepo(next, true, true); else setRepo(null);
|
|
782
821
|
}
|
|
783
822
|
} else {
|
|
784
|
-
setErr(r.error || "删除失败");
|
|
823
|
+
setErr(r.error || t("删除失败"));
|
|
785
824
|
}
|
|
786
|
-
} catch (e) { setErr("删除失败:" + e); }
|
|
825
|
+
} catch (e) { setErr(t("删除失败:") + e); }
|
|
787
826
|
finally { setBusy(false); }
|
|
788
827
|
}, [dir, loadRepo]);
|
|
789
828
|
|
|
790
829
|
// 强制对齐:本地完全重置为远程分支状态(丢弃本地差异)
|
|
791
830
|
const align = useCallback(async () => {
|
|
792
|
-
if (!window.confirm("强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?")) return;
|
|
831
|
+
if (!window.confirm(t("强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?"))) return;
|
|
793
832
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
794
833
|
try {
|
|
795
834
|
const r = await jpost("/align", { dir: dir || undefined });
|
|
796
835
|
setResult(r);
|
|
797
|
-
if (r.ok) setMsg("已强制对齐到远程分支");
|
|
798
|
-
else setErr(r.error || "强制对齐失败");
|
|
836
|
+
if (r.ok) setMsg(t("已强制对齐到远程分支"));
|
|
837
|
+
else setErr(r.error || t("强制对齐失败"));
|
|
799
838
|
void loadRepo(dir, true, true);
|
|
800
|
-
} catch (e) { setErr("强制对齐失败:" + e); }
|
|
839
|
+
} catch (e) { setErr(t("强制对齐失败:") + e); }
|
|
801
840
|
finally { setBusy(false); }
|
|
802
841
|
}, [dir, loadRepo]);
|
|
803
842
|
|
|
@@ -807,10 +846,10 @@ window.__ModuleLoader__.load({
|
|
|
807
846
|
try {
|
|
808
847
|
const r = await jpost("/init-git", { dir: dir || undefined });
|
|
809
848
|
setResult(r);
|
|
810
|
-
if (r.ok) setMsg(r.alreadyRepo ? "已是 git 仓库" : "已创建 git 仓库,请自行拉取或推送");
|
|
811
|
-
else setErr(r.error || "创建 git 失败");
|
|
849
|
+
if (r.ok) setMsg(r.alreadyRepo ? t("已是 git 仓库") : t("已创建 git 仓库,请自行拉取或推送"));
|
|
850
|
+
else setErr(r.error || t("创建 git 失败"));
|
|
812
851
|
void loadRepo(dir, true, true);
|
|
813
|
-
} catch (e) { setErr("创建 git 失败:" + e); }
|
|
852
|
+
} catch (e) { setErr(t("创建 git 失败:") + e); }
|
|
814
853
|
finally { setBusy(false); }
|
|
815
854
|
}, [dir, loadRepo]);
|
|
816
855
|
|
|
@@ -826,8 +865,8 @@ window.__ModuleLoader__.load({
|
|
|
826
865
|
// 重新拉取最新 repo(含最新「已暂存/未暂存」标记),并清 diff 展开状态。
|
|
827
866
|
if (dir) delete cache.reposByDir[dir];
|
|
828
867
|
loadRepo(dir, true, true);
|
|
829
|
-
} else setErr(r.error || "暂存失败");
|
|
830
|
-
} catch (e) { setErr("暂存失败:" + e); }
|
|
868
|
+
} else setErr(r.error || t("暂存失败"));
|
|
869
|
+
} catch (e) { setErr(t("暂存失败:") + e); }
|
|
831
870
|
finally { setStageBusy(false); }
|
|
832
871
|
}, [dir, loadRepo]);
|
|
833
872
|
|
|
@@ -839,8 +878,8 @@ window.__ModuleLoader__.load({
|
|
|
839
878
|
if (r.ok) {
|
|
840
879
|
if (dir) delete cache.reposByDir[dir];
|
|
841
880
|
loadRepo(dir, true, true);
|
|
842
|
-
} else setErr(r.error || "取消暂存失败");
|
|
843
|
-
} catch (e) { setErr("取消暂存失败:" + e); }
|
|
881
|
+
} else setErr(r.error || t("取消暂存失败"));
|
|
882
|
+
} catch (e) { setErr(t("取消暂存失败:") + e); }
|
|
844
883
|
finally { setStageBusy(false); }
|
|
845
884
|
}, [dir, loadRepo]);
|
|
846
885
|
|
|
@@ -853,11 +892,11 @@ window.__ModuleLoader__.load({
|
|
|
853
892
|
const r = await jpost("/commit", { dir: dir || undefined, message });
|
|
854
893
|
setResult(r);
|
|
855
894
|
if (r.ok) {
|
|
856
|
-
setMsg("已提交 " + r.staged + " 个文件" + (r.hash ? "(" + r.hash + ")" : ""));
|
|
895
|
+
setMsg(t("已提交 ") + r.staged + t(" 个文件") + (r.hash ? "(" + r.hash + ")" : ""));
|
|
857
896
|
setCommitMsg("");
|
|
858
897
|
void loadRepo(dir, true, true);
|
|
859
|
-
} else setErr(r.error || "提交失败");
|
|
860
|
-
} catch (e) { setErr("提交失败:" + e); }
|
|
898
|
+
} else setErr(r.error || t("提交失败"));
|
|
899
|
+
} catch (e) { setErr(t("提交失败:") + e); }
|
|
861
900
|
finally { setBusy(false); }
|
|
862
901
|
}, [dir, repo, commitMsg, repoName, loadRepo]);
|
|
863
902
|
|
|
@@ -870,14 +909,14 @@ window.__ModuleLoader__.load({
|
|
|
870
909
|
setResult(r);
|
|
871
910
|
if (r.ok) {
|
|
872
911
|
const info = r.committed
|
|
873
|
-
? "已提交「" + (r.message || "(自动生成)") + "」并推送" + (r.commitHash ? "(" + r.commitHash + ")" : "")
|
|
874
|
-
: "已推送本地已有的提交";
|
|
912
|
+
? t("已提交「") + (r.message || t("(自动生成)")) + t("」并推送") + (r.commitHash ? "(" + r.commitHash + ")" : "")
|
|
913
|
+
: t("已推送本地已有的提交");
|
|
875
914
|
setMsg(info);
|
|
876
915
|
setCommitMsg("");
|
|
877
|
-
} else setErr(r.error || r.pushError || "推送失败");
|
|
916
|
+
} else setErr(r.error || r.pushError || t("推送失败"));
|
|
878
917
|
// 推送成功后刷新状态(full 联网同步,显示「刷新中…」)。
|
|
879
918
|
void loadRepo(dir, true, true);
|
|
880
|
-
} catch (e) { setErr("推送失败:" + e); }
|
|
919
|
+
} catch (e) { setErr(t("推送失败:") + e); }
|
|
881
920
|
finally { setPushStagedBusy(false); }
|
|
882
921
|
}, [dir, commitMsg, loadRepo]);
|
|
883
922
|
|
|
@@ -887,24 +926,24 @@ window.__ModuleLoader__.load({
|
|
|
887
926
|
try {
|
|
888
927
|
const r = await jpost("/branches", { dir: dir || undefined });
|
|
889
928
|
if (r.ok) { setBranches(r.branches || []); setBranchCurrent(r.current || ""); }
|
|
890
|
-
else setErr(r.error || "读取分支失败");
|
|
891
|
-
} catch (e) { setErr("读取分支失败:" + e); }
|
|
929
|
+
else setErr(r.error || t("读取分支失败"));
|
|
930
|
+
} catch (e) { setErr(t("读取分支失败:") + e); }
|
|
892
931
|
finally { setBranchBusy(false); }
|
|
893
932
|
}, [dir]);
|
|
894
933
|
|
|
895
934
|
// 切换到某分支。
|
|
896
935
|
const doCheckout = useCallback(async (branch) => {
|
|
897
936
|
if (branch === branchCurrent) { setDetail(null); return; }
|
|
898
|
-
if (!window.confirm("切换到分支「" + branch + "」?(若当前有未提交改动,git 会拒绝切换)")) return;
|
|
937
|
+
if (!window.confirm(t("切换到分支「") + branch + t("」?(若当前有未提交改动,git 会拒绝切换)"))) return;
|
|
899
938
|
setBranchBusy(true); setMsg(null); setErr(null);
|
|
900
939
|
try {
|
|
901
940
|
const r = await jpost("/checkout", { dir: dir || undefined, branch });
|
|
902
941
|
if (r.ok) {
|
|
903
|
-
setMsg("已切换到分支 " + branch);
|
|
942
|
+
setMsg(t("已切换到分支 ") + branch);
|
|
904
943
|
setDetail(null);
|
|
905
944
|
void loadRepo(dir, true, true);
|
|
906
|
-
} else setErr(r.error || "切换分支失败");
|
|
907
|
-
} catch (e) { setErr("切换分支失败:" + e); }
|
|
945
|
+
} else setErr(r.error || t("切换分支失败"));
|
|
946
|
+
} catch (e) { setErr(t("切换分支失败:") + e); }
|
|
908
947
|
finally { setBranchBusy(false); }
|
|
909
948
|
}, [dir, branchCurrent, loadRepo]);
|
|
910
949
|
|
|
@@ -914,38 +953,38 @@ window.__ModuleLoader__.load({
|
|
|
914
953
|
try {
|
|
915
954
|
const r = await jpost("/log", { dir: dir || undefined, count: 30 });
|
|
916
955
|
if (r.ok) setCommits(r.commits || []);
|
|
917
|
-
else setErr(r.error || "读取历史失败");
|
|
918
|
-
} catch (e) { setErr("读取历史失败:" + e); }
|
|
956
|
+
else setErr(r.error || t("读取历史失败"));
|
|
957
|
+
} catch (e) { setErr(t("读取历史失败:") + e); }
|
|
919
958
|
finally { setCommitsLoading(false); }
|
|
920
959
|
}, [dir]);
|
|
921
960
|
|
|
922
961
|
// 对某提交执行 revert(改写历史,需确认)。
|
|
923
962
|
const doRevert = useCallback(async (hash, subject) => {
|
|
924
|
-
if (!window.confirm("revert 提交「" + (subject || hash) + "」——会生成一个反向提交,期间可能产生冲突,确定继续?")) return;
|
|
963
|
+
if (!window.confirm(t("revert 提交「") + (subject || hash) + t("」——会生成一个反向提交,期间可能产生冲突,确定继续?"))) return;
|
|
925
964
|
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
926
965
|
try {
|
|
927
966
|
const r = await jpost("/revert", { dir: dir || undefined, hash });
|
|
928
967
|
if (r.ok) {
|
|
929
|
-
setMsg("已 revert 提交 " + hash);
|
|
968
|
+
setMsg(t("已 revert 提交 ") + hash);
|
|
930
969
|
setDetail(null);
|
|
931
970
|
void loadRepo(dir, true, true);
|
|
932
|
-
} else setErr(r.error || "revert 失败");
|
|
933
|
-
} catch (e) { setErr("revert 失败:" + e); }
|
|
971
|
+
} else setErr(r.error || t("revert 失败"));
|
|
972
|
+
} catch (e) { setErr(t("revert 失败:") + e); }
|
|
934
973
|
finally { setHistoryBusy(false); }
|
|
935
974
|
}, [dir, loadRepo]);
|
|
936
975
|
|
|
937
976
|
// 对某提交执行 cherry-pick(改写历史,需确认)。
|
|
938
977
|
const doCherryPick = useCallback(async (hash, subject) => {
|
|
939
|
-
if (!window.confirm("cherry-pick 提交「" + (subject || hash) + "」到当前分支——期间可能产生冲突,确定继续?")) return;
|
|
978
|
+
if (!window.confirm(t("cherry-pick 提交「") + (subject || hash) + t("」到当前分支——期间可能产生冲突,确定继续?"))) return;
|
|
940
979
|
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
941
980
|
try {
|
|
942
981
|
const r = await jpost("/cherrypick", { dir: dir || undefined, hash });
|
|
943
982
|
if (r.ok) {
|
|
944
|
-
setMsg("已 cherry-pick 提交 " + hash);
|
|
983
|
+
setMsg(t("已 cherry-pick 提交 ") + hash);
|
|
945
984
|
setDetail(null);
|
|
946
985
|
void loadRepo(dir, true, true);
|
|
947
|
-
} else setErr(r.error || "cherry-pick 失败");
|
|
948
|
-
} catch (e) { setErr("cherry-pick 失败:" + e); }
|
|
986
|
+
} else setErr(r.error || t("cherry-pick 失败"));
|
|
987
|
+
} catch (e) { setErr(t("cherry-pick 失败:") + e); }
|
|
949
988
|
finally { setHistoryBusy(false); }
|
|
950
989
|
}, [dir, loadRepo]);
|
|
951
990
|
|
|
@@ -955,7 +994,7 @@ window.__ModuleLoader__.load({
|
|
|
955
994
|
setHistoryDetail({ hash, loading: true, diff: "", error: null });
|
|
956
995
|
try {
|
|
957
996
|
const r = await jpost("/commit-diff", { dir: dir || undefined, hash });
|
|
958
|
-
setHistoryDetail({ hash, loading: false, diff: (r && r.diff) || "", error: r && !r.ok ? (r.error || "读取失败") : null });
|
|
997
|
+
setHistoryDetail({ hash, loading: false, diff: (r && r.diff) || "", error: r && !r.ok ? (r.error || t("读取失败")) : null });
|
|
959
998
|
} catch (e) {
|
|
960
999
|
setHistoryDetail({ hash, loading: false, diff: "", error: String(e) });
|
|
961
1000
|
}
|
|
@@ -970,42 +1009,42 @@ window.__ModuleLoader__.load({
|
|
|
970
1009
|
const ok = await copyText(text);
|
|
971
1010
|
setCommitMenu(null);
|
|
972
1011
|
if (ok) {
|
|
973
|
-
setCopiedTip("已复制" + (kind === "short" ? "短哈希" : kind === "full" ? "完整哈希" : "提交信息"));
|
|
1012
|
+
setCopiedTip(t("已复制") + (kind === "short" ? t("短哈希") : kind === "full" ? t("完整哈希") : t("提交信息")));
|
|
974
1013
|
window.setTimeout(() => setCopiedTip(null), 1600);
|
|
975
1014
|
} else {
|
|
976
|
-
setErr("复制失败:请手动复制");
|
|
1015
|
+
setErr(t("复制失败:请手动复制"));
|
|
977
1016
|
}
|
|
978
1017
|
}, []);
|
|
979
1018
|
|
|
980
|
-
return h(Box, { title: "③ 代码管理" },
|
|
1019
|
+
return h(Box, { title: t("③ 代码管理") },
|
|
981
1020
|
// 平台提示 + Gitee 令牌(仅 gitee 模式)
|
|
982
1021
|
prov === "gitee" ? h("div", { style: { border: "1px solid " + T.border, borderRadius: 10, padding: 10, marginBottom: 10, background: T.layer1 } },
|
|
983
1022
|
h("div", { style: { fontSize: 12, fontWeight: 600, color: T.label, marginBottom: 6 } },
|
|
984
|
-
"Gitee 私人令牌(OpenAPI,需 projects 权限)"),
|
|
1023
|
+
t("Gitee 私人令牌(OpenAPI,需 projects 权限)")),
|
|
985
1024
|
giteeConfigured === true
|
|
986
1025
|
? h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
987
|
-
h("span", { style: { color: T.success, fontSize: 12 } }, "✅ 已配置" + (giteeOwnerName ? "(账号 " + giteeOwnerName + ")" : "")),
|
|
988
|
-
h(Btn, { label: "清除令牌", onClick: clearGiteeToken, disabled: giteeTokenBusy, noBg: true, tone: "danger" })
|
|
1026
|
+
h("span", { style: { color: T.success, fontSize: 12 } }, t("✅ 已配置") + (giteeOwnerName ? t("(账号 ") + giteeOwnerName + ")" : "")),
|
|
1027
|
+
h(Btn, { label: t("清除令牌"), onClick: clearGiteeToken, disabled: giteeTokenBusy, noBg: true, tone: "danger" })
|
|
989
1028
|
)
|
|
990
1029
|
: h("div", null,
|
|
991
1030
|
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
992
1031
|
h("input", {
|
|
993
1032
|
type: "password", value: giteeToken, disabled: giteeTokenBusy,
|
|
994
|
-
placeholder: "粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)",
|
|
1033
|
+
placeholder: t("粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)"),
|
|
995
1034
|
onChange: (e) => setGiteeToken(e.target.value),
|
|
996
1035
|
onKeyDown: (e) => { if (e.key === "Enter") void saveGiteeToken(); },
|
|
997
1036
|
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" },
|
|
998
1037
|
}),
|
|
999
|
-
h(Btn, { label: "保存令牌", onClick: saveGiteeToken, tone: "primary", noBg: true, disabled: giteeTokenBusy || !String(giteeToken || "").trim() }),
|
|
1038
|
+
h(Btn, { label: t("保存令牌"), onClick: saveGiteeToken, tone: "primary", noBg: true, disabled: giteeTokenBusy || !String(giteeToken || "").trim() }),
|
|
1000
1039
|
),
|
|
1001
1040
|
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 6, lineHeight: 1.6 } },
|
|
1002
|
-
"令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。")
|
|
1041
|
+
t("令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。"))
|
|
1003
1042
|
),
|
|
1004
1043
|
giteeTokenMsg ? h("div", { style: { marginTop: 6, color: T.success, fontSize: 12 } }, "✅ " + giteeTokenMsg) : null,
|
|
1005
1044
|
giteeTokenErr ? h("div", { style: { marginTop: 6, color: T.danger, fontSize: 12 } }, "❌ " + giteeTokenErr) : null
|
|
1006
1045
|
) : null,
|
|
1007
1046
|
h("div", { style: { display: "flex", gap: 8, marginBottom: 10, alignItems: "center" } },
|
|
1008
|
-
h("span", { style: { color: T.secondary, fontSize: 13, whiteSpace: "nowrap" } }, "选择工作区"),
|
|
1047
|
+
h("span", { style: { color: T.secondary, fontSize: 13, whiteSpace: "nowrap" } }, t("选择工作区")),
|
|
1009
1048
|
h("select", {
|
|
1010
1049
|
value: dir,
|
|
1011
1050
|
disabled: workspaces.length === 0,
|
|
@@ -1019,83 +1058,83 @@ window.__ModuleLoader__.load({
|
|
|
1019
1058
|
// full 同步:联网获取切换后工作区的最新状态,并显示「刷新中…」。
|
|
1020
1059
|
loadRepo(v, true, true);
|
|
1021
1060
|
},
|
|
1022
|
-
title: "选择 DSH 已登记的工作区文件夹",
|
|
1061
|
+
title: t("选择 DSH 已登记的工作区文件夹"),
|
|
1023
1062
|
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" },
|
|
1024
1063
|
},
|
|
1025
|
-
workspaces.length === 0 ? h("option", { key: "__empty", value: "" }, "(暂无可选工作区)")
|
|
1026
|
-
: h("option", { key: "__none", value: "" }, "— 请选择 —"),
|
|
1064
|
+
workspaces.length === 0 ? h("option", { key: "__empty", value: "" }, t("(暂无可选工作区)"))
|
|
1065
|
+
: h("option", { key: "__none", value: "" }, t("— 请选择 —")),
|
|
1027
1066
|
workspaces.map((w) =>
|
|
1028
1067
|
h("option", { key: w, value: w }, String(w).split(/[\\/]/).filter(Boolean).pop() || w)
|
|
1029
1068
|
)
|
|
1030
1069
|
),
|
|
1031
|
-
h(Btn, { label: "选择目录", onClick: openPicker, disabled: picking, noBg: true, tone: "primary" }),
|
|
1070
|
+
h(Btn, { label: t("选择目录"), onClick: openPicker, disabled: picking, noBg: true, tone: "primary" }),
|
|
1032
1071
|
),
|
|
1033
1072
|
dir && customDirs && customDirs.includes(dir) ? h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10, alignItems: "center" } },
|
|
1034
|
-
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, "自定义目录:"),
|
|
1073
|
+
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, t("自定义目录:")),
|
|
1035
1074
|
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, true, true); } },
|
|
1036
1075
|
String(dir).split(/[\\/]/).filter(Boolean).pop() || dir,
|
|
1037
1076
|
h("span", {
|
|
1038
|
-
role: "button", "aria-label": "删除该目录记录",
|
|
1039
|
-
title: "删除该目录记录(不删除实际文件夹)",
|
|
1077
|
+
role: "button", "aria-label": t("删除该目录记录"),
|
|
1078
|
+
title: t("删除该目录记录(不删除实际文件夹)"),
|
|
1040
1079
|
onClick: (e) => { e.stopPropagation(); void removeWorkspace(dir); },
|
|
1041
1080
|
style: { color: T.danger, fontWeight: 700, padding: "0 2px", cursor: "pointer" },
|
|
1042
1081
|
}, "✕")
|
|
1043
1082
|
)
|
|
1044
1083
|
) : null,
|
|
1045
1084
|
repo && repo.isGitRepo ? h("div", { style: { marginBottom: 10 } },
|
|
1046
|
-
h(Field, { label: "目录", value: repo.dir }),
|
|
1047
|
-
h(Field, { label: "分支" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1048
|
-
h("span", { style: { color: T.label, fontSize: 13 } }, repo.branch || "(无)"),
|
|
1085
|
+
h(Field, { label: t("目录"), value: repo.dir }),
|
|
1086
|
+
h(Field, { label: t("分支") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1087
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.branch || t("(无)")),
|
|
1049
1088
|
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1050
1089
|
repo.branch ? h("button", {
|
|
1051
1090
|
type: "button",
|
|
1052
1091
|
onClick: openBranches,
|
|
1053
|
-
title: "切换分支",
|
|
1092
|
+
title: t("切换分支"),
|
|
1054
1093
|
style: changeBtnStyle(),
|
|
1055
|
-
}, "切换") : null,
|
|
1094
|
+
}, t("切换")) : null,
|
|
1056
1095
|
h("button", {
|
|
1057
1096
|
type: "button",
|
|
1058
1097
|
onClick: openHistory,
|
|
1059
|
-
title: "查看提交历史",
|
|
1098
|
+
title: t("查看提交历史"),
|
|
1060
1099
|
style: changeBtnStyle(),
|
|
1061
|
-
}, "历史")
|
|
1100
|
+
}, t("历史"))
|
|
1062
1101
|
) : null
|
|
1063
1102
|
)),
|
|
1064
|
-
h(Field, { label: "远程", value: repo.remoteUrl || "(无)" }),
|
|
1065
|
-
h(Field, { label: "改动" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1066
|
-
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + " 个文件" : "无"),
|
|
1103
|
+
h(Field, { label: t("远程"), value: repo.remoteUrl || t("(无)") }),
|
|
1104
|
+
h(Field, { label: t("改动") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1105
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + t(" 个文件") : t("无")),
|
|
1067
1106
|
repo.changedFiles && repo.changedFiles.length > 0 ? h("button", {
|
|
1068
1107
|
type: "button",
|
|
1069
1108
|
onClick: () => setDetail("changes"),
|
|
1070
|
-
title: "查看改动的文件列表",
|
|
1109
|
+
title: t("查看改动的文件列表"),
|
|
1071
1110
|
style: changeBtnStyle(),
|
|
1072
|
-
}, "查看")
|
|
1111
|
+
}, t("查看"))
|
|
1073
1112
|
: null
|
|
1074
1113
|
)),
|
|
1075
|
-
h(Field, { label: "同步" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1114
|
+
h(Field, { label: t("同步") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1076
1115
|
h("span", { style: { color: T.label, fontSize: 13 } },
|
|
1077
1116
|
// 没有(属于当前账号/平台的)远程时不做同步判断,避免误显示「与远程一致」
|
|
1078
1117
|
!repo.hasRemote
|
|
1079
|
-
? "(无)"
|
|
1080
|
-
: (repo.ahead > 0 ? "本地领先 " + repo.ahead + " 提交" : "") +
|
|
1081
|
-
(repo.ahead > 0 && repo.behind > 0 ? "、落后 " + repo.behind + " 提交" : (repo.behind > 0 ? "落后 " + repo.behind + " 提交" : "")) +
|
|
1082
|
-
((repo.ahead === 0 && repo.behind === 0) ? "与远程一致" : "")
|
|
1118
|
+
? t("(无)")
|
|
1119
|
+
: (repo.ahead > 0 ? t("本地领先 ") + repo.ahead + t(" 提交") : "") +
|
|
1120
|
+
(repo.ahead > 0 && repo.behind > 0 ? t("、落后 ") + repo.behind + t(" 提交") : (repo.behind > 0 ? t("落后 ") + repo.behind + t(" 提交") : "")) +
|
|
1121
|
+
((repo.ahead === 0 && repo.behind === 0) ? t("与远程一致") : "")
|
|
1083
1122
|
),
|
|
1084
1123
|
(repo.ahead > 0 || repo.behind > 0) ? h("button", {
|
|
1085
1124
|
type: "button",
|
|
1086
1125
|
onClick: () => setDetail("sync"),
|
|
1087
|
-
title: "查看本地与远程的提交差异",
|
|
1126
|
+
title: t("查看本地与远程的提交差异"),
|
|
1088
1127
|
style: changeBtnStyle(),
|
|
1089
|
-
}, "查看")
|
|
1128
|
+
}, t("查看"))
|
|
1090
1129
|
: null
|
|
1091
1130
|
)),
|
|
1092
1131
|
repo.ignoredLarge && repo.ignoredLarge.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
1093
1132
|
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
1094
|
-
"⚠️ 以下 " + repo.ignoredLarge.length + " 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):"),
|
|
1133
|
+
t("⚠️ 以下 ") + repo.ignoredLarge.length + t(" 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):")),
|
|
1095
1134
|
h("div", { style: { maxHeight: 120, overflow: "auto", background: T.layer1, borderRadius: 8, padding: 8 } },
|
|
1096
1135
|
repo.ignoredLarge.slice(0, 30).map((f, i) =>
|
|
1097
1136
|
h("div", { key: i, style: { fontSize: 11, color: T.secondary, lineHeight: 1.5 } },
|
|
1098
|
-
(f.kind === "dir" ? "📁 " + f.path + "/(整个文件夹)" : "📄 " + f.path) +
|
|
1137
|
+
(f.kind === "dir" ? "📁 " + f.path + t("/(整个文件夹)") : "📄 " + f.path) +
|
|
1099
1138
|
" — " + fmtMB(f.bytes))
|
|
1100
1139
|
)
|
|
1101
1140
|
)
|
|
@@ -1111,7 +1150,7 @@ window.__ModuleLoader__.load({
|
|
|
1111
1150
|
const btns = [];
|
|
1112
1151
|
// 非 git 仓库但远程存在同名仓库 -> 只「创建 Git」(由用户决定后续拉取/推送)
|
|
1113
1152
|
if (repo && !repo.isGitRepo && repo.repoExists === true) {
|
|
1114
|
-
btns.push(h(Btn, { key: "init", label: "创建 Git", onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: "仅初始化 git 仓库,不拉取不推送,由你决定下一步" }));
|
|
1153
|
+
btns.push(h(Btn, { key: "init", label: t("创建 Git"), onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: t("仅初始化 git 仓库,不拉取不推送,由你决定下一步") }));
|
|
1115
1154
|
} else if (hasRemote) {
|
|
1116
1155
|
if (localChanges && remoteUpdates) {
|
|
1117
1156
|
// 本地和远程都有更新:原名展示「拉取更新并推送更改 / 强制推送 /
|
|
@@ -1119,28 +1158,28 @@ window.__ModuleLoader__.load({
|
|
|
1119
1158
|
// 分支保护等原因失败且行为难预测,故不再展示——该状态唯一的
|
|
1120
1159
|
// 操作按钮是「强制对齐」(fetch + reset --hard,本地完全重置为远程)。
|
|
1121
1160
|
btns.push(h("span", { key: "both-note", style: { color: T.warn, fontSize: 12 } },
|
|
1122
|
-
"本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态"));
|
|
1161
|
+
t("本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态")));
|
|
1123
1162
|
if (repo && repo.isGitRepo) {
|
|
1124
|
-
btns.push(h(Btn, { key: "align", label: "强制对齐", onClick: align, disabled: isBlocked, title: "本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况" }));
|
|
1163
|
+
btns.push(h(Btn, { key: "align", label: t("强制对齐"), onClick: align, disabled: isBlocked, title: t("本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况") }));
|
|
1125
1164
|
}
|
|
1126
1165
|
} else if (localChanges) {
|
|
1127
1166
|
// 只有本地有更改 -> 只显示推送(正常 push)
|
|
1128
|
-
btns.push(h(Btn, { key: "push", label: "推送更改", onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
1167
|
+
btns.push(h(Btn, { key: "push", label: t("推送更改"), onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
1129
1168
|
} else if (remoteUpdates) {
|
|
1130
1169
|
// 只有远程有更新(本地干净)-> 只显示「拉取更新」。
|
|
1131
1170
|
// 实现复用「强制对齐」逻辑(fetch + reset --hard):本地干净
|
|
1132
1171
|
// 无改动可丢,reset 等同快速前进到远程最新,直接执行不弹确认。
|
|
1133
|
-
btns.push(h(Btn, { key: "pull", label: "拉取更新", onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
1172
|
+
btns.push(h(Btn, { key: "pull", label: t("拉取更新"), onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
1134
1173
|
} else {
|
|
1135
1174
|
// 完全同步 -> 无按钮,显示已是最新
|
|
1136
|
-
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, "✓ 已是最新"));
|
|
1175
|
+
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, t("✓ 已是最新")));
|
|
1137
1176
|
}
|
|
1138
1177
|
}
|
|
1139
1178
|
// 刷新/切换平台进行中给出明确提示,让用户知道正在联网同步或重新检测。
|
|
1140
1179
|
const refreshingTip = refreshing
|
|
1141
|
-
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…")
|
|
1180
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, t("⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…"))
|
|
1142
1181
|
: switching
|
|
1143
|
-
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 切换平台,正在重新检测「" + (prov === "gitee" ? "Gitee" : "GitHub") + "」仓库状态…")
|
|
1182
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, t("⟳ 切换平台,正在重新检测「") + (prov === "gitee" ? "Gitee" : "GitHub") + t("」仓库状态…"))
|
|
1144
1183
|
: null
|
|
1145
1184
|
return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
1146
1185
|
btns,
|
|
@@ -1149,15 +1188,15 @@ window.__ModuleLoader__.load({
|
|
|
1149
1188
|
(!hasBetterSidebar && repo && repo.hasRemote
|
|
1150
1189
|
&& (repo.ahead > 0 || ((repo.changedFiles || []).some((f) => f.staged)))) ? h(Btn, {
|
|
1151
1190
|
key: "push-staged",
|
|
1152
|
-
label: pushStagedBusy ? "推送中…" : "推送暂存",
|
|
1191
|
+
label: pushStagedBusy ? t("推送中…") : t("推送暂存"),
|
|
1153
1192
|
onClick: doPushStaged,
|
|
1154
1193
|
tone: "primary", noBg: true,
|
|
1155
1194
|
disabled: busy || pushStagedBusy || !dir,
|
|
1156
|
-
title: "把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动",
|
|
1195
|
+
title: t("把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动"),
|
|
1157
1196
|
}) : null,
|
|
1158
1197
|
// keepRepo=true 保留旧内容避免闪烁;full=true 触发 loadRepo 的「刷新中…」提示 + 联网同步。
|
|
1159
1198
|
h(Btn, {
|
|
1160
|
-
label: refreshing ? "刷新中…" : "刷新状态",
|
|
1199
|
+
label: refreshing ? t("刷新中…") : t("刷新状态"),
|
|
1161
1200
|
onClick: () => { setResult(null); setMsg(null); setErr(null); loadRepo(dir, true, true); },
|
|
1162
1201
|
disabled: busy || refreshing,
|
|
1163
1202
|
}),
|
|
@@ -1168,62 +1207,62 @@ window.__ModuleLoader__.load({
|
|
|
1168
1207
|
!hasBetterSidebar && repo && repo.isGitRepo && repo.dirty ? h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
1169
1208
|
h("input", {
|
|
1170
1209
|
type: "text", value: commitMsg,
|
|
1171
|
-
placeholder: "提交信息(留空则用默认)",
|
|
1172
|
-
title: "填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)",
|
|
1210
|
+
placeholder: t("提交信息(留空则用默认)"),
|
|
1211
|
+
title: t("填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)"),
|
|
1173
1212
|
onChange: (e) => setCommitMsg(e.target.value),
|
|
1174
1213
|
onKeyDown: (e) => { if (e.key === "Enter") void doCommit(); },
|
|
1175
1214
|
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" },
|
|
1176
1215
|
}),
|
|
1177
|
-
h(Btn, { label: "提交", onClick: doCommit, tone: "primary", noBg: true, disabled: busy || stageBusy || repo.dirtyCount <= 0 || !dir }),
|
|
1216
|
+
h(Btn, { label: t("提交"), onClick: doCommit, tone: "primary", noBg: true, disabled: busy || stageBusy || repo.dirtyCount <= 0 || !dir }),
|
|
1178
1217
|
) : null,
|
|
1179
1218
|
h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
1180
1219
|
h("input", {
|
|
1181
1220
|
type: "text", value: repoName, readOnly: true,
|
|
1182
|
-
title: "仓库名称自动取文件夹名(不可修改)",
|
|
1183
|
-
placeholder: "(未加载文件夹)",
|
|
1221
|
+
title: t("仓库名称自动取文件夹名(不可修改)"),
|
|
1222
|
+
placeholder: t("(未加载文件夹)"),
|
|
1184
1223
|
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 },
|
|
1185
1224
|
}),
|
|
1186
1225
|
h("select", {
|
|
1187
1226
|
value: visibility,
|
|
1188
1227
|
onChange: (e) => setVisibility(e.target.value),
|
|
1189
|
-
title: "仓库可见性:私有 / 公开" + (repo && repo.repoExists === true ? "(修改当前仓库的可见性)" : ""),
|
|
1228
|
+
title: t("仓库可见性:私有 / 公开") + (repo && repo.repoExists === true ? t("(修改当前仓库的可见性)") : ""),
|
|
1190
1229
|
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" },
|
|
1191
1230
|
},
|
|
1192
|
-
h("option", { value: "private" }, "私有"),
|
|
1193
|
-
h("option", { value: "public" }, "公开")
|
|
1231
|
+
h("option", { value: "private" }, t("私有")),
|
|
1232
|
+
h("option", { value: "public" }, t("公开"))
|
|
1194
1233
|
),
|
|
1195
1234
|
repo && repo.repoExists === true ? (
|
|
1196
1235
|
// 仓库已存在:根据所选值与当前实际可见性决定按钮 / 提示
|
|
1197
1236
|
repo.visibility === visibility ? h(Btn, {
|
|
1198
|
-
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
1237
|
+
key: "create", label: t("新建仓库并推送"), onClick: create, tone: "success",
|
|
1199
1238
|
disabled: true,
|
|
1200
1239
|
}) : h(Btn, {
|
|
1201
|
-
key: "setvis", label: "修改仓库状态", onClick: changeVisibility, tone: "success",
|
|
1240
|
+
key: "setvis", label: t("修改仓库状态"), onClick: changeVisibility, tone: "success",
|
|
1202
1241
|
disabled: busy || !repoName,
|
|
1203
1242
|
})
|
|
1204
1243
|
) : h(Btn, {
|
|
1205
|
-
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
1244
|
+
key: "create", label: t("新建仓库并推送"), onClick: create, tone: "success",
|
|
1206
1245
|
disabled: busy || !repoName,
|
|
1207
1246
|
})
|
|
1208
1247
|
),
|
|
1209
1248
|
repo && repo.repoExists === true ? h("div", { style: { marginTop: 8, fontSize: 12 } },
|
|
1210
1249
|
repo.visibility
|
|
1211
1250
|
? (repo.visibility === visibility
|
|
1212
|
-
? h("span", { style: { color: T.success } }, "✓ 仓库已是「" + (repo.visibility === "public" ? "公开" : "私有") + "」状态,如需修改请调整左侧可见性选择。")
|
|
1213
|
-
: h("span", { style: { color: T.warn } }, "将把仓库从「" + (repo.visibility === "public" ? "公开" : "私有") + "」改为「" + (visibility === "public" ? "公开" : "私有") + "」,点击「修改仓库状态」执行。"))
|
|
1214
|
-
: h("span", { style: { color: T.secondary } }, "⚠️ 同名仓库已经创建(无法读取当前可见性,可能未" + (prov === "gitee" ? "配置 Gitee 令牌" : "登录 gh") + ")")
|
|
1251
|
+
? h("span", { style: { color: T.success } }, t("✓ 仓库已是「") + (repo.visibility === "public" ? t("公开") : t("私有")) + t("」状态,如需修改请调整左侧可见性选择。"))
|
|
1252
|
+
: h("span", { style: { color: T.warn } }, t("将把仓库从「") + (repo.visibility === "public" ? t("公开") : t("私有")) + t("」改为「") + (visibility === "public" ? t("公开") : t("私有")) + t("」,点击「修改仓库状态」执行。")))
|
|
1253
|
+
: h("span", { style: { color: T.secondary } }, t("⚠️ 同名仓库已经创建(无法读取当前可见性,可能未") + (prov === "gitee" ? t("配置 Gitee 令牌") : t("登录 gh")) + ")")
|
|
1215
1254
|
) : repo && repo.repoExists === false ? h("div", { style: { marginTop: 8, color: T.success, fontSize: 12 } },
|
|
1216
|
-
"✓ 同名仓库 " + repoName + " 不存在,可在 " + (prov === "gitee" ? "Gitee" : "GitHub") + "「新建仓库并推送」创建(可选私有/公开)。"
|
|
1255
|
+
t("✓ 同名仓库 ") + repoName + t(" 不存在,可在 ") + (prov === "gitee" ? "Gitee" : "GitHub") + t("「新建仓库并推送」创建(可选私有/公开)。")
|
|
1217
1256
|
) : null,
|
|
1218
1257
|
|
|
1219
1258
|
result ? h("div", { style: { marginTop: 12, borderTop: "1px solid " + T.border, paddingTop: 10 } },
|
|
1220
1259
|
msg ? h("div", { style: { color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
1221
1260
|
result.skipped && result.skipped.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
1222
1261
|
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
1223
|
-
"已忽略未上传(" + result.skipped.length + " 项 >100MB):"),
|
|
1262
|
+
t("已忽略未上传(") + result.skipped.length + t(" 项 >100MB):")),
|
|
1224
1263
|
result.skipped.map((s, i) => h("div", { key: i, style: { fontSize: 11, color: T.warn, lineHeight: 1.5 } }, "· " + s.path + " — " + s.reason)))
|
|
1225
1264
|
: result.ok ? h("div", { style: { color: T.success, fontSize: 12, marginTop: 6 } },
|
|
1226
|
-
"已提交" + (result.commitHash ? " " + result.commitHash : "") + (result.pushed ? " 并推送" : "(推送省略)"))
|
|
1265
|
+
t("已提交") + (result.commitHash ? " " + result.commitHash : "") + (result.pushed ? t(" 并推送") : t("(推送省略)")))
|
|
1227
1266
|
: err ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + err)
|
|
1228
1267
|
: result.pushError ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + result.pushError)
|
|
1229
1268
|
: null
|
|
@@ -1231,19 +1270,19 @@ window.__ModuleLoader__.load({
|
|
|
1231
1270
|
detail ? ReactDOM.createPortal(
|
|
1232
1271
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
1233
1272
|
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: () => setDetail(null) }),
|
|
1234
|
-
h("div", { style: { position: "relative", zIndex: 1, width: detail === "changes" ? 700 : 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": "查看详情" },
|
|
1273
|
+
h("div", { style: { position: "relative", zIndex: 1, width: detail === "changes" ? 700 : 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": t("查看详情") },
|
|
1235
1274
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 10 } },
|
|
1236
1275
|
h("div", { style: { fontSize: 15, fontWeight: 600, flex: 1 } },
|
|
1237
|
-
detail === "changes" ? "改动文件"
|
|
1238
|
-
: detail === "branches" ? "切换分支"
|
|
1239
|
-
: detail === "history" ? "提交历史"
|
|
1240
|
-
: "与远程同步差异"),
|
|
1241
|
-
h("button", { type: "button", style: closeBtnStyle(), "aria-label": "关闭", onClick: () => setDetail(null) }, "✕")
|
|
1276
|
+
detail === "changes" ? t("改动文件")
|
|
1277
|
+
: detail === "branches" ? t("切换分支")
|
|
1278
|
+
: detail === "history" ? t("提交历史")
|
|
1279
|
+
: t("与远程同步差异")),
|
|
1280
|
+
h("button", { type: "button", style: closeBtnStyle(), "aria-label": t("关闭"), onClick: () => setDetail(null) }, "✕")
|
|
1242
1281
|
),
|
|
1243
1282
|
h("div", { style: { flex: 1, overflow: "auto" } },
|
|
1244
1283
|
detail === "changes" ? h(React.Fragment, null,
|
|
1245
1284
|
hasBetterSidebar ? h("div", { style: { marginBottom: 10, padding: "8px 10px", borderRadius: 8, background: T.layer1, color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1246
|
-
"已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar
|
|
1285
|
+
t("已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。"))
|
|
1247
1286
|
: null,
|
|
1248
1287
|
repo && repo.changedFiles && repo.changedFiles.length > 0
|
|
1249
1288
|
? repo.changedFiles.map((f, i) => {
|
|
@@ -1285,46 +1324,46 @@ window.__ModuleLoader__.load({
|
|
|
1285
1324
|
borderBottom: "1px solid " + T.border, padding: "3px 2px",
|
|
1286
1325
|
cursor: canHaveDiff ? "pointer" : "default",
|
|
1287
1326
|
},
|
|
1288
|
-
title: canHaveDiff ? (open ? "点击收起" : "点击查看改动内容") : "新文件(untracked)暂无内容 diff",
|
|
1327
|
+
title: canHaveDiff ? (open ? t("点击收起") : t("点击查看改动内容")) : t("新文件(untracked)暂无内容 diff"),
|
|
1289
1328
|
onClick: onClick,
|
|
1290
1329
|
},
|
|
1291
1330
|
h("span", { style: { color: typeColor(f.type), fontSize: 11, flexShrink: 0, width: 52 } }, typeLabel(f.type)),
|
|
1292
1331
|
h("span", { style: { color: T.label, wordBreak: "break-all", flex: 1 } }, f.path),
|
|
1293
1332
|
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1294
|
-
h("span", { style: { fontSize: 11, flexShrink: 0, color: f.staged ? T.success : T.secondary } }, f.staged ? "已暂存" : "未暂存"),
|
|
1333
|
+
h("span", { style: { fontSize: 11, flexShrink: 0, color: f.staged ? T.success : T.secondary } }, f.staged ? t("已暂存") : t("未暂存")),
|
|
1295
1334
|
h("button", {
|
|
1296
1335
|
type: "button",
|
|
1297
|
-
title: f.staged ? "取消暂存" : "暂存",
|
|
1336
|
+
title: f.staged ? t("取消暂存") : t("暂存"),
|
|
1298
1337
|
disabled: stageBusy,
|
|
1299
1338
|
style: changeBtnStyle(),
|
|
1300
1339
|
onClick: (e) => { e.stopPropagation(); if (f.staged) void doUnstage(f.path); else void doStage(f.path); },
|
|
1301
|
-
}, f.staged ? "取消暂存" : "暂存"),
|
|
1340
|
+
}, f.staged ? t("取消暂存") : t("暂存")),
|
|
1302
1341
|
canHaveDiff ? h("span", { style: { color: T.brand, fontSize: 11, flexShrink: 0 } },
|
|
1303
|
-
d.loading ? "加载中…" : (open ? "▾ 收起" : "▸ 查看"))
|
|
1342
|
+
d.loading ? t("加载中…") : (open ? t("▾ 收起") : t("▸ 查看")))
|
|
1304
1343
|
: null
|
|
1305
1344
|
) : null
|
|
1306
1345
|
),
|
|
1307
1346
|
open && canHaveDiff ? (
|
|
1308
|
-
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载改动内容…")
|
|
1347
|
+
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("正在加载改动内容…"))
|
|
1309
1348
|
: d.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + d.error)
|
|
1310
|
-
: d.diff ? renderSideBySideDiff(d.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1349
|
+
: d.diff ? renderSideBySideDiff(d.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(无内容差异)"))
|
|
1311
1350
|
) : null
|
|
1312
1351
|
);
|
|
1313
1352
|
})
|
|
1314
|
-
: h("div", { style: { color: T.secondary, fontSize: 13 } }, "当前没有改动。")
|
|
1353
|
+
: h("div", { style: { color: T.secondary, fontSize: 13 } }, t("当前没有改动。"))
|
|
1315
1354
|
) : detail === "branches" ? (
|
|
1316
|
-
branchBusy ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取分支…")
|
|
1317
|
-
: branches.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(无分支)")
|
|
1355
|
+
branchBusy ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("正在读取分支…"))
|
|
1356
|
+
: branches.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("(无分支)"))
|
|
1318
1357
|
: branches.map((b) =>
|
|
1319
1358
|
h("div", { key: b, style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 2px", borderBottom: "1px solid " + T.border } },
|
|
1320
1359
|
h("span", { style: { flex: 1, color: b === branchCurrent ? T.success : T.label, fontSize: 13, fontWeight: b === branchCurrent ? 600 : 400 } },
|
|
1321
|
-
b + (b === branchCurrent ? "(当前)" : "")),
|
|
1322
|
-
b !== branchCurrent ? h(Btn, { label: "切换", onClick: () => doCheckout(b), disabled: historyBusy || branchBusy, noBg: true, tone: "primary" }) : null
|
|
1360
|
+
b + (b === branchCurrent ? t("(当前)") : "")),
|
|
1361
|
+
b !== branchCurrent ? h(Btn, { label: t("切换"), onClick: () => doCheckout(b), disabled: historyBusy || branchBusy, noBg: true, tone: "primary" }) : null
|
|
1323
1362
|
)
|
|
1324
1363
|
)
|
|
1325
1364
|
) : detail === "history" ? (
|
|
1326
|
-
commitsLoading ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取历史…")
|
|
1327
|
-
: commits.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(暂无提交)")
|
|
1365
|
+
commitsLoading ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("正在读取历史…"))
|
|
1366
|
+
: commits.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("(暂无提交)"))
|
|
1328
1367
|
: commits.map((c) => {
|
|
1329
1368
|
const hd = historyDetail && historyDetail.hash === c.hash ? historyDetail : null
|
|
1330
1369
|
const menuOpen = commitMenu === c.hashFull
|
|
@@ -1337,28 +1376,28 @@ window.__ModuleLoader__.load({
|
|
|
1337
1376
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1338
1377
|
h("span", { style: { fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, color: T.brand, flexShrink: 0 } }, c.hash),
|
|
1339
1378
|
h("span", { style: { flex: 1, color: T.label, fontSize: 13, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, c.subject || ""),
|
|
1340
|
-
h("button", { type: "button", title: "更多操作(右键也可打开)", disabled: historyBusy, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); setCommitMenu(menuOpen ? null : c.hashFull); } }, "⋯")
|
|
1379
|
+
h("button", { type: "button", title: t("更多操作(右键也可打开)"), disabled: historyBusy, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); setCommitMenu(menuOpen ? null : c.hashFull); } }, "⋯")
|
|
1341
1380
|
),
|
|
1342
1381
|
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 2 } },
|
|
1343
1382
|
c.author + (c.date ? " · " + c.date : "")),
|
|
1344
1383
|
menuOpen ? h("div", { style: menuPanelStyle() },
|
|
1345
1384
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => { setCommitMenu(null); openCommitDiff(c.hash); } },
|
|
1346
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "▸"), "查看提交差异"),
|
|
1385
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "▸"), t("查看提交差异")),
|
|
1347
1386
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("short", c) },
|
|
1348
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制短哈希"),
|
|
1387
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制短哈希")),
|
|
1349
1388
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("full", c) },
|
|
1350
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制完整哈希"),
|
|
1389
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制完整哈希")),
|
|
1351
1390
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("msg", c) },
|
|
1352
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制提交信息"),
|
|
1391
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制提交信息")),
|
|
1353
1392
|
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doRevert(c.hash, c.subject); } },
|
|
1354
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↩"), "还原此提交"),
|
|
1393
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↩"), t("还原此提交")),
|
|
1355
1394
|
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doCherryPick(c.hash, c.subject); } },
|
|
1356
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↪"), "拾取此提交")
|
|
1395
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↪"), t("拾取此提交"))
|
|
1357
1396
|
) : null,
|
|
1358
1397
|
hd ? (
|
|
1359
|
-
hd.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载提交 diff…")
|
|
1398
|
+
hd.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("正在加载提交 diff…"))
|
|
1360
1399
|
: hd.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + hd.error)
|
|
1361
|
-
: hd.diff ? renderSideBySideDiff(hd.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1400
|
+
: hd.diff ? renderSideBySideDiff(hd.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(无内容差异)"))
|
|
1362
1401
|
) : null
|
|
1363
1402
|
)
|
|
1364
1403
|
})
|
|
@@ -1366,18 +1405,18 @@ window.__ModuleLoader__.load({
|
|
|
1366
1405
|
h("div", null,
|
|
1367
1406
|
repo && (repo.ahead > 0 || repo.behind > 0) ? h("div", null,
|
|
1368
1407
|
repo.behind > 0 ? h("div", { style: { marginBottom: 10 } },
|
|
1369
|
-
h("div", { style: { color: T.warn, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地落后 " + repo.behind + " 个提交(远程有而本地没有):"),
|
|
1408
|
+
h("div", { style: { color: T.warn, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, t("本地落后 ") + repo.behind + t(" 个提交(远程有而本地没有):")),
|
|
1370
1409
|
repo.behindCommits && repo.behindCommits.length > 0
|
|
1371
1410
|
? 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))
|
|
1372
1411
|
: null
|
|
1373
1412
|
) : null,
|
|
1374
1413
|
repo.ahead > 0 ? h("div", null,
|
|
1375
|
-
h("div", { style: { color: T.brand, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地领先 " + repo.ahead + " 个提交(本地有而远程没有):"),
|
|
1414
|
+
h("div", { style: { color: T.brand, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, t("本地领先 ") + repo.ahead + t(" 个提交(本地有而远程没有):")),
|
|
1376
1415
|
repo.aheadCommits && repo.aheadCommits.length > 0
|
|
1377
1416
|
? 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))
|
|
1378
1417
|
: null
|
|
1379
1418
|
) : null
|
|
1380
|
-
) : h("div", { style: { color: T.success, fontSize: 13 } }, "✓ 已与远程同步,无差异。")
|
|
1419
|
+
) : h("div", { style: { color: T.success, fontSize: 13 } }, t("✓ 已与远程同步,无差异。"))
|
|
1381
1420
|
)
|
|
1382
1421
|
)
|
|
1383
1422
|
)
|
|
@@ -1388,22 +1427,22 @@ window.__ModuleLoader__.load({
|
|
|
1388
1427
|
pickOpen ? ReactDOM.createPortal(
|
|
1389
1428
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
1390
1429
|
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: cancelPick }),
|
|
1391
|
-
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": "选择目录" },
|
|
1392
|
-
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, "选择代码目录"),
|
|
1430
|
+
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": t("选择目录") },
|
|
1431
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, t("选择代码目录")),
|
|
1393
1432
|
h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
1394
|
-
"可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。"),
|
|
1433
|
+
t("可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。")),
|
|
1395
1434
|
h("input", {
|
|
1396
1435
|
type: "text", value: pickPath, disabled: picking,
|
|
1397
|
-
placeholder: "C:\\Users\\你的用户名\\项目目录",
|
|
1436
|
+
placeholder: t("C:\\Users\\你的用户名\\项目目录"),
|
|
1398
1437
|
onChange: (e) => setPickPath(e.target.value),
|
|
1399
1438
|
onKeyDown: (e) => { if (e.key === "Enter") void confirmPick(); },
|
|
1400
1439
|
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" },
|
|
1401
1440
|
}),
|
|
1402
1441
|
pickErr ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + pickErr) : null,
|
|
1403
1442
|
h("div", { style: { display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" } },
|
|
1404
|
-
h(Btn, { label: "浏览…", onClick: browseDir, disabled: picking, noBg: true }),
|
|
1405
|
-
h(Btn, { label: "取消", onClick: cancelPick, disabled: picking }),
|
|
1406
|
-
h(Btn, { label: "确定", onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
1443
|
+
h(Btn, { label: t("浏览…"), onClick: browseDir, disabled: picking, noBg: true }),
|
|
1444
|
+
h(Btn, { label: t("取消"), onClick: cancelPick, disabled: picking }),
|
|
1445
|
+
h(Btn, { label: t("确定"), onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
1407
1446
|
)
|
|
1408
1447
|
)
|
|
1409
1448
|
),
|
|
@@ -1473,13 +1512,13 @@ window.__ModuleLoader__.load({
|
|
|
1473
1512
|
} catch { return false; }
|
|
1474
1513
|
}
|
|
1475
1514
|
|
|
1476
|
-
/**
|
|
1477
|
-
function typeLabel(
|
|
1478
|
-
return { untracked: "新增", added: "新增", deleted: "删除", renamed: "重命名", modified: "修改" }[
|
|
1515
|
+
/** Localized label for a changed-file status type. */
|
|
1516
|
+
function typeLabel(type) {
|
|
1517
|
+
return { untracked: t("新增"), added: t("新增"), deleted: t("删除"), renamed: t("重命名"), modified: t("修改") }[type] || t("修改");
|
|
1479
1518
|
}
|
|
1480
1519
|
/** Color for a changed-file status type. */
|
|
1481
|
-
function typeColor(
|
|
1482
|
-
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[
|
|
1520
|
+
function typeColor(type) {
|
|
1521
|
+
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[type] || T.label;
|
|
1483
1522
|
}
|
|
1484
1523
|
|
|
1485
1524
|
/**
|
|
@@ -1546,8 +1585,8 @@ window.__ModuleLoader__.load({
|
|
|
1546
1585
|
const gutter = { ...mono, fontSize: 10, lineHeight: 1.55, color: T.secondary, textAlign: "right", padding: "0 6px", whiteSpace: "nowrap", wordBreak: "normal", opacity: 0.7 };
|
|
1547
1586
|
return h("div", { style: { border: "1px solid " + T.border, borderRadius: 8, overflow: "hidden", margin: "4px 0 8px", maxHeight: 280, overflowY: "auto", background: "rgba(0,0,0,.04)" } },
|
|
1548
1587
|
h("div", { style: { display: "flex", borderBottom: "1px solid " + T.border, position: "sticky", top: 0, background: "var(--dsw-alias-bg-layer-3, #fff)", zIndex: 1 } },
|
|
1549
|
-
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, "旧版本"),
|
|
1550
|
-
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, "新版本")
|
|
1588
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, t("旧版本")),
|
|
1589
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, t("新版本"))
|
|
1551
1590
|
),
|
|
1552
1591
|
rows.map((r, i) =>
|
|
1553
1592
|
h("div", { key: i, style: { display: "flex", background: cellBg[r.type] || "transparent", borderBottom: r.type === "ctx" ? "none" : "1px solid rgba(128,128,128,.1)" } },
|
|
@@ -1564,16 +1603,18 @@ window.__ModuleLoader__.load({
|
|
|
1564
1603
|
// `variant` 决定布局:'drawer'(右侧栏集成面板,显示关闭按钮、填充容器)与 'tab'
|
|
1565
1604
|
// (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
|
|
1566
1605
|
function ScmPanel({ onClose, variant }) {
|
|
1606
|
+
// 跟随 DSH 语言设置:切换语言时整个面板(①②③)实时重渲染。
|
|
1607
|
+
useLocale();
|
|
1567
1608
|
// 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
|
|
1568
1609
|
const [provider, setProvider] = useState("github");
|
|
1569
1610
|
const embedded = variant === "tab" || variant === "drawer";
|
|
1570
|
-
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": "源代码管理" },
|
|
1611
|
+
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": t("源代码管理") },
|
|
1571
1612
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
1572
|
-
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
|
|
1573
|
-
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
|
|
1613
|
+
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, t("源代码管理")),
|
|
1614
|
+
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": t("关闭"), onClick: onClose }, "✕")
|
|
1574
1615
|
),
|
|
1575
1616
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1576
|
-
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
|
|
1617
|
+
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(") + (provider === "gitee" ? "Gitee" : "GitHub") + t(" 限制)并说明原因。")),
|
|
1577
1618
|
h(EnvSection, null),
|
|
1578
1619
|
h("div", { style: { height: 12 } }),
|
|
1579
1620
|
h(SshSection, { provider, setProvider }),
|
|
@@ -1703,6 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1703
1744
|
/** 右上角「代码管理」按钮 + 点击后打开的右侧集成面板(dsh-better-sidebar 外观,
|
|
1704
1745
|
* 可拖拽左边缘调整宽度)。 */
|
|
1705
1746
|
function HeaderScmAction() {
|
|
1747
|
+
// 跟随 DSH 语言设置:按钮文字(代码管理)实时切换。
|
|
1748
|
+
useLocale();
|
|
1706
1749
|
const [open, setOpen] = useState(false);
|
|
1707
1750
|
const [panelW, setPanelW] = useState(SCM_PANEL_W);
|
|
1708
1751
|
const panelRef = useRef(null);
|
|
@@ -1719,8 +1762,8 @@ window.__ModuleLoader__.load({
|
|
|
1719
1762
|
(w) => setPanelW(w),
|
|
1720
1763
|
);
|
|
1721
1764
|
return h(React.Fragment, null,
|
|
1722
|
-
h("button", { type: "button", style: headerBtnStyle(), title: "源代码管理", "aria-label": "源代码管理", onClick: () => setOpen(true) },
|
|
1723
|
-
repoIcon(14), h("span", { style: { fontSize: 13 } }, "代码管理")),
|
|
1765
|
+
h("button", { type: "button", style: headerBtnStyle(), title: t("源代码管理"), "aria-label": t("源代码管理"), onClick: () => setOpen(true) },
|
|
1766
|
+
repoIcon(14), h("span", { style: { fontSize: 13 } }, t("代码管理"))),
|
|
1724
1767
|
open ? ReactDOM.createPortal(
|
|
1725
1768
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000, pointerEvents: "none" }, role: "presentation" },
|
|
1726
1769
|
h("div", { ref: panelRef, style: {
|
|
@@ -1733,8 +1776,8 @@ window.__ModuleLoader__.load({
|
|
|
1733
1776
|
// 左边缘拖拽条(改宽)
|
|
1734
1777
|
h("div", {
|
|
1735
1778
|
onPointerDown: onResize,
|
|
1736
|
-
title: "拖动调整面板宽度",
|
|
1737
|
-
"aria-label": "拖动调整面板宽度",
|
|
1779
|
+
title: t("拖动调整面板宽度"),
|
|
1780
|
+
"aria-label": t("拖动调整面板宽度"),
|
|
1738
1781
|
style: {
|
|
1739
1782
|
position: "absolute", top: 0, bottom: 0, left: -3, width: 7,
|
|
1740
1783
|
cursor: "col-resize", pointerEvents: "auto", zIndex: 1,
|
|
@@ -1748,6 +1791,25 @@ window.__ModuleLoader__.load({
|
|
|
1748
1791
|
);
|
|
1749
1792
|
}
|
|
1750
1793
|
|
|
1794
|
+
/** 空态(无会话)时的常驻右上角入口:注册进常驻的 shell.overlay 槽,用 fixed 钉在
|
|
1795
|
+
* 视口右上角。仅当「当前没有会话」(sessionList.current === undefined)时显示——
|
|
1796
|
+
* 有会话时交给 header utilities 按钮,避免两处重复。点击复用 HeaderScmAction 打开面板。 */
|
|
1797
|
+
function EmptyStateScmAction() {
|
|
1798
|
+
useSession();
|
|
1799
|
+
const snap = scmSessionList && typeof scmSessionList.getSnapshot === "function" ? scmSessionList.getSnapshot() : undefined;
|
|
1800
|
+
// 尚未捕获到 sessions 时先不渲染:避免刷新瞬间(captureSessions 的 1.2s 延迟窗口内)
|
|
1801
|
+
// 因 scmSessionList 为 null 而错误点亮常驻按钮、与「Session 日志」重叠。
|
|
1802
|
+
if (!snap) return null;
|
|
1803
|
+
const current = snap.current;
|
|
1804
|
+
const cur = current !== undefined ? (snap.byId ? snap.byId[current] : undefined) : undefined;
|
|
1805
|
+
// 活跃(非空白)会话 → header 里已有「代码管理」按钮(会话内);这里隐藏常驻按钮。
|
|
1806
|
+
// 空白(新对话/欢迎)或根本没有会话 → header 被 hideChrome 隐藏/不存在,显示常驻右上角按钮。
|
|
1807
|
+
const hasActiveSession = current !== undefined && cur && cur.blank === false;
|
|
1808
|
+
if (hasActiveSession) return null;
|
|
1809
|
+
return h("div", { style: { position: "fixed", top: 8, right: 12, zIndex: 40, pointerEvents: "auto" } },
|
|
1810
|
+
h(HeaderScmAction));
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1751
1813
|
/** 挂载一个经典脚本 React 根(React 18 用 createRoot,老版本回退 render)。 */
|
|
1752
1814
|
function mountClientRoot(container, element) {
|
|
1753
1815
|
try {
|
|
@@ -1765,10 +1827,11 @@ window.__ModuleLoader__.load({
|
|
|
1765
1827
|
}
|
|
1766
1828
|
|
|
1767
1829
|
// ---------- cordis plugin body ----------
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
|
|
1830
|
+
// 声明 'locale':DSH 的客户端插件都靠 inject 声明保证服务先于 apply 就绪(loader
|
|
1831
|
+
// 依赖图排序)。client-locale 是 web-app 核心服务(与 slots/connection 同级),
|
|
1832
|
+
// 必在 loader 树里;better-sidebar 仍是可选集成(不入 inject),照旧用
|
|
1833
|
+
// ctx.get('betterSidebar') 判空 + 延迟重试,未安装时绝不会因为缺服务而报错。
|
|
1834
|
+
const inject = ['locale'];
|
|
1772
1835
|
|
|
1773
1836
|
// 是否已安装(并激活)dsh-better-sidebar:决定「③代码管理」是否隐藏本插件自带的
|
|
1774
1837
|
// 本地 Git 工作流(暂存/提交/分支/历史/并排 diff)——这些能力 better-sidebar 的
|
|
@@ -1776,6 +1839,49 @@ window.__ModuleLoader__.load({
|
|
|
1776
1839
|
let hasBetterSidebar = false;
|
|
1777
1840
|
|
|
1778
1841
|
function apply(ctx) {
|
|
1842
|
+
// 接管 DSH 的语言设置:client-locale 插件提供 ctx.locale(LocaleRuntime),
|
|
1843
|
+
// t() 实时读它;语言切换经 subscribe 触发 useLocale 重渲染。
|
|
1844
|
+
// 注意:client-locale 可能比本插件晚激活(和 better-sidebar 同样的顺序问题),
|
|
1845
|
+
// 因此捕获要带重试;重试窗口内组件先以浏览器语言渲染,捕获后立即切到 DSH 设置。
|
|
1846
|
+
const captureLocale = () => {
|
|
1847
|
+
try {
|
|
1848
|
+
if (scmLocaleFace) return true;
|
|
1849
|
+
const lf = (ctx && ctx.locale) || (typeof ctx.get === "function" ? ctx.get("locale") : undefined);
|
|
1850
|
+
if (lf && typeof lf.getLocale === "function") {
|
|
1851
|
+
scmLocaleFace = lf;
|
|
1852
|
+
try { scmLang = lf.getLocale().active || scmLang } catch {}
|
|
1853
|
+
return true;
|
|
1854
|
+
}
|
|
1855
|
+
return false;
|
|
1856
|
+
} catch { return false }
|
|
1857
|
+
};
|
|
1858
|
+
if (!captureLocale()) {
|
|
1859
|
+
// 兜底:等 client-locale 激活后再补抓(单次延迟,零轮询、零 I/O)。
|
|
1860
|
+
window.setTimeout(() => {
|
|
1861
|
+
const ok = captureLocale();
|
|
1862
|
+
if (!ok && typeof console !== "undefined") {
|
|
1863
|
+
console.warn("[source-code-mgmt] DSH locale service not found — UI follows the browser language. Install/enable @deepseek-ai/dsh-client-locale for live language switching.");
|
|
1864
|
+
}
|
|
1865
|
+
}, 1200);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// 捕获 `sessions.list`(用于「是否无会话」判断)。sessions 是核心服务、通常早于本插件
|
|
1869
|
+
// 就绪;若还没就绪,单次延迟补抓一次(组件经 useSession 订阅后会自动重渲染)。
|
|
1870
|
+
const captureSessions = () => {
|
|
1871
|
+
try {
|
|
1872
|
+
if (scmSessionList) return true;
|
|
1873
|
+
const s = ctx && typeof ctx.get === "function" ? ctx.get("sessions") : undefined;
|
|
1874
|
+
if (s && typeof s.list === "object" && s.list) {
|
|
1875
|
+
scmSessionList = s.list;
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
return false;
|
|
1879
|
+
} catch { return false }
|
|
1880
|
+
};
|
|
1881
|
+
if (!captureSessions()) {
|
|
1882
|
+
window.setTimeout(() => { captureSessions(); }, 1200);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1779
1885
|
// DSH 打开(插件激活)时就预取环境/SSH/默认工作区/仓库状态,
|
|
1780
1886
|
// 点开「代码管理」面板时直接使用缓存,无需重新加载。
|
|
1781
1887
|
void preload();
|
|
@@ -1787,18 +1893,37 @@ window.__ModuleLoader__.load({
|
|
|
1787
1893
|
|
|
1788
1894
|
const tryRegisterTab = () => {
|
|
1789
1895
|
const bs = typeof ctx.get === "function" ? ctx.get("betterSidebar") : undefined;
|
|
1790
|
-
if (!bs || typeof bs.registerTab !== "function")
|
|
1896
|
+
if (!bs || typeof bs.registerTab !== "function") {
|
|
1897
|
+
return false;
|
|
1898
|
+
}
|
|
1791
1899
|
// 分支 A:已安装 dsh-better-sidebar —— 把「代码管理」注册成它的侧边栏新 Tab
|
|
1792
1900
|
// 页面。ctx.effect 保证 HMR / 插件卸载时自动注销该 Tab。
|
|
1793
1901
|
hasBetterSidebar = true;
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1902
|
+
// 语言切换时实时更新 Tab 标题(title 变了才重注册,避免注册抖动)。
|
|
1903
|
+
let lastTabTitle = null;
|
|
1904
|
+
let tabDisposer = null;
|
|
1905
|
+
const registerTabNow = () => {
|
|
1906
|
+
const title = t("代码管理");
|
|
1907
|
+
if (title === lastTabTitle) return;
|
|
1908
|
+
if (typeof tabDisposer === "function") { try { tabDisposer() } catch {} }
|
|
1909
|
+
lastTabTitle = title;
|
|
1910
|
+
tabDisposer = bs.registerTab({
|
|
1911
|
+
id: PLUGIN_ID,
|
|
1912
|
+
title,
|
|
1913
|
+
icon: (size) => repoIcon(size),
|
|
1914
|
+
order: 50,
|
|
1915
|
+
single: true,
|
|
1916
|
+
component: () => h(ScmPanel, { variant: "tab" }),
|
|
1917
|
+
});
|
|
1918
|
+
};
|
|
1919
|
+
ctx.effect(() => {
|
|
1920
|
+
registerTabNow();
|
|
1921
|
+
return () => { if (typeof tabDisposer === "function") { try { tabDisposer() } catch {} } };
|
|
1922
|
+
});
|
|
1923
|
+
if (scmLocaleFace && typeof scmLocaleFace.subscribe === "function") {
|
|
1924
|
+
ctx.effect(() => scmLocaleFace.subscribe(() => { try { registerTabNow(); } catch {} }),
|
|
1925
|
+
'source-code-mgmt: tab title locale sync');
|
|
1926
|
+
}
|
|
1802
1927
|
// 若此前已挂载了分支 B 的 header 入口(降级路径),立即拆除它。
|
|
1803
1928
|
if (entryUnmount) { entryUnmount(); entryUnmount = null; }
|
|
1804
1929
|
return true;
|
|
@@ -1806,17 +1931,38 @@ window.__ModuleLoader__.load({
|
|
|
1806
1931
|
|
|
1807
1932
|
if (tryRegisterTab()) return;
|
|
1808
1933
|
|
|
1809
|
-
// 分支 B:此刻未检测到 dsh-better-sidebar ——
|
|
1810
|
-
// conversation.session.header.utilities
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1934
|
+
// 分支 B:此刻未检测到 dsh-better-sidebar —— 注册两处「代码管理」入口,保证右上角常驻可见:
|
|
1935
|
+
// ① 有会话时:放进 conversation.session.header.utilities(Session 日志旁的右对齐列表);
|
|
1936
|
+
// ② 无会话空态时:放进常驻的 shell.overlay 槽、用 fixed 钉在右上角(EmptyStateScmAction
|
|
1937
|
+
// 内部按「是否有当前会话」决定显隐,有会话时返回 null,避免与 ① 重复)。
|
|
1938
|
+
// 点击都复用 HeaderScmAction 打开右侧 dsh-better-sidebar 外观的集成面板(推挤 #root)。
|
|
1813
1939
|
const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
|
|
1814
1940
|
if (slots && typeof slots.inject === "function") {
|
|
1815
|
-
ctx.effect(() =>
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1941
|
+
ctx.effect(() => {
|
|
1942
|
+
const teardowns = [];
|
|
1943
|
+
// ① 会话内:Session 日志旁
|
|
1944
|
+
teardowns.push(slots.inject('conversation.session.header.utilities', () => slots.register({
|
|
1945
|
+
name: 'conversation.session.header.utilities',
|
|
1946
|
+
id: 'source-code-mgmt',
|
|
1947
|
+
order: 200,
|
|
1948
|
+
}, HeaderScmAction)));
|
|
1949
|
+
// ② 空态:常驻右上角(仅无会话时显示)
|
|
1950
|
+
teardowns.push(slots.inject('shell.overlay', () => slots.register({
|
|
1951
|
+
name: 'shell.overlay',
|
|
1952
|
+
id: 'source-code-mgmt-empty',
|
|
1953
|
+
order: 300,
|
|
1954
|
+
}, EmptyStateScmAction)));
|
|
1955
|
+
// entryUnmount 一次性拆掉两处:这样当之后探测到 better-sidebar、切到分支 A
|
|
1956
|
+
// (注册 Tab)时,tryRegisterTab 里的 entryUnmount() 能把两处入口都移除。
|
|
1957
|
+
// (旧代码只给 ReactDOM 降级路径设了 entryUnmount,slots 路径漏了 —— 这正是
|
|
1958
|
+
// 「better-sidebar 已出来、代码管理按钮却仍在」的早期根因。)
|
|
1959
|
+
entryUnmount = () => {
|
|
1960
|
+
for (const td of teardowns) { try { if (typeof td === "function") td() } catch {} }
|
|
1961
|
+
teardowns.length = 0;
|
|
1962
|
+
entryUnmount = null;
|
|
1963
|
+
};
|
|
1964
|
+
return entryUnmount;
|
|
1965
|
+
}, 'source-code-mgmt: entry (session header + empty overlay)');
|
|
1820
1966
|
} else if (ReactDOM) {
|
|
1821
1967
|
// slots 服务不可用(极少数环境)——降级:右上角浮动按钮 + 右侧面板。
|
|
1822
1968
|
ctx.effect(() => {
|
|
@@ -1832,10 +1978,26 @@ window.__ModuleLoader__.load({
|
|
|
1832
1978
|
});
|
|
1833
1979
|
}
|
|
1834
1980
|
|
|
1835
|
-
//
|
|
1836
|
-
//
|
|
1837
|
-
|
|
1838
|
-
|
|
1981
|
+
// 有界重试:作为安全网,兜住 better-sidebar 的客户端服务晚于本插件挂载的情况
|
|
1982
|
+
// (两插件互不注入、加载器不保证顺序;其客户端 bundle 是重插件,偶发冷启动慢)。
|
|
1983
|
+
// 单次 1.5s 窗口不够,这里先用快节奏起步、再放慢,覆盖到约 44s;一旦拿到服务
|
|
1984
|
+
// (tryRegisterTab 返回 true)立即注册 Tab、拆除 header 入口并清空所有定时器;
|
|
1985
|
+
// 全程零 I/O(纯内存 ctx.get)。窗口耗尽仍无服务则停留分支 B(此时才是真正未安装)。
|
|
1986
|
+
const retryTimers = new Set();
|
|
1987
|
+
const retryTicks = [500, 500, 750, 1000, 1500, 2000, 3000, 5000, 5000, 5000, 5000, 5000, 5000, 5000];
|
|
1988
|
+
const clearRetry = () => {
|
|
1989
|
+
for (const timerId of retryTimers) { try { window.clearTimeout(timerId); } catch {} }
|
|
1990
|
+
retryTimers.clear();
|
|
1991
|
+
};
|
|
1992
|
+
for (const d of retryTicks) {
|
|
1993
|
+
const handle = window.setTimeout(() => {
|
|
1994
|
+
retryTimers.delete(handle);
|
|
1995
|
+
if (hasBetterSidebar) return;
|
|
1996
|
+
if (tryRegisterTab()) clearRetry();
|
|
1997
|
+
}, d);
|
|
1998
|
+
retryTimers.add(handle);
|
|
1999
|
+
}
|
|
2000
|
+
ctx.effect(() => clearRetry);
|
|
1839
2001
|
}
|
|
1840
2002
|
|
|
1841
2003
|
exports.name = PLUGIN_ID;
|