feat: 优化应用商店界面布局和交互体验

refactor(HomeView): 调整网格布局和卡片样式,增加欢迎页面
refactor(AppDetailModal): 重构应用详情弹窗布局,增加元数据展示和返回按钮
fix(spark-store): 添加dpkg命令检查逻辑
style: 统一调整部分间距和颜色样式
This commit is contained in:
2026-03-29 15:22:55 +08:00
parent 33c48f4543
commit ad5562700f
5 changed files with 364 additions and 194 deletions

View File

@@ -17,6 +17,12 @@ if [ "$IS_ACE_ENV" = "1" ]; then
ARGS="$ARGS --no-apm" ARGS="$ARGS --no-apm"
fi fi
# 检查是否存在 dpkg 指令
if ! command -v dpkg >/dev/null 2>&1; then
echo "未检测到 dpkg 指令"
ARGS="$ARGS --no-spark"
fi
# 注意:已移除原先针对 arm64 + wayland 添加 --disable-gpu 的逻辑, # 注意:已移除原先针对 arm64 + wayland 添加 --disable-gpu 的逻辑,
# 现在 arm64 设备无论是否使用 wayland 均不再添加此参数。 # 现在 arm64 设备无论是否使用 wayland 均不再添加此参数。

BIN
icons/amber-pm-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -51,6 +51,7 @@
:lists="homeLists" :lists="homeLists"
:loading="homeLoading" :loading="homeLoading"
:error="homeError" :error="homeError"
:store-filter="storeFilter"
@open-detail="openDetail" @open-detail="openDetail"
/> />
</template> </template>
@@ -378,6 +379,7 @@ const toggleTheme = () => {
const selectCategory = (category: string) => { const selectCategory = (category: string) => {
activeCategory.value = category; activeCategory.value = category;
isSidebarOpen.value = false; isSidebarOpen.value = false;
window.scrollTo({ top: 0, behavior: "smooth" });
if ( if (
category === "home" && category === "home" &&
homeLinks.value.length === 0 && homeLinks.value.length === 0 &&

View File

@@ -14,40 +14,69 @@
@click.self="closeModal" @click.self="closeModal"
> >
<div <div
class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900" class="modal-panel relative w-full max-w-5xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 px-6 pb-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
> >
<div class="flex flex-col gap-4 lg:flex-row lg:items-center"> <!-- 返回按钮 - sticky定位在模态框内部左上角滚动时始终可见 -->
<div class="flex flex-1 items-center gap-4"> <button
<div type="button"
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700" class="sticky top-2 left-0 z-10 inline-flex items-center gap-2 rounded-full border border-slate-200/70 bg-white/90 px-4 py-2 text-sm font-medium text-slate-600 shadow-lg backdrop-blur-sm transition hover:bg-slate-50 hover:text-slate-900 dark:border-slate-700 dark:bg-slate-800/90 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200 mt-4"
> @click="closeModal"
<img aria-label="返回"
v-if="app" >
:src="iconPath" <i class="fas fa-arrow-left"></i>
alt="icon" <span>返回</span>
class="h-full w-full object-cover transition-opacity duration-300" </button>
:class="isIconLoaded ? 'opacity-100' : 'opacity-0'"
loading="lazy" <!-- 主布局左侧信息 + 右侧内容 -->
@load="isIconLoaded = true" <div class="flex flex-col lg:flex-row gap-6">
/> <!-- 左侧图标版本来源按钮元信息 -->
<div class="w-full lg:w-72 flex-shrink-0 space-y-5">
<!-- 应用图标和名称 -->
<div class="text-center">
<div
class="mx-auto flex h-28 w-28 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-lg dark:from-slate-800 dark:to-slate-700"
>
<img
v-if="app"
:src="iconPath"
alt="icon"
class="h-full w-full object-cover transition-opacity duration-300"
:class="isIconLoaded ? 'opacity-100' : 'opacity-0'"
loading="lazy"
@load="isIconLoaded = true"
/>
</div>
<h2 class="mt-4 text-xl font-bold text-slate-900 dark:text-white">
{{ displayApp?.name || "" }}
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
{{ displayApp?.pkgname || "" }}
</p>
</div> </div>
<div class="space-y-1">
<div class="flex items-center gap-3"> <!-- 版本号和来源切换 -->
<p class="text-2xl font-bold text-slate-900 dark:text-white"> <div class="space-y-3">
{{ displayApp?.name || "" }} <!-- 版本号 -->
</p> <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">
<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 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-md shadow-sm border border-slate-200 dark:border-slate-700 font-medium ml-1" class="flex gap-1 overflow-hidden rounded-lg shadow-sm border border-slate-200 dark:border-slate-700"
> >
<button <button
v-if="app.sparkApp" v-if="app.sparkApp"
type="button" type="button"
class="px-2 py-0.5 text-[10px] uppercase tracking-wider transition-colors" class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class=" :class="
viewingOrigin === 'spark' viewingOrigin === 'spark'
? 'bg-orange-500 text-white' ? 'bg-orange-500 text-white'
: 'bg-slate-100/50 text-slate-500 dark:bg-slate-800 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700' : 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600'
" "
@click="viewingOrigin = 'spark'" @click="viewingOrigin = 'spark'"
> >
@@ -56,11 +85,11 @@
<button <button
v-if="app.apmApp" v-if="app.apmApp"
type="button" type="button"
class="px-2 py-0.5 text-[10px] uppercase tracking-wider transition-colors" class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class=" :class="
viewingOrigin === 'apm' viewingOrigin === 'apm'
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-slate-100/50 text-slate-500 dark:bg-slate-800 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700' : 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600'
" "
@click="viewingOrigin = 'apm'" @click="viewingOrigin = 'apm'"
> >
@@ -70,7 +99,7 @@
<span <span
v-else-if="displayApp" v-else-if="displayApp"
:class="[ :class="[
'rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm', 'rounded-md px-2 py-1 text-xs font-bold uppercase tracking-wider',
displayApp.origin === 'spark' displayApp.origin === 'spark'
? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400' ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400', : 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
@@ -79,175 +108,241 @@
{{ displayApp.origin === "spark" ? "Spark" : "APM" }} {{ displayApp.origin === "spark" ? "Spark" : "APM" }}
</span> </span>
</div> </div>
<p class="text-sm text-slate-500 dark:text-slate-400">
{{ displayApp?.pkgname || "" }} · <!-- 下载量 -->
{{ displayApp?.version || "" }} <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">
<span v-if="downloadCount"> · 下载量{{ downloadCount }}</span> <span class="text-sm text-slate-500 dark:text-slate-400">下载量</span>
</p> <span class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ downloadCount }}</span>
</div>
</div>
<!-- 功能按钮 -->
<div class="space-y-2">
<button
v-if="!isinstalled"
type="button"
class="w-full inline-flex items-center justify-center gap-2 rounded-2xl bg-gradient-to-r px-4 py-3 text-sm font-semibold text-white shadow-lg disabled:opacity-40 transition hover:-translate-y-0.5"
:class="
installFeedback
? 'from-emerald-500 to-emerald-600'
: 'from-brand to-brand-dark'
"
@click="handleInstall"
:disabled="installFeedback || isOtherVersionInstalled"
>
<i
class="fas"
:class="installFeedback ? 'fa-check' : 'fa-download'"
></i>
<span>{{ installBtnText }}</span>
</button>
<template v-else>
<button
type="button"
class="w-full inline-flex items-center justify-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-3 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5"
@click="
emit(
'open-app',
displayApp?.pkgname || '',
displayApp?.origin,
)
"
>
<i class="fas fa-external-link-alt"></i>
<span>打开</span>
</button>
<button
type="button"
class="w-full inline-flex items-center justify-center gap-2 rounded-2xl bg-gradient-to-r from-rose-500 to-rose-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 disabled:opacity-40 transition hover:-translate-y-0.5"
@click="handleRemove"
>
<i class="fas fa-trash"></i>
<span>卸载</span>
</button>
</template>
</div>
<!-- 其他元信息 -->
<div class="space-y-2 pt-2 border-t border-slate-200/60 dark:border-slate-800/60" @click="showAllMetaData">
<div
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"
>
<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>
</div>
<div
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"
>
<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>
</div>
<div
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"
>
<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>
</div>
<div
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"
>
<span class="text-xs text-slate-400">大小</span>
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">{{ displayApp.size }}</span>
</div>
<div
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"
>
<span class="text-xs text-slate-400">更新</span>
<span class="text-xs font-medium text-slate-700 dark:text-slate-300">{{ displayApp.update }}</span>
</div>
<div
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"
>
<span class="text-xs text-slate-400">网站</span>
<span class="text-xs font-medium text-brand truncate max-w-[140px]">{{ displayApp.website }}</span>
</div>
<div
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"
>
<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>
</div>
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-2 lg:ml-auto">
<button <!-- 右侧应用详情+ 截图 -->
v-if="!isinstalled" <div class="flex-1 min-w-0 space-y-5">
type="button" <!-- 应用详情 -->
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40 transition hover:-translate-y-0.5" <div
:class=" v-if="displayApp?.more && displayApp.more.trim() !== ''"
installFeedback class="rounded-2xl border border-slate-200/60 bg-slate-50/50 p-5 dark:border-slate-800/60 dark:bg-slate-800/30"
? 'from-emerald-500 to-emerald-600'
: 'from-brand to-brand-dark'
"
@click="handleInstall"
:disabled="installFeedback || isOtherVersionInstalled"
> >
<i <h3 class="text-base font-semibold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
class="fas" <i class="fas fa-info-circle text-slate-400"></i>
:class="installFeedback ? 'fa-check' : 'fa-download'" 应用详情
></i> </h3>
<span>{{ installBtnText }}</span> <div
</button> class="max-h-48 overflow-y-auto text-sm leading-relaxed text-slate-600 dark:text-slate-300 space-y-2"
<template v-else> v-html="displayApp.more.replace(/\n/g, '<br>')"
<button ></div>
type="button" </div>
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5" <div
@click=" v-else
emit( class="rounded-2xl border border-slate-200/60 bg-slate-50/30 p-8 text-center dark:border-slate-800/60 dark:bg-slate-800/20"
'open-app',
displayApp?.pkgname || '',
displayApp?.origin,
)
"
>
<i class="fas fa-external-link-alt"></i>
<span>打开</span>
</button>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-rose-500 to-rose-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 disabled:opacity-40 transition hover:-translate-y-0.5"
@click="handleRemove"
>
<i class="fas fa-trash"></i>
<span>卸载</span>
</button>
</template>
<button
type="button"
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
@click="closeModal"
aria-label="关闭"
> >
<i class="fas fa-xmark"></i> <p class="text-sm text-slate-400">暂无应用详情</p>
</button> </div>
<!-- 截图展示 -->
<div v-if="screenshots.length">
<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>
应用截图
</h3>
<div class="grid gap-3 sm:grid-cols-2">
<img
v-for="(screen, index) in screenshots"
:key="index"
:src="screen"
alt="screenshot"
class="h-44 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60"
loading="lazy"
@click="openPreview(index)"
@error="hideImage"
/>
</div>
</div>
<div
v-else
class="rounded-2xl border border-slate-200/60 bg-slate-50/30 p-8 text-center dark:border-slate-800/60 dark:bg-slate-800/20"
>
<p class="text-sm text-slate-400">暂无应用截图</p>
</div>
</div> </div>
</div> </div>
</div>
</div>
</Transition>
<!-- <div <!-- 元数据详情弹窗 -->
class="mt-4 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3 text-sm text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300" <Transition
enter-active-class="duration-200 ease-out"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="duration-150 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="showMetaModal"
class="fixed inset-0 z-[60] flex items-center justify-center overflow-hidden bg-slate-900/60 p-4"
@click.self="closeMetaModal"
>
<div
class="relative w-full max-w-md rounded-2xl border border-white/10 bg-white p-6 shadow-2xl dark:border-slate-700 dark:bg-slate-800"
>
<button
type="button"
class="absolute top-3 right-3 inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:hover:bg-slate-700 dark:hover:text-slate-300"
@click="closeMetaModal"
aria-label="关闭"
> >
首次安装 APM 后需要重启系统以在启动器中看到应用入口可前往 <i class="fas fa-xmark"></i>
<a </button>
href="https://gitee.com/amber-ce/amber-pm/releases" <h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 pr-8">
target="_blank" 应用信息
class="font-semibold text-brand hover:underline" </h3>
>APM Releases</a <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">
获取 APM <span class="text-sm text-slate-500">应用名称</span>
</div> --> <span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.name }}</span>
<div v-if="screenshots.length" class="mt-6 grid gap-3 sm:grid-cols-2">
<img
v-for="(screen, index) in screenshots"
:key="index"
:src="screen"
alt="screenshot"
class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60"
loading="lazy"
@click="openPreview(index)"
@error="hideImage"
/>
</div>
<div class="mt-6 grid gap-4 sm:grid-cols-2">
<div
v-if="displayApp?.author"
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
>
<p class="text-xs uppercase tracking-wide text-slate-400">作者</p>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ displayApp.author }}
</p>
</div> </div>
<div <div v-if="displayApp?.pkgname" class="flex justify-between">
v-if="displayApp?.contributor" <span class="text-sm text-slate-500">包名</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.pkgname }}</span>
>
<p class="text-xs uppercase tracking-wide text-slate-400">贡献者</p>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ displayApp.contributor }}
</p>
</div> </div>
<div <div v-if="displayApp?.version" class="flex justify-between">
v-if="displayApp?.size" <span class="text-sm text-slate-500">版本</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.version }}</span>
>
<p class="text-xs uppercase tracking-wide text-slate-400">大小</p>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ displayApp.size }}
</p>
</div> </div>
<div <div v-if="displayApp?.category" class="flex justify-between">
v-if="displayApp?.update" <span class="text-sm text-slate-500">分类</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.category }}</span>
>
<p class="text-xs uppercase tracking-wide text-slate-400">
更新时间
</p>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ displayApp.update }}
</p>
</div> </div>
<div <div v-if="displayApp?.author" class="flex justify-between">
v-if="displayApp?.website" <span class="text-sm text-slate-500">作者</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.author }}</span>
>
<p class="text-xs uppercase tracking-wide text-slate-400">网站</p>
<a
:href="displayApp.website"
target="_blank"
class="text-sm font-medium text-brand hover:underline"
>{{ displayApp.website }}</a
>
</div> </div>
<div <div v-if="displayApp?.contributor" class="flex justify-between">
v-if="displayApp?.version" <span class="text-sm text-slate-500">贡献者</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200 text-right max-w-[60%] break-all">{{ displayApp.contributor }}</span>
>
<p class="text-xs uppercase tracking-wide text-slate-400">版本</p>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
{{ displayApp.version }}
</p>
</div> </div>
<div <div v-if="displayApp?.size" class="flex justify-between">
v-if="displayApp?.tags" <span class="text-sm text-slate-500">大小</span>
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60" <span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.size }}</span>
> </div>
<p class="text-xs uppercase tracking-wide text-slate-400">标签</p> <div v-if="displayApp?.update" class="flex justify-between">
<p class="text-sm font-medium text-slate-800 dark:text-slate-200"> <span class="text-sm text-slate-500">更新时间</span>
{{ displayApp.tags }} <span class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ displayApp.update }}</span>
</p> </div>
<div v-if="displayApp?.website" class="flex justify-between">
<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>
</div>
<div v-if="displayApp?.tags" class="flex justify-between">
<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>
</div>
<div v-if="displayApp?.origin" class="flex justify-between">
<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>
</div> </div>
</div>
<div
v-if="displayApp?.more && displayApp.more.trim() !== ''"
class="mt-6 space-y-3"
>
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
应用详情
</h3>
<div
class="max-h-60 space-y-2 overflow-y-auto rounded-2xl border border-slate-200/60 bg-slate-50/80 p-4 text-sm leading-relaxed text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
v-html="displayApp.more.replace(/\n/g, '<br>')"
></div>
</div> </div>
</div> </div>
</div> </div>
@@ -286,6 +381,17 @@ const isIconLoaded = ref(false);
const viewingOrigin = ref<"spark" | "apm">("spark"); const viewingOrigin = ref<"spark" | "apm">("spark");
// 元数据弹窗相关
const showMetaModal = ref(false);
const showAllMetaData = () => {
showMetaModal.value = true;
};
const closeMetaModal = () => {
showMetaModal.value = false;
};
watch( watch(
() => props.app, () => props.app,
(newApp) => { (newApp) => {
@@ -419,3 +525,5 @@ const hideImage = (e: Event) => {
(e.target as HTMLElement).style.display = "none"; (e.target as HTMLElement).style.display = "none";
}; };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="space-y-8"> <div class="space-y-6">
<!-- 初始加载状态 - 只有在完全没有数据时显示 --> <!-- 初始加载状态 - 只有在完全没有数据时显示 -->
<div <div
v-if="loading && links.length === 0 && lists.length === 0" v-if="loading && links.length === 0 && lists.length === 0"
@@ -14,22 +14,57 @@
> >
{{ error }} {{ error }}
</div> </div>
<!-- 无数据时显示欢迎信息 -->
<div
v-else-if="links.length === 0 && lists.length === 0"
class="flex flex-col items-center justify-center py-20 text-center"
>
<img
v-if="storeFilter === 'apm'"
src="../assets/imgs/amber-pm-logo.png"
alt="Amber PM"
class="h-32 w-32 mb-6 opacity-90 object-contain"
/>
<img
v-else
src="../assets/imgs/spark-store.svg"
alt="星火应用商店"
class="h-32 w-32 mb-6 opacity-90"
/>
<h1 class="text-2xl font-bold text-slate-800 dark:text-slate-200 mb-2">
{{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }}
</h1>
<p class="text-sm text-slate-500 dark:text-slate-400">
{{ storeFilter === 'apm' ? '探索丰富的应用,发现更多精彩内容' : '探索丰富的应用,发现更多精彩内容' }}
</p>
</div>
<!-- 有数据就立即展示图片逐步加载 --> <!-- 有数据就立即展示图片逐步加载 -->
<div v-else> <div v-else>
<!-- 左上角欢迎语 -->
<div class="mb-4">
<h1 class="text-xl font-bold text-slate-800 dark:text-slate-200">
{{ storeFilter === 'apm' ? '欢迎来到星火应用商店 (Amber PM)' : '欢迎来到星火应用商店' }}
</h1>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
探索丰富的应用发现更多精彩内容
</p>
</div>
<!-- Links 区域 --> <!-- Links 区域 -->
<div v-if="links.length > 0" class="grid gap-4 auto-fit-grid"> <div v-if="links.length > 0" class="grid gap-5 auto-fit-grid">
<a <a
v-for="link in links" v-for="link in links"
:key="link.url + link.name" :key="link.url + link.name"
:href="link.type === '_blank' ? undefined : link.url" :href="link.type === '_blank' ? undefined : link.url"
@click.prevent="onLinkClick(link)" @click.prevent="onLinkClick(link)"
class="flex flex-col items-start gap-2 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm transition hover:shadow-lg dark:border-slate-800/70 dark:bg-slate-900/90" class="group block overflow-hidden rounded-xl transition-transform duration-300 hover:scale-[1.02]"
:title="link.more as string" :title="link.more as string"
> >
<div class="h-20 w-full flex items-center justify-center bg-slate-100/50 dark:bg-slate-800/50 rounded-xl overflow-hidden"> <!-- 图片区域 - 850:400 比例 -->
<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-contain" 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)"
@@ -40,14 +75,17 @@
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-8 w-8 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="text-base font-semibold text-slate-900 dark:text-white"> <!-- 文字信息区域 -->
{{ link.name }} <div class="mt-3 px-1">
</div> <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-sm text-slate-500 dark:text-slate-400"> {{ link.name }}
{{ link.more }} </div>
<div class="text-sm text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-1">
{{ link.more }}
</div>
</div> </div>
</a> </a>
</div> </div>
@@ -60,7 +98,7 @@
{{ section.title }} {{ section.title }}
</h3> </h3>
</div> </div>
<div class="mt-3 grid gap-4 auto-fit-grid"> <div class="mt-3 grid gap-4 app-grid">
<AppCard <AppCard
v-for="app in section.apps" v-for="app in section.apps"
:key="app.pkgname" :key="app.pkgname"
@@ -85,6 +123,7 @@ defineProps<{
lists: HomeList[]; lists: HomeList[];
loading: boolean; loading: boolean;
error: string; error: string;
storeFilter?: "spark" | "apm" | "both";
}>(); }>();
defineEmits<{ defineEmits<{
@@ -122,14 +161,29 @@ const onLinkClick = (link: HomeLink) => {
<style scoped></style> <style scoped></style>
<style scoped> <style scoped>
/* Link 卡片网格 - 固定最小宽度 180px */
.auto-fit-grid { .auto-fit-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
/* 小屏幕 - 最小宽度减小 */
@media (max-width: 640px) {
.auto-fit-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
}
/* 应用卡片网格 - 保持原来的样式 */
.app-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
} }
/* slight gap tuning for small screens */
@media (max-width: 640px) { @media (max-width: 640px) {
.auto-fit-grid { .app-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
} }
} }