mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-09 22:04:06 +08:00
refactor(已安装应用): 合并 APM/Spark 分页为列表标签并新增统计
- 移除 APM/Spark 分页切换器,改为在应用项标题旁标记来源标签 (APM=琥珀色, Spark=天蓝色), 同时保留依赖项标记 - 修复已安装应用为空回归: list-installed 收到空 pkgnameList 时直接 返回空列表, 避免退化为全量扫描整个系统后全部被跳过; 渲染端 refreshInstalledApps 仅当商店目录存在 spark 列表时才查询 spark; 目录加载完成后 watch 自动重查 - 头部新增 APM/Spark/总数 三段式统计徽章 (渐变图标 + 大数字) - 更新 InstalledAppsModal 单测以兼容页头与列表同名标签
This commit is contained in:
@@ -997,8 +997,16 @@ ipcMain.handle(
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (origin === "spark") {
|
if (origin === "spark") {
|
||||||
// 如果提供了包名列表,只检查这些包的安装状态(优化版)
|
// 显式传入了包名列表(可能是空数组):只检查这些包的安装状态(优化版)
|
||||||
if (pkgnameList && pkgnameList.length > 0) {
|
if (Array.isArray(pkgnameList)) {
|
||||||
|
if (pkgnameList.length === 0) {
|
||||||
|
// 商店目录尚未加载或该来源没有任何可枚举的包时,
|
||||||
|
// 直接返回空列表,避免退化为“全量扫描整个系统”后再被渲染端全部跳过,
|
||||||
|
// 否则会误报“已安装应用为空”。
|
||||||
|
logger.info("Spark 包名列表为空,跳过已安装检查");
|
||||||
|
return { success: true, apps: [] };
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`使用优化模式检查 ${pkgnameList.length} 个 Spark 包的安装状态`,
|
`使用优化模式检查 ${pkgnameList.length} 个 Spark 包的安装状态`,
|
||||||
);
|
);
|
||||||
@@ -1040,7 +1048,7 @@ ipcMain.handle(
|
|||||||
return { success: true, apps: installedApps };
|
return { success: true, apps: installedApps };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回退到全量扫描模式(未提供包名列表时)
|
// 回退到全量扫描模式(仅当调用方未传入 pkgnameList 时,例如旧版直接调用)
|
||||||
logger.info("使用全量扫描模式获取所有 Spark 已安装包");
|
logger.info("使用全量扫描模式获取所有 Spark 已安装包");
|
||||||
const { code, stdout } = await runCommandCapture("dpkg-query", [
|
const { code, stdout } = await runCommandCapture("dpkg-query", [
|
||||||
"-W",
|
"-W",
|
||||||
|
|||||||
+118
-107
@@ -172,10 +172,6 @@
|
|||||||
:apps="installedApps"
|
:apps="installedApps"
|
||||||
:loading="installedLoading"
|
:loading="installedLoading"
|
||||||
:error="installedError"
|
:error="installedError"
|
||||||
:active-origin="activeInstalledOrigin"
|
|
||||||
:store-filter="storeFilter"
|
|
||||||
:spark-available="sparkAvailable"
|
|
||||||
:apm-available="apmAvailable"
|
|
||||||
:logged-in="isLoggedIn"
|
:logged-in="isLoggedIn"
|
||||||
:syncing="syncLoading"
|
:syncing="syncLoading"
|
||||||
:sync-message="syncStatusMessage"
|
:sync-message="syncStatusMessage"
|
||||||
@@ -184,7 +180,6 @@
|
|||||||
@open-app="openDownloadedApp($event.pkgname, $event.origin)"
|
@open-app="openDownloadedApp($event.pkgname, $event.origin)"
|
||||||
@open-detail="openDetail"
|
@open-detail="openDetail"
|
||||||
@uninstall="uninstallInstalledApp"
|
@uninstall="uninstallInstalledApp"
|
||||||
@switch-origin="handleSwitchOrigin"
|
|
||||||
@sync-to-account="syncInstalledAppsToAccount"
|
@sync-to-account="syncInstalledAppsToAccount"
|
||||||
@restore-from-account="openRestoreFromAccount"
|
@restore-from-account="openRestoreFromAccount"
|
||||||
@request-login="requireLogin('云端同步需要登录星火账号。')"
|
@request-login="requireLogin('云端同步需要登录星火账号。')"
|
||||||
@@ -362,10 +357,9 @@ import {
|
|||||||
setAuthSession,
|
setAuthSession,
|
||||||
} from "./global/authState";
|
} from "./global/authState";
|
||||||
import {
|
import {
|
||||||
getAllowedInstalledOrigin,
|
|
||||||
getEffectiveStoreFilter,
|
getEffectiveStoreFilter,
|
||||||
getDefaultInstalledOrigin,
|
|
||||||
isOriginEnabled,
|
isOriginEnabled,
|
||||||
|
isOriginUsable,
|
||||||
} from "./modules/storeFilter";
|
} from "./modules/storeFilter";
|
||||||
import { createUpdateCenterStore } from "./modules/updateCenter";
|
import { createUpdateCenterStore } from "./modules/updateCenter";
|
||||||
import {
|
import {
|
||||||
@@ -468,7 +462,6 @@ const loading = ref(true);
|
|||||||
const showDownloadDetailModal = ref(false);
|
const showDownloadDetailModal = ref(false);
|
||||||
const currentDownload: Ref<DownloadItem | null> = ref(null);
|
const currentDownload: Ref<DownloadItem | null> = ref(null);
|
||||||
const showInstalledModal = ref(false);
|
const showInstalledModal = ref(false);
|
||||||
const activeInstalledOrigin = ref<"apm" | "spark">("apm");
|
|
||||||
const installedApps = ref<App[]>([]);
|
const installedApps = ref<App[]>([]);
|
||||||
const installedLoading = ref(false);
|
const installedLoading = ref(false);
|
||||||
const installedError = ref("");
|
const installedError = ref("");
|
||||||
@@ -1379,21 +1372,11 @@ const confirmMigrationStart = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openInstalledModal = () => {
|
const openInstalledModal = () => {
|
||||||
const defaultOrigin = getDefaultInstalledOrigin(
|
if (getEffectiveStoreFilter(storeFilter.value, availableSources.value) === null) {
|
||||||
storeFilter.value,
|
|
||||||
availableSources.value,
|
|
||||||
);
|
|
||||||
if (!defaultOrigin) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showInstalledModal.value = true;
|
showInstalledModal.value = true;
|
||||||
activeInstalledOrigin.value =
|
|
||||||
getAllowedInstalledOrigin(
|
|
||||||
storeFilter.value,
|
|
||||||
activeInstalledOrigin.value,
|
|
||||||
availableSources.value,
|
|
||||||
) ?? defaultOrigin;
|
|
||||||
refreshInstalledApps();
|
refreshInstalledApps();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1401,100 +1384,116 @@ const closeInstalledModal = () => {
|
|||||||
showInstalledModal.value = false;
|
showInstalledModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchOrigin = (origin: "apm" | "spark") => {
|
// 根据当前启动模式和系统可用性,确定需要查询哪些 origin 的已安装应用
|
||||||
activeInstalledOrigin.value =
|
const resolveInstalledOrigins = (): Array<"spark" | "apm"> => {
|
||||||
getAllowedInstalledOrigin(
|
const origins: Array<"spark" | "apm"> = [];
|
||||||
storeFilter.value,
|
if (isOriginUsable(storeFilter.value, "spark", availableSources.value)) {
|
||||||
origin,
|
origins.push("spark");
|
||||||
availableSources.value,
|
}
|
||||||
) ?? activeInstalledOrigin.value;
|
if (isOriginUsable(storeFilter.value, "apm", availableSources.value)) {
|
||||||
refreshInstalledApps();
|
origins.push("apm");
|
||||||
|
}
|
||||||
|
return origins;
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshInstalledApps = async () => {
|
const refreshInstalledApps = async () => {
|
||||||
installedLoading.value = true;
|
installedLoading.value = true;
|
||||||
installedError.value = "";
|
installedError.value = "";
|
||||||
try {
|
try {
|
||||||
const origin = getAllowedInstalledOrigin(
|
const origins = resolveInstalledOrigins();
|
||||||
storeFilter.value,
|
// Spark 已安装列表依赖商店目录(apps.value)来枚举包名。
|
||||||
activeInstalledOrigin.value,
|
// 仅当目录中存在可枚举的 Spark 包时才查询 Spark,
|
||||||
availableSources.value,
|
// 否则空目录会触发“全量扫描整个系统”的陷阱导致列表被全部跳过而误报为空。
|
||||||
);
|
const sparkPkgnameList = apps.value
|
||||||
if (!origin) {
|
|
||||||
installedApps.value = [];
|
|
||||||
installedError.value = "当前系统不可用应用管理功能";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeInstalledOrigin.value = origin;
|
|
||||||
|
|
||||||
if (!isOriginEnabled(storeFilter.value, origin)) {
|
|
||||||
installedApps.value = [];
|
|
||||||
installedError.value = `当前启动模式已禁用 ${origin === "spark" ? "Spark" : "APM"} 软件管理`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spark 优化:只检查远端商店目录中的应用,避免全量扫描
|
|
||||||
let pkgnameList: string[] | undefined;
|
|
||||||
if (origin === "spark") {
|
|
||||||
pkgnameList = apps.value
|
|
||||||
.filter((a) => a.origin === "spark")
|
.filter((a) => a.origin === "spark")
|
||||||
.map((a) => a.pkgname);
|
.map((a) => a.pkgname);
|
||||||
}
|
const effectiveOrigins = origins.filter(
|
||||||
|
(o) => o !== "spark" || sparkPkgnameList.length > 0,
|
||||||
const result = await window.ipcRenderer.invoke("list-installed", {
|
|
||||||
origin,
|
|
||||||
pkgnameList,
|
|
||||||
});
|
|
||||||
if (!result?.success) {
|
|
||||||
installedApps.value = [];
|
|
||||||
installedError.value = result?.message || "读取已安装应用失败";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
installedApps.value = [];
|
|
||||||
for (const app of result.apps) {
|
|
||||||
// Find matching remote app to enrich data. We look exactly for that origin.
|
|
||||||
let appInfo = apps.value.find(
|
|
||||||
(a) => a.pkgname === app.pkgname && a.origin === origin,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (origin === "spark" && !appInfo) {
|
if (effectiveOrigins.length === 0) {
|
||||||
// Only show Spark packages that exist in the App Store catalogue
|
installedApps.value = [];
|
||||||
|
// 目录尚未加载完成(但来源可用)时给出过渡提示,待目录加载后会自动重查
|
||||||
|
installedError.value =
|
||||||
|
apps.value.length === 0
|
||||||
|
? "正在加载应用目录,请稍候…"
|
||||||
|
: "当前系统不可用应用管理功能";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行查询每个 origin 的已安装应用
|
||||||
|
const results = await Promise.all(
|
||||||
|
effectiveOrigins.map((origin) =>
|
||||||
|
window.ipcRenderer.invoke("list-installed", {
|
||||||
|
origin,
|
||||||
|
pkgnameList: origin === "spark" ? sparkPkgnameList : undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const combinedApps: App[] = [];
|
||||||
|
const failedOrigins: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < effectiveOrigins.length; i++) {
|
||||||
|
const origin = effectiveOrigins[i];
|
||||||
|
const result = results[i];
|
||||||
|
if (!result?.success) {
|
||||||
|
failedOrigins.push(origin);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appInfo) {
|
for (const app of result.apps) {
|
||||||
appInfo.flags = app.flags;
|
// Find matching remote app to enrich data. We look exactly for that origin.
|
||||||
appInfo.arch = app.arch;
|
let appInfo = apps.value.find(
|
||||||
appInfo.currentStatus = "installed";
|
(a) => a.pkgname === app.pkgname && a.origin === origin,
|
||||||
appInfo.isDependency = app.isDependency;
|
);
|
||||||
} else {
|
|
||||||
// 如果在当前应用列表中找不到该应用,创建一个最小的 App 对象
|
if (origin === "spark" && !appInfo) {
|
||||||
appInfo = {
|
// Only show Spark packages that exist in the App Store catalogue
|
||||||
name: app.name || app.pkgname,
|
continue;
|
||||||
pkgname: app.pkgname,
|
}
|
||||||
version: app.version,
|
|
||||||
category: "unknown",
|
if (appInfo) {
|
||||||
tags: "",
|
appInfo.flags = app.flags;
|
||||||
more: "",
|
appInfo.arch = app.arch;
|
||||||
filename: "",
|
appInfo.currentStatus = "installed";
|
||||||
torrent_address: "",
|
appInfo.isDependency = app.isDependency;
|
||||||
author: "",
|
} else {
|
||||||
contributor: "",
|
// 如果在当前应用列表中找不到该应用,创建一个最小的 App 对象
|
||||||
website: "",
|
appInfo = {
|
||||||
update: "",
|
name: app.name || app.pkgname,
|
||||||
size: "",
|
pkgname: app.pkgname,
|
||||||
img_urls: [],
|
version: app.version,
|
||||||
icons: app.icon || "",
|
category: "unknown",
|
||||||
origin: app.origin || (app.arch?.includes("apm") ? "apm" : "spark"),
|
tags: "",
|
||||||
currentStatus: "installed",
|
more: "",
|
||||||
arch: app.arch,
|
filename: "",
|
||||||
flags: app.flags,
|
torrent_address: "",
|
||||||
isDependency: app.isDependency,
|
author: "",
|
||||||
};
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: app.icon || "",
|
||||||
|
origin: app.origin || (app.arch?.includes("apm") ? "apm" : "spark"),
|
||||||
|
currentStatus: "installed",
|
||||||
|
arch: app.arch,
|
||||||
|
flags: app.flags,
|
||||||
|
isDependency: app.isDependency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
combinedApps.push(appInfo);
|
||||||
}
|
}
|
||||||
installedApps.value.push(appInfo);
|
}
|
||||||
|
|
||||||
|
installedApps.value = combinedApps;
|
||||||
|
|
||||||
|
if (combinedApps.length === 0 && failedOrigins.length > 0) {
|
||||||
|
const labels = failedOrigins
|
||||||
|
.map((o) => (o === "spark" ? "Spark" : "APM"))
|
||||||
|
.join("、");
|
||||||
|
installedError.value = `读取${labels}已安装应用失败`;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
installedApps.value = [];
|
installedApps.value = [];
|
||||||
@@ -1504,6 +1503,22 @@ const refreshInstalledApps = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 应用目录(apps.value)可能在打开“已安装应用”模态框之后才加载完成。
|
||||||
|
// 当目录从无到有、且模态框处于打开状态时,自动重查已安装应用,避免列表一直为空。
|
||||||
|
watch(
|
||||||
|
() => apps.value.length,
|
||||||
|
(len, prevLen) => {
|
||||||
|
if (
|
||||||
|
showInstalledModal.value &&
|
||||||
|
prevLen === 0 &&
|
||||||
|
len > 0 &&
|
||||||
|
!installedLoading.value
|
||||||
|
) {
|
||||||
|
void refreshInstalledApps();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const mapInstalledAppToCatalogApp = (
|
const mapInstalledAppToCatalogApp = (
|
||||||
app: InstalledAppInfo,
|
app: InstalledAppInfo,
|
||||||
origin: "spark" | "apm",
|
origin: "spark" | "apm",
|
||||||
@@ -2827,10 +2842,6 @@ onMounted(async () => {
|
|||||||
apmAvailable.value = await window.ipcRenderer.invoke("check-apm-available");
|
apmAvailable.value = await window.ipcRenderer.invoke("check-apm-available");
|
||||||
}
|
}
|
||||||
|
|
||||||
activeInstalledOrigin.value =
|
|
||||||
getDefaultInstalledOrigin(storeFilter.value, availableSources.value) ??
|
|
||||||
"spark";
|
|
||||||
|
|
||||||
await loadCategories();
|
await loadCategories();
|
||||||
|
|
||||||
await loadSidebarConfig();
|
await loadSidebarConfig();
|
||||||
|
|||||||
@@ -26,21 +26,19 @@ const createApp = (overrides: Partial<App> = {}): App => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("InstalledAppsModal", () => {
|
describe("InstalledAppsModal", () => {
|
||||||
|
const baseProps = {
|
||||||
|
show: true,
|
||||||
|
apps: [] as App[],
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
loggedIn: false,
|
||||||
|
syncing: false,
|
||||||
|
syncMessage: "",
|
||||||
|
};
|
||||||
|
|
||||||
it("keeps scroll chaining inside the modal list", () => {
|
it("keeps scroll chaining inside the modal list", () => {
|
||||||
const { container } = render(InstalledAppsModal, {
|
const { container } = render(InstalledAppsModal, {
|
||||||
props: {
|
props: baseProps,
|
||||||
show: true,
|
|
||||||
apps: [],
|
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("已安装应用")).toBeTruthy();
|
expect(screen.getByText("已安装应用")).toBeTruthy();
|
||||||
@@ -52,17 +50,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("renders open and detail actions for a store-backed installed app", () => {
|
it("renders open and detail actions for a store-backed installed app", () => {
|
||||||
render(InstalledAppsModal, {
|
render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [createApp()],
|
apps: [createApp()],
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,20 +59,42 @@ describe("InstalledAppsModal", () => {
|
|||||||
expect(screen.getByRole("button", { name: "查看详情" })).toBeTruthy();
|
expect(screen.getByRole("button", { name: "查看详情" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the spark origin tag for spark apps", () => {
|
||||||
|
render(InstalledAppsModal, {
|
||||||
|
props: {
|
||||||
|
...baseProps,
|
||||||
|
apps: [createApp({ origin: "spark", name: "Spark Notes" })],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页头统计区也含 "Spark" 标签,故用 getAllByText 校验至少一个匹配
|
||||||
|
expect(screen.getAllByText("Spark").length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the APM origin tag for APM apps", () => {
|
||||||
|
render(InstalledAppsModal, {
|
||||||
|
props: {
|
||||||
|
...baseProps,
|
||||||
|
apps: [
|
||||||
|
createApp({
|
||||||
|
origin: "apm",
|
||||||
|
name: "APM Container",
|
||||||
|
pkgname: "amber-pm-container",
|
||||||
|
version: "1.0.0",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页头统计区也含 "APM" 标签,故用 getAllByText 校验至少一个匹配
|
||||||
|
expect(screen.getAllByText("APM").length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("emits open-app when clicking 打开", async () => {
|
it("emits open-app when clicking 打开", async () => {
|
||||||
const rendered = render(InstalledAppsModal, {
|
const rendered = render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [createApp()],
|
apps: [createApp()],
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,17 +109,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("emits open-detail when clicking 查看详情", async () => {
|
it("emits open-detail when clicking 查看详情", async () => {
|
||||||
const rendered = render(InstalledAppsModal, {
|
const rendered = render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [createApp()],
|
apps: [createApp()],
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,17 +125,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("shows 查看详情 for metadata-rich unknown-category apps", () => {
|
it("shows 查看详情 for metadata-rich unknown-category apps", () => {
|
||||||
render(InstalledAppsModal, {
|
render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [createApp({ category: "unknown", more: "Has store metadata" })],
|
apps: [createApp({ category: "unknown", more: "Has store metadata" })],
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,17 +136,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("hides 查看详情 for unknown-category apps", () => {
|
it("hides 查看详情 for unknown-category apps", () => {
|
||||||
render(InstalledAppsModal, {
|
render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [createApp({ category: "unknown" })],
|
apps: [createApp({ category: "unknown" })],
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,19 +146,7 @@ describe("InstalledAppsModal", () => {
|
|||||||
|
|
||||||
it("requests login for cloud actions when logged out", async () => {
|
it("requests login for cloud actions when logged out", async () => {
|
||||||
const rendered = render(InstalledAppsModal, {
|
const rendered = render(InstalledAppsModal, {
|
||||||
props: {
|
props: baseProps,
|
||||||
show: true,
|
|
||||||
apps: [],
|
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: false,
|
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await fireEvent.click(screen.getByRole("button", { name: "同步到账号" }));
|
await fireEvent.click(screen.getByRole("button", { name: "同步到账号" }));
|
||||||
@@ -186,17 +158,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("emits cloud sync and restore events when logged in", async () => {
|
it("emits cloud sync and restore events when logged in", async () => {
|
||||||
const rendered = render(InstalledAppsModal, {
|
const rendered = render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [],
|
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
syncing: false,
|
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,17 +173,9 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("disables sync button while syncing", () => {
|
it("disables sync button while syncing", () => {
|
||||||
render(InstalledAppsModal, {
|
render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [],
|
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
syncing: true,
|
syncing: true,
|
||||||
syncMessage: "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,16 +185,8 @@ describe("InstalledAppsModal", () => {
|
|||||||
it("shows account sync feedback in the installed apps modal", () => {
|
it("shows account sync feedback in the installed apps modal", () => {
|
||||||
render(InstalledAppsModal, {
|
render(InstalledAppsModal, {
|
||||||
props: {
|
props: {
|
||||||
show: true,
|
...baseProps,
|
||||||
apps: [],
|
|
||||||
loading: false,
|
|
||||||
error: "",
|
|
||||||
activeOrigin: "spark",
|
|
||||||
storeFilter: "both",
|
|
||||||
sparkAvailable: true,
|
|
||||||
apmAvailable: true,
|
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
syncing: false,
|
|
||||||
syncMessage: "同步完成",
|
syncMessage: "同步完成",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,66 +19,87 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-start justify-between border-b border-slate-200/70 p-6 dark:border-slate-800/70"
|
class="flex items-start justify-between border-b border-slate-200/70 p-6 dark:border-slate-800/70"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
已安装应用
|
已安装应用
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
管理本机安装的应用程序
|
管理本机安装的应用程序
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="!loading && !error"
|
||||||
|
class="mt-2 inline-flex flex-wrap items-stretch overflow-hidden rounded-2xl border border-slate-200/70 bg-slate-50/60 text-sm shadow-sm dark:border-slate-700/60 dark:bg-slate-800/40"
|
||||||
|
>
|
||||||
|
<!-- APM -->
|
||||||
|
<div
|
||||||
|
class="group flex items-center gap-2.5 px-3.5 py-2 transition hover:bg-amber-50/80 dark:hover:bg-amber-500/10"
|
||||||
|
title="APM 应用数量"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm shadow-amber-500/30"
|
||||||
|
>
|
||||||
|
<i class="fas fa-box-open text-[12px]"></i>
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col leading-tight">
|
||||||
|
<span
|
||||||
|
class="text-[15px] font-bold tabular-nums text-amber-700 dark:text-amber-300"
|
||||||
|
>{{ apmCount }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-medium uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80"
|
||||||
|
>APM</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<span class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"></span>
|
||||||
|
<!-- Spark -->
|
||||||
|
<div
|
||||||
|
class="group flex items-center gap-2.5 px-3.5 py-2 transition hover:bg-sky-50/80 dark:hover:bg-sky-500/10"
|
||||||
|
title="Spark 应用数量"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-sky-400 to-blue-500 text-white shadow-sm shadow-sky-500/30"
|
||||||
|
>
|
||||||
|
<i class="fas fa-bolt text-[12px]"></i>
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col leading-tight">
|
||||||
|
<span
|
||||||
|
class="text-[15px] font-bold tabular-nums text-sky-700 dark:text-sky-300"
|
||||||
|
>{{ sparkCount }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-medium uppercase tracking-wider text-sky-600/80 dark:text-sky-400/80"
|
||||||
|
>Spark</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<span class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"></span>
|
||||||
|
<!-- 总数 -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2.5 px-3.5 py-2"
|
||||||
|
title="已安装应用总数"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-slate-700 text-white shadow-sm dark:from-slate-400 dark:to-slate-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-cubes text-[12px]"></i>
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col leading-tight">
|
||||||
|
<span
|
||||||
|
class="text-[15px] font-bold tabular-nums text-slate-900 dark:text-white"
|
||||||
|
>{{ totalCount }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400"
|
||||||
|
>总数</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- 云端同步功能暂时关闭
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-brand/30 px-4 py-2 text-sm font-semibold text-brand transition hover:bg-brand/10 disabled:opacity-40"
|
|
||||||
:disabled="syncing"
|
|
||||||
@click="handleSyncClick"
|
|
||||||
>
|
|
||||||
<i class="fas fa-cloud-arrow-up"></i>
|
|
||||||
{{ syncing ? "同步中" : "同步到账号" }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
|
||||||
@click="handleRestoreClick"
|
|
||||||
>
|
|
||||||
<i class="fas fa-cloud-arrow-down"></i>
|
|
||||||
从账号恢复
|
|
||||||
</button>
|
|
||||||
-->
|
|
||||||
<div
|
|
||||||
v-if="showOriginSwitcher"
|
|
||||||
class="flex items-center rounded-2xl border border-slate-200/70 p-1 dark:border-slate-800/70"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="apmEnabled"
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
|
|
||||||
:class="
|
|
||||||
activeOrigin === 'apm'
|
|
||||||
? 'bg-brand/10 text-brand dark:bg-brand/15'
|
|
||||||
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
|
|
||||||
"
|
|
||||||
:disabled="!apmAvailable"
|
|
||||||
@click="$emit('switch-origin', 'apm')"
|
|
||||||
>
|
|
||||||
APM 软件
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="sparkEnabled"
|
|
||||||
type="button"
|
|
||||||
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
|
|
||||||
:class="
|
|
||||||
activeOrigin === 'spark'
|
|
||||||
? 'bg-brand/10 text-brand dark:bg-brand/15'
|
|
||||||
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
|
|
||||||
"
|
|
||||||
@click="$emit('switch-origin', 'spark')"
|
|
||||||
>
|
|
||||||
Spark 软件
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||||
@@ -155,6 +176,20 @@
|
|||||||
>
|
>
|
||||||
{{ app.name }}
|
{{ app.name }}
|
||||||
</p>
|
</p>
|
||||||
|
<span
|
||||||
|
v-if="app.origin === 'apm'"
|
||||||
|
class="rounded-md bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700 dark:bg-amber-500/20 dark:text-amber-400"
|
||||||
|
title="APM 软件"
|
||||||
|
>
|
||||||
|
APM
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="app.origin === 'spark'"
|
||||||
|
class="rounded-md bg-sky-100 px-2 py-0.5 text-[11px] font-semibold text-sky-700 dark:bg-sky-500/20 dark:text-sky-400"
|
||||||
|
title="Spark 软件"
|
||||||
|
>
|
||||||
|
Spark
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="app.isDependency"
|
v-if="app.isDependency"
|
||||||
class="rounded-md bg-rose-100 px-2 py-0.5 text-[11px] font-semibold text-rose-600 dark:bg-rose-500/20 dark:text-rose-400"
|
class="rounded-md bg-rose-100 px-2 py-0.5 text-[11px] font-semibold text-rose-600 dark:bg-rose-500/20 dark:text-rose-400"
|
||||||
@@ -241,21 +276,24 @@ const props = defineProps<{
|
|||||||
apps: App[];
|
apps: App[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
activeOrigin: "apm" | "spark";
|
|
||||||
storeFilter: "spark" | "apm" | "both";
|
|
||||||
sparkAvailable: boolean;
|
|
||||||
apmAvailable: boolean;
|
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
syncMessage: string;
|
syncMessage: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const apmCount = computed(
|
||||||
|
() => props.apps.filter((a) => a.origin === "apm").length,
|
||||||
|
);
|
||||||
|
const sparkCount = computed(
|
||||||
|
() => props.apps.filter((a) => a.origin === "spark").length,
|
||||||
|
);
|
||||||
|
const totalCount = computed(() => props.apps.length);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void;
|
(e: "close"): void;
|
||||||
(e: "refresh"): void;
|
(e: "refresh"): void;
|
||||||
(e: "uninstall", app: App): void;
|
(e: "uninstall", app: App): void;
|
||||||
(e: "switch-origin", origin: "apm" | "spark"): void;
|
|
||||||
(e: "open-app", app: App): void;
|
(e: "open-app", app: App): void;
|
||||||
(e: "open-detail", app: App): void;
|
(e: "open-detail", app: App): void;
|
||||||
(e: "sync-to-account"): void;
|
(e: "sync-to-account"): void;
|
||||||
@@ -287,16 +325,4 @@ const onOverlayWheel = (e: WheelEvent) => {
|
|||||||
if (target.closest(".overflow-y-auto, .overflow-auto")) return;
|
if (target.closest(".overflow-y-auto, .overflow-auto")) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const sparkEnabled = computed(() => {
|
|
||||||
return props.storeFilter !== "apm" && props.sparkAvailable;
|
|
||||||
});
|
|
||||||
|
|
||||||
const apmEnabled = computed(() => {
|
|
||||||
return props.storeFilter !== "spark" && props.apmAvailable;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showOriginSwitcher = computed(() => {
|
|
||||||
return sparkEnabled.value && apmEnabled.value;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user