refactor(已安装应用): 合并 APM/Spark 分页为列表标签并新增统计

- 移除 APM/Spark 分页切换器,改为在应用项标题旁标记来源标签
  (APM=琥珀色, Spark=天蓝色), 同时保留依赖项标记
- 修复已安装应用为空回归: list-installed 收到空 pkgnameList 时直接
  返回空列表, 避免退化为全量扫描整个系统后全部被跳过; 渲染端
  refreshInstalledApps 仅当商店目录存在 spark 列表时才查询 spark;
  目录加载完成后 watch 自动重查
- 头部新增 APM/Spark/总数 三段式统计徽章 (渐变图标 + 大数字)
- 更新 InstalledAppsModal 单测以兼容页头与列表同名标签
This commit is contained in:
xiyidaiwa
2026-07-28 19:14:21 +08:00
parent c1b89cab1a
commit 1e60a4c2cb
4 changed files with 275 additions and 283 deletions
+11 -3
View File
@@ -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",
+74 -63
View File
@@ -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,58 +1384,64 @@ 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", { if (effectiveOrigins.length === 0) {
origin,
pkgnameList,
});
if (!result?.success) {
installedApps.value = []; installedApps.value = [];
installedError.value = result?.message || "读取已安装应用失败"; // 目录尚未加载完成(但来源可用)时给出过渡提示,待目录加载后会自动重查
installedError.value =
apps.value.length === 0
? "正在加载应用目录,请稍候…"
: "当前系统不可用应用管理功能";
return; return;
} }
installedApps.value = []; // 并行查询每个 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;
}
for (const app of result.apps) { for (const app of result.apps) {
// Find matching remote app to enrich data. We look exactly for that origin. // Find matching remote app to enrich data. We look exactly for that origin.
let appInfo = apps.value.find( let appInfo = apps.value.find(
@@ -1494,7 +1483,17 @@ const refreshInstalledApps = async () => {
isDependency: app.isDependency, isDependency: app.isDependency,
}; };
} }
installedApps.value.push(appInfo); combinedApps.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();
+47 -100
View File
@@ -26,21 +26,19 @@ const createApp = (overrides: Partial<App> = {}): App => ({
}); });
describe("InstalledAppsModal", () => { describe("InstalledAppsModal", () => {
it("keeps scroll chaining inside the modal list", () => { const baseProps = {
const { container } = render(InstalledAppsModal, {
props: {
show: true, show: true,
apps: [], apps: [] as App[],
loading: false, loading: false,
error: "", error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false, loggedIn: false,
syncing: false, syncing: false,
syncMessage: "", syncMessage: "",
}, };
it("keeps scroll chaining inside the modal list", () => {
const { container } = render(InstalledAppsModal, {
props: baseProps,
}); });
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: "同步完成",
}, },
}); });
+95 -69
View File
@@ -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>