feat(UI): 优化应用加载体验和界面样式

改进应用网格布局的边距设置
在App.vue中改为非阻塞方式加载数据
为侧边栏添加内边距
在HomeView中优化图片加载状态显示和条件渲染
This commit is contained in:
2026-03-29 14:07:54 +08:00
parent 3f9447d2cc
commit d144d0d398
4 changed files with 48 additions and 14 deletions

View File

@@ -1068,8 +1068,12 @@ onMounted(async () => {
await loadCategories(); await loadCategories();
// 分类目录加载后,并行加载主页数据和所有应用列表 // 分类目录加载后,并行加载主页数据和所有应用列表
// 使用非阻塞方式加载让UI先展示出来
loading.value = true; loading.value = true;
await Promise.all([ homeLoading.value = true;
// 启动加载任务,但不等待它们完成
Promise.all([
loadHome(), loadHome(),
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
loadApps(() => { loadApps(() => {
@@ -1077,7 +1081,10 @@ onMounted(async () => {
resolve(); resolve();
}); });
}), }),
]); ]).then(() => {
// 所有数据加载完成后的回调(可选)
logger.info("所有应用数据加载完成");
});
// 设置键盘导航 // 设置键盘导航
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="!loading" v-if="!loading"
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
> >
<AppCard <AppCard
v-for="(app, index) in apps" v-for="(app, index) in apps"
@@ -12,7 +12,7 @@
</div> </div>
<div <div
v-else v-else
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
> >
<div <div
v-for="n in 8" v-for="n in 8"

View File

@@ -32,7 +32,7 @@
<StoreModeSwitcher /> <StoreModeSwitcher />
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2"> <div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted px-2 py-1">
<button <button
type="button" type="button"
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800" class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"

View File

@@ -1,7 +1,8 @@
<template> <template>
<div class="space-y-8"> <div class="space-y-8">
<!-- 初始加载状态 - 只有在完全没有数据时显示 -->
<div <div
v-if="loading" v-if="loading && links.length === 0 && lists.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>
@@ -13,9 +14,10 @@
> >
{{ error }} {{ error }}
</div> </div>
<div v-else-if="error" class="text-center text-rose-600">{{ error }}</div> <!-- 有数据就立即展示图片逐步加载 -->
<div v-else> <div v-else>
<div class="grid gap-4 auto-fit-grid"> <!-- Links 区域 -->
<div v-if="links.length > 0" class="grid gap-4 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"
@@ -24,11 +26,23 @@
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="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"
:title="link.more as string" :title="link.more as string"
> >
<img <div class="h-20 w-full flex items-center justify-center bg-slate-100/50 dark:bg-slate-800/50 rounded-xl overflow-hidden">
:src="computedImgUrl(link)" <img
class="h-20 w-full object-contain" :src="computedImgUrl(link)"
loading="lazy" class="h-full w-full object-contain"
/> loading="lazy"
@load="onImageLoad(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] }"
/>
<!-- 图片加载占位符 -->
<div
v-if="!imageLoaded[link.url + link.name]"
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>
</div>
<div class="text-base font-semibold text-slate-900 dark:text-white"> <div class="text-base font-semibold text-slate-900 dark:text-white">
{{ link.name }} {{ link.name }}
</div> </div>
@@ -38,7 +52,8 @@
</a> </a>
</div> </div>
<div class="space-y-6 mt-6"> <!-- Lists 区域 -->
<div v-if="lists.length > 0" class="space-y-6 mt-6">
<section v-for="section in lists" :key="section.title"> <section v-for="section in lists" :key="section.title">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-slate-900"> <h3 class="text-lg font-semibold text-slate-900">
@@ -62,6 +77,7 @@
<script setup lang="ts"> <script setup lang="ts">
import AppCard from "./AppCard.vue"; 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 type { HomeLink, HomeList, App } from "../global/typedefinition"; import type { HomeLink, HomeList, App } from "../global/typedefinition";
defineProps<{ defineProps<{
@@ -75,6 +91,17 @@ defineEmits<{
(e: "open-detail", app: App | Record<string, unknown>): void; (e: "open-detail", app: App | Record<string, unknown>): void;
}>(); }>();
// 图片加载状态跟踪
const imageLoaded = reactive<Record<string, boolean>>({});
const onImageLoad = (key: string) => {
imageLoaded[key] = true;
};
const onImageError = (key: string) => {
imageLoaded[key] = true; // 即使加载失败也标记为完成,隐藏占位符
};
const computedImgUrl = (link: HomeLink) => { const computedImgUrl = (link: HomeLink) => {
if (!link.imgUrl) return ""; if (!link.imgUrl) return "";
const arch = window.apm_store.arch || "amd64"; const arch = window.apm_store.arch || "amd64";