mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
feat(滚动): 添加分类切换时重置虚拟滚动位置功能
添加 scrollKey 属性到 AppGrid 组件,当分类变化时自动重置滚动位置 添加相关单元测试验证滚动重置功能
This commit is contained in:
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
101
src/__tests__/unit/AppGrid.test.ts
Normal file
101
src/__tests__/unit/AppGrid.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user