diff --git a/electron/main/index.ts b/electron/main/index.ts index 0062b016..5d060a23 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -40,6 +40,23 @@ function getAppVersion(): string { } } +function getSystemInfo(): { distro: string } { + try { + const raw = fs.readFileSync("/etc/os-release", "utf8"); + const fields = Object.fromEntries( + raw + .split("\n") + .map((line) => line.match(/^([A-Z_]+)=(.*)$/)) + .filter((match): match is RegExpMatchArray => match !== null) + .map((match) => [match[1], match[2].replace(/^"|"$/g, "")]), + ); + const distro = fields.PRETTY_NAME || fields.NAME || "unknown"; + return { distro }; + } catch { + return { distro: "unknown" }; + } +} + // 处理 --version 参数(在单实例检查之前) if (process.argv.includes("--version") || process.argv.includes("-v")) { console.log(getAppVersion()); @@ -118,6 +135,7 @@ ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" => ); ipcMain.handle("get-app-version", (): string => getAppVersion()); +ipcMain.handle("get-system-info", (): { distro: string } => getSystemInfo()); ipcMain.handle("request-flarum-token", async (_event, payload: unknown) => { if (!payload || typeof payload !== "object" || Array.isArray(payload)) { diff --git a/src/App.vue b/src/App.vue index 887f5cba..9e64dfcf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -286,6 +286,7 @@ import { exchangeFlarumToken, listFavoriteFolders, listFavoriteItems, + recordDownloadedApp, } from "./modules/backendApi"; import { requestFlarumToken } from "./modules/flarumAuth"; import { @@ -306,6 +307,7 @@ import { buildFavoriteAppKey, buildReviewTags, getDisplayApp, + parsePackageArch, } from "./modules/appIdentity"; import { resolveFavoriteItems } from "./modules/favoriteAvailability"; import type { @@ -324,6 +326,7 @@ import type { FavoriteItem, InstalledAppInfo, ResolvedFavoriteItem, + SystemInfo, } from "./global/typedefinition"; import type { Ref } from "vue"; import type { IpcRendererEvent } from "electron"; @@ -403,6 +406,7 @@ const favoriteTargetApp = ref(null); const favoriteLoading = ref(false); const favoriteError = ref(""); const favoriteRequestGeneration = ref(0); +const systemInfo = ref({ distro: "unknown" }); /** 启动参数 --no-apm => 仅 Spark;--no-spark => 仅 APM;由主进程 IPC 提供 */ const storeFilter = ref<"spark" | "apm" | "both">("both"); @@ -500,7 +504,7 @@ const currentReviewTags = computed(() => { if (!currentDisplayApp.value) return null; return buildReviewTags(currentDisplayApp.value, { clientArch: clientArch.value, - distro: "unknown", + distro: systemInfo.value.distro, }); }); @@ -1236,7 +1240,22 @@ const onDetailRemove = (app: App) => { }; const onDetailInstall = async (app: App) => { - await handleInstall(app); + const download = await handleInstall(app); + if (!download || !isLoggedIn.value) return; + + try { + await recordDownloadedApp({ + appKey: buildFavoriteAppKey(app), + pkgname: app.pkgname, + name: app.name, + category: app.category, + selectedOrigin: app.origin, + version: app.version, + packageArch: app.arch || parsePackageArch(app.filename), + }); + } catch (error: unknown) { + logger.warn({ err: error }, "记录下载应用失败"); + } }; const onDetailFavorite = async (app: App) => { @@ -1783,6 +1802,13 @@ onMounted(async () => { initTheme(); updateCenterStore.bind(); + try { + systemInfo.value = await window.ipcRenderer.invoke("get-system-info"); + } catch (error: unknown) { + logger.warn({ err: error }, "读取系统信息失败"); + systemInfo.value = { distro: "unknown" }; + } + // 从主进程获取启动参数(--no-apm / --no-spark),再加载数据 storeFilter.value = await window.ipcRenderer.invoke("get-store-filter"); diff --git a/src/__tests__/unit/ReviewsPanel.test.ts b/src/__tests__/unit/ReviewsPanel.test.ts new file mode 100644 index 00000000..b8cf0f7a --- /dev/null +++ b/src/__tests__/unit/ReviewsPanel.test.ts @@ -0,0 +1,27 @@ +import { render, screen } from "@testing-library/vue"; +import { describe, expect, it } from "vitest"; + +import ReviewsPanel from "@/components/ReviewsPanel.vue"; +import type { ReviewTags } from "@/global/typedefinition"; + +const tags: ReviewTags = { + origin: "apm", + category: "office", + pkgname: "wps", + version: "1.0.0", + packageArch: "amd64", + clientArch: "amd64", + distro: "deepin 25", +}; + +describe("ReviewsPanel", () => { + it("shows anonymous login prompt and read-only review tags", () => { + render(ReviewsPanel, { + props: { appKey: "apm:amd64-apm:office:wps", tags, loggedIn: false }, + }); + + expect(screen.getByText("登录后发表评论")).toBeTruthy(); + expect(screen.getByText("1.0.0")).toBeTruthy(); + expect(screen.getByText("deepin 25")).toBeTruthy(); + }); +}); diff --git a/src/__tests__/unit/processInstall.test.ts b/src/__tests__/unit/processInstall.test.ts index de2b85c2..aca2a9ee 100644 --- a/src/__tests__/unit/processInstall.test.ts +++ b/src/__tests__/unit/processInstall.test.ts @@ -107,4 +107,44 @@ describe("processInstall queue forwarding", () => { expect.stringContaining('"id":5'), ); }); + + it("returns queued download metadata for account records", async () => { + vi.doMock("axios", () => ({ + default: { + create: vi.fn(() => ({ + post: vi.fn(() => Promise.resolve({ data: { ok: true } })), + })), + }, + })); + Object.assign(window.ipcRenderer, { + on: vi.fn(), + send: vi.fn(), + invoke: vi.fn(() => Promise.resolve(true)), + }); + window.apm_store.arch = "amd64"; + const { handleInstall } = await import("@/modules/processInstall"); + + const result = await handleInstall({ + name: "WPS", + pkgname: "wps", + version: "1.0.0", + filename: "wps_1.0.0_amd64.deb", + torrent_address: "", + author: "", + contributor: "", + website: "", + update: "", + size: "", + more: "", + tags: "", + img_urls: [], + icons: "", + category: "office", + origin: "apm", + currentStatus: "not-installed", + }); + + expect(result?.pkgname).toBe("wps"); + expect(result?.origin).toBe("apm"); + }); }); diff --git a/src/components/AppDetailPage.vue b/src/components/AppDetailPage.vue index 52c82a2f..ad710083 100644 --- a/src/components/AppDetailPage.vue +++ b/src/components/AppDetailPage.vue @@ -194,6 +194,14 @@

