mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-06 10:23:57 +08:00
refactor(home): 重构首页与侧边栏,移除旧HomeList类型并支持首页列表侧边栏入口
1. 移除废弃的HomeList类型定义,新增homeList侧边栏类型 2. 重构AppGrid组件,添加showOrigin属性支持自定义显示来源标记 3. 简化HomeView组件,移除旧的列表展示逻辑与相关代码 4. 新增侧边栏首页列表入口加载与应用数据加载逻辑 5. 优化侧边栏计数与应用预加载逻辑,适配新的首页列表类型
This commit is contained in:
+237
-101
@@ -86,14 +86,14 @@
|
|||||||
@open-detail="openDetail"
|
@open-detail="openDetail"
|
||||||
/>
|
/>
|
||||||
<template v-else-if="activeTab === 'home'">
|
<template v-else-if="activeTab === 'home'">
|
||||||
<div class="max-h-[calc(100vh-8rem)] overflow-y-auto pr-2 scrollbar-nowidth">
|
<div
|
||||||
|
class="max-h-[calc(100vh-8rem)] overflow-y-auto pr-2 scrollbar-nowidth"
|
||||||
|
>
|
||||||
<HomeView
|
<HomeView
|
||||||
:links="homeLinks"
|
:links="homeLinks"
|
||||||
:lists="homeLists"
|
|
||||||
:loading="homeLoading"
|
:loading="homeLoading"
|
||||||
:error="homeError"
|
:error="homeError"
|
||||||
:store-filter="storeFilter"
|
:store-filter="storeFilter"
|
||||||
@open-detail="openDetail"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -103,7 +103,8 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:scroll-key="activeTab + '-' + selectedCategory"
|
:scroll-key="activeTab + '-' + selectedCategory"
|
||||||
:store-filter="storeFilter"
|
:store-filter="storeFilter"
|
||||||
@open-detail="openDetail"
|
:show-origin="storeFilter === 'both' && !isHomeListTab"
|
||||||
|
@open-detail="handleAppCardOpenDetail"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,7 +389,6 @@ import type {
|
|||||||
ChannelPayload,
|
ChannelPayload,
|
||||||
CategoryInfo,
|
CategoryInfo,
|
||||||
HomeLink,
|
HomeLink,
|
||||||
HomeList,
|
|
||||||
FlarumLoginPayload,
|
FlarumLoginPayload,
|
||||||
SidebarEntry,
|
SidebarEntry,
|
||||||
UpdateCenterItem,
|
UpdateCenterItem,
|
||||||
@@ -448,6 +448,8 @@ const tabCategories: Ref<Record<string, Record<string, CategoryInfo>>> = ref(
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const tabApps: Ref<Record<string, App[]>> = ref({});
|
const tabApps: Ref<Record<string, App[]>> = ref({});
|
||||||
|
// 首页推荐列表入口对应的各来源 jsonUrl:{ [entryId]: { spark?, apm? } }
|
||||||
|
const homeListUrls = ref<Record<string, { spark?: string; apm?: string }>>({});
|
||||||
const activeTab = ref("home");
|
const activeTab = ref("home");
|
||||||
type MainView = "default" | "favorites";
|
type MainView = "default" | "favorites";
|
||||||
const currentView = ref<MainView>("default");
|
const currentView = ref<MainView>("default");
|
||||||
@@ -601,9 +603,14 @@ const entryCounts = computed(() => {
|
|||||||
|
|
||||||
sidebarEntries.value.forEach((entry) => {
|
sidebarEntries.value.forEach((entry) => {
|
||||||
if (entry.type === "category" && entry.value) {
|
if (entry.type === "category" && entry.value) {
|
||||||
counts[entry.id] = allApps.filter(
|
// 优先使用已加载的入口应用总数;未加载时回退到全局分类计数
|
||||||
(app) => app.category === entry.value,
|
const tabLen = tabApps.value[entry.id]?.length;
|
||||||
).length;
|
counts[entry.id] =
|
||||||
|
tabLen !== undefined
|
||||||
|
? tabLen
|
||||||
|
: allApps.filter((app) => app.category === entry.value).length;
|
||||||
|
} else if (entry.type === "homeList") {
|
||||||
|
counts[entry.id] = tabApps.value[entry.id]?.length || 0;
|
||||||
} else {
|
} else {
|
||||||
counts[entry.id] = 0;
|
counts[entry.id] = 0;
|
||||||
}
|
}
|
||||||
@@ -614,6 +621,25 @@ const entryCounts = computed(() => {
|
|||||||
|
|
||||||
const currentDisplayApp = computed(() => getDisplayApp(currentApp.value));
|
const currentDisplayApp = computed(() => getDisplayApp(currentApp.value));
|
||||||
|
|
||||||
|
// 当前激活的侧栏入口是否为首页推荐列表类型
|
||||||
|
const isHomeListTab = computed(
|
||||||
|
() =>
|
||||||
|
activeTab.value !== "home" &&
|
||||||
|
activeTab.value !== "all" &&
|
||||||
|
sidebarEntries.value.some(
|
||||||
|
(e) => e.id === activeTab.value && e.type === "homeList",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 应用卡片点击:首页推荐列表复用首页逻辑,标记 _fromHomeView 以便从双仓库获取完整信息
|
||||||
|
const handleAppCardOpenDetail = (app: App) => {
|
||||||
|
if (isHomeListTab.value) {
|
||||||
|
openDetail({ ...app, _fromHomeView: true });
|
||||||
|
} else {
|
||||||
|
openDetail(app);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clientArch = computed(() => window.apm_store.arch || "amd64");
|
const clientArch = computed(() => window.apm_store.arch || "amd64");
|
||||||
|
|
||||||
const currentReviewAppKey = computed(() => {
|
const currentReviewAppKey = computed(() => {
|
||||||
@@ -731,17 +757,15 @@ const selectTab = (tab: string) => {
|
|||||||
selectedCategory.value = "all";
|
selectedCategory.value = "all";
|
||||||
isSidebarOpen.value = false;
|
isSidebarOpen.value = false;
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
if (
|
if (tab === "home" && homeLinks.value.length === 0) {
|
||||||
tab === "home" &&
|
|
||||||
homeLinks.value.length === 0 &&
|
|
||||||
homeLists.value.length === 0
|
|
||||||
) {
|
|
||||||
loadHome();
|
loadHome();
|
||||||
}
|
}
|
||||||
if (tab !== "home" && tab !== "all") {
|
if (tab !== "home" && tab !== "all") {
|
||||||
const entry = sidebarEntries.value.find((e) => e.id === tab);
|
const entry = sidebarEntries.value.find((e) => e.id === tab);
|
||||||
if (entry && entry.type === "category") {
|
if (entry && entry.type === "category") {
|
||||||
loadTabApps(tab);
|
loadTabApps(tab);
|
||||||
|
} else if (entry && entry.type === "homeList") {
|
||||||
|
loadHomeListApps(tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1036,7 +1060,6 @@ const closeScreenPreview = () => {
|
|||||||
|
|
||||||
// Home data
|
// Home data
|
||||||
const homeLinks = ref<HomeLink[]>([]);
|
const homeLinks = ref<HomeLink[]>([]);
|
||||||
const homeLists = ref<HomeList[]>([]);
|
|
||||||
const homeLoading = ref(false);
|
const homeLoading = ref(false);
|
||||||
const homeError = ref("");
|
const homeError = ref("");
|
||||||
|
|
||||||
@@ -1044,7 +1067,6 @@ const loadHome = async () => {
|
|||||||
homeLoading.value = true;
|
homeLoading.value = true;
|
||||||
homeError.value = "";
|
homeError.value = "";
|
||||||
homeLinks.value = [];
|
homeLinks.value = [];
|
||||||
homeLists.value = [];
|
|
||||||
try {
|
try {
|
||||||
const arch = window.apm_store.arch || "amd64";
|
const arch = window.apm_store.arch || "amd64";
|
||||||
const modes: Array<"spark" | "apm"> =
|
const modes: Array<"spark" | "apm"> =
|
||||||
@@ -1068,65 +1090,6 @@ const loadHome = async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// homelist.json
|
|
||||||
try {
|
|
||||||
const res2 = await fetch(`${base}/homelist.json`);
|
|
||||||
if (res2.ok) {
|
|
||||||
const lists = await res2.json();
|
|
||||||
for (const item of lists) {
|
|
||||||
if (item.type === "appList" && item.jsonUrl) {
|
|
||||||
try {
|
|
||||||
const url = `${APM_STORE_BASE_URL}/${finalArch}${item.jsonUrl}`;
|
|
||||||
const r = await fetch(url);
|
|
||||||
if (r.ok) {
|
|
||||||
const appsJson = await r.json();
|
|
||||||
const rawApps = appsJson || [];
|
|
||||||
const apps = await Promise.all(
|
|
||||||
rawApps.map(async (a: Record<string, string>) => {
|
|
||||||
const baseApp = {
|
|
||||||
name: a.Name || a.name || a.Pkgname || a.PkgName || "",
|
|
||||||
pkgname: a.Pkgname || a.pkgname || "",
|
|
||||||
category: a.Category || a.category || "unknown",
|
|
||||||
more: a.More || a.more || "",
|
|
||||||
version: a.Version || "",
|
|
||||||
filename: a.Filename || a.filename || "",
|
|
||||||
origin: mode as "spark" | "apm",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const realAppUrl = `${APM_STORE_BASE_URL}/${finalArch}/${baseApp.category}/${baseApp.pkgname}/app.json`;
|
|
||||||
const realRes = await fetch(realAppUrl);
|
|
||||||
if (realRes.ok) {
|
|
||||||
const realApp = await realRes.json();
|
|
||||||
if (realApp.Filename)
|
|
||||||
baseApp.filename = realApp.Filename;
|
|
||||||
if (realApp.More) baseApp.more = realApp.More;
|
|
||||||
if (realApp.Name) baseApp.name = realApp.Name;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(
|
|
||||||
`Failed to fetch real app.json for ${baseApp.pkgname}`,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return baseApp;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
homeLists.value.push({
|
|
||||||
title: `${item.name || "推荐"} (${mode === "spark" ? "星火" : "APM"})`,
|
|
||||||
apps,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to load home list", item, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to load ${mode} homelist.json`, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
homeError.value = (error as Error)?.message || "加载首页失败";
|
homeError.value = (error as Error)?.message || "加载首页失败";
|
||||||
@@ -1135,6 +1098,166 @@ const loadHome = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载首页推荐列表为侧边栏入口(按名称合并 spark/apm,置于分类入口上方)
|
||||||
|
const loadHomeListEntries = async () => {
|
||||||
|
try {
|
||||||
|
const arch = window.apm_store.arch || "amd64";
|
||||||
|
const modes: Array<"spark" | "apm"> =
|
||||||
|
storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value];
|
||||||
|
|
||||||
|
// 按列表名称合并各来源的 jsonUrl
|
||||||
|
const byName = new Map<
|
||||||
|
string,
|
||||||
|
{ name: string; urls: { spark?: string; apm?: string } }
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const mode of modes) {
|
||||||
|
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
|
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${base}/homelist.json`);
|
||||||
|
if (res.ok) {
|
||||||
|
const lists = await res.json();
|
||||||
|
lists.forEach(
|
||||||
|
(item: { name?: string; type?: string; jsonUrl?: string }) => {
|
||||||
|
if (item.type === "appList" && item.jsonUrl) {
|
||||||
|
const name = item.name || "推荐";
|
||||||
|
const existing = byName.get(name);
|
||||||
|
if (existing) {
|
||||||
|
existing.urls[mode] = item.jsonUrl;
|
||||||
|
} else {
|
||||||
|
byName.set(name, {
|
||||||
|
name,
|
||||||
|
urls: { [mode]: item.jsonUrl } as {
|
||||||
|
spark?: string;
|
||||||
|
apm?: string;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load ${mode} homelist.json`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: SidebarEntry[] = [];
|
||||||
|
const urlsMap: Record<string, { spark?: string; apm?: string }> = {};
|
||||||
|
|
||||||
|
byName.forEach((info, name) => {
|
||||||
|
const id = `home-list-${name}`;
|
||||||
|
entries.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
icon: "fas fa-star",
|
||||||
|
type: "homeList",
|
||||||
|
});
|
||||||
|
urlsMap[id] = info.urls;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
// 首页推荐入口置于分类入口上方
|
||||||
|
sidebarEntries.value = [...entries, ...sidebarEntries.value];
|
||||||
|
homeListUrls.value = { ...homeListUrls.value, ...urlsMap };
|
||||||
|
logger.info(`已加载 ${entries.length} 个首页推荐列表入口`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`加载首页推荐列表入口失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载首页推荐列表的应用数据(复用首页逻辑,合并展示 spark+apm)
|
||||||
|
const loadHomeListApps = async (entryId: string) => {
|
||||||
|
if (tabApps.value[entryId]) return;
|
||||||
|
|
||||||
|
const urls = homeListUrls.value[entryId];
|
||||||
|
if (!urls) return;
|
||||||
|
|
||||||
|
const arch = window.apm_store.arch || "amd64";
|
||||||
|
const loadedApps: App[] = [];
|
||||||
|
|
||||||
|
// 同时加载各来源的推荐列表应用
|
||||||
|
await Promise.all(
|
||||||
|
(Object.keys(urls) as Array<"spark" | "apm">).map(async (mode) => {
|
||||||
|
const jsonUrl = urls[mode];
|
||||||
|
if (!jsonUrl) return;
|
||||||
|
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = `/${finalArch}${jsonUrl}`;
|
||||||
|
const rawApps =
|
||||||
|
(await fetchWithRetry<Record<string, string>[]>(path)) || [];
|
||||||
|
const apps = await Promise.all(
|
||||||
|
rawApps.map(async (a) => {
|
||||||
|
const category = a.Category || a.category || "unknown";
|
||||||
|
const pkgname = a.Pkgname || a.pkgname || "";
|
||||||
|
|
||||||
|
// 复用首页逻辑:从仓库获取完整应用信息
|
||||||
|
try {
|
||||||
|
const realAppUrl = `/${finalArch}/${category}/${pkgname}/app.json`;
|
||||||
|
const realApp = await fetchWithRetry<AppJson>(realAppUrl);
|
||||||
|
return normalizeAppJson(realApp, category, mode);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to fetch app.json for ${pkgname}`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:使用列表中的基本信息构建 App 对象
|
||||||
|
return {
|
||||||
|
name: a.Name || a.name || pkgname || "",
|
||||||
|
pkgname,
|
||||||
|
version: a.Version || "",
|
||||||
|
filename: a.Filename || a.filename || "",
|
||||||
|
category,
|
||||||
|
more: a.More || a.more || "",
|
||||||
|
torrent_address: "",
|
||||||
|
author: "",
|
||||||
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
tags: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: "",
|
||||||
|
origin: mode,
|
||||||
|
currentStatus: "not-installed" as const,
|
||||||
|
} as App;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
loadedApps.push(...apps);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`加载首页列表 ${entryId} (${mode}) 失败: ${e}`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tabApps.value = { ...tabApps.value, [entryId]: loadedApps };
|
||||||
|
logger.info(`首页列表 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 并行预加载所有侧边栏入口的应用数据,避免点击时才加载导致缓慢
|
||||||
|
const preloadSidebarTabApps = (): Promise<void> => {
|
||||||
|
const tasks: Promise<void>[] = [];
|
||||||
|
for (const entry of sidebarEntries.value) {
|
||||||
|
if (entry.type === "category") {
|
||||||
|
tasks.push(
|
||||||
|
loadTabApps(entry.id).catch((e: unknown) =>
|
||||||
|
logger.warn(`预加载入口 ${entry.id} 失败: ${e}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (entry.type === "homeList") {
|
||||||
|
tasks.push(
|
||||||
|
loadHomeListApps(entry.id).catch((e: unknown) =>
|
||||||
|
logger.warn(`预加载首页列表 ${entry.id} 失败: ${e}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(tasks).then(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
const prevScreen = () => {
|
const prevScreen = () => {
|
||||||
if (currentScreenIndex.value > 0) {
|
if (currentScreenIndex.value > 0) {
|
||||||
currentScreenIndex.value--;
|
currentScreenIndex.value--;
|
||||||
@@ -2449,9 +2572,11 @@ const loadTabApps = async (entryId: string) => {
|
|||||||
const modes: Array<"spark" | "apm"> =
|
const modes: Array<"spark" | "apm"> =
|
||||||
storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value];
|
storeFilter.value === "both" ? ["spark", "apm"] : [storeFilter.value];
|
||||||
const folderName = entry.value || entry.id;
|
const folderName = entry.value || entry.id;
|
||||||
const loadedApps: App[] = [];
|
|
||||||
const subCats = tabCategories.value[entryId];
|
const subCats = tabCategories.value[entryId];
|
||||||
|
|
||||||
|
// 收集所有需要发起的请求任务(mode × 子分类),然后全并发加载
|
||||||
|
const tasks: Promise<App[]>[] = [];
|
||||||
|
|
||||||
for (const mode of modes) {
|
for (const mode of modes) {
|
||||||
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
|
|
||||||
@@ -2464,37 +2589,44 @@ const loadTabApps = async (entryId: string) => {
|
|||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
try {
|
const path = `/${finalArch}/${folderName}/${subCat}/applist.json`;
|
||||||
const path = `/${finalArch}/${folderName}/${subCat}/applist.json`;
|
logger.info(`加载入口子分类: ${entryId}/${subCat} (来源: ${mode})`);
|
||||||
logger.info(`加载入口子分类: ${entryId}/${subCat} (来源: ${mode})`);
|
tasks.push(
|
||||||
const categoryApps = await fetchWithRetry<AppJson[]>(path);
|
fetchWithRetry<AppJson[]>(path)
|
||||||
loadedApps.push(
|
.then((categoryApps) =>
|
||||||
...(categoryApps || []).map((aj) =>
|
(categoryApps || []).map((aj) =>
|
||||||
normalizeAppJson(aj, subCat, mode),
|
normalizeAppJson(aj, subCat, mode),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
} catch (e) {
|
.catch((e: unknown) => {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`加载入口子分类 ${entryId}/${subCat} (${mode}) 失败: ${e}`,
|
`加载入口子分类 ${entryId}/${subCat} (${mode}) 失败: ${e}`,
|
||||||
);
|
);
|
||||||
}
|
return [] as App[];
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
const path = `/${finalArch}/${folderName}/applist.json`;
|
||||||
const path = `/${finalArch}/${folderName}/applist.json`;
|
logger.info(`加载入口目录: ${entryId} (来源: ${mode})`);
|
||||||
logger.info(`加载入口目录: ${entryId} (来源: ${mode})`);
|
tasks.push(
|
||||||
const categoryApps = await fetchWithRetry<AppJson[]>(path);
|
fetchWithRetry<AppJson[]>(path)
|
||||||
loadedApps.push(
|
.then((categoryApps) =>
|
||||||
...(categoryApps || []).map((aj) =>
|
(categoryApps || []).map((aj) =>
|
||||||
normalizeAppJson(aj, folderName, mode),
|
normalizeAppJson(aj, folderName, mode),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
} catch (e) {
|
.catch((e: unknown) => {
|
||||||
logger.warn(`加载入口目录 ${entryId} (${mode}) 失败: ${e}`);
|
logger.warn(`加载入口目录 ${entryId} (${mode}) 失败: ${e}`);
|
||||||
}
|
return [] as App[];
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(tasks);
|
||||||
|
const loadedApps = results.flat();
|
||||||
|
|
||||||
tabApps.value = { ...tabApps.value, [entryId]: loadedApps };
|
tabApps.value = { ...tabApps.value, [entryId]: loadedApps };
|
||||||
logger.info(`入口 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`);
|
logger.info(`入口 "${entryId}" 加载完成,共 ${loadedApps.length} 个应用`);
|
||||||
};
|
};
|
||||||
@@ -2608,6 +2740,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await loadSidebarConfig();
|
await loadSidebarConfig();
|
||||||
|
|
||||||
|
await loadHomeListEntries();
|
||||||
|
|
||||||
await loadTabCategories();
|
await loadTabCategories();
|
||||||
|
|
||||||
// 分类目录加载后,并行加载主页数据和所有应用列表
|
// 分类目录加载后,并行加载主页数据和所有应用列表
|
||||||
@@ -2624,6 +2758,8 @@ onMounted(async () => {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
// 并行预加载所有侧边栏入口的应用数据
|
||||||
|
preloadSidebarTabApps(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
// 所有数据加载完成后的回调(可选)
|
// 所有数据加载完成后的回调(可选)
|
||||||
logger.info("所有应用数据加载完成");
|
logger.info("所有应用数据加载完成");
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
v-for="(app, index) in apps"
|
v-for="(app, index) in apps"
|
||||||
:key="index"
|
:key="index"
|
||||||
:app="app"
|
:app="app"
|
||||||
:show-origin="storeFilter === 'both'"
|
:show-origin="effectiveShowOrigin"
|
||||||
@open-detail="$emit('open-detail', app)"
|
@open-detail="$emit('open-detail', app)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
v-for="app in item.apps"
|
v-for="app in item.apps"
|
||||||
:key="app.pkgname"
|
:key="app.pkgname"
|
||||||
:app="app"
|
:app="app"
|
||||||
:show-origin="storeFilter === 'both'"
|
:show-origin="effectiveShowOrigin"
|
||||||
@open-detail="$emit('open-detail', app)"
|
@open-detail="$emit('open-detail', app)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,8 +93,16 @@ const props = defineProps<{
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
storeFilter?: "spark" | "apm" | "both";
|
storeFilter?: "spark" | "apm" | "both";
|
||||||
scrollKey?: string;
|
scrollKey?: string;
|
||||||
|
showOrigin?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// 显式传入 showOrigin 时优先使用;否则回退到根据 storeFilter 推断
|
||||||
|
const effectiveShowOrigin = computed(() =>
|
||||||
|
props.showOrigin !== undefined
|
||||||
|
? props.showOrigin
|
||||||
|
: props.storeFilter === "both",
|
||||||
|
);
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: "open-detail", app: App): void;
|
(e: "open-detail", app: App): void;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- 初始加载状态 - 只有在完全没有数据时显示 -->
|
<!-- 初始加载状态 - 只有在完全没有数据时显示 -->
|
||||||
<div
|
<div
|
||||||
v-if="loading && links.length === 0 && lists.length === 0"
|
v-if="loading && links.length === 0"
|
||||||
class="flex flex-col items-center justify-center py-12 text-slate-500 dark:text-slate-400"
|
class="flex flex-col items-center justify-center py-12 text-slate-500 dark:text-slate-400"
|
||||||
>
|
>
|
||||||
<i class="fas fa-spinner fa-spin text-2xl mb-3"></i>
|
<i class="fas fa-spinner fa-spin text-2xl mb-3"></i>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 无数据时显示欢迎信息 -->
|
<!-- 无数据时显示欢迎信息 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="links.length === 0 && lists.length === 0"
|
v-else-if="links.length === 0"
|
||||||
class="flex flex-col items-center justify-center py-20 text-center"
|
class="flex flex-col items-center justify-center py-20 text-center"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -110,54 +110,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lists 区域 -->
|
|
||||||
<div v-if="lists.length > 0" class="space-y-6 mt-6">
|
|
||||||
<section v-for="section in lists" :key="section.title">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3
|
|
||||||
class="text-lg font-semibold text-slate-900 dark:text-slate-200"
|
|
||||||
>
|
|
||||||
{{ section.title }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 grid gap-4 app-grid">
|
|
||||||
<AppCard
|
|
||||||
v-for="app in section.apps"
|
|
||||||
:key="app.pkgname"
|
|
||||||
:app="app"
|
|
||||||
@open-detail="handleOpenDetail(app)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppCard from "./AppCard.vue";
|
|
||||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
import type { HomeLink, HomeList, App } from "../global/typedefinition";
|
import type { HomeLink } from "../global/typedefinition";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
links: HomeLink[];
|
links: HomeLink[];
|
||||||
lists: HomeList[];
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
storeFilter?: "spark" | "apm" | "both";
|
storeFilter?: "spark" | "apm" | "both";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "open-detail", app: App | Record<string, unknown>): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// 处理应用卡片点击,添加来自首页的标记
|
|
||||||
const handleOpenDetail = (app: App) => {
|
|
||||||
emit("open-detail", { ...app, _fromHomeView: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 图片加载状态跟踪
|
// 图片加载状态跟踪
|
||||||
const imageLoaded = reactive<Record<string, boolean>>({});
|
const imageLoaded = reactive<Record<string, boolean>>({});
|
||||||
|
|
||||||
@@ -203,16 +171,4 @@ const onLinkClick = (link: HomeLink) => {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 应用卡片网格 - 保持原来的样式 */
|
|
||||||
.app-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.app-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -237,16 +237,11 @@ export interface HomeLink {
|
|||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeList {
|
|
||||||
title: string;
|
|
||||||
apps: App[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SidebarEntry {
|
export interface SidebarEntry {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
type?: "category" | "search" | "link";
|
type?: "category" | "search" | "link" | "homeList";
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user