mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
feat(应用详情): 增强应用详情页功能并优化代码格式
重构应用详情页逻辑,支持从首页和深度链接直接打开应用时自动获取完整信息 优化应用卡片来源标识显示,支持同时显示多个来源 统一代码格式,修复多行字符串和模板字符串的换行问题
This commit is contained in:
@@ -480,9 +480,7 @@ async function processNextInQueue() {
|
|||||||
) {
|
) {
|
||||||
clearInterval(timeoutChecker);
|
clearInterval(timeoutChecker);
|
||||||
child.kill();
|
child.kill();
|
||||||
reject(
|
reject(new Error(`下载卡在0%超过 ${currentTimeout / 1000} 秒`));
|
||||||
new Error(`下载卡在0%超过 ${currentTimeout / 1000} 秒`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, progressCheckInterval);
|
}, progressCheckInterval);
|
||||||
|
|
||||||
@@ -527,7 +525,9 @@ async function processNextInQueue() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
if (retryCount >= timeoutList.length) {
|
if (retryCount >= timeoutList.length) {
|
||||||
throw new Error(`下载失败,已重试 ${timeoutList.length} 次: ${err}`);
|
throw new Error(
|
||||||
|
`下载失败,已重试 ${timeoutList.length} 次: ${err}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
sendLog(`下载失败,准备重试 (${retryCount}/${timeoutList.length})`);
|
sendLog(`下载失败,准备重试 (${retryCount}/${timeoutList.length})`);
|
||||||
// 等待2秒后重试
|
// 等待2秒后重试
|
||||||
@@ -860,7 +860,9 @@ ipcMain.handle("list-installed", async () => {
|
|||||||
if (hasEntries) {
|
if (hasEntries) {
|
||||||
try {
|
try {
|
||||||
const desktopFiles = fs.readdirSync(entriesPath);
|
const desktopFiles = fs.readdirSync(entriesPath);
|
||||||
logger.debug(`Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`);
|
logger.debug(
|
||||||
|
`Found desktop files for ${pkgname}: ${desktopFiles.join(", ")}`,
|
||||||
|
);
|
||||||
for (const file of desktopFiles) {
|
for (const file of desktopFiles) {
|
||||||
if (file.endsWith(".desktop")) {
|
if (file.endsWith(".desktop")) {
|
||||||
const desktopPath = path.join(entriesPath, file);
|
const desktopPath = path.join(entriesPath, file);
|
||||||
@@ -870,7 +872,9 @@ ipcMain.handle("list-installed", async () => {
|
|||||||
const iconMatch = content.match(/^Icon=(.+)$/m);
|
const iconMatch = content.match(/^Icon=(.+)$/m);
|
||||||
if (nameMatch) appName = nameMatch[1].trim();
|
if (nameMatch) appName = nameMatch[1].trim();
|
||||||
if (iconMatch) icon = iconMatch[1].trim();
|
if (iconMatch) icon = iconMatch[1].trim();
|
||||||
logger.debug(`Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`);
|
logger.debug(
|
||||||
|
`Parsed desktop file for ${pkgname}: name=${appName}, icon=${icon}`,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
196
src/App.vue
196
src/App.vue
@@ -389,84 +389,191 @@ const selectCategory = (category: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从仓库获取应用详细信息的辅助函数
|
||||||
|
const fetchAppFromStore = async (
|
||||||
|
pkgname: string,
|
||||||
|
category: string,
|
||||||
|
origin: "spark" | "apm",
|
||||||
|
): Promise<App | null> => {
|
||||||
|
try {
|
||||||
|
const arch = window.apm_store.arch || "amd64";
|
||||||
|
const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
|
const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${category}/${pkgname}/app.json`;
|
||||||
|
const response = await fetch(cacheBuster(appJsonUrl));
|
||||||
|
if (!response.ok) return null;
|
||||||
|
const appJson = await response.json();
|
||||||
|
return {
|
||||||
|
name: appJson.Name || "",
|
||||||
|
pkgname: appJson.Pkgname || pkgname,
|
||||||
|
version: appJson.Version || "",
|
||||||
|
filename: appJson.Filename || "",
|
||||||
|
torrent_address: appJson.Torrent_address || "",
|
||||||
|
author: appJson.Author || "",
|
||||||
|
contributor: appJson.Contributor || "",
|
||||||
|
website: appJson.Website || "",
|
||||||
|
update: appJson.Update || "",
|
||||||
|
size: appJson.Size || "",
|
||||||
|
more: appJson.More || "",
|
||||||
|
tags: appJson.Tags || "",
|
||||||
|
img_urls:
|
||||||
|
typeof appJson.img_urls === "string"
|
||||||
|
? (JSON.parse(appJson.img_urls) as string[])
|
||||||
|
: appJson.img_urls || [],
|
||||||
|
icons: appJson.icons || "",
|
||||||
|
category: category,
|
||||||
|
origin: origin,
|
||||||
|
currentStatus: "not-installed",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to fetch ${origin} app info for ${pkgname}`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openDetail = async (app: App | Record<string, unknown>) => {
|
const openDetail = async (app: App | Record<string, unknown>) => {
|
||||||
// 提取 pkgname(必须存在)
|
// 提取 pkgname 和 category(必须存在)
|
||||||
const pkgname = (app as Record<string, unknown>).pkgname as string;
|
const pkgname = (app as Record<string, unknown>).pkgname as string;
|
||||||
|
const category =
|
||||||
|
((app as Record<string, unknown>).category as string) || "unknown";
|
||||||
|
// 检查是否来自 HomeView 或 DeepLink(需要重新获取完整信息)
|
||||||
|
const fromHomeView = (app as Record<string, unknown>)._fromHomeView === true;
|
||||||
|
const fromDeepLink = (app as Record<string, unknown>)._fromDeepLink === true;
|
||||||
|
const needFetchFromStore = fromHomeView || fromDeepLink;
|
||||||
if (!pkgname) {
|
if (!pkgname) {
|
||||||
console.warn("openDetail: 缺少 pkgname", app);
|
console.warn("openDetail: 缺少 pkgname", app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先尝试从当前已经处理好(合并/筛选)的 filteredApps 中查找,以便获取 isMerged 状态等
|
// 首先尝试从当前已经处理好(合并/筛选)的 filteredApps 中查找
|
||||||
let fullApp = filteredApps.value.find((a) => a.pkgname === pkgname);
|
let fullApp = filteredApps.value.find((a) => a.pkgname === pkgname);
|
||||||
// 如果没找到(可能是从已安装列表之类的其他入口打开的),回退到全局 apps 中查找完整 App
|
// 如果没找到,回退到全局 apps 中查找
|
||||||
if (!fullApp) {
|
if (!fullApp) {
|
||||||
fullApp = apps.value.find((a) => a.pkgname === pkgname);
|
fullApp = apps.value.find((a) => a.pkgname === pkgname);
|
||||||
}
|
}
|
||||||
if (!fullApp) {
|
|
||||||
// 构造一个最小可用的 App 对象
|
let finalApp: App;
|
||||||
fullApp = {
|
|
||||||
name: ((app as Record<string, unknown>).name as string) || "",
|
// 来自 HomeView 或 DeepLink 的应用需要重新从仓库获取完整信息
|
||||||
pkgname: pkgname,
|
if (needFetchFromStore) {
|
||||||
version: ((app as Record<string, unknown>).version as string) || "",
|
// 从 Spark 和 APM 仓库获取完整的应用信息
|
||||||
filename: ((app as Record<string, unknown>).filename as string) || "",
|
const [sparkApp, apmApp] = await Promise.all([
|
||||||
category:
|
storeFilter.value !== "apm"
|
||||||
((app as Record<string, unknown>).category as string) || "unknown",
|
? fetchAppFromStore(pkgname, category, "spark")
|
||||||
torrent_address: "",
|
: Promise.resolve(null),
|
||||||
author: "",
|
storeFilter.value !== "spark"
|
||||||
contributor: "",
|
? fetchAppFromStore(pkgname, category, "apm")
|
||||||
website: "",
|
: Promise.resolve(null),
|
||||||
update: "",
|
]);
|
||||||
size: "",
|
|
||||||
more: ((app as Record<string, unknown>).more as string) || "",
|
// 构建合并的应用对象
|
||||||
tags: "",
|
if (sparkApp || apmApp) {
|
||||||
img_urls: [],
|
// 如果两个仓库都有这个应用,创建合并对象
|
||||||
icons: "",
|
if (sparkApp && apmApp) {
|
||||||
origin:
|
finalApp = {
|
||||||
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
|
...sparkApp, // 默认使用 Spark 的信息作为主显示
|
||||||
currentStatus: "not-installed",
|
isMerged: true,
|
||||||
} as App;
|
sparkApp: sparkApp,
|
||||||
|
apmApp: apmApp,
|
||||||
|
viewingOrigin: "spark", // 默认查看 Spark 版本
|
||||||
|
};
|
||||||
|
} else if (sparkApp) {
|
||||||
|
finalApp = sparkApp;
|
||||||
|
} else {
|
||||||
|
finalApp = apmApp!;
|
||||||
|
}
|
||||||
|
} else if (fullApp) {
|
||||||
|
finalApp = fullApp;
|
||||||
|
} else {
|
||||||
|
// 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象
|
||||||
|
finalApp = {
|
||||||
|
name: ((app as Record<string, unknown>).name as string) || "",
|
||||||
|
pkgname: pkgname,
|
||||||
|
version: ((app as Record<string, unknown>).version as string) || "",
|
||||||
|
filename: ((app as Record<string, unknown>).filename as string) || "",
|
||||||
|
category: category,
|
||||||
|
torrent_address: "",
|
||||||
|
author: "",
|
||||||
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
more: ((app as Record<string, unknown>).more as string) || "",
|
||||||
|
tags: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: "",
|
||||||
|
origin:
|
||||||
|
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
|
||||||
|
currentStatus: "not-installed",
|
||||||
|
} as App;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非 HomeView 来源,使用原来的逻辑
|
||||||
|
if (fullApp) {
|
||||||
|
finalApp = fullApp;
|
||||||
|
} else {
|
||||||
|
// 构造一个最小可用的 App 对象
|
||||||
|
finalApp = {
|
||||||
|
name: ((app as Record<string, unknown>).name as string) || "",
|
||||||
|
pkgname: pkgname,
|
||||||
|
version: ((app as Record<string, unknown>).version as string) || "",
|
||||||
|
filename: ((app as Record<string, unknown>).filename as string) || "",
|
||||||
|
category: category,
|
||||||
|
torrent_address: "",
|
||||||
|
author: "",
|
||||||
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
more: ((app as Record<string, unknown>).more as string) || "",
|
||||||
|
tags: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: "",
|
||||||
|
origin:
|
||||||
|
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
|
||||||
|
currentStatus: "not-installed",
|
||||||
|
} as App;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并应用:先检查 Spark/APM 安装状态,已安装的版本优先展示
|
// 检查 Spark/APM 安装状态,已安装的版本优先展示
|
||||||
if (fullApp.isMerged && (fullApp.sparkApp || fullApp.apmApp)) {
|
if (finalApp.isMerged && (finalApp.sparkApp || finalApp.apmApp)) {
|
||||||
const [sparkInstalled, apmInstalled] = await Promise.all([
|
const [sparkInstalled, apmInstalled] = await Promise.all([
|
||||||
fullApp.sparkApp
|
finalApp.sparkApp
|
||||||
? (window.ipcRenderer.invoke("check-installed", {
|
? (window.ipcRenderer.invoke("check-installed", {
|
||||||
pkgname: fullApp.sparkApp.pkgname,
|
pkgname: finalApp.sparkApp.pkgname,
|
||||||
origin: "spark",
|
origin: "spark",
|
||||||
}) as Promise<boolean>)
|
}) as Promise<boolean>)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
fullApp.apmApp
|
finalApp.apmApp
|
||||||
? (window.ipcRenderer.invoke("check-installed", {
|
? (window.ipcRenderer.invoke("check-installed", {
|
||||||
pkgname: fullApp.apmApp.pkgname,
|
pkgname: finalApp.apmApp.pkgname,
|
||||||
origin: "apm",
|
origin: "apm",
|
||||||
}) as Promise<boolean>)
|
}) as Promise<boolean>)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
]);
|
]);
|
||||||
if (sparkInstalled && !apmInstalled) {
|
if (sparkInstalled && !apmInstalled) {
|
||||||
fullApp.viewingOrigin = "spark";
|
finalApp.viewingOrigin = "spark";
|
||||||
} else if (apmInstalled && !sparkInstalled) {
|
} else if (apmInstalled && !sparkInstalled) {
|
||||||
fullApp.viewingOrigin = "apm";
|
finalApp.viewingOrigin = "apm";
|
||||||
}
|
}
|
||||||
// 若都安装或都未安装,不设置 viewingOrigin,由模态框默认展示 spark
|
// 若都安装或都未安装,默认展示 spark
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayAppForScreenshots =
|
const displayAppForScreenshots =
|
||||||
fullApp.viewingOrigin !== undefined && fullApp.isMerged
|
finalApp.viewingOrigin !== undefined && finalApp.isMerged
|
||||||
? ((fullApp.viewingOrigin === "spark"
|
? ((finalApp.viewingOrigin === "spark"
|
||||||
? fullApp.sparkApp
|
? finalApp.sparkApp
|
||||||
: fullApp.apmApp) ?? fullApp)
|
: finalApp.apmApp) ?? finalApp)
|
||||||
: fullApp;
|
: finalApp;
|
||||||
|
|
||||||
currentApp.value = fullApp;
|
currentApp.value = finalApp;
|
||||||
currentScreenIndex.value = 0;
|
currentScreenIndex.value = 0;
|
||||||
loadScreenshots(displayAppForScreenshots);
|
loadScreenshots(displayAppForScreenshots);
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
|
|
||||||
currentAppSparkInstalled.value = false;
|
currentAppSparkInstalled.value = false;
|
||||||
currentAppApmInstalled.value = false;
|
currentAppApmInstalled.value = false;
|
||||||
checkAppInstalled(fullApp);
|
checkAppInstalled(finalApp);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const modal = document.querySelector(
|
const modal = document.querySelector(
|
||||||
@@ -1181,9 +1288,12 @@ onMounted(async () => {
|
|||||||
(_event: IpcRendererEvent, data: { pkgname: string }) => {
|
(_event: IpcRendererEvent, data: { pkgname: string }) => {
|
||||||
// 根据包名直接打开应用详情
|
// 根据包名直接打开应用详情
|
||||||
const tryOpen = () => {
|
const tryOpen = () => {
|
||||||
|
// 先切换到"全部应用"分类
|
||||||
|
activeCategory.value = "all";
|
||||||
|
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
|
||||||
const target = apps.value.find((a) => a.pkgname === data.pkgname);
|
const target = apps.value.find((a) => a.pkgname === data.pkgname);
|
||||||
if (target) {
|
if (target) {
|
||||||
openDetail(target);
|
openDetail({ ...target, _fromDeepLink: true });
|
||||||
} else {
|
} else {
|
||||||
// 如果找不到应用,回退到搜索模式
|
// 如果找不到应用,回退到搜索模式
|
||||||
searchQuery.value = data.pkgname;
|
searchQuery.value = data.pkgname;
|
||||||
|
|||||||
@@ -21,30 +21,29 @@
|
|||||||
>
|
>
|
||||||
{{ app.name || "" }}
|
{{ app.name || "" }}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<!-- 来源标识:支持同时显示多个 -->
|
||||||
:class="[
|
<div class="flex shrink-0 gap-1">
|
||||||
'shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm',
|
<span
|
||||||
app.isMerged
|
v-if="showSparkBadge"
|
||||||
? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
class="rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400"
|
||||||
: app.origin === 'spark'
|
>
|
||||||
? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
Spark
|
||||||
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
</span>
|
||||||
]"
|
<span
|
||||||
>
|
v-if="showApmBadge"
|
||||||
{{
|
class="rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400"
|
||||||
app.isMerged
|
>
|
||||||
? "Spark/APM"
|
APM
|
||||||
: app.origin === "spark"
|
</span>
|
||||||
? "Spark"
|
</div>
|
||||||
: "APM"
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400 leading-tight">
|
<div class="text-sm text-slate-500 dark:text-slate-400 leading-tight">
|
||||||
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="truncate text-xs text-slate-500 dark:text-slate-400 leading-tight">
|
<div
|
||||||
{{ description || '\u00A0' }}
|
class="truncate text-xs text-slate-500 dark:text-slate-400 leading-tight"
|
||||||
|
>
|
||||||
|
{{ description || "\u00A0" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,6 +56,9 @@ import type { App } from "../global/typedefinition";
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
app: App;
|
app: App;
|
||||||
|
// 从外部传入的 Spark/APM 可用性信息
|
||||||
|
sparkAvailable?: boolean;
|
||||||
|
apmAvailable?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -69,6 +71,22 @@ const loadedIcon = ref(
|
|||||||
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E',
|
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 是否显示 Spark 标识
|
||||||
|
const showSparkBadge = computed(() => {
|
||||||
|
// 如果明确指定了 sparkAvailable,使用它
|
||||||
|
if (props.sparkAvailable !== undefined) return props.sparkAvailable;
|
||||||
|
// 否则根据 app 的 origin 或 isMerged 判断
|
||||||
|
return props.app.origin === "spark" || props.app.isMerged === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否显示 APM 标识
|
||||||
|
const showApmBadge = computed(() => {
|
||||||
|
// 如果明确指定了 apmAvailable,使用它
|
||||||
|
if (props.apmAvailable !== undefined) return props.apmAvailable;
|
||||||
|
// 否则根据 app 的 origin 或 isMerged 判断
|
||||||
|
return props.app.origin === "apm" || props.app.isMerged === true;
|
||||||
|
});
|
||||||
|
|
||||||
const iconPath = computed(() => {
|
const iconPath = computed(() => {
|
||||||
const arch = window.apm_store.arch || "amd64";
|
const arch = window.apm_store.arch || "amd64";
|
||||||
const finalArch =
|
const finalArch =
|
||||||
|
|||||||
@@ -57,14 +57,25 @@
|
|||||||
<!-- 版本号和来源切换 -->
|
<!-- 版本号和来源切换 -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- 版本号 -->
|
<!-- 版本号 -->
|
||||||
<div class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50">
|
<div
|
||||||
<span class="text-sm text-slate-500 dark:text-slate-400">版本</span>
|
class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50"
|
||||||
<span class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ displayApp?.version || "-" }}</span>
|
>
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400"
|
||||||
|
>版本</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm font-semibold text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp?.version || "-" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 应用来源切换 -->
|
<!-- 应用来源切换 -->
|
||||||
<div class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50">
|
<div
|
||||||
<span class="text-sm text-slate-500 dark:text-slate-400">来源</span>
|
class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400"
|
||||||
|
>来源</span
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="app?.isMerged"
|
v-if="app?.isMerged"
|
||||||
class="flex gap-1 overflow-hidden rounded-lg shadow-sm border border-slate-200 dark:border-slate-700"
|
class="flex gap-1 overflow-hidden rounded-lg shadow-sm border border-slate-200 dark:border-slate-700"
|
||||||
@@ -110,9 +121,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 下载量 -->
|
<!-- 下载量 -->
|
||||||
<div v-if="downloadCount" class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50">
|
<div
|
||||||
<span class="text-sm text-slate-500 dark:text-slate-400">下载量</span>
|
v-if="downloadCount"
|
||||||
<span class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ downloadCount }}</span>
|
class="flex items-center justify-between rounded-2xl border border-slate-200/60 bg-slate-50/50 px-4 py-3 dark:border-slate-800/60 dark:bg-slate-800/50"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-500 dark:text-slate-400"
|
||||||
|
>下载量</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm font-semibold text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ downloadCount }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,55 +182,79 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 其他元信息 -->
|
<!-- 其他元信息 -->
|
||||||
<div class="space-y-2 pt-2 border-t border-slate-200/60 dark:border-slate-800/60" @click="showAllMetaData">
|
<div
|
||||||
|
class="space-y-2 pt-2 border-t border-slate-200/60 dark:border-slate-800/60"
|
||||||
|
@click="showAllMetaData"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.category"
|
v-if="displayApp?.category"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">分类</span>
|
<span class="text-xs text-slate-400">分类</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]">{{ displayApp.category }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]"
|
||||||
|
>{{ displayApp.category }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.author"
|
v-if="displayApp?.author"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">作者</span>
|
<span class="text-xs text-slate-400">作者</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]">{{ displayApp.author }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]"
|
||||||
|
>{{ displayApp.author }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.contributor"
|
v-if="displayApp?.contributor"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">贡献者</span>
|
<span class="text-xs text-slate-400">贡献者</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]">{{ displayApp.contributor }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]"
|
||||||
|
>{{ displayApp.contributor }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.size"
|
v-if="displayApp?.size"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">大小</span>
|
<span class="text-xs text-slate-400">大小</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">{{ displayApp.size }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300"
|
||||||
|
>{{ displayApp.size }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.update"
|
v-if="displayApp?.update"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">更新</span>
|
<span class="text-xs text-slate-400">更新</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">{{ displayApp.update }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300"
|
||||||
|
>{{ displayApp.update }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.website"
|
v-if="displayApp?.website"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">网站</span>
|
<span class="text-xs text-slate-400">网站</span>
|
||||||
<span class="text-xs font-medium text-brand truncate max-w-[140px]">{{ displayApp.website }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-brand truncate max-w-[140px]"
|
||||||
|
>{{ displayApp.website }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="displayApp?.tags"
|
v-if="displayApp?.tags"
|
||||||
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
class="flex items-center justify-between px-1 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded px-1 -mx-1 transition-colors"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-slate-400">标签</span>
|
<span class="text-xs text-slate-400">标签</span>
|
||||||
<span class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]">{{ displayApp.tags }}</span>
|
<span
|
||||||
|
class="text-xs font-medium text-slate-700 dark:text-slate-300 truncate max-w-[140px]"
|
||||||
|
>{{ displayApp.tags }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +266,9 @@
|
|||||||
v-if="displayApp?.more && displayApp.more.trim() !== ''"
|
v-if="displayApp?.more && displayApp.more.trim() !== ''"
|
||||||
class="rounded-2xl border border-slate-200/60 bg-slate-50/50 p-5 dark:border-slate-800/60 dark:bg-slate-800/30"
|
class="rounded-2xl border border-slate-200/60 bg-slate-50/50 p-5 dark:border-slate-800/60 dark:bg-slate-800/30"
|
||||||
>
|
>
|
||||||
<h3 class="text-base font-semibold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
<h3
|
||||||
|
class="text-base font-semibold text-slate-900 dark:text-white mb-3 flex items-center gap-2"
|
||||||
|
>
|
||||||
<i class="fas fa-info-circle text-slate-400"></i>
|
<i class="fas fa-info-circle text-slate-400"></i>
|
||||||
应用详情
|
应用详情
|
||||||
</h3>
|
</h3>
|
||||||
@@ -241,7 +286,9 @@
|
|||||||
|
|
||||||
<!-- 截图展示 -->
|
<!-- 截图展示 -->
|
||||||
<div v-if="screenshots.length">
|
<div v-if="screenshots.length">
|
||||||
<h3 class="text-base font-semibold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
<h3
|
||||||
|
class="text-base font-semibold text-slate-900 dark:text-white mb-3 flex items-center gap-2"
|
||||||
|
>
|
||||||
<i class="fas fa-images text-slate-400"></i>
|
<i class="fas fa-images text-slate-400"></i>
|
||||||
应用截图
|
应用截图
|
||||||
</h3>
|
</h3>
|
||||||
@@ -295,53 +342,92 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 pr-8">
|
<h3
|
||||||
|
class="text-lg font-semibold text-slate-900 dark:text-white mb-4 pr-8"
|
||||||
|
>
|
||||||
应用信息
|
应用信息
|
||||||
</h3>
|
</h3>
|
||||||
<div class="max-h-80 overflow-y-auto rounded-xl bg-slate-50 p-4 dark:bg-slate-900/50 space-y-3">
|
<div
|
||||||
|
class="max-h-80 overflow-y-auto rounded-xl bg-slate-50 p-4 dark:bg-slate-900/50 space-y-3"
|
||||||
|
>
|
||||||
<div v-if="displayApp?.name" class="flex justify-between">
|
<div v-if="displayApp?.name" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">应用名称</span>
|
<span class="text-sm text-slate-500">应用名称</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.name }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.name }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.pkgname" class="flex justify-between">
|
<div v-if="displayApp?.pkgname" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">包名</span>
|
<span class="text-sm text-slate-500">包名</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.pkgname }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.pkgname }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.version" class="flex justify-between">
|
<div v-if="displayApp?.version" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">版本</span>
|
<span class="text-sm text-slate-500">版本</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.version }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp.version }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.category" class="flex justify-between">
|
<div v-if="displayApp?.category" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">分类</span>
|
<span class="text-sm text-slate-500">分类</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.category }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp.category }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.author" class="flex justify-between">
|
<div v-if="displayApp?.author" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">作者</span>
|
<span class="text-sm text-slate-500">作者</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.author }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.author }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.contributor" class="flex justify-between">
|
<div v-if="displayApp?.contributor" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">贡献者</span>
|
<span class="text-sm text-slate-500">贡献者</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.contributor }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.contributor }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.size" class="flex justify-between">
|
<div v-if="displayApp?.size" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">大小</span>
|
<span class="text-sm text-slate-500">大小</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.size }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp.size }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.update" class="flex justify-between">
|
<div v-if="displayApp?.update" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">更新时间</span>
|
<span class="text-sm text-slate-500">更新时间</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.update }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp.update }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.website" class="flex justify-between">
|
<div v-if="displayApp?.website" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">网站</span>
|
<span class="text-sm text-slate-500">网站</span>
|
||||||
<a :href="displayApp.website" target="_blank" class="text-sm font-medium text-brand hover:underline text-right max-w-[60%] break-all">{{ displayApp.website }}</a>
|
<a
|
||||||
|
:href="displayApp.website"
|
||||||
|
target="_blank"
|
||||||
|
class="text-sm font-medium text-brand hover:underline text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.website }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.tags" class="flex justify-between">
|
<div v-if="displayApp?.tags" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">标签</span>
|
<span class="text-sm text-slate-500">标签</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.tags }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all"
|
||||||
|
>{{ displayApp.tags }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayApp?.origin" class="flex justify-between">
|
<div v-if="displayApp?.origin" class="flex justify-between">
|
||||||
<span class="text-sm text-slate-500">来源</span>
|
<span class="text-sm text-slate-500">来源</span>
|
||||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.origin === 'spark' ? 'Spark' : 'APM' }}</span>
|
<span
|
||||||
|
class="text-sm font-medium text-slate-800 dark:text-slate-200"
|
||||||
|
>{{ displayApp.origin === "spark" ? "Spark" : "APM" }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -525,5 +611,3 @@ const hideImage = (e: Event) => {
|
|||||||
(e.target as HTMLElement).style.display = "none";
|
(e.target as HTMLElement).style.display = "none";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,7 @@
|
|||||||
@open-detail="$emit('open-detail', app)"
|
@open-detail="$emit('open-detail', app)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
v-else
|
|
||||||
class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="n in 8"
|
v-for="n in 8"
|
||||||
:key="n"
|
:key="n"
|
||||||
|
|||||||
@@ -32,10 +32,18 @@
|
|||||||
class="h-32 w-32 mb-6 opacity-90"
|
class="h-32 w-32 mb-6 opacity-90"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-200 mb-2">
|
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-200 mb-2">
|
||||||
{{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }}
|
{{
|
||||||
|
storeFilter === "apm"
|
||||||
|
? "欢迎来到星火应用商店 (Amber PM)"
|
||||||
|
: "欢迎来到星火应用商店"
|
||||||
|
}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
{{ storeFilter === 'apm' ? '探索丰富的应用,发现更多精彩内容' : '探索丰富的应用,发现更多精彩内容' }}
|
{{
|
||||||
|
storeFilter === "apm"
|
||||||
|
? "探索丰富的应用,发现更多精彩内容"
|
||||||
|
: "探索丰富的应用,发现更多精彩内容"
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 有数据就立即展示,图片逐步加载 -->
|
<!-- 有数据就立即展示,图片逐步加载 -->
|
||||||
@@ -43,7 +51,11 @@
|
|||||||
<!-- 左上角欢迎语 -->
|
<!-- 左上角欢迎语 -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h1 class="text-xl font-bold text-slate-800 dark:text-slate-200">
|
<h1 class="text-xl font-bold text-slate-800 dark:text-slate-200">
|
||||||
{{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }}
|
{{
|
||||||
|
storeFilter === "apm"
|
||||||
|
? "欢迎来到星火应用商店 (Amber PM)"
|
||||||
|
: "欢迎来到星火应用商店"
|
||||||
|
}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||||
探索丰富的应用,发现更多精彩内容
|
探索丰富的应用,发现更多精彩内容
|
||||||
@@ -61,29 +73,41 @@
|
|||||||
:title="link.more as string"
|
:title="link.more as string"
|
||||||
>
|
>
|
||||||
<!-- 图片区域 - 850:400 比例 -->
|
<!-- 图片区域 - 850:400 比例 -->
|
||||||
<div class="relative w-full aspect-[850/400] overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800">
|
<div
|
||||||
|
class="relative w-full aspect-[850/400] overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
:src="computedImgUrl(link)"
|
:src="computedImgUrl(link)"
|
||||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@load="onImageLoad(link.url + link.name)"
|
@load="onImageLoad(link.url + link.name)"
|
||||||
@error="onImageError(link.url + link.name)"
|
@error="onImageError(link.url + link.name)"
|
||||||
:class="{ 'opacity-0': !imageLoaded[link.url + link.name], 'opacity-100 transition-opacity duration-300': imageLoaded[link.url + link.name] }"
|
:class="{
|
||||||
|
'opacity-0': !imageLoaded[link.url + link.name],
|
||||||
|
'opacity-100 transition-opacity duration-300':
|
||||||
|
imageLoaded[link.url + link.name],
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<!-- 图片加载占位符 -->
|
<!-- 图片加载占位符 -->
|
||||||
<div
|
<div
|
||||||
v-if="!imageLoaded[link.url + link.name]"
|
v-if="!imageLoaded[link.url + link.name]"
|
||||||
class="absolute inset-0 flex items-center justify-center"
|
class="absolute inset-0 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div class="h-10 w-10 animate-pulse rounded-full bg-slate-200 dark:bg-slate-700"></div>
|
<div
|
||||||
|
class="h-10 w-10 animate-pulse rounded-full bg-slate-200 dark:bg-slate-700"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 文字信息区域 -->
|
<!-- 文字信息区域 -->
|
||||||
<div class="mt-3 px-1">
|
<div class="mt-3 px-1">
|
||||||
<div class="text-base font-semibold text-slate-900 dark:text-white group-hover:text-brand dark:group-hover:text-brand transition-colors">
|
<div
|
||||||
|
class="text-base font-semibold text-slate-900 dark:text-white group-hover:text-brand dark:group-hover:text-brand transition-colors"
|
||||||
|
>
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-1">
|
<div
|
||||||
|
class="text-sm text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-1"
|
||||||
|
>
|
||||||
{{ link.more }}
|
{{ link.more }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +127,7 @@
|
|||||||
v-for="app in section.apps"
|
v-for="app in section.apps"
|
||||||
:key="app.pkgname"
|
:key="app.pkgname"
|
||||||
:app="app"
|
:app="app"
|
||||||
@open-detail="$emit('open-detail', $event)"
|
@open-detail="handleOpenDetail(app)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -126,10 +150,15 @@ defineProps<{
|
|||||||
storeFilter?: "spark" | "apm" | "both";
|
storeFilter?: "spark" | "apm" | "both";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "open-detail", app: App | Record<string, unknown>): void;
|
(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>>({});
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export const handleInstall = (appObj?: App) => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
downloads.value.find(
|
downloads.value.find(
|
||||||
(d) =>
|
(d) => d.pkgname === targetApp.pkgname && d.origin === targetApp.origin,
|
||||||
d.pkgname === targetApp.pkgname && d.origin === targetApp.origin,
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -107,9 +106,7 @@ export const handleUpgrade = (app: App) => {
|
|||||||
(d) => d.pkgname === app.pkgname && d.origin === app.origin,
|
(d) => d.pkgname === app.pkgname && d.origin === app.origin,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(`任务已存在,忽略重复添加: ${app.pkgname} (${app.origin})`);
|
||||||
`任务已存在,忽略重复添加: ${app.pkgname} (${app.origin})`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user