feat(desktop): 收口桌面工作台视觉与活动反馈

This commit is contained in:
Cheng Zhou
2026-07-02 15:52:16 +08:00
parent 5b387226a3
commit 4aa1eb00b6
19 changed files with 2027 additions and 222 deletions
+49 -3
View File
@@ -6,6 +6,9 @@ const pickFilesMock = vi.hoisted(() => vi.fn());
const saveFileMock = vi.hoisted(() => vi.fn());
const messageInfoMock = vi.hoisted(() => vi.fn());
const messageSuccessMock = vi.hoisted(() => vi.fn());
const finishActivityMock = vi.hoisted(() => vi.fn());
const startActivityMock = vi.hoisted(() => vi.fn());
const updateActivityMock = vi.hoisted(() => vi.fn());
vi.mock("../runtime", () => ({
clientRuntime: {
@@ -23,6 +26,12 @@ vi.mock("element-plus", () => ({
},
}));
vi.mock("../session/desktopActivityCenter", () => ({
finishDesktopActivity: finishActivityMock,
startDesktopActivity: startActivityMock,
updateDesktopActivity: updateActivityMock,
}));
const output = {
suggestedName: "report.pdf",
mimeType: "application/pdf",
@@ -36,6 +45,7 @@ describe("file task feedback", () => {
pickFilesMock.mockResolvedValue([]);
saveFileMock.mockResolvedValue("saved");
openFileMock.mockResolvedValue(undefined);
startActivityMock.mockReturnValue("activity-1");
});
it("reports native file picker selections", async () => {
@@ -70,16 +80,26 @@ describe("file task feedback", () => {
expect(messageSuccessMock).toHaveBeenCalledWith("文件下载已开始");
});
it("uses saved feedback for native save flows", async () => {
it("uses persistent desktop activity for native save flows", async () => {
capabilitiesMock.mockReturnValue({ nativeFiles: true });
const { saveFileWithFeedback } = await import("./fileTaskFeedback");
await expect(saveFileWithFeedback(output)).resolves.toBe("saved");
expect(messageSuccessMock).toHaveBeenCalledWith("文件已保存");
expect(startActivityMock).toHaveBeenCalledWith({
kind: "download",
title: "保存文件",
detail: "等待选择保存位置",
});
expect(updateActivityMock).toHaveBeenCalledWith("activity-1", {
detail: "等待选择保存位置",
progress: 70,
});
expect(finishActivityMock).toHaveBeenCalledWith("activity-1", "completed", { detail: "文件已保存" });
expect(messageSuccessMock).not.toHaveBeenCalled();
});
it("reports cancelled save flows without treating them as downloads", async () => {
it("reports cancelled web save flows without treating them as downloads", async () => {
saveFileMock.mockResolvedValue("cancelled");
const { saveFileWithFeedback } = await import("./fileTaskFeedback");
@@ -89,6 +109,17 @@ describe("file task feedback", () => {
expect(messageSuccessMock).not.toHaveBeenCalled();
});
it("reports cancelled native save flows through desktop activity", async () => {
capabilitiesMock.mockReturnValue({ nativeFiles: true });
saveFileMock.mockResolvedValue("cancelled");
const { saveFileWithFeedback } = await import("./fileTaskFeedback");
await expect(saveFileWithFeedback(output)).resolves.toBe("cancelled");
expect(finishActivityMock).toHaveBeenCalledWith("activity-1", "cancelled", { detail: "已取消保存" });
expect(messageInfoMock).not.toHaveBeenCalled();
});
it("reports external open completion", async () => {
const { openFileWithFeedback } = await import("./fileTaskFeedback");
@@ -97,4 +128,19 @@ describe("file task feedback", () => {
expect(openFileMock).toHaveBeenCalledWith(output);
expect(messageSuccessMock).toHaveBeenCalledWith("文件已打开");
});
it("uses persistent desktop activity for native open flows", async () => {
capabilitiesMock.mockReturnValue({ nativeFiles: true });
const { openFileWithFeedback } = await import("./fileTaskFeedback");
await openFileWithFeedback(output);
expect(startActivityMock).toHaveBeenCalledWith({
kind: "open",
title: "打开文件",
detail: "正在准备文件",
});
expect(finishActivityMock).toHaveBeenCalledWith("activity-1", "completed", { detail: "已交给系统打开" });
expect(messageSuccessMock).not.toHaveBeenCalled();
});
});
+70 -9
View File
@@ -8,9 +8,25 @@ import {
type FilePickerOptions,
type SaveFileResult,
} from "../runtime";
import { finishDesktopActivity, startDesktopActivity, updateDesktopActivity } from "../session/desktopActivityCenter";
const selectedFilesMessage = (count: number) => (count > 1 ? `已选择 ${count} 个文件` : "已选择 1 个文件");
type FileSaveActivityKind = "download" | "export";
export interface FileTaskFeedbackOptions {
activityId?: string;
kind?: FileSaveActivityKind;
title?: string;
pendingDetail?: string;
completedDetail?: string;
cancelledDetail?: string;
}
const desktopFileActivitiesEnabled = () => clientRuntime.capabilities().nativeFiles;
const defaultSaveTitle = (kind: FileSaveActivityKind) => (kind === "export" ? "导出文件" : "保存文件");
export const pickFilesWithFeedback = async (options: FilePickerOptions = {}): Promise<File[]> => {
const files = await pickFiles(options);
if (files.length) {
@@ -19,17 +35,62 @@ export const pickFilesWithFeedback = async (options: FilePickerOptions = {}): Pr
return files;
};
export const saveFileWithFeedback = async (output: FileOutput): Promise<SaveFileResult> => {
const result = await saveFile(output);
if (result === "cancelled") {
ElMessage.info("已取消保存文件");
export const saveFileWithFeedback = async (
output: FileOutput,
options: FileTaskFeedbackOptions = {},
): Promise<SaveFileResult> => {
const kind = options.kind || "download";
if (!desktopFileActivitiesEnabled()) {
const result = await saveFile(output);
if (result === "cancelled") {
ElMessage.info("已取消保存文件");
return result;
}
ElMessage.success(clientRuntime.capabilities().nativeFiles ? "文件已保存" : "文件下载已开始");
return result;
}
ElMessage.success(clientRuntime.capabilities().nativeFiles ? "文件已保存" : "文件下载已开始");
return result;
const activityId = options.activityId || startDesktopActivity({
kind,
title: options.title || defaultSaveTitle(kind),
detail: options.pendingDetail || "等待选择保存位置",
});
updateDesktopActivity(activityId, { detail: options.pendingDetail || "等待选择保存位置", progress: 70 });
try {
const result = await saveFile(output);
if (result === "cancelled") {
finishDesktopActivity(activityId, "cancelled", { detail: options.cancelledDetail || "已取消保存" });
return result;
}
finishDesktopActivity(activityId, "completed", { detail: options.completedDetail || "文件已保存" });
return result;
} catch (error) {
finishDesktopActivity(activityId, "failed", { detail: "文件保存失败" });
throw error;
}
};
export const openFileWithFeedback = async (output: FileOutput): Promise<void> => {
await openFile(output);
ElMessage.success("文件已打开");
export const openFileWithFeedback = async (
output: FileOutput,
options: Omit<FileTaskFeedbackOptions, "kind"> = {},
): Promise<void> => {
if (!desktopFileActivitiesEnabled()) {
await openFile(output);
ElMessage.success("文件已打开");
return;
}
const activityId = options.activityId || startDesktopActivity({
kind: "open",
title: options.title || "打开文件",
detail: options.pendingDetail || "正在准备文件",
});
updateDesktopActivity(activityId, { detail: options.pendingDetail || "正在准备文件", progress: 70 });
try {
await openFile(output);
finishDesktopActivity(activityId, "completed", { detail: options.completedDetail || "已交给系统打开" });
} catch (error) {
finishDesktopActivity(activityId, "failed", { detail: "文件打开失败" });
throw error;
}
};