Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a5b2f16b | ||
|
|
bba6e89a16 | ||
|
|
b247a18155 | ||
|
|
b156f505eb | ||
|
|
b3454eb157 |
@@ -214,7 +214,7 @@ public class MacTfsCli {
|
|||||||
private static String readServerToken() throws Exception {
|
private static String readServerToken() throws Exception {
|
||||||
File tokenFile = serverTokenFile();
|
File tokenFile = serverTokenFile();
|
||||||
if (!tokenFile.exists()) {
|
if (!tokenFile.exists()) {
|
||||||
throw new IllegalArgumentException("Token file not found: " + tokenFile.getAbsolutePath() + ". Start macTFS API server first.");
|
throw new IllegalArgumentException("Token file not found: " + tokenFile.getAbsolutePath() + ". Start MacTFS API server first.");
|
||||||
}
|
}
|
||||||
String token = new String(java.nio.file.Files.readAllBytes(tokenFile.toPath()), Charset.forName("UTF-8")).trim();
|
String token = new String(java.nio.file.Files.readAllBytes(tokenFile.toPath()), Charset.forName("UTF-8")).trim();
|
||||||
if (token.length() == 0) {
|
if (token.length() == 0) {
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ public class TfsPhaseOneService {
|
|||||||
required(merged, "workspace"),
|
required(merged, "workspace"),
|
||||||
required(merged, "server-path"),
|
required(merged, "server-path"),
|
||||||
merged.getProperty("local-path"),
|
merged.getProperty("local-path"),
|
||||||
!"false".equalsIgnoreCase(merged.getProperty("recursive"))
|
!"false".equalsIgnoreCase(merged.getProperty("recursive")),
|
||||||
|
!"false".equalsIgnoreCase(merged.getProperty("include-local-only"))
|
||||||
);
|
);
|
||||||
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
||||||
data.put("diffs", folderDiffsToMaps(result.getData()));
|
data.put("diffs", folderDiffsToMaps(result.getData()));
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macTFS 核心服务层,封装 TFS 连接、目录、Workspace、文件操作、历史和 Diff 能力。
|
* MacTFS 核心服务层,封装 TFS 连接、目录、Workspace、文件操作、历史和 Diff 能力。
|
||||||
*/
|
*/
|
||||||
public class MacTfsCoreService {
|
public class MacTfsCoreService {
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ public class MacTfsCoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询或创建 Collection 默认 Workspace,未传名称时按 macTFS 规则生成。
|
* 查询或创建 Collection 默认 Workspace,未传名称时按 MacTFS 规则生成。
|
||||||
*/
|
*/
|
||||||
public CoreOperationResult<TfsWorkspaceInfo> ensureWorkspace(final TfsConnectionConfig config,
|
public CoreOperationResult<TfsWorkspaceInfo> ensureWorkspace(final TfsConnectionConfig config,
|
||||||
final String collectionName,
|
final String collectionName,
|
||||||
@@ -579,7 +579,8 @@ public class MacTfsCoreService {
|
|||||||
final String workspaceName,
|
final String workspaceName,
|
||||||
final String serverPath,
|
final String serverPath,
|
||||||
final String localPath,
|
final String localPath,
|
||||||
final boolean recursive) {
|
final boolean recursive,
|
||||||
|
final boolean includeLocalOnly) {
|
||||||
return execute("compareFolder", new CoreCallable<List<TfsFolderDiffItem>>() {
|
return execute("compareFolder", new CoreCallable<List<TfsFolderDiffItem>>() {
|
||||||
@Override
|
@Override
|
||||||
public List<TfsFolderDiffItem> call(List<String> logs) throws Exception {
|
public List<TfsFolderDiffItem> call(List<String> logs) throws Exception {
|
||||||
@@ -593,7 +594,10 @@ public class MacTfsCoreService {
|
|||||||
}
|
}
|
||||||
Map<String, ExtendedItem> serverItems = queryExtendedItems(workspace, normalizedServerPath, recursive);
|
Map<String, ExtendedItem> serverItems = queryExtendedItems(workspace, normalizedServerPath, recursive);
|
||||||
Map<String, PendingChange> pendingChanges = mapPendingChanges(queryPendingChanges(workspace, Arrays.asList(normalizedServerPath), recursive));
|
Map<String, PendingChange> pendingChanges = mapPendingChanges(queryPendingChanges(workspace, Arrays.asList(normalizedServerPath), recursive));
|
||||||
Set<String> localFiles = scanLocalFiles(new File(normalizedLocalPath), recursive);
|
// 不统计仅本地存在的项时直接跳过本地全量扫描(如 node_modules 大目录),只对比两端都有的文件。
|
||||||
|
Set<String> localFiles = includeLocalOnly
|
||||||
|
? scanLocalFiles(new File(normalizedLocalPath), recursive)
|
||||||
|
: new LinkedHashSet<String>();
|
||||||
Map<String, TfsFolderDiffItem> result = new LinkedHashMap<String, TfsFolderDiffItem>();
|
Map<String, TfsFolderDiffItem> result = new LinkedHashMap<String, TfsFolderDiffItem>();
|
||||||
|
|
||||||
for (Map.Entry<String, ExtendedItem> entry : serverItems.entrySet()) {
|
for (Map.Entry<String, ExtendedItem> entry : serverItems.entrySet()) {
|
||||||
@@ -1237,8 +1241,12 @@ public class MacTfsCoreService {
|
|||||||
}
|
}
|
||||||
boolean exists = localFile != null && localFile.exists();
|
boolean exists = localFile != null && localFile.exists();
|
||||||
boolean remoteChanged = item.getLocalVersion() > 0 && item.getLatestVersion() > item.getLocalVersion();
|
boolean remoteChanged = item.getLocalVersion() > 0 && item.getLatestVersion() > item.getLocalVersion();
|
||||||
// mtime 只能作为远端版本变化时的辅助信号,避免 Get Latest 后因本地时间晚于签入时间误报本地修改。
|
// 服务器工作区下文件取下来默认只读,变为可写说明被本地解锁修改过(对齐 VS 的 writable 判定),
|
||||||
boolean localModified = remoteChanged && exists && item.getCheckinDate() != null && localFile.lastModified() > item.getCheckinDate().getTimeInMillis();
|
// 这样未签出的纯本地修改也能在目录对比中暴露出来。
|
||||||
|
boolean writableFile = exists && !isFolder(item.getItemType()) && localFile.canWrite();
|
||||||
|
// mtime 仅作为远端版本变化时的辅助信号,避免 Get Latest 后因本地时间晚于签入时间误报本地修改。
|
||||||
|
boolean mtimeNewer = exists && item.getCheckinDate() != null && localFile.lastModified() > item.getCheckinDate().getTimeInMillis();
|
||||||
|
boolean localModified = writableFile || (remoteChanged && mtimeNewer);
|
||||||
if (!exists && item.getLocalVersion() > 0) {
|
if (!exists && item.getLocalVersion() > 0) {
|
||||||
return "localDeleted";
|
return "localDeleted";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macTFS 第二阶段本地 API 服务入口,负责 HTTP 路由、Token 校验、配置、会话、日志和超时控制。
|
* MacTFS 第二阶段本地 API 服务入口,负责 HTTP 路由、Token 校验、配置、会话、日志和超时控制。
|
||||||
*/
|
*/
|
||||||
public class MacTfsServer {
|
public class MacTfsServer {
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ public class MacTfsServer {
|
|||||||
registerFilters();
|
registerFilters();
|
||||||
registerRoutes();
|
registerRoutes();
|
||||||
Spark.awaitInitialization();
|
Spark.awaitInitialization();
|
||||||
System.out.println("macTFS API server listening on http://" + HOST + ":" + PORT);
|
System.out.println("MacTFS API server listening on http://" + HOST + ":" + PORT);
|
||||||
System.out.println("macTFS token file: " + tokenStore.getTokenFile().getAbsolutePath());
|
System.out.println("MacTFS token file: " + tokenStore.getTokenFile().getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -597,7 +597,7 @@ public class MacTfsServer {
|
|||||||
public ApiResult call(Request request) throws Exception {
|
public ApiResult call(Request request) throws Exception {
|
||||||
Map<String, Object> body = readBody(request);
|
Map<String, Object> body = readBody(request);
|
||||||
AppConfig config = requestConfig(body);
|
AppConfig config = requestConfig(body);
|
||||||
CoreOperationResult<List<TfsFolderDiffItem>> result = coreService.compareFolder(toTfsConfig(config), require(config.collection, "collection"), require(config.workspace, "workspace"), require(stringValue(body, "serverPath"), "serverPath"), stringValue(body, "localPath"), booleanValue(body, "recursive", true));
|
CoreOperationResult<List<TfsFolderDiffItem>> result = coreService.compareFolder(toTfsConfig(config), require(config.collection, "collection"), require(config.workspace, "workspace"), require(stringValue(body, "serverPath"), "serverPath"), stringValue(body, "localPath"), booleanValue(body, "recursive", true), booleanValue(body, "includeLocalOnly", true));
|
||||||
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
||||||
data.put("diffs", result.getData() == null ? Collections.emptyList() : result.getData());
|
data.put("diffs", result.getData() == null ? Collections.emptyList() : result.getData());
|
||||||
return fromCore(result, data);
|
return fromCore(result, data);
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function ConnectView({ onReady }: { onReady: (session: WorkspaceSession)
|
|||||||
<div className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
|
<div className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
|
||||||
<div className="w-full max-w-md rounded-lg border bg-background p-6 shadow-sm">
|
<div className="w-full max-w-md rounded-lg border bg-background p-6 shadow-sm">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-base font-semibold">macTFS</h1>
|
<h1 className="text-base font-semibold">MacTFS</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{phase === "form" ? "连接 TFS 服务器" : "选择 Collection"}
|
{phase === "form" ? "连接 TFS 服务器" : "选择 Collection"}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function TopBar({
|
|||||||
return (
|
return (
|
||||||
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b bg-background px-3">
|
<header className="flex h-12 shrink-0 items-center justify-between gap-3 border-b bg-background px-3">
|
||||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||||
<span className="shrink-0 font-semibold">macTFS</span>
|
<span className="shrink-0 font-semibold">MacTFS</span>
|
||||||
<Separator orientation="vertical" className="h-4!" />
|
<Separator orientation="vertical" className="h-4!" />
|
||||||
<span className="hidden shrink-0 text-xs text-muted-foreground lg:inline">
|
<span className="hidden shrink-0 text-xs text-muted-foreground lg:inline">
|
||||||
服务器
|
服务器
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { FileText, FolderClosed, Loader2, RefreshCw } from "lucide-react"
|
import { FileText, FolderClosed, Loader2, RefreshCw } from "lucide-react"
|
||||||
|
|
||||||
import { FileTargetMenu } from "~/components/app/file-target-menu"
|
import { FileTargetMenu } from "~/components/app/file-target-menu"
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog"
|
} from "~/components/ui/dialog"
|
||||||
@@ -30,6 +31,9 @@ import { cn } from "~/lib/utils"
|
|||||||
* 目录对比弹窗:对已映射目录执行本地与服务端对比,
|
* 目录对比弹窗:对已映射目录执行本地与服务端对比,
|
||||||
* 默认隐藏 upToDate,支持状态筛选、刷新重比,结果项走统一右键菜单。
|
* 默认隐藏 upToDate,支持状态筛选、刷新重比,结果项走统一右键菜单。
|
||||||
*/
|
*/
|
||||||
|
// 在对比弹窗内直接执行、完成后需要刷新对比结果的动作。
|
||||||
|
const ACTIONS_NEED_RECOMPARE: FileActionId[] = ["checkout", "getLatest", "undo", "delete", "add"]
|
||||||
|
|
||||||
export function CompareDialog({
|
export function CompareDialog({
|
||||||
serverPath,
|
serverPath,
|
||||||
mappings,
|
mappings,
|
||||||
@@ -39,33 +43,51 @@ export function CompareDialog({
|
|||||||
serverPath: string
|
serverPath: string
|
||||||
mappings: MappingInfo[]
|
mappings: MappingInfo[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onFileAction: (target: FileTarget, action: FileActionId) => void
|
onFileAction: (target: FileTarget, action: FileActionId) => void | Promise<void>
|
||||||
}) {
|
}) {
|
||||||
|
// 先选项后对比:setup 阶段确认选项,result 阶段展示对比结果。
|
||||||
|
const [phase, setPhase] = useState<"setup" | "result">("setup")
|
||||||
const [diffs, setDiffs] = useState<FolderDiffItem[]>([])
|
const [diffs, setDiffs] = useState<FolderDiffItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [showUpToDate, setShowUpToDate] = useState(false)
|
const [showUpToDate, setShowUpToDate] = useState(false)
|
||||||
|
// 忽略仅本地存在的项(如 node_modules):勾选后只对比两端都有的文件,并跳过本地全量扫描。
|
||||||
|
const [ignoreLocalOnly, setIgnoreLocalOnly] = useState(false)
|
||||||
// 状态筛选:空集合表示不过滤(除 upToDate 开关外)。
|
// 状态筛选:空集合表示不过滤(除 upToDate 开关外)。
|
||||||
const [statusFilter, setStatusFilter] = useState<Set<string>>(new Set())
|
const [statusFilter, setStatusFilter] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行(或重新执行)目录对比。
|
* 执行(或重新执行)目录对比;忽略仅本地存在的项时由后端直接跳过本地扫描。
|
||||||
*/
|
*/
|
||||||
const runCompare = useCallback(async () => {
|
const runCompare = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
const result = await api.compareFolder({ serverPath, recursive: true })
|
const result = await api.compareFolder({
|
||||||
|
serverPath,
|
||||||
|
recursive: true,
|
||||||
|
includeLocalOnly: !ignoreLocalOnly,
|
||||||
|
})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
setError(result.errorMessage ?? "目录对比失败")
|
setError(result.errorMessage ?? "目录对比失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setDiffs(result.data?.diffs ?? [])
|
setDiffs(result.data?.diffs ?? [])
|
||||||
}, [serverPath])
|
}, [serverPath, ignoreLocalOnly])
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
void runCompare()
|
* 转发右键动作:签出 / 获取 / 撤销等就地执行的动作完成后自动重新对比,
|
||||||
}, [runCompare])
|
* 让结果状态(如 签出编辑)立即反映最新事实。
|
||||||
|
*/
|
||||||
|
const handleAction = useCallback(
|
||||||
|
async (target: FileTarget, action: FileActionId) => {
|
||||||
|
await onFileAction(target, action)
|
||||||
|
if (ACTIONS_NEED_RECOMPARE.includes(action)) {
|
||||||
|
await runCompare()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onFileAction, runCompare],
|
||||||
|
)
|
||||||
|
|
||||||
// 结果中出现过的状态(不含 upToDate),用于生成筛选 chips。
|
// 结果中出现过的状态(不含 upToDate),用于生成筛选 chips。
|
||||||
const presentStatuses = useMemo(() => {
|
const presentStatuses = useMemo(() => {
|
||||||
@@ -104,11 +126,56 @@ export function CompareDialog({
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 选项确认阶段:先确定对比方式再执行,避免反复触发全量对比。
|
||||||
|
if (phase === "setup") {
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>目录对比</DialogTitle>
|
||||||
|
<DialogDescription className="font-mono text-xs">{serverPath}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-3 rounded-md border bg-muted/30 p-4">
|
||||||
|
<label className="flex items-start gap-2 text-sm">
|
||||||
|
<Checkbox
|
||||||
|
checked={ignoreLocalOnly}
|
||||||
|
onCheckedChange={(value) => setIgnoreLocalOnly(value === true)}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">忽略仅本地存在的项</span>
|
||||||
|
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
||||||
|
只对比两端都有的文件;仅本地存在的项(如 node_modules、构建产物)不计入差异,
|
||||||
|
并跳过本地全量扫描,大目录速度更快。
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPhase("result")
|
||||||
|
void runCompare()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
开始对比
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent className="flex h-[88svh] max-h-[88svh] flex-col sm:max-w-[90vw]">
|
<DialogContent className="flex h-[88svh] max-h-[88svh] flex-col sm:max-w-[90vw]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>目录对比</DialogTitle>
|
<DialogTitle>目录对比{ignoreLocalOnly ? "(已忽略仅本地存在的项)" : ""}</DialogTitle>
|
||||||
<DialogDescription className="font-mono text-xs">{serverPath}</DialogDescription>
|
<DialogDescription className="font-mono text-xs">{serverPath}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -134,6 +201,14 @@ export function CompareDialog({
|
|||||||
/>
|
/>
|
||||||
显示最新项
|
显示最新项
|
||||||
</label>
|
</label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPhase("setup")}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
调整选项
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -181,7 +256,11 @@ export function CompareDialog({
|
|||||||
compareStatus: diff.status,
|
compareStatus: diff.status,
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<FileTargetMenu key={diff.serverPath} target={target} onAction={onFileAction}>
|
<FileTargetMenu
|
||||||
|
key={diff.serverPath}
|
||||||
|
target={target}
|
||||||
|
onAction={(menuTarget, action) => void handleAction(menuTarget, action)}
|
||||||
|
>
|
||||||
<TableRow className="cursor-default select-none">
|
<TableRow className="cursor-default select-none">
|
||||||
<TableCell className="py-1.5">
|
<TableCell className="py-1.5">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -225,8 +225,14 @@ export function getFileContent(params: { serverPath?: string; localPath?: string
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 对已映射目录执行目录对比。
|
* 对已映射目录执行目录对比。
|
||||||
|
* includeLocalOnly=false 时跳过本地全量扫描,仅本地存在的项(如 node_modules)不计入差异。
|
||||||
*/
|
*/
|
||||||
export function compareFolder(body: { serverPath: string; localPath?: string; recursive?: boolean }) {
|
export function compareFolder(body: {
|
||||||
|
serverPath: string
|
||||||
|
localPath?: string
|
||||||
|
recursive?: boolean
|
||||||
|
includeLocalOnly?: boolean
|
||||||
|
}) {
|
||||||
return apiClient.post<{ diffs: FolderDiffItem[] }>("/api/compare/folder", { body })
|
return apiClient.post<{ diffs: FolderDiffItem[] }>("/api/compare/folder", { body })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import type { WorkspaceSession } from "~/lib/tfs/session"
|
|||||||
// 顶层视图状态:检测服务、服务未就绪、连接入口、已进入工作台。
|
// 顶层视图状态:检测服务、服务未就绪、连接入口、已进入工作台。
|
||||||
type AppPhase = "checking" | "blocked" | "connect" | "workspace"
|
type AppPhase = "checking" | "blocked" | "connect" | "workspace"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面标题(同时作为 Electron 窗口标题)。
|
||||||
|
*/
|
||||||
|
export function meta() {
|
||||||
|
return [{ title: "MacTFS" }]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 第一版前端入口:先确保本地服务就绪,再走连接 / Collection / Workspace 流程,
|
* 第一版前端入口:先确保本地服务就绪,再走连接 / Collection / Workspace 流程,
|
||||||
* 进入工作台后持有固定上下文,并统一维护左树与中间列表共享的当前路径(FE-004)。
|
* 进入工作台后持有固定上下文,并统一维护左树与中间列表共享的当前路径(FE-004)。
|
||||||
@@ -88,7 +95,7 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
|
<div className="flex min-h-svh items-center justify-center bg-muted/40 p-6">
|
||||||
<div className="w-full max-w-md rounded-lg border bg-background p-6 shadow-sm">
|
<div className="w-full max-w-md rounded-lg border bg-background p-6 shadow-sm">
|
||||||
<h1 className="text-base font-semibold">macTFS</h1>
|
<h1 className="text-base font-semibold">MacTFS</h1>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">本地源码管理工作台</p>
|
<p className="mt-1 text-sm text-muted-foreground">本地源码管理工作台</p>
|
||||||
|
|
||||||
{phase === "checking" && (
|
{phase === "checking" && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron")
|
const { app, BrowserWindow, ipcMain, dialog, shell, nativeImage } = require("electron")
|
||||||
const path = require("node:path")
|
const path = require("node:path")
|
||||||
const fs = require("node:fs")
|
const fs = require("node:fs")
|
||||||
const os = require("node:os")
|
const os = require("node:os")
|
||||||
@@ -260,13 +260,32 @@ function registerIpcHandlers() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析应用图标路径:开发态用 public/logo.png,打包态用构建产物中的 logo.png。
|
||||||
|
*/
|
||||||
|
function resolveAppIcon() {
|
||||||
|
const candidates = [
|
||||||
|
path.join(__dirname, "..", "public", "logo.png"),
|
||||||
|
path.join(__dirname, "..", "build", "client", "logo.png"),
|
||||||
|
]
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建 Electron 主窗口,开发环境加载本地服务,生产环境加载构建后的静态页面。
|
* 创建 Electron 主窗口,开发环境加载本地服务,生产环境加载构建后的静态页面。
|
||||||
*/
|
*/
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
const iconPath = resolveAppIcon()
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 840,
|
height: 840,
|
||||||
|
title: "MacTFS",
|
||||||
|
...(iconPath ? { icon: nativeImage.createFromPath(iconPath) } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.cjs"),
|
preload: path.join(__dirname, "preload.cjs"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@@ -282,7 +301,14 @@ function createWindow() {
|
|||||||
mainWindow.loadFile(path.join(__dirname, "../build/client/index.html"))
|
mainWindow.loadFile(path.join(__dirname, "../build/client/index.html"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.setName("MacTFS")
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
// macOS 程序坞图标在开发态需要显式设置(BrowserWindow icon 仅对 Windows / Linux 生效)。
|
||||||
|
const iconPath = resolveAppIcon()
|
||||||
|
if (process.platform === "darwin" && iconPath && app.dock) {
|
||||||
|
app.dock.setIcon(nativeImage.createFromPath(iconPath))
|
||||||
|
}
|
||||||
registerIpcHandlers()
|
registerIpcHandlers()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Reference in New Issue
Block a user