source-code-mgmt 1.5.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +210 -0
- package/README.md +56 -83
- package/cordis.patch.yml +15 -0
- package/lib/client.js +514 -366
- package/lib/index.js +143 -64
- package/package.json +41 -38
package/lib/client.js
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - branch A: dsh-better-sidebar installed → register「代码管理」as a new
|
|
9
9
|
* sidebar Tab page through `ctx.betterSidebar.registerTab`.
|
|
10
|
-
* - branch B: not installed →
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - branch B: not installed → register a「代码管理」button into the DSH
|
|
11
|
+
* `conversation.session.header.utilities` slot (the right-aligned list that
|
|
12
|
+
* holds "Session log"), so it sits beside "Session log" with the same pill
|
|
13
|
+
* look and 8px gap; clicking opens a RIGHT-side integrated panel (pushing
|
|
14
|
+
* #root left, dsh-better-sidebar style) holding the existing panel.
|
|
15
|
+
* If the `slots` service is unavailable, it falls back to a floating
|
|
16
|
+
* top-right button opening the same right-side panel.
|
|
13
17
|
*
|
|
14
18
|
* The old left-rail bottom button (`sidebar.footer.action` slot) is removed.
|
|
15
19
|
* Either way clicking the entry opens a "源代码管理" panel with:
|
|
@@ -55,13 +59,14 @@ window.__ModuleLoader__.load({
|
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
// ---------- fetch helpers ----------
|
|
62
|
+
// 每个请求都带上当前语言(?lang=zh|en),host 端据此返回对应语言的错误/结果消息。
|
|
58
63
|
async function jget(path) {
|
|
59
|
-
const res = await fetch(API + path);
|
|
64
|
+
const res = await fetch(API + path + (path.indexOf("?") >= 0 ? "&" : "?") + "lang=" + currentLang());
|
|
60
65
|
if (!res.ok) throw new Error("HTTP " + String(res.status));
|
|
61
66
|
return await res.json();
|
|
62
67
|
}
|
|
63
68
|
async function jpost(path, body) {
|
|
64
|
-
const res = await fetch(API + path, {
|
|
69
|
+
const res = await fetch(API + path + (path.indexOf("?") >= 0 ? "&" : "?") + "lang=" + currentLang(), {
|
|
65
70
|
method: "POST",
|
|
66
71
|
headers: { "content-type": "application/json" },
|
|
67
72
|
body: JSON.stringify(body || {}),
|
|
@@ -71,14 +76,15 @@ window.__ModuleLoader__.load({
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
// ---------- preload cache ----------
|
|
74
|
-
// DSH
|
|
75
|
-
//
|
|
79
|
+
// 只在打开插件时按需拉取仓库状态;DSH 打开(插件激活)时只预取静态的 env / ssh /
|
|
80
|
+
// workspaces / 默认工作区,绝不联网同步仓库——避免打开 DSH 就 fetch、并把可能过期的
|
|
81
|
+
// 数据缓存起来,导致打开/重开面板时显示旧状态(如旧的「无改动」)。
|
|
76
82
|
const cache = {
|
|
77
83
|
env: null,
|
|
78
84
|
ssh: null,
|
|
79
85
|
defDir: null,
|
|
80
86
|
repo: null,
|
|
81
|
-
//
|
|
87
|
+
// 各工作区的最近一次结果(dir -> repo 状态),仅作展示历史,不再用于「秒显无刷新」。
|
|
82
88
|
reposByDir: {},
|
|
83
89
|
// 单文件改动 diff 缓存("dir\u0000path" -> diff 文本),点击「查看」秒出。
|
|
84
90
|
diffs: {},
|
|
@@ -122,7 +128,8 @@ window.__ModuleLoader__.load({
|
|
|
122
128
|
await Promise.all(workers)
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
/** 预取 env / ssh / default-dir /
|
|
131
|
+
/** 预取 env / ssh / default-dir / workspaces(仅静态资源),结果写入 cache。可并发安全。
|
|
132
|
+
* 注意:仓库状态不在此预取(需要用户打开面板/切换工作区时才按需联网获取,并显示刷新中)。 */
|
|
126
133
|
function preload() {
|
|
127
134
|
if (!preloadPromise) {
|
|
128
135
|
preloadPromise = (async () => {
|
|
@@ -144,19 +151,6 @@ window.__ModuleLoader__.load({
|
|
|
144
151
|
if (def && def.dir) {
|
|
145
152
|
cache.defDir = def.dir;
|
|
146
153
|
}
|
|
147
|
-
// 打开 DSH 时即对所有工作区做一次完整同步(full=1 会 git fetch 联网),
|
|
148
|
-
// 这样点开「代码管理」或切换任意工作区都直接显示已同步的最新状态,无需再等。
|
|
149
|
-
// 用并发上限 5 控制,避免一次性对大量仓库同时打满网络。
|
|
150
|
-
const dirs = Array.from(new Set([
|
|
151
|
-
...(cache.workspaces || []),
|
|
152
|
-
...(cache.customDirs || []),
|
|
153
|
-
...(def && def.dir ? [def.dir] : []),
|
|
154
|
-
].filter(Boolean)))
|
|
155
|
-
await syncAllRepos(dirs, 5)
|
|
156
|
-
// 默认工作区的同步结果作为面板首次秒显内容。
|
|
157
|
-
if (def && def.dir && cache.reposByDir[def.dir]) {
|
|
158
|
-
cache.repo = cache.reposByDir[def.dir];
|
|
159
|
-
}
|
|
160
154
|
cache.ready = true;
|
|
161
155
|
} catch {
|
|
162
156
|
/* 保持部分缓存 */
|
|
@@ -166,23 +160,6 @@ window.__ModuleLoader__.load({
|
|
|
166
160
|
return preloadPromise;
|
|
167
161
|
}
|
|
168
162
|
|
|
169
|
-
/** 并发上限 N 地对所有目录做 full 同步,结果写入 cache.reposByDir[dir]。 */
|
|
170
|
-
async function syncAllRepos(dirs, limit) {
|
|
171
|
-
let i = 0
|
|
172
|
-
const worker = async () => {
|
|
173
|
-
while (i < dirs.length) {
|
|
174
|
-
const d = dirs[i++]
|
|
175
|
-
try {
|
|
176
|
-
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&full=1").catch(() => null)
|
|
177
|
-
if (r) cache.reposByDir[d] = r
|
|
178
|
-
} catch { /* 单个失败不影响其他 */ }
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const workers = []
|
|
182
|
-
const n = Math.max(1, Math.min(limit || 5, dirs.length || 1))
|
|
183
|
-
for (let k = 0; k < n; k++) workers.push(worker())
|
|
184
|
-
await Promise.all(workers)
|
|
185
|
-
}
|
|
186
163
|
/** 忽略缓存强制重新拉取某个资源并更新 cache。 */
|
|
187
164
|
async function refreshCache(key) {
|
|
188
165
|
if (key === "env") cache.env = await jget("/env").catch(() => cache.env);
|
|
@@ -201,6 +178,31 @@ window.__ModuleLoader__.load({
|
|
|
201
178
|
// ---------- small presentational bits ----------
|
|
202
179
|
const h = React.createElement;
|
|
203
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 的「源代码管理 / Git 面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control / Git\" 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
|
+
function currentLang() { try { if (scmLocaleFace && typeof scmLocaleFace.getLocale === "function") return scmLocaleFace.getLocale().active } catch {} return scmLang }
|
|
186
|
+
function t(key, params) {
|
|
187
|
+
let s = key;
|
|
188
|
+
try { if (currentLang() === "en") s = EN_DICT[key] ?? key } catch {}
|
|
189
|
+
if (params && params.length) { let i = 0; s = String(s).replace(/\$\{\}/g, () => String(params[i++] ?? "")) }
|
|
190
|
+
return s
|
|
191
|
+
}
|
|
192
|
+
/** Re-render the subtree when DSH switches language (subscribe to the LocaleRuntime).
|
|
193
|
+
* 依赖 scmLocaleFace:若 client-locale 晚于本插件激活、捕获发生在组件挂载之后,
|
|
194
|
+
* effect 会重跑并补上订阅;订阅后立即重渲染一次,晚捕获时立刻用上当前语言。 */
|
|
195
|
+
function useLocale() {
|
|
196
|
+
const [, force] = useState(0);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!scmLocaleFace || typeof scmLocaleFace.subscribe !== "function") return;
|
|
199
|
+
const off = scmLocaleFace.subscribe(() => force((n) => n + 1));
|
|
200
|
+
force((n) => n + 1);
|
|
201
|
+
return off;
|
|
202
|
+
}, [scmLocaleFace]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
204
206
|
function Field({ label, value, children }) {
|
|
205
207
|
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
|
|
206
208
|
h("span", { style: { color: T.secondary, fontSize: 13, width: 84, flexShrink: 0 } }, label),
|
|
@@ -251,8 +253,8 @@ window.__ModuleLoader__.load({
|
|
|
251
253
|
canCollapse ? h("span", { style: { flex: 1 } }) : null,
|
|
252
254
|
canCollapse ? h("button", {
|
|
253
255
|
type: "button",
|
|
254
|
-
title: collapsed ? "展开" : "折叠",
|
|
255
|
-
"aria-label": collapsed ? "展开该部分" : "折叠该部分",
|
|
256
|
+
title: collapsed ? t("展开") : t("折叠"),
|
|
257
|
+
"aria-label": collapsed ? t("展开该部分") : t("折叠该部分"),
|
|
256
258
|
onClick: () => setCollapsed(!collapsed),
|
|
257
259
|
style: collapseBtnStyle(),
|
|
258
260
|
}, collapsed ? "▸" : "▾") : null
|
|
@@ -316,16 +318,16 @@ window.__ModuleLoader__.load({
|
|
|
316
318
|
const toolRow = (tool, label, installed, version) => {
|
|
317
319
|
const hint = INSTALL_HINTS[tool] || ""
|
|
318
320
|
const status = installed
|
|
319
|
-
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || "已安装"))
|
|
321
|
+
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || t("已安装")))
|
|
320
322
|
: h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
321
|
-
h("span", { style: { color: T.danger, fontSize: 13 } }, "❌ 未安装"),
|
|
322
|
-
h("button", { type: "button", title: "复制安装命令", disabled: installing !== null, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); void copyText(hint); } }, "复制安装命令"),
|
|
323
|
-
h("button", { type: "button", title: "一键安装(会自动选包管理器)", disabled: installing !== null, style: { ...changeBtnStyle(), color: T.brand, fontWeight: 600 }, onClick: (e) => { e.stopPropagation(); void doInstall(tool); } }, installing === tool ? "安装中…" : "安装")
|
|
323
|
+
h("span", { style: { color: T.danger, fontSize: 13 } }, t("❌ 未安装")),
|
|
324
|
+
h("button", { type: "button", title: t("复制安装命令"), disabled: installing !== null, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); void copyText(hint); } }, t("复制安装命令")),
|
|
325
|
+
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("安装"))
|
|
324
326
|
)
|
|
325
327
|
const feedback = installResult && installResult.tool === tool
|
|
326
328
|
? (installResult.ok
|
|
327
|
-
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || "已安装") + " 执行成功" + (installResult.needElevation ? "(如未生效需以管理员身份重试)" : ""))
|
|
328
|
-
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || "安装失败") + (installResult.command ? ":" + installResult.command : "")))
|
|
329
|
+
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || t("已安装")) + t(" 执行成功") + (installResult.needElevation ? t("(如未生效需以管理员身份重试)") : ""))
|
|
330
|
+
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || t("安装失败")) + (installResult.command ? ":" + installResult.command : "")))
|
|
329
331
|
: null
|
|
330
332
|
return h("div", { key: tool, style: { marginTop: 4 } },
|
|
331
333
|
h(Field, { label }, status),
|
|
@@ -333,16 +335,16 @@ window.__ModuleLoader__.load({
|
|
|
333
335
|
)
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
return h(Box, { title: "① 环境检查", badge: allPresent ? "✅ 均存在" : null, defaultCollapsed: !!allPresent },
|
|
337
|
-
h(Field, { label: "操作系统" }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
338
|
+
return h(Box, { title: t("① 环境检查"), badge: allPresent ? t("✅ 均存在") : null, defaultCollapsed: !!allPresent },
|
|
339
|
+
h(Field, { label: t("操作系统") }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
338
340
|
env ? h(React.Fragment, null,
|
|
339
341
|
toolRow("git", "Git", env.git.installed, env.git.version),
|
|
340
342
|
toolRow("gh", "GitHub CLI", env.gh.installed, env.gh.version),
|
|
341
|
-
toolRow("ssh", "SSH", !!(env.ssh && env.ssh.installed), env.ssh && env.ssh.installed ? "已找到" : null)
|
|
342
|
-
) : h(Field, { label: "检测中", value: err ? "失败: " + err : "…" }),
|
|
343
|
+
toolRow("ssh", "SSH", !!(env.ssh && env.ssh.installed), env.ssh && env.ssh.installed ? t("已找到") : null)
|
|
344
|
+
) : h(Field, { label: t("检测中"), value: err ? t("失败: ") + err : "…" }),
|
|
343
345
|
(!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 } },
|
|
344
|
-
"未找到的工具可用上方「安装」按钮一键安装(" + (winOs ? "Windows 用 winget / 内置功能" : "用系统包管理器") + ",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。"
|
|
345
|
-
) : h(Btn, { label: "重新检查", onClick: load, tone: "ghost" })
|
|
346
|
+
t("未找到的工具可用上方「安装」按钮一键安装(") + (winOs ? t("Windows 用 winget / 内置功能") : t("用系统包管理器")) + t(",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。")
|
|
347
|
+
) : h(Btn, { label: t("重新检查"), onClick: load, tone: "ghost" })
|
|
346
348
|
);
|
|
347
349
|
}
|
|
348
350
|
|
|
@@ -370,11 +372,11 @@ window.__ModuleLoader__.load({
|
|
|
370
372
|
setBusy(true); setMsg(null); setErr(null);
|
|
371
373
|
try {
|
|
372
374
|
const r = await jpost(path, payload);
|
|
373
|
-
if (r.alreadyExists || r.alreadyConfigured) setMsg(label + ":已存在,无需重复操作");
|
|
374
|
-
else if (r.ok) setMsg(label + "成功");
|
|
375
|
-
else setErr(r.error || label + "失败");
|
|
375
|
+
if (r.alreadyExists || r.alreadyConfigured) setMsg(label + t(":已存在,无需重复操作"));
|
|
376
|
+
else if (r.ok) setMsg(label + t("成功"));
|
|
377
|
+
else setErr(r.error || label + t("失败"));
|
|
376
378
|
void load();
|
|
377
|
-
} catch (e) { setErr(label + "失败:" + e); }
|
|
379
|
+
} catch (e) { setErr(label + t("失败:") + e); }
|
|
378
380
|
finally { setBusy(false); }
|
|
379
381
|
}, [load]);
|
|
380
382
|
|
|
@@ -386,52 +388,52 @@ window.__ModuleLoader__.load({
|
|
|
386
388
|
|
|
387
389
|
// 标题行的平台切换下拉:折叠时也可见、可交互;切换后自动展开(要做配置)。
|
|
388
390
|
const titleExtra = h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, flexShrink: 0 } },
|
|
389
|
-
h("span", { style: { fontSize: 12, color: T.secondary } }, "平台"),
|
|
391
|
+
h("span", { style: { fontSize: 12, color: T.secondary } }, t("平台")),
|
|
390
392
|
h("select", {
|
|
391
393
|
value: prov,
|
|
392
394
|
onChange: (e) => { setProv(e.target.value); setCollapsed(false); },
|
|
393
|
-
title: "选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换",
|
|
395
|
+
title: t("选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换"),
|
|
394
396
|
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" },
|
|
395
397
|
},
|
|
396
|
-
h("option", { value: "github" }, "GitHub(默认)"),
|
|
398
|
+
h("option", { value: "github" }, t("GitHub(默认)")),
|
|
397
399
|
h("option", { value: "gitee" }, "Gitee"),
|
|
398
400
|
)
|
|
399
401
|
);
|
|
400
402
|
|
|
401
403
|
return h(Box, {
|
|
402
|
-
title: "② SSH 密钥与连接",
|
|
404
|
+
title: t("② SSH 密钥与连接"),
|
|
403
405
|
defaultCollapsed: true,
|
|
404
406
|
collapsed,
|
|
405
407
|
onToggle: (v) => setCollapsed(v),
|
|
406
408
|
titleExtra,
|
|
407
409
|
},
|
|
408
410
|
// key status(显示实际检测到的密钥名,可能不是 id_ed25519)
|
|
409
|
-
h(Field, { label: "密钥" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
410
|
-
ssh ? (ssh.hasKey ? "✅ " + (ssh.keyBase || "id_ed25519") + " 已生成" : "⚠️ 未生成") : "…")
|
|
411
|
+
h(Field, { label: t("密钥") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
412
|
+
ssh ? (ssh.hasKey ? "✅ " + (ssh.keyBase || "id_ed25519") + t(" 已生成") : t("⚠️ 未生成")) : "…")
|
|
411
413
|
),
|
|
412
|
-
h(Field, { label: "GH 登录" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
413
|
-
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || "已登录") : "⚠️ 未登录") : "…")
|
|
414
|
+
h(Field, { label: t("GH 登录") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
415
|
+
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || t("已登录")) : t("⚠️ 未登录")) : "…")
|
|
414
416
|
),
|
|
415
|
-
h(Field, { label: "SSH 配置" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
416
|
-
ssh ? (providerConfigured ? "✅ " + providerHost + " 已配置(443)" : "⚠️ 未配置/限制 22 端口需配置") : "…")
|
|
417
|
+
h(Field, { label: t("SSH 配置") }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
418
|
+
ssh ? (providerConfigured ? "✅ " + providerHost + t(" 已配置(443)") : t("⚠️ 未配置/限制 22 端口需配置")) : "…")
|
|
417
419
|
),
|
|
418
420
|
|
|
419
421
|
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 } },
|
|
420
|
-
h(Btn, { label: "生成密钥", onClick: () => act("/gen-key", "生成密钥"), disabled: busy }),
|
|
421
|
-
h(Btn, { label: "配置 SSH(config)", onClick: () => act("/write-config", "配置 SSH", { provider: prov }), disabled: busy }),
|
|
422
|
-
h(Btn, { label: "测试连接", onClick: async () => {
|
|
422
|
+
h(Btn, { label: t("生成密钥"), onClick: () => act("/gen-key", t("生成密钥")), disabled: busy }),
|
|
423
|
+
h(Btn, { label: t("配置 SSH(config)"), onClick: () => act("/write-config", t("配置 SSH"), { provider: prov }), disabled: busy }),
|
|
424
|
+
h(Btn, { label: t("测试连接"), onClick: async () => {
|
|
423
425
|
setBusy(true); setErr(null); setMsg(null);
|
|
424
426
|
try {
|
|
425
427
|
const r = await jpost("/ssh-test", { provider: prov });
|
|
426
|
-
if (r.connected) setMsg("SSH 连接成功(" + (prov === "gitee" ? "Gitee" : "GitHub") + "):" + (r.account ? "Hi " + r.account : "已认证"));
|
|
427
|
-
else setErr("连接失败,请确认密钥已上传到 " + (prov === "gitee" ? "Gitee" : "GitHub") + " 或已登录 gh");
|
|
428
|
+
if (r.connected) setMsg(t("SSH 连接成功(") + (prov === "gitee" ? "Gitee" : "GitHub") + "):" + (r.account ? "Hi " + r.account : t("已认证")));
|
|
429
|
+
else setErr(t("连接失败,请确认密钥已上传到 ") + (prov === "gitee" ? "Gitee" : "GitHub") + t(" 或已登录 gh"));
|
|
428
430
|
void load();
|
|
429
|
-
} catch (e) { setErr("测试失败:" + e); }
|
|
431
|
+
} catch (e) { setErr(t("测试失败:") + e); }
|
|
430
432
|
finally { setBusy(false); }
|
|
431
433
|
}, tone: "primary", noBg: true, disabled: busy }),
|
|
432
434
|
),
|
|
433
435
|
ssh && ssh.pubContent ? h("div", { style: { marginTop: 10 } },
|
|
434
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, "公钥(复制上传到 " + (prov === "gitee" ? "Gitee → 设置 → SSH 公钥" : "GitHub → Settings → SSH keys") + ",或运行 gh auth login 自动上传):"),
|
|
436
|
+
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("公钥(复制上传到 ") + (prov === "gitee" ? t("Gitee → 设置 → SSH 公钥") : "GitHub → Settings → SSH keys") + t(",或运行 gh auth login 自动上传):")),
|
|
435
437
|
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)
|
|
436
438
|
) : null,
|
|
437
439
|
msg ? h("div", { style: { marginTop: 10, color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
@@ -448,6 +450,8 @@ window.__ModuleLoader__.load({
|
|
|
448
450
|
const [dirDraft, setDirDraft] = useState(() => cache.defDir ?? "");
|
|
449
451
|
const [repo, setRepo] = useState(() => cache.repo);
|
|
450
452
|
const [busy, setBusy] = useState(false);
|
|
453
|
+
// 「推送暂存」进行中标记:把已暂存的内容用填写的(或自动生成的)信息提交后推送。
|
|
454
|
+
const [pushStagedBusy, setPushStagedBusy] = useState(false);
|
|
451
455
|
// 刷新状态(联网同步)进行中的提示状态,避免刷新时界面闪成「未加载文件夹」。
|
|
452
456
|
const [refreshing, setRefreshing] = useState(false);
|
|
453
457
|
// 切换平台(② 里 GitHub/Gitee)时,③ 需要重新拉取对应平台的仓库状态;此标记在拉取期间为真,
|
|
@@ -526,28 +530,15 @@ window.__ModuleLoader__.load({
|
|
|
526
530
|
// 结果/错误提示,造成「点了按钮没有任何反馈」。需要清空 err 的调用方
|
|
527
531
|
// (切换目录 / 切换平台 / 刷新状态)自行清空。
|
|
528
532
|
// full=true 时才让 host 端执行 git fetch 联网同步(用于「刷新状态」及
|
|
529
|
-
//
|
|
530
|
-
//
|
|
533
|
+
// 推送/拉取/对齐等操作后的刷新);full 模式自动显示「刷新中…」提示,
|
|
534
|
+
// 提示只在最新一次调用归来时关闭。**始终走网络获取最新仓库状态,绝不命中
|
|
535
|
+
// 缓存秒显**——否则打开/重开/切换工作区会显示旧数据(如旧的「无改动」)。
|
|
531
536
|
const doFull = full === true;
|
|
532
537
|
const mySeq = ++loadSeq.current;
|
|
533
538
|
if (doFull) setRefreshing(true);
|
|
534
539
|
const dirty = !keepRepo;
|
|
535
540
|
if (dirty) setRepo(null);
|
|
536
|
-
// 快速模式且预取已同步过该目录:直接秒显缓存,不再等网络(打开 DSH 时已全部 full 同步)。
|
|
537
|
-
// 注意:缓存是按目录存的,但内容随平台(github/gitee)不同。只有缓存的 provider 与
|
|
538
|
-
// 当前平台一致才可秒显;否则跳过缓存,走网络重新拉取对应平台的仓库状态,避免切换
|
|
539
|
-
// 平台后仍显示上一个平台的检测内容。
|
|
540
541
|
const curProv = provRef.current || "github";
|
|
541
|
-
if (!doFull && cache.reposByDir[d] && cache.reposByDir[d].provider === curProv) {
|
|
542
|
-
const cached = cache.reposByDir[d];
|
|
543
|
-
if (mySeq === loadSeq.current) {
|
|
544
|
-
setRepo(cached);
|
|
545
|
-
if (cached.dir) { setDirDraft(cached.dir); setDir(cached.dir); }
|
|
546
|
-
syncVisibility(cached);
|
|
547
|
-
if (!hasBetterSidebar) void prefetchDiffs(cached.dir, cached.changedFiles, 3);
|
|
548
|
-
}
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
542
|
try {
|
|
552
543
|
const p = curProv;
|
|
553
544
|
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p) + (doFull ? "&full=1" : ""));
|
|
@@ -565,10 +556,8 @@ window.__ModuleLoader__.load({
|
|
|
565
556
|
setDir(r.dir);
|
|
566
557
|
cache.defDir = r.dir;
|
|
567
558
|
}
|
|
568
|
-
//
|
|
559
|
+
// 记录最新 repo 状态(仅作展示历史,绝不再用于「无刷新秒显」)。
|
|
569
560
|
cache.repo = r;
|
|
570
|
-
// 按目录缓存(含 provider),下次同一平台可用快速缓存秒显;切平台时因 provider
|
|
571
|
-
// 不匹配会跳过缓存重新拉取,避免显示上个平台的检测内容。
|
|
572
561
|
if (r && r.dir) cache.reposByDir[r.dir] = r;
|
|
573
562
|
// 默认可见性:优先当前仓库实际状态;无远程(即将新建的仓库)默认「公开」。
|
|
574
563
|
syncVisibility(r);
|
|
@@ -576,7 +565,7 @@ window.__ModuleLoader__.load({
|
|
|
576
565
|
// 先清掉上次残留的缓存(文件集刚可能变化),再并发拉取。
|
|
577
566
|
clearDiffCache(r && r.dir);
|
|
578
567
|
if (!hasBetterSidebar) void prefetchDiffs(r && r.dir, r && r.changedFiles, 3);
|
|
579
|
-
if (r && !r.isGitRepo) setErr(r.error || "该目录不是 git 仓库");
|
|
568
|
+
if (r && !r.isGitRepo) setErr(r.error || t("该目录不是 git 仓库"));
|
|
580
569
|
} catch (e) { setErr(String(e)); }
|
|
581
570
|
finally {
|
|
582
571
|
// 只有本次是最新调用时才收起「刷新中…」提示,避免旧请求失败把提示提前关掉。
|
|
@@ -585,9 +574,9 @@ window.__ModuleLoader__.load({
|
|
|
585
574
|
}, []);
|
|
586
575
|
|
|
587
576
|
useEffect(() => {
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
577
|
+
// 首次加载:只预取静态的 env/ssh/workspaces/默认目录;仓库状态一律在面板打开后
|
|
578
|
+
// 通过 loadRepo(..., true) 联网获取最新(并显示「刷新中…」),绝不命中可能过期的
|
|
579
|
+
// 缓存——这样关闭面板再打开、或切换工作区,都会重新同步,不会停留在旧的「无改动」。
|
|
591
580
|
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
592
581
|
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
593
582
|
void preload().then(() => {
|
|
@@ -598,15 +587,8 @@ window.__ModuleLoader__.load({
|
|
|
598
587
|
|| dir || "";
|
|
599
588
|
if (!d) return;
|
|
600
589
|
setDir(d); setDirDraft(d);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
setRepo(cache.repo);
|
|
604
|
-
syncVisibility(cache.repo);
|
|
605
|
-
if (!hasBetterSidebar) void prefetchDiffs(cache.repo.dir, cache.repo.changedFiles, 3);
|
|
606
|
-
} else {
|
|
607
|
-
// 兜底:预加载未拿到(如请求失败),发一次 full 同步刷新。
|
|
608
|
-
loadRepo(d, true, true);
|
|
609
|
-
}
|
|
590
|
+
// 始终 full 同步(联网 fetch + 显示刷新中),获取当前工作区的最新状态。
|
|
591
|
+
loadRepo(d, true, true);
|
|
610
592
|
});
|
|
611
593
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
612
594
|
}, []);
|
|
@@ -619,7 +601,7 @@ window.__ModuleLoader__.load({
|
|
|
619
601
|
setGiteeOwnerName(r.owner || "");
|
|
620
602
|
setGiteeTokenMsg(null);
|
|
621
603
|
setGiteeTokenErr(null);
|
|
622
|
-
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("读取令牌状态失败:" + e); }
|
|
604
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr(t("读取令牌状态失败:") + e); }
|
|
623
605
|
}, []);
|
|
624
606
|
|
|
625
607
|
const saveGiteeToken = useCallback(async () => {
|
|
@@ -630,14 +612,14 @@ window.__ModuleLoader__.load({
|
|
|
630
612
|
setGiteeConfigured(true);
|
|
631
613
|
setGiteeOwnerName(r.owner || "");
|
|
632
614
|
setGiteeToken("");
|
|
633
|
-
setGiteeTokenMsg("已保存 Gitee 令牌(账号:" + (r.owner || "?") + ")");
|
|
615
|
+
setGiteeTokenMsg(t("已保存 Gitee 令牌(账号:") + (r.owner || "?") + ")");
|
|
634
616
|
// 令牌就绪后刷新仓库,让同名检测等按 Gitee 生效
|
|
635
|
-
loadRepo(dir || cache.defDir || "", true);
|
|
617
|
+
loadRepo(dir || cache.defDir || "", true, true);
|
|
636
618
|
} else {
|
|
637
619
|
setGiteeConfigured(false);
|
|
638
|
-
setGiteeTokenErr(r.error || "保存令牌失败");
|
|
620
|
+
setGiteeTokenErr(r.error || t("保存令牌失败"));
|
|
639
621
|
}
|
|
640
|
-
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr("保存令牌失败:" + e); }
|
|
622
|
+
} catch (e) { setGiteeConfigured(false); setGiteeTokenErr(t("保存令牌失败:") + e); }
|
|
641
623
|
finally { setGiteeTokenBusy(false); }
|
|
642
624
|
}, [giteeToken, dir, loadRepo]);
|
|
643
625
|
|
|
@@ -648,9 +630,9 @@ window.__ModuleLoader__.load({
|
|
|
648
630
|
setGiteeConfigured(false);
|
|
649
631
|
setGiteeOwnerName("");
|
|
650
632
|
setGiteeToken("");
|
|
651
|
-
setGiteeTokenMsg(r.ok ? "已清除 Gitee 令牌" : "清除失败");
|
|
652
|
-
loadRepo(dir || cache.defDir || "", true);
|
|
653
|
-
} catch (e) { setGiteeTokenErr("清除令牌失败:" + e); }
|
|
633
|
+
setGiteeTokenMsg(r.ok ? t("已清除 Gitee 令牌") : t("清除失败"));
|
|
634
|
+
loadRepo(dir || cache.defDir || "", true, true);
|
|
635
|
+
} catch (e) { setGiteeTokenErr(t("清除令牌失败:") + e); }
|
|
654
636
|
finally { setGiteeTokenBusy(false); }
|
|
655
637
|
}, [dir, loadRepo]);
|
|
656
638
|
|
|
@@ -662,7 +644,7 @@ window.__ModuleLoader__.load({
|
|
|
662
644
|
if (dir || cache.defDir) {
|
|
663
645
|
// 显示「切换平台,正在重新检测…」提示,避免切换后旧内容停留几秒看起来像没变化。
|
|
664
646
|
setSwitching(true);
|
|
665
|
-
loadRepo(dir || cache.defDir || "", true).finally(() => setSwitching(false));
|
|
647
|
+
loadRepo(dir || cache.defDir || "", true, true).finally(() => setSwitching(false));
|
|
666
648
|
}
|
|
667
649
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
668
650
|
}, [prov]);
|
|
@@ -672,10 +654,10 @@ window.__ModuleLoader__.load({
|
|
|
672
654
|
try {
|
|
673
655
|
const r = await jpost("/push", { dir: dir || undefined });
|
|
674
656
|
setResult(r);
|
|
675
|
-
if (r.ok) setMsg("已推送");
|
|
676
|
-
else setErr(r.error || r.pushError || "推送失败");
|
|
657
|
+
if (r.ok) setMsg(t("已推送"));
|
|
658
|
+
else setErr(r.error || r.pushError || t("推送失败"));
|
|
677
659
|
void loadRepo(dir, true, true);
|
|
678
|
-
} catch (e) { setErr("推送失败:" + e); }
|
|
660
|
+
} catch (e) { setErr(t("推送失败:") + e); }
|
|
679
661
|
finally { setBusy(false); }
|
|
680
662
|
}, [dir, loadRepo]);
|
|
681
663
|
|
|
@@ -688,10 +670,10 @@ window.__ModuleLoader__.load({
|
|
|
688
670
|
try {
|
|
689
671
|
const r = await jpost("/align", { dir: dir || undefined });
|
|
690
672
|
setResult(r);
|
|
691
|
-
if (r.ok) setMsg("已拉取远程更新(本地已对齐到远程最新状态)");
|
|
692
|
-
else setErr(r.error || "拉取失败");
|
|
673
|
+
if (r.ok) setMsg(t("已拉取远程更新(本地已对齐到远程最新状态)"));
|
|
674
|
+
else setErr(r.error || t("拉取失败"));
|
|
693
675
|
void loadRepo(dir, true, true);
|
|
694
|
-
} catch (e) { setErr("拉取失败:" + e); }
|
|
676
|
+
} catch (e) { setErr(t("拉取失败:") + e); }
|
|
695
677
|
finally { setBusy(false); }
|
|
696
678
|
}, [dir, loadRepo]);
|
|
697
679
|
|
|
@@ -700,66 +682,66 @@ window.__ModuleLoader__.load({
|
|
|
700
682
|
try {
|
|
701
683
|
const r = await jpost("/merge-push", { dir: dir || undefined });
|
|
702
684
|
setResult(r);
|
|
703
|
-
if (r.ok) setMsg("已拉取远程更新并推送更改");
|
|
704
|
-
else setErr(r.error || "合并推送失败");
|
|
685
|
+
if (r.ok) setMsg(t("已拉取远程更新并推送更改"));
|
|
686
|
+
else setErr(r.error || t("合并推送失败"));
|
|
705
687
|
void loadRepo(dir, true, true);
|
|
706
|
-
} catch (e) { setErr("合并推送失败:" + e); }
|
|
688
|
+
} catch (e) { setErr(t("合并推送失败:") + e); }
|
|
707
689
|
finally { setBusy(false); }
|
|
708
690
|
}, [dir, loadRepo]);
|
|
709
691
|
|
|
710
692
|
const forcePush = useCallback(async () => {
|
|
711
|
-
if (!window.confirm("强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?")) return;
|
|
693
|
+
if (!window.confirm(t("强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?"))) return;
|
|
712
694
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
713
695
|
try {
|
|
714
696
|
const r = await jpost("/force-push", { dir: dir || undefined });
|
|
715
697
|
setResult(r);
|
|
716
|
-
if (r.ok) setMsg("已强制推送,远程已更新为本地状态");
|
|
717
|
-
else setErr(r.error || "强制推送失败");
|
|
698
|
+
if (r.ok) setMsg(t("已强制推送,远程已更新为本地状态"));
|
|
699
|
+
else setErr(r.error || t("强制推送失败"));
|
|
718
700
|
void loadRepo(dir, true, true);
|
|
719
|
-
} catch (e) { setErr("强制推送失败:" + e); }
|
|
701
|
+
} catch (e) { setErr(t("强制推送失败:") + e); }
|
|
720
702
|
finally { setBusy(false); }
|
|
721
703
|
}, [dir, loadRepo]);
|
|
722
704
|
|
|
723
705
|
const forcePull = useCallback(async () => {
|
|
724
|
-
if (!window.confirm("强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?")) return;
|
|
706
|
+
if (!window.confirm(t("强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?"))) return;
|
|
725
707
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
726
708
|
try {
|
|
727
709
|
const r = await jpost("/force-pull", { dir: dir || undefined });
|
|
728
710
|
setResult(r);
|
|
729
|
-
if (r.ok) setMsg("已强制拉取远程更新");
|
|
730
|
-
else setErr(r.error || "强制拉取失败");
|
|
711
|
+
if (r.ok) setMsg(t("已强制拉取远程更新"));
|
|
712
|
+
else setErr(r.error || t("强制拉取失败"));
|
|
731
713
|
void loadRepo(dir, true, true);
|
|
732
|
-
} catch (e) { setErr("强制拉取失败:" + e); }
|
|
714
|
+
} catch (e) { setErr(t("强制拉取失败:") + e); }
|
|
733
715
|
finally { setBusy(false); }
|
|
734
716
|
}, [dir, loadRepo]);
|
|
735
717
|
|
|
736
718
|
const changeVisibility = useCallback(async () => {
|
|
737
719
|
const name = (repo && repo.defaultRepoName) || "";
|
|
738
|
-
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
720
|
+
if (!name) { setErr(t("请先加载一个有效的文件夹")); return; }
|
|
739
721
|
const target = visibility === "public" ? "public" : "private";
|
|
740
|
-
if (!window.confirm("确定把仓库 " + name + " 改为「" + (target === "public" ? "公开" : "私有") + "」?修改可见性可能影响 Star、关注者等。")) return;
|
|
722
|
+
if (!window.confirm(t("确定把仓库 ") + name + t(" 改为「") + (target === "public" ? t("公开") : t("私有")) + t("」?修改可见性可能影响 Star、关注者等。"))) return;
|
|
741
723
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
742
724
|
try {
|
|
743
725
|
const r = await jpost("/set-visibility", { dir: dir || undefined, name, visibility: target, provider: provRef.current || "github" });
|
|
744
726
|
setResult(r);
|
|
745
|
-
if (r.ok) setMsg("已把仓库设置为「" + (target === "public" ? "公开" : "私有") + "」");
|
|
746
|
-
else setErr(r.error || "修改可见性失败");
|
|
727
|
+
if (r.ok) setMsg(t("已把仓库设置为「") + (target === "public" ? t("公开") : t("私有")) + "」");
|
|
728
|
+
else setErr(r.error || t("修改可见性失败"));
|
|
747
729
|
void loadRepo(dir, true, true);
|
|
748
|
-
} catch (e) { setErr("修改可见性失败:" + e); }
|
|
730
|
+
} catch (e) { setErr(t("修改可见性失败:") + e); }
|
|
749
731
|
finally { setBusy(false); }
|
|
750
732
|
}, [dir, repo, visibility, loadRepo]);
|
|
751
733
|
|
|
752
734
|
const create = useCallback(async () => {
|
|
753
735
|
const name = (repo && repo.defaultRepoName) || "";
|
|
754
|
-
if (!name) { setErr("请先加载一个有效的文件夹"); return; }
|
|
736
|
+
if (!name) { setErr(t("请先加载一个有效的文件夹")); return; }
|
|
755
737
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
756
738
|
try {
|
|
757
739
|
const r = await jpost("/create", { dir: dir || undefined, name, visibility, provider: provRef.current || "github" });
|
|
758
740
|
setResult(r);
|
|
759
|
-
if (r.ok) setMsg("仓库已创建并推送:" + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
760
|
-
else setErr(r.error || "创建失败");
|
|
741
|
+
if (r.ok) setMsg(t("仓库已创建并推送:") + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
742
|
+
else setErr(r.error || t("创建失败"));
|
|
761
743
|
void loadRepo(dir, true, true);
|
|
762
|
-
} catch (e) { setErr("创建失败:" + e); }
|
|
744
|
+
} catch (e) { setErr(t("创建失败:") + e); }
|
|
763
745
|
finally { setBusy(false); }
|
|
764
746
|
}, [dir, repo, visibility, loadRepo]);
|
|
765
747
|
|
|
@@ -776,15 +758,15 @@ window.__ModuleLoader__.load({
|
|
|
776
758
|
try {
|
|
777
759
|
const r = await jpost("/pick-dir", { initial: pickPath || cache.defDir || undefined });
|
|
778
760
|
if (r.ok && r.dir) setPickPath(r.dir);
|
|
779
|
-
else setPickErr(r.error || "未选择目录");
|
|
780
|
-
} catch (e) { setPickErr("选择失败:" + e); }
|
|
761
|
+
else setPickErr(r.error || t("未选择目录"));
|
|
762
|
+
} catch (e) { setPickErr(t("选择失败:") + e); }
|
|
781
763
|
finally { setPicking(false); }
|
|
782
764
|
}, [pickPath]);
|
|
783
765
|
|
|
784
766
|
// 确认:持久化加入列表 + 加载
|
|
785
767
|
const confirmPick = useCallback(async () => {
|
|
786
768
|
const path = String(pickPath || "").trim();
|
|
787
|
-
if (!path) { setPickErr("请输入或选择目录路径"); return; }
|
|
769
|
+
if (!path) { setPickErr(t("请输入或选择目录路径")); return; }
|
|
788
770
|
setPicking(true); setPickErr(null);
|
|
789
771
|
try {
|
|
790
772
|
const r = await jpost("/add-workspace", { dir: path });
|
|
@@ -796,11 +778,11 @@ window.__ModuleLoader__.load({
|
|
|
796
778
|
// 切换到新目录,清空上一个目录的操作结果/提示。
|
|
797
779
|
setResult(null); setMsg(null); setErr(null);
|
|
798
780
|
setPickOpen(false);
|
|
799
|
-
loadRepo(r.dir);
|
|
781
|
+
loadRepo(r.dir, true, true);
|
|
800
782
|
} else {
|
|
801
|
-
setPickErr(r.error || "添加目录失败");
|
|
783
|
+
setPickErr(r.error || t("添加目录失败"));
|
|
802
784
|
}
|
|
803
|
-
} catch (e) { setPickErr("添加目录失败:" + e); }
|
|
785
|
+
} catch (e) { setPickErr(t("添加目录失败:") + e); }
|
|
804
786
|
finally { setPicking(false); }
|
|
805
787
|
}, [pickPath, loadRepo]);
|
|
806
788
|
|
|
@@ -822,26 +804,26 @@ window.__ModuleLoader__.load({
|
|
|
822
804
|
if (dir === path) {
|
|
823
805
|
const next = (r.workspaces && r.workspaces[0]) || "";
|
|
824
806
|
setDir(next); setDirDraft(next);
|
|
825
|
-
if (next) loadRepo(next); else setRepo(null);
|
|
807
|
+
if (next) loadRepo(next, true, true); else setRepo(null);
|
|
826
808
|
}
|
|
827
809
|
} else {
|
|
828
|
-
setErr(r.error || "删除失败");
|
|
810
|
+
setErr(r.error || t("删除失败"));
|
|
829
811
|
}
|
|
830
|
-
} catch (e) { setErr("删除失败:" + e); }
|
|
812
|
+
} catch (e) { setErr(t("删除失败:") + e); }
|
|
831
813
|
finally { setBusy(false); }
|
|
832
814
|
}, [dir, loadRepo]);
|
|
833
815
|
|
|
834
816
|
// 强制对齐:本地完全重置为远程分支状态(丢弃本地差异)
|
|
835
817
|
const align = useCallback(async () => {
|
|
836
|
-
if (!window.confirm("强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?")) return;
|
|
818
|
+
if (!window.confirm(t("强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?"))) return;
|
|
837
819
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
838
820
|
try {
|
|
839
821
|
const r = await jpost("/align", { dir: dir || undefined });
|
|
840
822
|
setResult(r);
|
|
841
|
-
if (r.ok) setMsg("已强制对齐到远程分支");
|
|
842
|
-
else setErr(r.error || "强制对齐失败");
|
|
843
|
-
void loadRepo(dir);
|
|
844
|
-
} catch (e) { setErr("强制对齐失败:" + e); }
|
|
823
|
+
if (r.ok) setMsg(t("已强制对齐到远程分支"));
|
|
824
|
+
else setErr(r.error || t("强制对齐失败"));
|
|
825
|
+
void loadRepo(dir, true, true);
|
|
826
|
+
} catch (e) { setErr(t("强制对齐失败:") + e); }
|
|
845
827
|
finally { setBusy(false); }
|
|
846
828
|
}, [dir, loadRepo]);
|
|
847
829
|
|
|
@@ -851,10 +833,10 @@ window.__ModuleLoader__.load({
|
|
|
851
833
|
try {
|
|
852
834
|
const r = await jpost("/init-git", { dir: dir || undefined });
|
|
853
835
|
setResult(r);
|
|
854
|
-
if (r.ok) setMsg(r.alreadyRepo ? "已是 git 仓库" : "已创建 git 仓库,请自行拉取或推送");
|
|
855
|
-
else setErr(r.error || "创建 git 失败");
|
|
856
|
-
void loadRepo(dir);
|
|
857
|
-
} catch (e) { setErr("创建 git 失败:" + e); }
|
|
836
|
+
if (r.ok) setMsg(r.alreadyRepo ? t("已是 git 仓库") : t("已创建 git 仓库,请自行拉取或推送"));
|
|
837
|
+
else setErr(r.error || t("创建 git 失败"));
|
|
838
|
+
void loadRepo(dir, true, true);
|
|
839
|
+
} catch (e) { setErr(t("创建 git 失败:") + e); }
|
|
858
840
|
finally { setBusy(false); }
|
|
859
841
|
}, [dir, loadRepo]);
|
|
860
842
|
|
|
@@ -869,9 +851,9 @@ window.__ModuleLoader__.load({
|
|
|
869
851
|
// 暂存会改变文件的 staged 状态:清掉该目录的缓存快照,强制走网络路径
|
|
870
852
|
// 重新拉取最新 repo(含最新「已暂存/未暂存」标记),并清 diff 展开状态。
|
|
871
853
|
if (dir) delete cache.reposByDir[dir];
|
|
872
|
-
loadRepo(dir, true);
|
|
873
|
-
} else setErr(r.error || "暂存失败");
|
|
874
|
-
} catch (e) { setErr("暂存失败:" + e); }
|
|
854
|
+
loadRepo(dir, true, true);
|
|
855
|
+
} else setErr(r.error || t("暂存失败"));
|
|
856
|
+
} catch (e) { setErr(t("暂存失败:") + e); }
|
|
875
857
|
finally { setStageBusy(false); }
|
|
876
858
|
}, [dir, loadRepo]);
|
|
877
859
|
|
|
@@ -882,9 +864,9 @@ window.__ModuleLoader__.load({
|
|
|
882
864
|
const r = await jpost("/unstage", { dir: dir || undefined, path: path || undefined });
|
|
883
865
|
if (r.ok) {
|
|
884
866
|
if (dir) delete cache.reposByDir[dir];
|
|
885
|
-
loadRepo(dir, true);
|
|
886
|
-
} else setErr(r.error || "取消暂存失败");
|
|
887
|
-
} catch (e) { setErr("取消暂存失败:" + e); }
|
|
867
|
+
loadRepo(dir, true, true);
|
|
868
|
+
} else setErr(r.error || t("取消暂存失败"));
|
|
869
|
+
} catch (e) { setErr(t("取消暂存失败:") + e); }
|
|
888
870
|
finally { setStageBusy(false); }
|
|
889
871
|
}, [dir, loadRepo]);
|
|
890
872
|
|
|
@@ -897,38 +879,58 @@ window.__ModuleLoader__.load({
|
|
|
897
879
|
const r = await jpost("/commit", { dir: dir || undefined, message });
|
|
898
880
|
setResult(r);
|
|
899
881
|
if (r.ok) {
|
|
900
|
-
setMsg("已提交 " + r.staged + " 个文件" + (r.hash ? "(" + r.hash + ")" : ""));
|
|
882
|
+
setMsg(t("已提交 ") + r.staged + t(" 个文件") + (r.hash ? "(" + r.hash + ")" : ""));
|
|
901
883
|
setCommitMsg("");
|
|
902
884
|
void loadRepo(dir, true, true);
|
|
903
|
-
} else setErr(r.error || "提交失败");
|
|
904
|
-
} catch (e) { setErr("提交失败:" + e); }
|
|
885
|
+
} else setErr(r.error || t("提交失败"));
|
|
886
|
+
} catch (e) { setErr(t("提交失败:") + e); }
|
|
905
887
|
finally { setBusy(false); }
|
|
906
888
|
}, [dir, repo, commitMsg, repoName, loadRepo]);
|
|
907
889
|
|
|
890
|
+
// 「推送暂存」:把已暂存的内容用填写的(或自动生成的)信息提交后推送远程。
|
|
891
|
+
// 与「推送更改」不同:它只提交暂存区、沿用自定义信息,不自动 add 全部改动。
|
|
892
|
+
const doPushStaged = useCallback(async () => {
|
|
893
|
+
setPushStagedBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
894
|
+
try {
|
|
895
|
+
const r = await jpost("/push-staged", { dir: dir || undefined, message: commitMsg.trim() });
|
|
896
|
+
setResult(r);
|
|
897
|
+
if (r.ok) {
|
|
898
|
+
const info = r.committed
|
|
899
|
+
? t("已提交「") + (r.message || t("(自动生成)")) + t("」并推送") + (r.commitHash ? "(" + r.commitHash + ")" : "")
|
|
900
|
+
: t("已推送本地已有的提交");
|
|
901
|
+
setMsg(info);
|
|
902
|
+
setCommitMsg("");
|
|
903
|
+
} else setErr(r.error || r.pushError || t("推送失败"));
|
|
904
|
+
// 推送成功后刷新状态(full 联网同步,显示「刷新中…」)。
|
|
905
|
+
void loadRepo(dir, true, true);
|
|
906
|
+
} catch (e) { setErr(t("推送失败:") + e); }
|
|
907
|
+
finally { setPushStagedBusy(false); }
|
|
908
|
+
}, [dir, commitMsg, loadRepo]);
|
|
909
|
+
|
|
908
910
|
// 打开分支切换弹窗。
|
|
909
911
|
const openBranches = useCallback(async () => {
|
|
910
912
|
setDetail("branches"); setBranchBusy(true); setBranches([]);
|
|
911
913
|
try {
|
|
912
914
|
const r = await jpost("/branches", { dir: dir || undefined });
|
|
913
915
|
if (r.ok) { setBranches(r.branches || []); setBranchCurrent(r.current || ""); }
|
|
914
|
-
else setErr(r.error || "读取分支失败");
|
|
915
|
-
} catch (e) { setErr("读取分支失败:" + e); }
|
|
916
|
+
else setErr(r.error || t("读取分支失败"));
|
|
917
|
+
} catch (e) { setErr(t("读取分支失败:") + e); }
|
|
916
918
|
finally { setBranchBusy(false); }
|
|
917
919
|
}, [dir]);
|
|
918
920
|
|
|
919
921
|
// 切换到某分支。
|
|
920
922
|
const doCheckout = useCallback(async (branch) => {
|
|
921
923
|
if (branch === branchCurrent) { setDetail(null); return; }
|
|
922
|
-
if (!window.confirm("切换到分支「" + branch + "」?(若当前有未提交改动,git 会拒绝切换)")) return;
|
|
924
|
+
if (!window.confirm(t("切换到分支「") + branch + t("」?(若当前有未提交改动,git 会拒绝切换)"))) return;
|
|
923
925
|
setBranchBusy(true); setMsg(null); setErr(null);
|
|
924
926
|
try {
|
|
925
927
|
const r = await jpost("/checkout", { dir: dir || undefined, branch });
|
|
926
928
|
if (r.ok) {
|
|
927
|
-
setMsg("已切换到分支 " + branch);
|
|
929
|
+
setMsg(t("已切换到分支 ") + branch);
|
|
928
930
|
setDetail(null);
|
|
929
931
|
void loadRepo(dir, true, true);
|
|
930
|
-
} else setErr(r.error || "切换分支失败");
|
|
931
|
-
} catch (e) { setErr("切换分支失败:" + e); }
|
|
932
|
+
} else setErr(r.error || t("切换分支失败"));
|
|
933
|
+
} catch (e) { setErr(t("切换分支失败:") + e); }
|
|
932
934
|
finally { setBranchBusy(false); }
|
|
933
935
|
}, [dir, branchCurrent, loadRepo]);
|
|
934
936
|
|
|
@@ -938,38 +940,38 @@ window.__ModuleLoader__.load({
|
|
|
938
940
|
try {
|
|
939
941
|
const r = await jpost("/log", { dir: dir || undefined, count: 30 });
|
|
940
942
|
if (r.ok) setCommits(r.commits || []);
|
|
941
|
-
else setErr(r.error || "读取历史失败");
|
|
942
|
-
} catch (e) { setErr("读取历史失败:" + e); }
|
|
943
|
+
else setErr(r.error || t("读取历史失败"));
|
|
944
|
+
} catch (e) { setErr(t("读取历史失败:") + e); }
|
|
943
945
|
finally { setCommitsLoading(false); }
|
|
944
946
|
}, [dir]);
|
|
945
947
|
|
|
946
948
|
// 对某提交执行 revert(改写历史,需确认)。
|
|
947
949
|
const doRevert = useCallback(async (hash, subject) => {
|
|
948
|
-
if (!window.confirm("revert 提交「" + (subject || hash) + "」——会生成一个反向提交,期间可能产生冲突,确定继续?")) return;
|
|
950
|
+
if (!window.confirm(t("revert 提交「") + (subject || hash) + t("」——会生成一个反向提交,期间可能产生冲突,确定继续?"))) return;
|
|
949
951
|
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
950
952
|
try {
|
|
951
953
|
const r = await jpost("/revert", { dir: dir || undefined, hash });
|
|
952
954
|
if (r.ok) {
|
|
953
|
-
setMsg("已 revert 提交 " + hash);
|
|
955
|
+
setMsg(t("已 revert 提交 ") + hash);
|
|
954
956
|
setDetail(null);
|
|
955
957
|
void loadRepo(dir, true, true);
|
|
956
|
-
} else setErr(r.error || "revert 失败");
|
|
957
|
-
} catch (e) { setErr("revert 失败:" + e); }
|
|
958
|
+
} else setErr(r.error || t("revert 失败"));
|
|
959
|
+
} catch (e) { setErr(t("revert 失败:") + e); }
|
|
958
960
|
finally { setHistoryBusy(false); }
|
|
959
961
|
}, [dir, loadRepo]);
|
|
960
962
|
|
|
961
963
|
// 对某提交执行 cherry-pick(改写历史,需确认)。
|
|
962
964
|
const doCherryPick = useCallback(async (hash, subject) => {
|
|
963
|
-
if (!window.confirm("cherry-pick 提交「" + (subject || hash) + "」到当前分支——期间可能产生冲突,确定继续?")) return;
|
|
965
|
+
if (!window.confirm(t("cherry-pick 提交「") + (subject || hash) + t("」到当前分支——期间可能产生冲突,确定继续?"))) return;
|
|
964
966
|
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
965
967
|
try {
|
|
966
968
|
const r = await jpost("/cherrypick", { dir: dir || undefined, hash });
|
|
967
969
|
if (r.ok) {
|
|
968
|
-
setMsg("已 cherry-pick 提交 " + hash);
|
|
970
|
+
setMsg(t("已 cherry-pick 提交 ") + hash);
|
|
969
971
|
setDetail(null);
|
|
970
972
|
void loadRepo(dir, true, true);
|
|
971
|
-
} else setErr(r.error || "cherry-pick 失败");
|
|
972
|
-
} catch (e) { setErr("cherry-pick 失败:" + e); }
|
|
973
|
+
} else setErr(r.error || t("cherry-pick 失败"));
|
|
974
|
+
} catch (e) { setErr(t("cherry-pick 失败:") + e); }
|
|
973
975
|
finally { setHistoryBusy(false); }
|
|
974
976
|
}, [dir, loadRepo]);
|
|
975
977
|
|
|
@@ -979,7 +981,7 @@ window.__ModuleLoader__.load({
|
|
|
979
981
|
setHistoryDetail({ hash, loading: true, diff: "", error: null });
|
|
980
982
|
try {
|
|
981
983
|
const r = await jpost("/commit-diff", { dir: dir || undefined, hash });
|
|
982
|
-
setHistoryDetail({ hash, loading: false, diff: (r && r.diff) || "", error: r && !r.ok ? (r.error || "读取失败") : null });
|
|
984
|
+
setHistoryDetail({ hash, loading: false, diff: (r && r.diff) || "", error: r && !r.ok ? (r.error || t("读取失败")) : null });
|
|
983
985
|
} catch (e) {
|
|
984
986
|
setHistoryDetail({ hash, loading: false, diff: "", error: String(e) });
|
|
985
987
|
}
|
|
@@ -994,42 +996,42 @@ window.__ModuleLoader__.load({
|
|
|
994
996
|
const ok = await copyText(text);
|
|
995
997
|
setCommitMenu(null);
|
|
996
998
|
if (ok) {
|
|
997
|
-
setCopiedTip("已复制" + (kind === "short" ? "短哈希" : kind === "full" ? "完整哈希" : "提交信息"));
|
|
999
|
+
setCopiedTip(t("已复制") + (kind === "short" ? t("短哈希") : kind === "full" ? t("完整哈希") : t("提交信息")));
|
|
998
1000
|
window.setTimeout(() => setCopiedTip(null), 1600);
|
|
999
1001
|
} else {
|
|
1000
|
-
setErr("复制失败:请手动复制");
|
|
1002
|
+
setErr(t("复制失败:请手动复制"));
|
|
1001
1003
|
}
|
|
1002
1004
|
}, []);
|
|
1003
1005
|
|
|
1004
|
-
return h(Box, { title: "③ 代码管理" },
|
|
1006
|
+
return h(Box, { title: t("③ 代码管理") },
|
|
1005
1007
|
// 平台提示 + Gitee 令牌(仅 gitee 模式)
|
|
1006
1008
|
prov === "gitee" ? h("div", { style: { border: "1px solid " + T.border, borderRadius: 10, padding: 10, marginBottom: 10, background: T.layer1 } },
|
|
1007
1009
|
h("div", { style: { fontSize: 12, fontWeight: 600, color: T.label, marginBottom: 6 } },
|
|
1008
|
-
"Gitee 私人令牌(OpenAPI,需 projects 权限)"),
|
|
1010
|
+
t("Gitee 私人令牌(OpenAPI,需 projects 权限)")),
|
|
1009
1011
|
giteeConfigured === true
|
|
1010
1012
|
? h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
1011
|
-
h("span", { style: { color: T.success, fontSize: 12 } }, "✅ 已配置" + (giteeOwnerName ? "(账号 " + giteeOwnerName + ")" : "")),
|
|
1012
|
-
h(Btn, { label: "清除令牌", onClick: clearGiteeToken, disabled: giteeTokenBusy, noBg: true, tone: "danger" })
|
|
1013
|
+
h("span", { style: { color: T.success, fontSize: 12 } }, t("✅ 已配置") + (giteeOwnerName ? t("(账号 ") + giteeOwnerName + ")" : "")),
|
|
1014
|
+
h(Btn, { label: t("清除令牌"), onClick: clearGiteeToken, disabled: giteeTokenBusy, noBg: true, tone: "danger" })
|
|
1013
1015
|
)
|
|
1014
1016
|
: h("div", null,
|
|
1015
1017
|
h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
1016
1018
|
h("input", {
|
|
1017
1019
|
type: "password", value: giteeToken, disabled: giteeTokenBusy,
|
|
1018
|
-
placeholder: "粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)",
|
|
1020
|
+
placeholder: t("粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)"),
|
|
1019
1021
|
onChange: (e) => setGiteeToken(e.target.value),
|
|
1020
1022
|
onKeyDown: (e) => { if (e.key === "Enter") void saveGiteeToken(); },
|
|
1021
1023
|
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" },
|
|
1022
1024
|
}),
|
|
1023
|
-
h(Btn, { label: "保存令牌", onClick: saveGiteeToken, tone: "primary", noBg: true, disabled: giteeTokenBusy || !String(giteeToken || "").trim() }),
|
|
1025
|
+
h(Btn, { label: t("保存令牌"), onClick: saveGiteeToken, tone: "primary", noBg: true, disabled: giteeTokenBusy || !String(giteeToken || "").trim() }),
|
|
1024
1026
|
),
|
|
1025
1027
|
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 6, lineHeight: 1.6 } },
|
|
1026
|
-
"令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。")
|
|
1028
|
+
t("令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。"))
|
|
1027
1029
|
),
|
|
1028
1030
|
giteeTokenMsg ? h("div", { style: { marginTop: 6, color: T.success, fontSize: 12 } }, "✅ " + giteeTokenMsg) : null,
|
|
1029
1031
|
giteeTokenErr ? h("div", { style: { marginTop: 6, color: T.danger, fontSize: 12 } }, "❌ " + giteeTokenErr) : null
|
|
1030
1032
|
) : null,
|
|
1031
1033
|
h("div", { style: { display: "flex", gap: 8, marginBottom: 10, alignItems: "center" } },
|
|
1032
|
-
h("span", { style: { color: T.secondary, fontSize: 13, whiteSpace: "nowrap" } }, "选择工作区"),
|
|
1034
|
+
h("span", { style: { color: T.secondary, fontSize: 13, whiteSpace: "nowrap" } }, t("选择工作区")),
|
|
1033
1035
|
h("select", {
|
|
1034
1036
|
value: dir,
|
|
1035
1037
|
disabled: workspaces.length === 0,
|
|
@@ -1040,85 +1042,86 @@ window.__ModuleLoader__.load({
|
|
|
1040
1042
|
setResult(null); setMsg(null); setErr(null);
|
|
1041
1043
|
setDir(v);
|
|
1042
1044
|
setDirDraft(v);
|
|
1043
|
-
|
|
1045
|
+
// full 同步:联网获取切换后工作区的最新状态,并显示「刷新中…」。
|
|
1046
|
+
loadRepo(v, true, true);
|
|
1044
1047
|
},
|
|
1045
|
-
title: "选择 DSH 已登记的工作区文件夹",
|
|
1048
|
+
title: t("选择 DSH 已登记的工作区文件夹"),
|
|
1046
1049
|
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" },
|
|
1047
1050
|
},
|
|
1048
|
-
workspaces.length === 0 ? h("option", { key: "__empty", value: "" }, "(暂无可选工作区)")
|
|
1049
|
-
: h("option", { key: "__none", value: "" }, "— 请选择 —"),
|
|
1051
|
+
workspaces.length === 0 ? h("option", { key: "__empty", value: "" }, t("(暂无可选工作区)"))
|
|
1052
|
+
: h("option", { key: "__none", value: "" }, t("— 请选择 —")),
|
|
1050
1053
|
workspaces.map((w) =>
|
|
1051
1054
|
h("option", { key: w, value: w }, String(w).split(/[\\/]/).filter(Boolean).pop() || w)
|
|
1052
1055
|
)
|
|
1053
1056
|
),
|
|
1054
|
-
h(Btn, { label: "选择目录", onClick: openPicker, disabled: picking, noBg: true, tone: "primary" }),
|
|
1057
|
+
h(Btn, { label: t("选择目录"), onClick: openPicker, disabled: picking, noBg: true, tone: "primary" }),
|
|
1055
1058
|
),
|
|
1056
1059
|
dir && customDirs && customDirs.includes(dir) ? h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10, alignItems: "center" } },
|
|
1057
|
-
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, "自定义目录:"),
|
|
1058
|
-
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); } },
|
|
1060
|
+
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, t("自定义目录:")),
|
|
1061
|
+
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); } },
|
|
1059
1062
|
String(dir).split(/[\\/]/).filter(Boolean).pop() || dir,
|
|
1060
1063
|
h("span", {
|
|
1061
|
-
role: "button", "aria-label": "删除该目录记录",
|
|
1062
|
-
title: "删除该目录记录(不删除实际文件夹)",
|
|
1064
|
+
role: "button", "aria-label": t("删除该目录记录"),
|
|
1065
|
+
title: t("删除该目录记录(不删除实际文件夹)"),
|
|
1063
1066
|
onClick: (e) => { e.stopPropagation(); void removeWorkspace(dir); },
|
|
1064
1067
|
style: { color: T.danger, fontWeight: 700, padding: "0 2px", cursor: "pointer" },
|
|
1065
1068
|
}, "✕")
|
|
1066
1069
|
)
|
|
1067
1070
|
) : null,
|
|
1068
1071
|
repo && repo.isGitRepo ? h("div", { style: { marginBottom: 10 } },
|
|
1069
|
-
h(Field, { label: "目录", value: repo.dir }),
|
|
1070
|
-
h(Field, { label: "分支" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1071
|
-
h("span", { style: { color: T.label, fontSize: 13 } }, repo.branch || "(无)"),
|
|
1072
|
+
h(Field, { label: t("目录"), value: repo.dir }),
|
|
1073
|
+
h(Field, { label: t("分支") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1074
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.branch || t("(无)")),
|
|
1072
1075
|
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1073
1076
|
repo.branch ? h("button", {
|
|
1074
1077
|
type: "button",
|
|
1075
1078
|
onClick: openBranches,
|
|
1076
|
-
title: "切换分支",
|
|
1079
|
+
title: t("切换分支"),
|
|
1077
1080
|
style: changeBtnStyle(),
|
|
1078
|
-
}, "切换") : null,
|
|
1081
|
+
}, t("切换")) : null,
|
|
1079
1082
|
h("button", {
|
|
1080
1083
|
type: "button",
|
|
1081
1084
|
onClick: openHistory,
|
|
1082
|
-
title: "查看提交历史",
|
|
1085
|
+
title: t("查看提交历史"),
|
|
1083
1086
|
style: changeBtnStyle(),
|
|
1084
|
-
}, "历史")
|
|
1087
|
+
}, t("历史"))
|
|
1085
1088
|
) : null
|
|
1086
1089
|
)),
|
|
1087
|
-
h(Field, { label: "远程", value: repo.remoteUrl || "(无)" }),
|
|
1088
|
-
h(Field, { label: "改动" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1089
|
-
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + " 个文件" : "无"),
|
|
1090
|
+
h(Field, { label: t("远程"), value: repo.remoteUrl || t("(无)") }),
|
|
1091
|
+
h(Field, { label: t("改动") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1092
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + t(" 个文件") : t("无")),
|
|
1090
1093
|
repo.changedFiles && repo.changedFiles.length > 0 ? h("button", {
|
|
1091
1094
|
type: "button",
|
|
1092
1095
|
onClick: () => setDetail("changes"),
|
|
1093
|
-
title: "查看改动的文件列表",
|
|
1096
|
+
title: t("查看改动的文件列表"),
|
|
1094
1097
|
style: changeBtnStyle(),
|
|
1095
|
-
}, "查看")
|
|
1098
|
+
}, t("查看"))
|
|
1096
1099
|
: null
|
|
1097
1100
|
)),
|
|
1098
|
-
h(Field, { label: "同步" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1101
|
+
h(Field, { label: t("同步") }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1099
1102
|
h("span", { style: { color: T.label, fontSize: 13 } },
|
|
1100
1103
|
// 没有(属于当前账号/平台的)远程时不做同步判断,避免误显示「与远程一致」
|
|
1101
1104
|
!repo.hasRemote
|
|
1102
|
-
? "(无)"
|
|
1103
|
-
: (repo.ahead > 0 ? "本地领先 " + repo.ahead + " 提交" : "") +
|
|
1104
|
-
(repo.ahead > 0 && repo.behind > 0 ? "、落后 " + repo.behind + " 提交" : (repo.behind > 0 ? "落后 " + repo.behind + " 提交" : "")) +
|
|
1105
|
-
((repo.ahead === 0 && repo.behind === 0) ? "与远程一致" : "")
|
|
1105
|
+
? t("(无)")
|
|
1106
|
+
: (repo.ahead > 0 ? t("本地领先 ") + repo.ahead + t(" 提交") : "") +
|
|
1107
|
+
(repo.ahead > 0 && repo.behind > 0 ? t("、落后 ") + repo.behind + t(" 提交") : (repo.behind > 0 ? t("落后 ") + repo.behind + t(" 提交") : "")) +
|
|
1108
|
+
((repo.ahead === 0 && repo.behind === 0) ? t("与远程一致") : "")
|
|
1106
1109
|
),
|
|
1107
1110
|
(repo.ahead > 0 || repo.behind > 0) ? h("button", {
|
|
1108
1111
|
type: "button",
|
|
1109
1112
|
onClick: () => setDetail("sync"),
|
|
1110
|
-
title: "查看本地与远程的提交差异",
|
|
1113
|
+
title: t("查看本地与远程的提交差异"),
|
|
1111
1114
|
style: changeBtnStyle(),
|
|
1112
|
-
}, "查看")
|
|
1115
|
+
}, t("查看"))
|
|
1113
1116
|
: null
|
|
1114
1117
|
)),
|
|
1115
1118
|
repo.ignoredLarge && repo.ignoredLarge.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
1116
1119
|
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
1117
|
-
"⚠️ 以下 " + repo.ignoredLarge.length + " 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):"),
|
|
1120
|
+
t("⚠️ 以下 ") + repo.ignoredLarge.length + t(" 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):")),
|
|
1118
1121
|
h("div", { style: { maxHeight: 120, overflow: "auto", background: T.layer1, borderRadius: 8, padding: 8 } },
|
|
1119
1122
|
repo.ignoredLarge.slice(0, 30).map((f, i) =>
|
|
1120
1123
|
h("div", { key: i, style: { fontSize: 11, color: T.secondary, lineHeight: 1.5 } },
|
|
1121
|
-
(f.kind === "dir" ? "📁 " + f.path + "/(整个文件夹)" : "📄 " + f.path) +
|
|
1124
|
+
(f.kind === "dir" ? "📁 " + f.path + t("/(整个文件夹)") : "📄 " + f.path) +
|
|
1122
1125
|
" — " + fmtMB(f.bytes))
|
|
1123
1126
|
)
|
|
1124
1127
|
)
|
|
@@ -1134,7 +1137,7 @@ window.__ModuleLoader__.load({
|
|
|
1134
1137
|
const btns = [];
|
|
1135
1138
|
// 非 git 仓库但远程存在同名仓库 -> 只「创建 Git」(由用户决定后续拉取/推送)
|
|
1136
1139
|
if (repo && !repo.isGitRepo && repo.repoExists === true) {
|
|
1137
|
-
btns.push(h(Btn, { key: "init", label: "创建 Git", onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: "仅初始化 git 仓库,不拉取不推送,由你决定下一步" }));
|
|
1140
|
+
btns.push(h(Btn, { key: "init", label: t("创建 Git"), onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: t("仅初始化 git 仓库,不拉取不推送,由你决定下一步") }));
|
|
1138
1141
|
} else if (hasRemote) {
|
|
1139
1142
|
if (localChanges && remoteUpdates) {
|
|
1140
1143
|
// 本地和远程都有更新:原名展示「拉取更新并推送更改 / 强制推送 /
|
|
@@ -1142,34 +1145,45 @@ window.__ModuleLoader__.load({
|
|
|
1142
1145
|
// 分支保护等原因失败且行为难预测,故不再展示——该状态唯一的
|
|
1143
1146
|
// 操作按钮是「强制对齐」(fetch + reset --hard,本地完全重置为远程)。
|
|
1144
1147
|
btns.push(h("span", { key: "both-note", style: { color: T.warn, fontSize: 12 } },
|
|
1145
|
-
"本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态"));
|
|
1148
|
+
t("本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态")));
|
|
1146
1149
|
if (repo && repo.isGitRepo) {
|
|
1147
|
-
btns.push(h(Btn, { key: "align", label: "强制对齐", onClick: align, disabled: isBlocked, title: "本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况" }));
|
|
1150
|
+
btns.push(h(Btn, { key: "align", label: t("强制对齐"), onClick: align, disabled: isBlocked, title: t("本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况") }));
|
|
1148
1151
|
}
|
|
1149
1152
|
} else if (localChanges) {
|
|
1150
1153
|
// 只有本地有更改 -> 只显示推送(正常 push)
|
|
1151
|
-
btns.push(h(Btn, { key: "push", label: "推送更改", onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
1154
|
+
btns.push(h(Btn, { key: "push", label: t("推送更改"), onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
1152
1155
|
} else if (remoteUpdates) {
|
|
1153
1156
|
// 只有远程有更新(本地干净)-> 只显示「拉取更新」。
|
|
1154
1157
|
// 实现复用「强制对齐」逻辑(fetch + reset --hard):本地干净
|
|
1155
1158
|
// 无改动可丢,reset 等同快速前进到远程最新,直接执行不弹确认。
|
|
1156
|
-
btns.push(h(Btn, { key: "pull", label: "拉取更新", onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
1159
|
+
btns.push(h(Btn, { key: "pull", label: t("拉取更新"), onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
1157
1160
|
} else {
|
|
1158
1161
|
// 完全同步 -> 无按钮,显示已是最新
|
|
1159
|
-
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, "✓ 已是最新"));
|
|
1162
|
+
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, t("✓ 已是最新")));
|
|
1160
1163
|
}
|
|
1161
1164
|
}
|
|
1162
1165
|
// 刷新/切换平台进行中给出明确提示,让用户知道正在联网同步或重新检测。
|
|
1163
1166
|
const refreshingTip = refreshing
|
|
1164
|
-
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…")
|
|
1167
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, t("⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…"))
|
|
1165
1168
|
: switching
|
|
1166
|
-
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 切换平台,正在重新检测「" + (prov === "gitee" ? "Gitee" : "GitHub") + "」仓库状态…")
|
|
1169
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, t("⟳ 切换平台,正在重新检测「") + (prov === "gitee" ? "Gitee" : "GitHub") + t("」仓库状态…"))
|
|
1167
1170
|
: null
|
|
1168
1171
|
return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
1169
1172
|
btns,
|
|
1173
|
+
// 「推送暂存」:放在「推送更改」与「刷新状态」之间,按需显示——
|
|
1174
|
+
// 有远程且「本地领先有提交」或「有已暂存的改动」时才出现(未装 better-sidebar 时)。
|
|
1175
|
+
(!hasBetterSidebar && repo && repo.hasRemote
|
|
1176
|
+
&& (repo.ahead > 0 || ((repo.changedFiles || []).some((f) => f.staged)))) ? h(Btn, {
|
|
1177
|
+
key: "push-staged",
|
|
1178
|
+
label: pushStagedBusy ? t("推送中…") : t("推送暂存"),
|
|
1179
|
+
onClick: doPushStaged,
|
|
1180
|
+
tone: "primary", noBg: true,
|
|
1181
|
+
disabled: busy || pushStagedBusy || !dir,
|
|
1182
|
+
title: t("把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动"),
|
|
1183
|
+
}) : null,
|
|
1170
1184
|
// keepRepo=true 保留旧内容避免闪烁;full=true 触发 loadRepo 的「刷新中…」提示 + 联网同步。
|
|
1171
1185
|
h(Btn, {
|
|
1172
|
-
label: refreshing ? "刷新中…" : "刷新状态",
|
|
1186
|
+
label: refreshing ? t("刷新中…") : t("刷新状态"),
|
|
1173
1187
|
onClick: () => { setResult(null); setMsg(null); setErr(null); loadRepo(dir, true, true); },
|
|
1174
1188
|
disabled: busy || refreshing,
|
|
1175
1189
|
}),
|
|
@@ -1180,62 +1194,62 @@ window.__ModuleLoader__.load({
|
|
|
1180
1194
|
!hasBetterSidebar && repo && repo.isGitRepo && repo.dirty ? h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
1181
1195
|
h("input", {
|
|
1182
1196
|
type: "text", value: commitMsg,
|
|
1183
|
-
placeholder: "提交信息(留空则用默认)",
|
|
1184
|
-
title: "填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)",
|
|
1197
|
+
placeholder: t("提交信息(留空则用默认)"),
|
|
1198
|
+
title: t("填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)"),
|
|
1185
1199
|
onChange: (e) => setCommitMsg(e.target.value),
|
|
1186
1200
|
onKeyDown: (e) => { if (e.key === "Enter") void doCommit(); },
|
|
1187
1201
|
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" },
|
|
1188
1202
|
}),
|
|
1189
|
-
h(Btn, { label: "提交", onClick: doCommit, tone: "primary", noBg: true, disabled: busy || stageBusy || repo.dirtyCount <= 0 || !dir }),
|
|
1203
|
+
h(Btn, { label: t("提交"), onClick: doCommit, tone: "primary", noBg: true, disabled: busy || stageBusy || repo.dirtyCount <= 0 || !dir }),
|
|
1190
1204
|
) : null,
|
|
1191
1205
|
h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
1192
1206
|
h("input", {
|
|
1193
1207
|
type: "text", value: repoName, readOnly: true,
|
|
1194
|
-
title: "仓库名称自动取文件夹名(不可修改)",
|
|
1195
|
-
placeholder: "(未加载文件夹)",
|
|
1208
|
+
title: t("仓库名称自动取文件夹名(不可修改)"),
|
|
1209
|
+
placeholder: t("(未加载文件夹)"),
|
|
1196
1210
|
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 },
|
|
1197
1211
|
}),
|
|
1198
1212
|
h("select", {
|
|
1199
1213
|
value: visibility,
|
|
1200
1214
|
onChange: (e) => setVisibility(e.target.value),
|
|
1201
|
-
title: "仓库可见性:私有 / 公开" + (repo && repo.repoExists === true ? "(修改当前仓库的可见性)" : ""),
|
|
1215
|
+
title: t("仓库可见性:私有 / 公开") + (repo && repo.repoExists === true ? t("(修改当前仓库的可见性)") : ""),
|
|
1202
1216
|
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" },
|
|
1203
1217
|
},
|
|
1204
|
-
h("option", { value: "private" }, "私有"),
|
|
1205
|
-
h("option", { value: "public" }, "公开")
|
|
1218
|
+
h("option", { value: "private" }, t("私有")),
|
|
1219
|
+
h("option", { value: "public" }, t("公开"))
|
|
1206
1220
|
),
|
|
1207
1221
|
repo && repo.repoExists === true ? (
|
|
1208
1222
|
// 仓库已存在:根据所选值与当前实际可见性决定按钮 / 提示
|
|
1209
1223
|
repo.visibility === visibility ? h(Btn, {
|
|
1210
|
-
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
1224
|
+
key: "create", label: t("新建仓库并推送"), onClick: create, tone: "success",
|
|
1211
1225
|
disabled: true,
|
|
1212
1226
|
}) : h(Btn, {
|
|
1213
|
-
key: "setvis", label: "修改仓库状态", onClick: changeVisibility, tone: "success",
|
|
1227
|
+
key: "setvis", label: t("修改仓库状态"), onClick: changeVisibility, tone: "success",
|
|
1214
1228
|
disabled: busy || !repoName,
|
|
1215
1229
|
})
|
|
1216
1230
|
) : h(Btn, {
|
|
1217
|
-
key: "create", label: "新建仓库并推送", onClick: create, tone: "success",
|
|
1231
|
+
key: "create", label: t("新建仓库并推送"), onClick: create, tone: "success",
|
|
1218
1232
|
disabled: busy || !repoName,
|
|
1219
1233
|
})
|
|
1220
1234
|
),
|
|
1221
1235
|
repo && repo.repoExists === true ? h("div", { style: { marginTop: 8, fontSize: 12 } },
|
|
1222
1236
|
repo.visibility
|
|
1223
1237
|
? (repo.visibility === visibility
|
|
1224
|
-
? h("span", { style: { color: T.success } }, "✓ 仓库已是「" + (repo.visibility === "public" ? "公开" : "私有") + "」状态,如需修改请调整左侧可见性选择。")
|
|
1225
|
-
: h("span", { style: { color: T.warn } }, "将把仓库从「" + (repo.visibility === "public" ? "公开" : "私有") + "」改为「" + (visibility === "public" ? "公开" : "私有") + "」,点击「修改仓库状态」执行。"))
|
|
1226
|
-
: h("span", { style: { color: T.secondary } }, "⚠️ 同名仓库已经创建(无法读取当前可见性,可能未" + (prov === "gitee" ? "配置 Gitee 令牌" : "登录 gh") + ")")
|
|
1238
|
+
? h("span", { style: { color: T.success } }, t("✓ 仓库已是「") + (repo.visibility === "public" ? t("公开") : t("私有")) + t("」状态,如需修改请调整左侧可见性选择。"))
|
|
1239
|
+
: h("span", { style: { color: T.warn } }, t("将把仓库从「") + (repo.visibility === "public" ? t("公开") : t("私有")) + t("」改为「") + (visibility === "public" ? t("公开") : t("私有")) + t("」,点击「修改仓库状态」执行。")))
|
|
1240
|
+
: h("span", { style: { color: T.secondary } }, t("⚠️ 同名仓库已经创建(无法读取当前可见性,可能未") + (prov === "gitee" ? t("配置 Gitee 令牌") : t("登录 gh")) + ")")
|
|
1227
1241
|
) : repo && repo.repoExists === false ? h("div", { style: { marginTop: 8, color: T.success, fontSize: 12 } },
|
|
1228
|
-
"✓ 同名仓库 " + repoName + " 不存在,可在 " + (prov === "gitee" ? "Gitee" : "GitHub") + "「新建仓库并推送」创建(可选私有/公开)。"
|
|
1242
|
+
t("✓ 同名仓库 ") + repoName + t(" 不存在,可在 ") + (prov === "gitee" ? "Gitee" : "GitHub") + t("「新建仓库并推送」创建(可选私有/公开)。")
|
|
1229
1243
|
) : null,
|
|
1230
1244
|
|
|
1231
1245
|
result ? h("div", { style: { marginTop: 12, borderTop: "1px solid " + T.border, paddingTop: 10 } },
|
|
1232
1246
|
msg ? h("div", { style: { color: T.success, fontSize: 13 } }, "✅ " + msg) : null,
|
|
1233
1247
|
result.skipped && result.skipped.length > 0 ? h("div", { style: { marginTop: 8 } },
|
|
1234
1248
|
h("div", { style: { color: T.warn, fontSize: 12, marginBottom: 4 } },
|
|
1235
|
-
"已忽略未上传(" + result.skipped.length + " 项 >100MB):"),
|
|
1249
|
+
t("已忽略未上传(") + result.skipped.length + t(" 项 >100MB):")),
|
|
1236
1250
|
result.skipped.map((s, i) => h("div", { key: i, style: { fontSize: 11, color: T.warn, lineHeight: 1.5 } }, "· " + s.path + " — " + s.reason)))
|
|
1237
1251
|
: result.ok ? h("div", { style: { color: T.success, fontSize: 12, marginTop: 6 } },
|
|
1238
|
-
"已提交" + (result.commitHash ? " " + result.commitHash : "") + (result.pushed ? " 并推送" : "(推送省略)"))
|
|
1252
|
+
t("已提交") + (result.commitHash ? " " + result.commitHash : "") + (result.pushed ? t(" 并推送") : t("(推送省略)")))
|
|
1239
1253
|
: err ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + err)
|
|
1240
1254
|
: result.pushError ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + result.pushError)
|
|
1241
1255
|
: null
|
|
@@ -1243,19 +1257,19 @@ window.__ModuleLoader__.load({
|
|
|
1243
1257
|
detail ? ReactDOM.createPortal(
|
|
1244
1258
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
1245
1259
|
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: () => setDetail(null) }),
|
|
1246
|
-
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": "查看详情" },
|
|
1260
|
+
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("查看详情") },
|
|
1247
1261
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 10 } },
|
|
1248
1262
|
h("div", { style: { fontSize: 15, fontWeight: 600, flex: 1 } },
|
|
1249
|
-
detail === "changes" ? "改动文件"
|
|
1250
|
-
: detail === "branches" ? "切换分支"
|
|
1251
|
-
: detail === "history" ? "提交历史"
|
|
1252
|
-
: "与远程同步差异"),
|
|
1253
|
-
h("button", { type: "button", style: closeBtnStyle(), "aria-label": "关闭", onClick: () => setDetail(null) }, "✕")
|
|
1263
|
+
detail === "changes" ? t("改动文件")
|
|
1264
|
+
: detail === "branches" ? t("切换分支")
|
|
1265
|
+
: detail === "history" ? t("提交历史")
|
|
1266
|
+
: t("与远程同步差异")),
|
|
1267
|
+
h("button", { type: "button", style: closeBtnStyle(), "aria-label": t("关闭"), onClick: () => setDetail(null) }, "✕")
|
|
1254
1268
|
),
|
|
1255
1269
|
h("div", { style: { flex: 1, overflow: "auto" } },
|
|
1256
1270
|
detail === "changes" ? h(React.Fragment, null,
|
|
1257
1271
|
hasBetterSidebar ? h("div", { style: { marginBottom: 10, padding: "8px 10px", borderRadius: 8, background: T.layer1, color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1258
|
-
"已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理 / Git 面板」查看。")
|
|
1272
|
+
t("已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理 / Git 面板」查看。"))
|
|
1259
1273
|
: null,
|
|
1260
1274
|
repo && repo.changedFiles && repo.changedFiles.length > 0
|
|
1261
1275
|
? repo.changedFiles.map((f, i) => {
|
|
@@ -1297,46 +1311,46 @@ window.__ModuleLoader__.load({
|
|
|
1297
1311
|
borderBottom: "1px solid " + T.border, padding: "3px 2px",
|
|
1298
1312
|
cursor: canHaveDiff ? "pointer" : "default",
|
|
1299
1313
|
},
|
|
1300
|
-
title: canHaveDiff ? (open ? "点击收起" : "点击查看改动内容") : "新文件(untracked)暂无内容 diff",
|
|
1314
|
+
title: canHaveDiff ? (open ? t("点击收起") : t("点击查看改动内容")) : t("新文件(untracked)暂无内容 diff"),
|
|
1301
1315
|
onClick: onClick,
|
|
1302
1316
|
},
|
|
1303
1317
|
h("span", { style: { color: typeColor(f.type), fontSize: 11, flexShrink: 0, width: 52 } }, typeLabel(f.type)),
|
|
1304
1318
|
h("span", { style: { color: T.label, wordBreak: "break-all", flex: 1 } }, f.path),
|
|
1305
1319
|
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1306
|
-
h("span", { style: { fontSize: 11, flexShrink: 0, color: f.staged ? T.success : T.secondary } }, f.staged ? "已暂存" : "未暂存"),
|
|
1320
|
+
h("span", { style: { fontSize: 11, flexShrink: 0, color: f.staged ? T.success : T.secondary } }, f.staged ? t("已暂存") : t("未暂存")),
|
|
1307
1321
|
h("button", {
|
|
1308
1322
|
type: "button",
|
|
1309
|
-
title: f.staged ? "取消暂存" : "暂存",
|
|
1323
|
+
title: f.staged ? t("取消暂存") : t("暂存"),
|
|
1310
1324
|
disabled: stageBusy,
|
|
1311
1325
|
style: changeBtnStyle(),
|
|
1312
1326
|
onClick: (e) => { e.stopPropagation(); if (f.staged) void doUnstage(f.path); else void doStage(f.path); },
|
|
1313
|
-
}, f.staged ? "取消暂存" : "暂存"),
|
|
1327
|
+
}, f.staged ? t("取消暂存") : t("暂存")),
|
|
1314
1328
|
canHaveDiff ? h("span", { style: { color: T.brand, fontSize: 11, flexShrink: 0 } },
|
|
1315
|
-
d.loading ? "加载中…" : (open ? "▾ 收起" : "▸ 查看"))
|
|
1329
|
+
d.loading ? t("加载中…") : (open ? t("▾ 收起") : t("▸ 查看")))
|
|
1316
1330
|
: null
|
|
1317
1331
|
) : null
|
|
1318
1332
|
),
|
|
1319
1333
|
open && canHaveDiff ? (
|
|
1320
|
-
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载改动内容…")
|
|
1334
|
+
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("正在加载改动内容…"))
|
|
1321
1335
|
: d.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + d.error)
|
|
1322
|
-
: d.diff ? renderSideBySideDiff(d.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1336
|
+
: d.diff ? renderSideBySideDiff(d.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(无内容差异)"))
|
|
1323
1337
|
) : null
|
|
1324
1338
|
);
|
|
1325
1339
|
})
|
|
1326
|
-
: h("div", { style: { color: T.secondary, fontSize: 13 } }, "当前没有改动。")
|
|
1340
|
+
: h("div", { style: { color: T.secondary, fontSize: 13 } }, t("当前没有改动。"))
|
|
1327
1341
|
) : detail === "branches" ? (
|
|
1328
|
-
branchBusy ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取分支…")
|
|
1329
|
-
: branches.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(无分支)")
|
|
1342
|
+
branchBusy ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("正在读取分支…"))
|
|
1343
|
+
: branches.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("(无分支)"))
|
|
1330
1344
|
: branches.map((b) =>
|
|
1331
1345
|
h("div", { key: b, style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 2px", borderBottom: "1px solid " + T.border } },
|
|
1332
1346
|
h("span", { style: { flex: 1, color: b === branchCurrent ? T.success : T.label, fontSize: 13, fontWeight: b === branchCurrent ? 600 : 400 } },
|
|
1333
|
-
b + (b === branchCurrent ? "(当前)" : "")),
|
|
1334
|
-
b !== branchCurrent ? h(Btn, { label: "切换", onClick: () => doCheckout(b), disabled: historyBusy || branchBusy, noBg: true, tone: "primary" }) : null
|
|
1347
|
+
b + (b === branchCurrent ? t("(当前)") : "")),
|
|
1348
|
+
b !== branchCurrent ? h(Btn, { label: t("切换"), onClick: () => doCheckout(b), disabled: historyBusy || branchBusy, noBg: true, tone: "primary" }) : null
|
|
1335
1349
|
)
|
|
1336
1350
|
)
|
|
1337
1351
|
) : detail === "history" ? (
|
|
1338
|
-
commitsLoading ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取历史…")
|
|
1339
|
-
: commits.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(暂无提交)")
|
|
1352
|
+
commitsLoading ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("正在读取历史…"))
|
|
1353
|
+
: commits.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, t("(暂无提交)"))
|
|
1340
1354
|
: commits.map((c) => {
|
|
1341
1355
|
const hd = historyDetail && historyDetail.hash === c.hash ? historyDetail : null
|
|
1342
1356
|
const menuOpen = commitMenu === c.hashFull
|
|
@@ -1349,28 +1363,28 @@ window.__ModuleLoader__.load({
|
|
|
1349
1363
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1350
1364
|
h("span", { style: { fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, color: T.brand, flexShrink: 0 } }, c.hash),
|
|
1351
1365
|
h("span", { style: { flex: 1, color: T.label, fontSize: 13, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, c.subject || ""),
|
|
1352
|
-
h("button", { type: "button", title: "更多操作(右键也可打开)", disabled: historyBusy, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); setCommitMenu(menuOpen ? null : c.hashFull); } }, "⋯")
|
|
1366
|
+
h("button", { type: "button", title: t("更多操作(右键也可打开)"), disabled: historyBusy, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); setCommitMenu(menuOpen ? null : c.hashFull); } }, "⋯")
|
|
1353
1367
|
),
|
|
1354
1368
|
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 2 } },
|
|
1355
1369
|
c.author + (c.date ? " · " + c.date : "")),
|
|
1356
1370
|
menuOpen ? h("div", { style: menuPanelStyle() },
|
|
1357
1371
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => { setCommitMenu(null); openCommitDiff(c.hash); } },
|
|
1358
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "▸"), "查看提交差异"),
|
|
1372
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "▸"), t("查看提交差异")),
|
|
1359
1373
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("short", c) },
|
|
1360
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制短哈希"),
|
|
1374
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制短哈希")),
|
|
1361
1375
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("full", c) },
|
|
1362
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制完整哈希"),
|
|
1376
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制完整哈希")),
|
|
1363
1377
|
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("msg", c) },
|
|
1364
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制提交信息"),
|
|
1378
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), t("复制提交信息")),
|
|
1365
1379
|
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doRevert(c.hash, c.subject); } },
|
|
1366
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↩"), "还原此提交"),
|
|
1380
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↩"), t("还原此提交")),
|
|
1367
1381
|
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doCherryPick(c.hash, c.subject); } },
|
|
1368
|
-
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↪"), "拾取此提交")
|
|
1382
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↪"), t("拾取此提交"))
|
|
1369
1383
|
) : null,
|
|
1370
1384
|
hd ? (
|
|
1371
|
-
hd.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载提交 diff…")
|
|
1385
|
+
hd.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("正在加载提交 diff…"))
|
|
1372
1386
|
: hd.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + hd.error)
|
|
1373
|
-
: hd.diff ? renderSideBySideDiff(hd.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1387
|
+
: hd.diff ? renderSideBySideDiff(hd.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(无内容差异)"))
|
|
1374
1388
|
) : null
|
|
1375
1389
|
)
|
|
1376
1390
|
})
|
|
@@ -1378,18 +1392,18 @@ window.__ModuleLoader__.load({
|
|
|
1378
1392
|
h("div", null,
|
|
1379
1393
|
repo && (repo.ahead > 0 || repo.behind > 0) ? h("div", null,
|
|
1380
1394
|
repo.behind > 0 ? h("div", { style: { marginBottom: 10 } },
|
|
1381
|
-
h("div", { style: { color: T.warn, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地落后 " + repo.behind + " 个提交(远程有而本地没有):"),
|
|
1395
|
+
h("div", { style: { color: T.warn, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, t("本地落后 ") + repo.behind + t(" 个提交(远程有而本地没有):")),
|
|
1382
1396
|
repo.behindCommits && repo.behindCommits.length > 0
|
|
1383
1397
|
? 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))
|
|
1384
1398
|
: null
|
|
1385
1399
|
) : null,
|
|
1386
1400
|
repo.ahead > 0 ? h("div", null,
|
|
1387
|
-
h("div", { style: { color: T.brand, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, "本地领先 " + repo.ahead + " 个提交(本地有而远程没有):"),
|
|
1401
|
+
h("div", { style: { color: T.brand, fontSize: 12, fontWeight: 600, marginBottom: 4 } }, t("本地领先 ") + repo.ahead + t(" 个提交(本地有而远程没有):")),
|
|
1388
1402
|
repo.aheadCommits && repo.aheadCommits.length > 0
|
|
1389
1403
|
? 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))
|
|
1390
1404
|
: null
|
|
1391
1405
|
) : null
|
|
1392
|
-
) : h("div", { style: { color: T.success, fontSize: 13 } }, "✓ 已与远程同步,无差异。")
|
|
1406
|
+
) : h("div", { style: { color: T.success, fontSize: 13 } }, t("✓ 已与远程同步,无差异。"))
|
|
1393
1407
|
)
|
|
1394
1408
|
)
|
|
1395
1409
|
)
|
|
@@ -1400,22 +1414,22 @@ window.__ModuleLoader__.load({
|
|
|
1400
1414
|
pickOpen ? ReactDOM.createPortal(
|
|
1401
1415
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
1402
1416
|
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: cancelPick }),
|
|
1403
|
-
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": "选择目录" },
|
|
1404
|
-
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, "选择代码目录"),
|
|
1417
|
+
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("选择目录") },
|
|
1418
|
+
h("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, t("选择代码目录")),
|
|
1405
1419
|
h("div", { style: { fontSize: 12, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
1406
|
-
"可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。"),
|
|
1420
|
+
t("可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。")),
|
|
1407
1421
|
h("input", {
|
|
1408
1422
|
type: "text", value: pickPath, disabled: picking,
|
|
1409
|
-
placeholder: "C:\\Users\\你的用户名\\项目目录",
|
|
1423
|
+
placeholder: t("C:\\Users\\你的用户名\\项目目录"),
|
|
1410
1424
|
onChange: (e) => setPickPath(e.target.value),
|
|
1411
1425
|
onKeyDown: (e) => { if (e.key === "Enter") void confirmPick(); },
|
|
1412
1426
|
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" },
|
|
1413
1427
|
}),
|
|
1414
1428
|
pickErr ? h("div", { style: { marginTop: 8, color: T.danger, fontSize: 12 } }, "❌ " + pickErr) : null,
|
|
1415
1429
|
h("div", { style: { display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" } },
|
|
1416
|
-
h(Btn, { label: "浏览…", onClick: browseDir, disabled: picking, noBg: true }),
|
|
1417
|
-
h(Btn, { label: "取消", onClick: cancelPick, disabled: picking }),
|
|
1418
|
-
h(Btn, { label: "确定", onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
1430
|
+
h(Btn, { label: t("浏览…"), onClick: browseDir, disabled: picking, noBg: true }),
|
|
1431
|
+
h(Btn, { label: t("取消"), onClick: cancelPick, disabled: picking }),
|
|
1432
|
+
h(Btn, { label: t("确定"), onClick: confirmPick, tone: "primary", noBg: true, disabled: picking || !String(pickPath || "").trim() })
|
|
1419
1433
|
)
|
|
1420
1434
|
)
|
|
1421
1435
|
),
|
|
@@ -1485,13 +1499,13 @@ window.__ModuleLoader__.load({
|
|
|
1485
1499
|
} catch { return false; }
|
|
1486
1500
|
}
|
|
1487
1501
|
|
|
1488
|
-
/**
|
|
1489
|
-
function typeLabel(
|
|
1490
|
-
return { untracked: "新增", added: "新增", deleted: "删除", renamed: "重命名", modified: "修改" }[
|
|
1502
|
+
/** Localized label for a changed-file status type. */
|
|
1503
|
+
function typeLabel(type) {
|
|
1504
|
+
return { untracked: t("新增"), added: t("新增"), deleted: t("删除"), renamed: t("重命名"), modified: t("修改") }[type] || t("修改");
|
|
1491
1505
|
}
|
|
1492
1506
|
/** Color for a changed-file status type. */
|
|
1493
|
-
function typeColor(
|
|
1494
|
-
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[
|
|
1507
|
+
function typeColor(type) {
|
|
1508
|
+
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[type] || T.label;
|
|
1495
1509
|
}
|
|
1496
1510
|
|
|
1497
1511
|
/**
|
|
@@ -1558,8 +1572,8 @@ window.__ModuleLoader__.load({
|
|
|
1558
1572
|
const gutter = { ...mono, fontSize: 10, lineHeight: 1.55, color: T.secondary, textAlign: "right", padding: "0 6px", whiteSpace: "nowrap", wordBreak: "normal", opacity: 0.7 };
|
|
1559
1573
|
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)" } },
|
|
1560
1574
|
h("div", { style: { display: "flex", borderBottom: "1px solid " + T.border, position: "sticky", top: 0, background: "var(--dsw-alias-bg-layer-3, #fff)", zIndex: 1 } },
|
|
1561
|
-
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, "旧版本"),
|
|
1562
|
-
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, "新版本")
|
|
1575
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, t("旧版本")),
|
|
1576
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, t("新版本"))
|
|
1563
1577
|
),
|
|
1564
1578
|
rows.map((r, i) =>
|
|
1565
1579
|
h("div", { key: i, style: { display: "flex", background: cellBg[r.type] || "transparent", borderBottom: r.type === "ctx" ? "none" : "1px solid rgba(128,128,128,.1)" } },
|
|
@@ -1573,19 +1587,21 @@ window.__ModuleLoader__.load({
|
|
|
1573
1587
|
}
|
|
1574
1588
|
|
|
1575
1589
|
// ---------- the main panel ----------
|
|
1576
|
-
// `variant` 决定布局:'drawer'
|
|
1590
|
+
// `variant` 决定布局:'drawer'(右侧栏集成面板,显示关闭按钮、填充容器)与 'tab'
|
|
1577
1591
|
// (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
|
|
1578
1592
|
function ScmPanel({ onClose, variant }) {
|
|
1593
|
+
// 跟随 DSH 语言设置:切换语言时整个面板(①②③)实时重渲染。
|
|
1594
|
+
useLocale();
|
|
1579
1595
|
// 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
|
|
1580
1596
|
const [provider, setProvider] = useState("github");
|
|
1581
1597
|
const embedded = variant === "tab" || variant === "drawer";
|
|
1582
|
-
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": "源代码管理" },
|
|
1598
|
+
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": t("源代码管理") },
|
|
1583
1599
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
1584
|
-
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
|
|
1585
|
-
|
|
1600
|
+
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, t("源代码管理")),
|
|
1601
|
+
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": t("关闭"), onClick: onClose }, "✕")
|
|
1586
1602
|
),
|
|
1587
1603
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1588
|
-
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
|
|
1604
|
+
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(") + (provider === "gitee" ? "Gitee" : "GitHub") + t(" 限制)并说明原因。")),
|
|
1589
1605
|
h(EnvSection, null),
|
|
1590
1606
|
h("div", { style: { height: 12 } }),
|
|
1591
1607
|
h(SshSection, { provider, setProvider }),
|
|
@@ -1595,8 +1611,8 @@ window.__ModuleLoader__.load({
|
|
|
1595
1611
|
}
|
|
1596
1612
|
|
|
1597
1613
|
function panelStyle(variant) {
|
|
1598
|
-
// 'tab' / 'drawer'
|
|
1599
|
-
//
|
|
1614
|
+
// 'tab' / 'drawer':填充宿主容器(不设固定宽高、无边框阴影圆角),
|
|
1615
|
+
// 由外层(dsh-better-sidebar 的 Tab 区或本插件的右侧面板)负责尺寸与表面。
|
|
1600
1616
|
if (variant === "tab" || variant === "drawer") {
|
|
1601
1617
|
return {
|
|
1602
1618
|
position: "relative", display: "flex", flexDirection: "column", gap: 4,
|
|
@@ -1619,11 +1635,31 @@ window.__ModuleLoader__.load({
|
|
|
1619
1635
|
return { appearance: "none", border: "none", background: "transparent", color: T.secondary, cursor: "pointer", fontSize: 18, lineHeight: 1, padding: "2px 6px", borderRadius: 6 };
|
|
1620
1636
|
}
|
|
1621
1637
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1638
|
+
/** 注入的布局推挤样式(仅一次):右侧面板展开时把 #root 往左推,形成「集成侧边栏」,
|
|
1639
|
+
* 与 dsh-better-sidebar 右侧面板同一机制(margin + width calc)。 */
|
|
1640
|
+
const LAYOUT_CSS_ID = "source-code-mgmt-layout";
|
|
1641
|
+
let layoutCssInjected = false;
|
|
1642
|
+
function ensureLayoutCss() {
|
|
1643
|
+
if (layoutCssInjected) return;
|
|
1644
|
+
layoutCssInjected = true;
|
|
1645
|
+
const tag = document.createElement("style");
|
|
1646
|
+
tag.id = LAYOUT_CSS_ID;
|
|
1647
|
+
tag.setAttribute("data-source-code-mgmt-css", "");
|
|
1648
|
+
tag.textContent =
|
|
1649
|
+
// 推挤 #root:右侧面板占据布局而非浮在内容上方(VSCode 侧边栏手感),
|
|
1650
|
+
// 用 calc(100% - var) 避免桌面壳把 #root 设成 width:100% 时的加性溢出。
|
|
1651
|
+
"#root {" +
|
|
1652
|
+
" margin-right: var(--scm-push, 0px);" +
|
|
1653
|
+
" width: calc(100% - var(--scm-push, 0px));" +
|
|
1654
|
+
" transition: margin-right var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease)," +
|
|
1655
|
+
" width var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease);" +
|
|
1656
|
+
" }\n" +
|
|
1657
|
+
"body[data-source-code-mgmt-dragging] #root { transition: none; }\n" +
|
|
1658
|
+
"body[data-source-code-mgmt-dragging] { cursor: col-resize; user-select: none; }\n";
|
|
1659
|
+
document.head.appendChild(tag);
|
|
1624
1660
|
}
|
|
1625
1661
|
|
|
1626
|
-
/** 共享的「仓库 /
|
|
1662
|
+
/** 共享的「仓库 / 分支」小图标:既用于左轨按钮,也用作侧边栏 Tab 图标。 */
|
|
1627
1663
|
function repoIcon(size) {
|
|
1628
1664
|
const s = size || 18;
|
|
1629
1665
|
return h("svg", {
|
|
@@ -1637,47 +1673,103 @@ window.__ModuleLoader__.load({
|
|
|
1637
1673
|
);
|
|
1638
1674
|
}
|
|
1639
1675
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1676
|
+
// ---------- branch B:右上角 header 入口 + 右侧集成面板 ----------
|
|
1677
|
+
// 未安装 dsh-better-sidebar 时使用:把「代码管理」按钮注册进 DSH 的
|
|
1678
|
+
// conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
|
|
1679
|
+
// 因此它天然出现在 Session log 旁、样式一致的胶囊按钮、间距 8px 不挤在一起;
|
|
1680
|
+
// 点击后在右侧展开一个 dsh-better-sidebar 外观的固定面板(推挤 #root),复用 ScmPanel。
|
|
1681
|
+
const SCM_PANEL_W = 620;
|
|
1682
|
+
|
|
1683
|
+
/** Session log 同款胶囊按钮样式(描边、圆角 18、透明底),放在 header utilities 列表里。 */
|
|
1684
|
+
function headerBtnStyle() {
|
|
1642
1685
|
return {
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
fontSize: 13, lineHeight: 1,
|
|
1648
|
-
|
|
1686
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 4,
|
|
1687
|
+
height: 32, padding: "6px 12px",
|
|
1688
|
+
border: "1px solid " + T.border, borderRadius: 18,
|
|
1689
|
+
color: T.label, background: "transparent",
|
|
1690
|
+
font: "inherit", fontSize: 13, fontWeight: 400, lineHeight: 1,
|
|
1691
|
+
cursor: "pointer", whiteSpace: "nowrap",
|
|
1649
1692
|
};
|
|
1650
1693
|
}
|
|
1651
1694
|
|
|
1652
|
-
/**
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1695
|
+
/** 右侧面板的最小 / 最大宽度(拖拽改宽时钳制,防止拖得过窄或超出视口)。 */
|
|
1696
|
+
const SCM_PANEL_MIN = 360;
|
|
1697
|
+
const SCM_PANEL_MAX_FRAC = 0.92; // 拖拽上限:视口宽度的 92%
|
|
1698
|
+
|
|
1699
|
+
/** 右侧集成面板拖拽改宽:拖动面板左边缘调整宽度(像 dsh-better-sidebar)。要点:
|
|
1700
|
+
* 拖拽期间逐帧直接写 DOM(面板 style.width + --scm-push CSS 变量),避免每帧触发
|
|
1701
|
+
* React 重渲染造成卡顿;在 pointerup 时把最终宽度提交进 state,重开面板仍保持。 */
|
|
1702
|
+
function makeResizeHandler(getPanelEl, getW, onCommit) {
|
|
1703
|
+
return (e) => {
|
|
1704
|
+
e.preventDefault();
|
|
1705
|
+
const startX = e.clientX;
|
|
1706
|
+
const startW = getW();
|
|
1707
|
+
let lastW = startW;
|
|
1708
|
+
const body = document.body;
|
|
1709
|
+
body.setAttribute("data-source-code-mgmt-dragging", "");
|
|
1710
|
+
const onMove = (ev) => {
|
|
1711
|
+
// 面板贴右(right:0),往左拖 = 变宽;增量 = 起点X - 当前X。
|
|
1712
|
+
let w = startW + (startX - ev.clientX);
|
|
1713
|
+
const max = Math.round(window.innerWidth * SCM_PANEL_MAX_FRAC);
|
|
1714
|
+
w = Math.round(Math.max(SCM_PANEL_MIN, Math.min(max, w)));
|
|
1715
|
+
lastW = w;
|
|
1716
|
+
const el = getPanelEl();
|
|
1717
|
+
if (el) el.style.width = w + "px";
|
|
1718
|
+
document.documentElement.style.setProperty("--scm-push", w + "px");
|
|
1719
|
+
};
|
|
1720
|
+
const onUp = () => {
|
|
1721
|
+
body.removeAttribute("data-source-code-mgmt-dragging");
|
|
1722
|
+
window.removeEventListener("pointermove", onMove);
|
|
1723
|
+
window.removeEventListener("pointerup", onUp);
|
|
1724
|
+
if (lastW !== startW) onCommit(lastW);
|
|
1725
|
+
};
|
|
1726
|
+
window.addEventListener("pointermove", onMove);
|
|
1727
|
+
window.addEventListener("pointerup", onUp);
|
|
1661
1728
|
};
|
|
1662
1729
|
}
|
|
1663
1730
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1731
|
+
/** 右上角「代码管理」按钮 + 点击后打开的右侧集成面板(dsh-better-sidebar 外观,
|
|
1732
|
+
* 可拖拽左边缘调整宽度)。 */
|
|
1733
|
+
function HeaderScmAction() {
|
|
1734
|
+
// 跟随 DSH 语言设置:按钮文字(代码管理)实时切换。
|
|
1735
|
+
useLocale();
|
|
1668
1736
|
const [open, setOpen] = useState(false);
|
|
1737
|
+
const [panelW, setPanelW] = useState(SCM_PANEL_W);
|
|
1738
|
+
const panelRef = useRef(null);
|
|
1739
|
+
useEffect(() => { ensureLayoutCss(); }, []);
|
|
1740
|
+
// 右侧面板打开时推挤 #root(margin-right),关闭时归零。宽度跟随 panelW。
|
|
1741
|
+
useEffect(() => {
|
|
1742
|
+
document.documentElement.style.setProperty("--scm-push", open ? panelW + "px" : "0px");
|
|
1743
|
+
return () => { document.documentElement.style.removeProperty("--scm-push"); };
|
|
1744
|
+
}, [open, panelW]);
|
|
1745
|
+
// 拖拽改宽处理器(面板左边缘的拖拽条)。
|
|
1746
|
+
const onResize = makeResizeHandler(
|
|
1747
|
+
() => panelRef.current,
|
|
1748
|
+
() => panelW,
|
|
1749
|
+
(w) => setPanelW(w),
|
|
1750
|
+
);
|
|
1669
1751
|
return h(React.Fragment, null,
|
|
1670
|
-
h("button", {
|
|
1671
|
-
|
|
1672
|
-
style: floatBtnStyle(),
|
|
1673
|
-
title: "源代码管理",
|
|
1674
|
-
"aria-label": "源代码管理",
|
|
1675
|
-
onClick: () => setOpen(true),
|
|
1676
|
-
}, repoIcon(16), h("span", { style: { fontSize: 13 } }, "代码管理")),
|
|
1752
|
+
h("button", { type: "button", style: headerBtnStyle(), title: t("源代码管理"), "aria-label": t("源代码管理"), onClick: () => setOpen(true) },
|
|
1753
|
+
repoIcon(14), h("span", { style: { fontSize: 13 } }, t("代码管理"))),
|
|
1677
1754
|
open ? ReactDOM.createPortal(
|
|
1678
|
-
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000 }, role: "presentation" },
|
|
1679
|
-
h("div", {
|
|
1680
|
-
|
|
1755
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000, pointerEvents: "none" }, role: "presentation" },
|
|
1756
|
+
h("div", { ref: panelRef, style: {
|
|
1757
|
+
position: "absolute", top: 0, right: 0, bottom: 0, width: panelW + "px",
|
|
1758
|
+
boxSizing: "border-box", overflow: "visible", pointerEvents: "auto",
|
|
1759
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08))",
|
|
1760
|
+
borderLeft: "1px solid " + T.border,
|
|
1761
|
+
boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
|
|
1762
|
+
} },
|
|
1763
|
+
// 左边缘拖拽条(改宽)
|
|
1764
|
+
h("div", {
|
|
1765
|
+
onPointerDown: onResize,
|
|
1766
|
+
title: t("拖动调整面板宽度"),
|
|
1767
|
+
"aria-label": t("拖动调整面板宽度"),
|
|
1768
|
+
style: {
|
|
1769
|
+
position: "absolute", top: 0, bottom: 0, left: -3, width: 7,
|
|
1770
|
+
cursor: "col-resize", pointerEvents: "auto", zIndex: 1,
|
|
1771
|
+
},
|
|
1772
|
+
}),
|
|
1681
1773
|
h(ScmPanel, { variant: "drawer", onClose: () => setOpen(false) })
|
|
1682
1774
|
)
|
|
1683
1775
|
),
|
|
@@ -1703,10 +1795,11 @@ window.__ModuleLoader__.load({
|
|
|
1703
1795
|
}
|
|
1704
1796
|
|
|
1705
1797
|
// ---------- cordis plugin body ----------
|
|
1706
|
-
//
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1709
|
-
|
|
1798
|
+
// 声明 'locale':DSH 的客户端插件都靠 inject 声明保证服务先于 apply 就绪(loader
|
|
1799
|
+
// 依赖图排序)。client-locale 是 web-app 核心服务(与 slots/connection 同级),
|
|
1800
|
+
// 必在 loader 树里;better-sidebar 仍是可选集成(不入 inject),照旧用
|
|
1801
|
+
// ctx.get('betterSidebar') 判空 + 延迟重试,未安装时绝不会因为缺服务而报错。
|
|
1802
|
+
const inject = ['locale'];
|
|
1710
1803
|
|
|
1711
1804
|
// 是否已安装(并激活)dsh-better-sidebar:决定「③代码管理」是否隐藏本插件自带的
|
|
1712
1805
|
// 本地 Git 工作流(暂存/提交/分支/历史/并排 diff)——这些能力 better-sidebar 的
|
|
@@ -1714,6 +1807,32 @@ window.__ModuleLoader__.load({
|
|
|
1714
1807
|
let hasBetterSidebar = false;
|
|
1715
1808
|
|
|
1716
1809
|
function apply(ctx) {
|
|
1810
|
+
// 接管 DSH 的语言设置:client-locale 插件提供 ctx.locale(LocaleRuntime),
|
|
1811
|
+
// t() 实时读它;语言切换经 subscribe 触发 useLocale 重渲染。
|
|
1812
|
+
// 注意:client-locale 可能比本插件晚激活(和 better-sidebar 同样的顺序问题),
|
|
1813
|
+
// 因此捕获要带重试;重试窗口内组件先以浏览器语言渲染,捕获后立即切到 DSH 设置。
|
|
1814
|
+
const captureLocale = () => {
|
|
1815
|
+
try {
|
|
1816
|
+
if (scmLocaleFace) return true;
|
|
1817
|
+
const lf = (ctx && ctx.locale) || (typeof ctx.get === "function" ? ctx.get("locale") : undefined);
|
|
1818
|
+
if (lf && typeof lf.getLocale === "function") {
|
|
1819
|
+
scmLocaleFace = lf;
|
|
1820
|
+
try { scmLang = lf.getLocale().active || scmLang } catch {}
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
} catch { return false }
|
|
1825
|
+
};
|
|
1826
|
+
if (!captureLocale()) {
|
|
1827
|
+
// 兜底:等 client-locale 激活后再补抓(单次延迟,零轮询、零 I/O)。
|
|
1828
|
+
window.setTimeout(() => {
|
|
1829
|
+
const ok = captureLocale();
|
|
1830
|
+
if (!ok && typeof console !== "undefined") {
|
|
1831
|
+
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.");
|
|
1832
|
+
}
|
|
1833
|
+
}, 1200);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1717
1836
|
// DSH 打开(插件激活)时就预取环境/SSH/默认工作区/仓库状态,
|
|
1718
1837
|
// 点开「代码管理」面板时直接使用缓存,无需重新加载。
|
|
1719
1838
|
void preload();
|
|
@@ -1729,36 +1848,65 @@ window.__ModuleLoader__.load({
|
|
|
1729
1848
|
// 分支 A:已安装 dsh-better-sidebar —— 把「代码管理」注册成它的侧边栏新 Tab
|
|
1730
1849
|
// 页面。ctx.effect 保证 HMR / 插件卸载时自动注销该 Tab。
|
|
1731
1850
|
hasBetterSidebar = true;
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1851
|
+
// 语言切换时实时更新 Tab 标题(title 变了才重注册,避免注册抖动)。
|
|
1852
|
+
let lastTabTitle = null;
|
|
1853
|
+
let tabDisposer = null;
|
|
1854
|
+
const registerTabNow = () => {
|
|
1855
|
+
const title = t("代码管理");
|
|
1856
|
+
if (title === lastTabTitle) return;
|
|
1857
|
+
if (typeof tabDisposer === "function") { try { tabDisposer() } catch {} }
|
|
1858
|
+
lastTabTitle = title;
|
|
1859
|
+
tabDisposer = bs.registerTab({
|
|
1860
|
+
id: PLUGIN_ID,
|
|
1861
|
+
title,
|
|
1862
|
+
icon: (size) => repoIcon(size),
|
|
1863
|
+
order: 50,
|
|
1864
|
+
single: true,
|
|
1865
|
+
component: () => h(ScmPanel, { variant: "tab" }),
|
|
1866
|
+
});
|
|
1867
|
+
};
|
|
1868
|
+
ctx.effect(() => {
|
|
1869
|
+
registerTabNow();
|
|
1870
|
+
return () => { if (typeof tabDisposer === "function") { try { tabDisposer() } catch {} } };
|
|
1871
|
+
});
|
|
1872
|
+
if (scmLocaleFace && typeof scmLocaleFace.subscribe === "function") {
|
|
1873
|
+
ctx.effect(() => scmLocaleFace.subscribe(() => { try { registerTabNow(); } catch {} }),
|
|
1874
|
+
'source-code-mgmt: tab title locale sync');
|
|
1875
|
+
}
|
|
1876
|
+
// 若此前已挂载了分支 B 的 header 入口(降级路径),立即拆除它。
|
|
1741
1877
|
if (entryUnmount) { entryUnmount(); entryUnmount = null; }
|
|
1742
1878
|
return true;
|
|
1743
1879
|
};
|
|
1744
1880
|
|
|
1745
1881
|
if (tryRegisterTab()) return;
|
|
1746
1882
|
|
|
1747
|
-
// 分支 B:此刻未检测到 dsh-better-sidebar ——
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
};
|
|
1758
|
-
|
|
1759
|
-
|
|
1883
|
+
// 分支 B:此刻未检测到 dsh-better-sidebar —— 把「代码管理」按钮注册进 DSH 的
|
|
1884
|
+
// conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
|
|
1885
|
+
// 因此它天然出现在 Session log 旁、间距 8px 不挤在一起;点击后 HeaderScmAction
|
|
1886
|
+
// 打开右侧 dsh-better-sidebar 外观的集成面板(推挤 #root)。
|
|
1887
|
+
const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
|
|
1888
|
+
if (slots && typeof slots.inject === "function") {
|
|
1889
|
+
ctx.effect(() => slots.inject('conversation.session.header.utilities', () => slots.register({
|
|
1890
|
+
name: 'conversation.session.header.utilities',
|
|
1891
|
+
id: 'source-code-mgmt',
|
|
1892
|
+
order: 200,
|
|
1893
|
+
}, HeaderScmAction)), 'source-code-mgmt: header utility');
|
|
1894
|
+
} else if (ReactDOM) {
|
|
1895
|
+
// slots 服务不可用(极少数环境)——降级:右上角浮动按钮 + 右侧面板。
|
|
1896
|
+
ctx.effect(() => {
|
|
1897
|
+
const hostEl = document.createElement("div");
|
|
1898
|
+
hostEl.setAttribute("data-source-code-mgmt-entry", "");
|
|
1899
|
+
document.body.appendChild(hostEl);
|
|
1900
|
+
const root = mountClientRoot(hostEl, h(HeaderScmAction));
|
|
1901
|
+
entryUnmount = () => {
|
|
1902
|
+
try { if (root && typeof root.unmount === "function") root.unmount(); } catch {}
|
|
1903
|
+
hostEl.remove();
|
|
1904
|
+
};
|
|
1905
|
+
return () => { if (entryUnmount) { entryUnmount(); entryUnmount = null; } };
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1760
1908
|
|
|
1761
|
-
// 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab
|
|
1909
|
+
// 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab 并拆除 header 入口。
|
|
1762
1910
|
// 单次 setTimeout,无轮询、零 I/O,几乎不耗资源;插件卸载时清除。
|
|
1763
1911
|
const retryTimer = window.setTimeout(() => { tryRegisterTab(); }, 1500);
|
|
1764
1912
|
ctx.effect(() => () => window.clearTimeout(retryTimer));
|