Files
spark-store/src/composables/useFavorites.ts
T
xiyidaiwa d7bdcf3680 fix: 标签策略启动初始化加固 + 已安装应用过滤/统计性能优化
- 标签优先显示策略 initTagPriorityStrategy 提到 App.vue onMounted 统一调用,
  消除组件挂载顺序导致先读内存默认 auto 的竞态窗口(AppDetailModal 移除重复调用)
- InstalledAppsModal 过滤逻辑合并为单次遍历(保留 hasOrigin 双来源语义)
- 统计 apm/spark/total 改为基于 searchFilteredApps 单次遍历
- canOpenDetail 补充业务含义注释与可读性变量
- 将 App.vue 重构的 9 个 composable 纳入版本管理
- 验证:vue-tsc 0 / eslint 0 / dpkg-buildpackage 打包 spark-store_5.2.1.24-test 成功
2026-08-12 23:16:17 +08:00

490 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* useFavorites —— 收藏夹相关的全部状态与逻辑。
*
* 从原 App.vue 原样搬移(loadFavoriteFolders / loadActiveFavoriteItems /
* loadAllFavoriteItems / loadFavoriteMetadataForDetail / refreshFavorites /
* openFavoriteSelector / toFavoritePayload / saveCurrentFavoriteFolders /
* createFavoriteFolderFromSelector / openFavoriteManagement / selectFavoriteFolder /
* createFavoriteFolderFromPrompt / removeSelectedFavorites / installResolvedFavorites /
* clearFavoriteState / 代次守卫 / currentFavoriteMetadata / currentFavoriteFolderIds /
* resolvedFavoriteItems),逻辑零改动。
*
* 共享状态来自 useAppStateclientArch 来自 useAppDetail。
*/
import { computed } from "vue";
import type {
App,
FavoriteItem,
ResolvedFavoriteItem,
} from "../global/typedefinition";
import {
favoriteFolders,
favoriteItems,
favoriteItemsByFolder,
favoriteLoading,
favoriteError,
favoriteRequestGeneration,
favoriteTargetApp,
favoriteSelectorDraftFolderIds,
activeFavoriteFolderId,
showFavoriteSelector,
currentView,
activeTab,
isSidebarOpen,
showLoginPrompt,
apps,
installedApps,
availableSources,
storeFilter,
} from "./useAppState";
import { clientArch } from "./useAppDetail";
import {
listFavoriteFolders,
listFavoriteItems,
addFavoriteItem,
deleteFavoriteItem,
createFavoriteFolder,
bulkDeleteFavoriteItems,
} from "../modules/backendApi";
import { buildFavoriteAppKey } from "../modules/appIdentity";
import { resolveFavoriteItems } from "../modules/favoriteAvailability";
import { isLoggedIn } from "../global/authState";
import { refreshFavoriteInstalledApps } from "./useInstalledApps";
const nextFavoriteRequestGeneration = (): number => {
favoriteRequestGeneration.value += 1;
return favoriteRequestGeneration.value;
};
const isCurrentFavoriteRequest = (generation: number): boolean =>
favoriteRequestGeneration.value === generation && isLoggedIn.value;
const loadFavoriteFolders = async (
generation = favoriteRequestGeneration.value,
): Promise<boolean> => {
const folders = await listFavoriteFolders();
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteFolders.value = folders;
const activeFolderExists = folders.some(
(folder) => folder.id === activeFavoriteFolderId.value,
);
if (!activeFolderExists) {
activeFavoriteFolderId.value = folders[0]?.id ?? null;
}
return true;
};
const loadActiveFavoriteItems = async (
generation = favoriteRequestGeneration.value,
): Promise<boolean> => {
if (!activeFavoriteFolderId.value) {
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteItems.value = [];
return true;
}
const items = await listFavoriteItems(activeFavoriteFolderId.value);
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteItems.value = items;
favoriteItemsByFolder.value = {
...favoriteItemsByFolder.value,
[activeFavoriteFolderId.value]: items,
};
return true;
};
const loadAllFavoriteItems = async (
generation = favoriteRequestGeneration.value,
): Promise<boolean> => {
const folderIds = favoriteFolders.value.map((folder) => folder.id);
const entries = await Promise.all(
folderIds.map(async (folderId) => ({
folderId,
items: await listFavoriteItems(folderId),
})),
);
if (!isCurrentFavoriteRequest(generation)) return false;
favoriteItemsByFolder.value = Object.fromEntries(
entries.map(({ folderId, items }) => [folderId, items]),
);
favoriteItems.value = activeFavoriteFolderId.value
? (favoriteItemsByFolder.value[activeFavoriteFolderId.value] ?? [])
: [];
return true;
};
const loadFavoriteMetadataForDetail = async (): Promise<void> => {
const generation = favoriteRequestGeneration.value;
try {
const folders = await listFavoriteFolders();
if (!isCurrentFavoriteRequest(generation)) return;
const entries = await Promise.all(
folders.map(async (folder) => ({
folderId: folder.id,
items: await listFavoriteItems(folder.id),
})),
);
if (!isCurrentFavoriteRequest(generation)) return;
favoriteFolders.value = folders;
favoriteItemsByFolder.value = Object.fromEntries(
entries.map(({ folderId, items }) => [folderId, items]),
);
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
}
};
const refreshFavorites = async (): Promise<void> => {
const generation = nextFavoriteRequestGeneration();
favoriteLoading.value = true;
favoriteError.value = "";
try {
await Promise.all([
refreshFavoriteInstalledApps(),
loadFavoriteFolders(generation),
]);
if (!isCurrentFavoriteRequest(generation)) return;
await loadAllFavoriteItems(generation);
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
} finally {
if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
}
};
const openFavoriteSelector = async (app: App) => {
if (!requireLoginRef("收藏应用需要登录星火账号。")) return;
const generation = nextFavoriteRequestGeneration();
favoriteTargetApp.value = app;
favoriteSelectorDraftFolderIds.value = null;
favoriteError.value = "";
try {
const foldersLoaded = await loadFavoriteFolders(generation);
if (!foldersLoaded || !isCurrentFavoriteRequest(generation)) return;
const itemsLoaded = await loadAllFavoriteItems(generation);
if (!itemsLoaded || !isCurrentFavoriteRequest(generation)) return;
showFavoriteSelector.value = true;
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
}
};
const toFavoritePayload = (
app: App,
): Omit<FavoriteItem, "id" | "createdAt"> => ({
appKey: buildFavoriteAppKey(app),
pkgname: app.pkgname,
name: app.name,
category: app.category,
iconUrl: app.icons,
});
const saveCurrentFavoriteFolders = async (
folderIds: Array<number | "default">,
) => {
const generation = favoriteRequestGeneration.value;
const app = favoriteTargetApp.value;
if (!app) return;
try {
const numericFolderIds = folderIds.filter(
(folderId): folderId is number => typeof folderId === "number",
);
const includesFallbackDefault = folderIds.includes("default");
const nextFolderIds = new Set(numericFolderIds);
const existingByFolder = favoriteFolders.value
.map((folder) => ({
folderId: folder.id,
item: (favoriteItemsByFolder.value[folder.id] ?? []).find(
(favorite) =>
favorite.pkgname === app.pkgname &&
favorite.category === app.category,
),
}))
.filter(
(entry): entry is { folderId: number; item: FavoriteItem } =>
entry.item !== undefined,
);
const existingFolderIds = new Set(
existingByFolder.map((entry) => entry.folderId),
);
const payload = toFavoritePayload(app);
const addedItemPromises = numericFolderIds
.filter((folderId) => !existingFolderIds.has(folderId))
.map(async (folderId) => ({
folderId,
item: await addFavoriteItem(folderId, payload),
}));
const deletedEntries = existingByFolder.filter(
({ folderId }) => !nextFolderIds.has(folderId),
);
const [addedEntries] = await Promise.all([
Promise.all(addedItemPromises),
...(includesFallbackDefault ? [addFavoriteItem("default", payload)] : []),
...deletedEntries.map(({ folderId, item }) =>
deleteFavoriteItem(folderId, item.id),
),
]);
if (!isCurrentFavoriteRequest(generation)) return;
const nextItemsByFolder = { ...favoriteItemsByFolder.value };
deletedEntries.forEach(({ folderId, item }) => {
nextItemsByFolder[folderId] = (nextItemsByFolder[folderId] ?? []).filter(
(favorite) => favorite.id !== item.id,
);
});
addedEntries.forEach(({ folderId, item }) => {
nextItemsByFolder[folderId] = [
...(nextItemsByFolder[folderId] ?? []).filter(
(favorite) =>
favorite.pkgname !== app.pkgname ||
favorite.category !== app.category,
),
item,
];
});
favoriteItemsByFolder.value = nextItemsByFolder;
if (activeFavoriteFolderId.value) {
favoriteItems.value =
nextItemsByFolder[activeFavoriteFolderId.value] ?? [];
}
showFavoriteSelector.value = false;
favoriteTargetApp.value = null;
favoriteSelectorDraftFolderIds.value = null;
if (includesFallbackDefault) await refreshFavorites();
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "更新收藏失败";
}
};
const createFavoriteFolderFromSelector = async (
draftFolderIds: Array<
number | "default"
> = favoriteSelectorDraftFolderIds.value ?? [],
): Promise<void> => {
const generation = favoriteRequestGeneration.value;
const name = window.prompt("请输入收藏夹名称");
const folderName = name?.trim();
if (!folderName) return;
const app = favoriteTargetApp.value;
if (!app) return;
favoriteLoading.value = true;
favoriteError.value = "";
try {
const folder = await createFavoriteFolder(folderName);
if (!isCurrentFavoriteRequest(generation)) return;
favoriteFolders.value = [
...favoriteFolders.value.filter((item) => item.id !== folder.id),
folder,
];
favoriteItemsByFolder.value = {
...favoriteItemsByFolder.value,
[folder.id]: favoriteItemsByFolder.value[folder.id] ?? [],
};
favoriteSelectorDraftFolderIds.value = [
...new Set([...draftFolderIds, folder.id]),
];
showFavoriteSelector.value = true;
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "创建收藏夹失败";
} finally {
if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
}
};
const openFavoriteManagement = async () => {
if (!requireLoginRef("请登录后查看我的收藏。")) return;
currentView.value = "favorites";
activeTab.value = "favorites";
isSidebarOpen.value = false;
showLoginPrompt.value = false;
await refreshFavorites();
};
const selectFavoriteFolder = async (folderId: number) => {
const generation = nextFavoriteRequestGeneration();
activeFavoriteFolderId.value = folderId;
favoriteLoading.value = true;
favoriteError.value = "";
try {
await loadActiveFavoriteItems(generation);
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "读取收藏应用失败";
} finally {
if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
}
};
const createFavoriteFolderFromPrompt = async () => {
const name = window.prompt("请输入收藏夹名称");
const folderName = name?.trim();
if (!folderName) return;
const generation = favoriteRequestGeneration.value;
favoriteLoading.value = true;
favoriteError.value = "";
try {
const folder = await createFavoriteFolder(folderName);
if (!isCurrentFavoriteRequest(generation)) return;
favoriteFolders.value = [
...favoriteFolders.value.filter((item) => item.id !== folder.id),
folder,
];
favoriteItemsByFolder.value = {
...favoriteItemsByFolder.value,
[folder.id]: favoriteItemsByFolder.value[folder.id] ?? [],
};
favoriteItems.value = [];
activeFavoriteFolderId.value = folder.id;
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "创建收藏夹失败";
} finally {
if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
}
};
const removeSelectedFavorites = async (ids: number[]) => {
if (!activeFavoriteFolderId.value || ids.length === 0) return;
const generation = favoriteRequestGeneration.value;
favoriteLoading.value = true;
favoriteError.value = "";
try {
await bulkDeleteFavoriteItems(activeFavoriteFolderId.value, ids);
if (!isCurrentFavoriteRequest(generation)) return;
favoriteItemsByFolder.value = {
...favoriteItemsByFolder.value,
[activeFavoriteFolderId.value]: (
favoriteItemsByFolder.value[activeFavoriteFolderId.value] ?? []
).filter((favorite) => !ids.includes(favorite.id)),
};
await refreshFavorites();
} catch (error: unknown) {
if (!isCurrentFavoriteRequest(generation)) return;
favoriteError.value = (error as Error)?.message || "移除收藏失败";
} finally {
if (isCurrentFavoriteRequest(generation)) favoriteLoading.value = false;
}
};
const installResolvedFavorites = async (items: ResolvedFavoriteItem[]) => {
for (const item of items) {
if (item.selectedApp) {
await onDetailInstallRef(item.selectedApp);
}
}
};
const clearFavoriteState = () => {
favoriteRequestGeneration.value += 1;
favoriteFolders.value = [];
activeFavoriteFolderId.value = null;
favoriteItems.value = [];
favoriteItemsByFolder.value = {};
showFavoriteSelector.value = false;
favoriteTargetApp.value = null;
favoriteSelectorDraftFolderIds.value = null;
favoriteLoading.value = false;
favoriteError.value = "";
};
// requireLogin / onDetailInstall 分别定义在 useAccountSync / useDownloads
// 通过模块级可赋值引用注入,避免循环依赖。
let requireLoginRef: (message: string) => boolean = () => true;
export const registerRequireLogin = (fn: (message: string) => boolean) => {
requireLoginRef = fn;
};
let onDetailInstallRef: (app: App) => Promise<void> = async () => undefined;
export const registerOnDetailInstall = (fn: (app: App) => Promise<void>) => {
onDetailInstallRef = fn;
};
// 这些 computed 依赖 currentDisplayApp / favorite 状态,放在本模块汇总派生。
export const currentFavoriteMetadata = computed(
(): {
favorited: boolean;
folderName: string;
} => {
// 延迟引用 currentDisplayApp,避免顶层循环导入时的初始化顺序问题
const app = currentDisplayAppRef();
if (!app) return { favorited: false, folderName: "" };
const folder = favoriteFolders.value.find((favoriteFolder) => {
const items = favoriteItemsByFolder.value[favoriteFolder.id] ?? [];
return items.some(
(favorite) =>
favorite.pkgname === app.pkgname &&
favorite.category === app.category,
);
});
if (!folder) return { favorited: false, folderName: "" };
return { favorited: true, folderName: folder.name.trim() };
},
);
export const currentFavoriteFolderIds = computed(
(): Array<number | "default"> => {
if (favoriteSelectorDraftFolderIds.value) {
return favoriteSelectorDraftFolderIds.value;
}
const app = favoriteTargetApp.value ?? currentDisplayAppRef();
if (!app) return [];
return favoriteFolders.value
.filter((folder) =>
(favoriteItemsByFolder.value[folder.id] ?? []).some(
(favorite) =>
favorite.pkgname === app.pkgname &&
favorite.category === app.category,
),
)
.map((folder) => folder.id);
},
);
export const resolvedFavoriteItems = computed<ResolvedFavoriteItem[]>(() =>
resolveFavoriteItems(
favoriteItems.value,
apps.value,
installedApps.value,
availableSources.value,
storeFilter.value,
clientArch.value,
),
);
// currentDisplayApp 由 useAppDetail 持有,此处通过注入引用获取,避免循环导入
let currentDisplayAppRef: () => App | null = () => null;
export const registerCurrentDisplayApp = (fn: () => App | null) => {
currentDisplayAppRef = fn;
};
export {
loadFavoriteFolders,
loadActiveFavoriteItems,
loadAllFavoriteItems,
loadFavoriteMetadataForDetail,
refreshFavorites,
openFavoriteSelector,
toFavoritePayload,
saveCurrentFavoriteFolders,
createFavoriteFolderFromSelector,
openFavoriteManagement,
selectFavoriteFolder,
createFavoriteFolderFromPrompt,
removeSelectedFavorites,
installResolvedFavorites,
clearFavoriteState,
nextFavoriteRequestGeneration,
isCurrentFavoriteRequest,
};