mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-21 06:00:11 +08:00
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 成功
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* useFavorites —— 收藏夹相关的全部状态与逻辑。
|
||||
*
|
||||
* 从原 App.vue 原样搬移(loadFavoriteFolders / loadActiveFavoriteItems /
|
||||
* loadAllFavoriteItems / loadFavoriteMetadataForDetail / refreshFavorites /
|
||||
* openFavoriteSelector / toFavoritePayload / saveCurrentFavoriteFolders /
|
||||
* createFavoriteFolderFromSelector / openFavoriteManagement / selectFavoriteFolder /
|
||||
* createFavoriteFolderFromPrompt / removeSelectedFavorites / installResolvedFavorites /
|
||||
* clearFavoriteState / 代次守卫 / currentFavoriteMetadata / currentFavoriteFolderIds /
|
||||
* resolvedFavoriteItems),逻辑零改动。
|
||||
*
|
||||
* 共享状态来自 useAppState;clientArch 来自 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,
|
||||
};
|
||||
Reference in New Issue
Block a user