暂无应用截图

+ + @@ -201,6 +209,7 @@ diff --git a/src/modules/processInstall.ts b/src/modules/processInstall.ts index 67c77c61..397003a0 100644 --- a/src/modules/processInstall.ts +++ b/src/modules/processInstall.ts @@ -21,16 +21,18 @@ import axios from "axios"; const logger = pino({ name: "processInstall.ts" }); -export const handleInstall = async (appObj?: App) => { +export const handleInstall = async ( + appObj?: App, +): Promise => { const targetApp = appObj || currentApp.value; - if (!targetApp?.pkgname) return; + if (!targetApp?.pkgname) return null; // APM 应用:在创建下载任务前检查 APM 是否可用 if (targetApp.origin === "apm") { const hasApm = await window.ipcRenderer.invoke("check-apm-available"); if (!hasApm) { showApmInstallDialog.value = true; - return; + return null; } } @@ -42,7 +44,7 @@ export const handleInstall = async (appObj?: App) => { logger.info( `任务已存在,忽略重复添加: ${targetApp.pkgname} (${targetApp.origin})`, ); - return; + return null; } // 创建下载任务 @@ -98,6 +100,7 @@ export const handleInstall = async (appObj?: App) => { .then((response) => { logger.info("下载次数统计已发送,状态:", response.data); }); + return download; }; export const handleRetry = (download_: DownloadItem) => { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d17b2d9a..1eca2b6e 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,7 +1,7 @@ /* eslint-disable */ /// -import type { UpdateCenterBridge } from "@/global/typedefinition"; +import type { SystemInfo, UpdateCenterBridge } from "@/global/typedefinition"; declare module "*.vue" { import type { DefineComponent } from "vue"; @@ -34,6 +34,7 @@ interface IpcRendererFacade { // IPC channel type definitions declare interface IpcChannels { "get-app-version": () => string; + "get-system-info": () => Promise; "request-flarum-token": (payload: { identification: string; password: string;