feat(滚动): 添加分类切换时重置虚拟滚动位置功能

添加 scrollKey 属性到 AppGrid 组件,当分类变化时自动重置滚动位置
添加相关单元测试验证滚动重置功能
This commit is contained in:
2026-04-10 16:17:38 +08:00
parent 4a2cbe1f2a
commit 1d51f38e64
3 changed files with 123 additions and 1 deletions

View File

@@ -60,6 +60,7 @@
<AppGrid <AppGrid
:apps="filteredApps" :apps="filteredApps"
:loading="loading" :loading="loading"
:scroll-key="activeCategory"
:store-filter="storeFilter" :store-filter="storeFilter"
@open-detail="openDetail" @open-detail="openDetail"
/> />

View File

@@ -0,0 +1,101 @@
import { render } from "@testing-library/vue";
import { defineComponent, h, nextTick } from "vue";
import { describe, expect, it, vi } from "vitest";
import AppGrid from "@/components/AppGrid.vue";
import type { App } from "@/global/typedefinition";
vi.mock("@/components/AppCard.vue", () => ({
default: defineComponent({
name: "AppCard",
props: {
app: {
type: Object,
required: true,
},
},
setup(props) {
return () => h("div", props.app.name);
},
}),
}));
vi.mock("vue-virtual-scroller", () => ({
RecycleScroller: defineComponent({
name: "RecycleScroller",
props: {
items: {
type: Array,
required: true,
},
},
setup(props, { attrs, slots }) {
return () =>
h(
"div",
{
...attrs,
style: "max-height: 320px; overflow-y: auto;",
},
(props.items as Array<{ id: number; apps: App[] }>).map((item) =>
slots.default?.({ item }),
),
);
},
}),
}));
const createApp = (index: number, category: string): App => ({
name: `App ${index}`,
pkgname: `app-${category}-${index}`,
version: "1.0.0",
filename: "app.deb",
torrent_address: "",
author: "",
contributor: "",
website: "",
update: "",
size: "1 MB",
more: "",
tags: "",
img_urls: [],
icons: "",
category,
origin: "spark",
currentStatus: "not-installed",
});
const createApps = (count: number, category: string): App[] =>
Array.from({ length: count }, (_, index) => createApp(index, category));
describe("AppGrid", () => {
it("resets the virtual scroller when the category changes", async () => {
const { container, rerender } = render(AppGrid, {
props: {
apps: createApps(60, "development"),
loading: false,
scrollKey: "development",
} as Record<string, unknown>,
});
const scroller = container.querySelector(".scroller");
expect(scroller).toBeInstanceOf(HTMLElement);
if (!(scroller instanceof HTMLElement)) {
throw new Error("Expected virtual scroller element to exist");
}
scroller.scrollTop = 240;
expect(scroller.scrollTop).toBe(240);
await rerender({
apps: createApps(60, "games"),
loading: false,
scrollKey: "games",
} as Record<string, unknown>);
await nextTick();
expect(scroller.scrollTop).toBe(0);
});
});

View File

@@ -34,6 +34,7 @@
<!-- 应用数量较多时使用虚拟滚动 --> <!-- 应用数量较多时使用虚拟滚动 -->
<RecycleScroller <RecycleScroller
v-else-if="!loading" v-else-if="!loading"
ref="scrollerRef"
class="scroller" class="scroller"
:items="gridRows" :items="gridRows"
:item-size="itemHeight" :item-size="itemHeight"
@@ -77,16 +78,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from "vue"; import { computed, ref, onMounted, onUnmounted, nextTick, watch } from "vue";
import { RecycleScroller } from "vue-virtual-scroller"; import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import AppCard from "./AppCard.vue"; import AppCard from "./AppCard.vue";
import type { App } from "../global/typedefinition"; import type { App } from "../global/typedefinition";
interface RecycleScrollerInstance {
$el: HTMLElement;
}
const props = defineProps<{ const props = defineProps<{
apps: App[]; apps: App[];
loading: boolean; loading: boolean;
storeFilter?: "spark" | "apm" | "both"; storeFilter?: "spark" | "apm" | "both";
scrollKey?: string;
}>(); }>();
defineEmits<{ defineEmits<{
@@ -95,6 +101,7 @@ defineEmits<{
// 当前列数 // 当前列数
const columns = ref(4); const columns = ref(4);
const scrollerRef = ref<RecycleScrollerInstance | null>(null);
// 根据窗口宽度更新列数 // 根据窗口宽度更新列数
const updateColumns = () => { const updateColumns = () => {
@@ -114,6 +121,19 @@ onUnmounted(() => {
window.removeEventListener("resize", updateColumns); window.removeEventListener("resize", updateColumns);
}); });
watch(
() => props.scrollKey,
async (nextKey, prevKey) => {
if (nextKey === prevKey || prevKey === undefined) return;
if (props.loading || props.apps.length <= 50) return;
await nextTick();
if (scrollerRef.value) {
scrollerRef.value.$el.scrollTop = 0;
}
},
);
// 网格列数类名 // 网格列数类名
const gridColumnsClass = computed(() => { const gridColumnsClass = computed(() => {
const map: Record<number, string> = { const map: Record<number, string> = {