mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat(UI): 优化应用加载体验和界面样式
改进应用网格布局的边距设置 在App.vue中改为非阻塞方式加载数据 为侧边栏添加内边距 在HomeView中优化图片加载状态显示和条件渲染
This commit is contained in:
11
src/App.vue
11
src/App.vue
@@ -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) => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user