Files
Enterprise-Knowledge-Base/frontend/src/main.ts
T
wizardchen a4c3c39556 perf(wiki): lazy-load sidebar page list with virtual-scroll tabs
?tab=wiki used to call listWikiPages({page_size: 500}) in a loop up to
50 times, loading ~25k pages into memory before rendering the sidebar
on knowledge bases with tens of thousands of pages. All rows were then
materialized as DOM nodes inside collapsible groups stacked vertically.

This commit reshapes the sidebar around per-type buckets that are
fetched on demand and rendered through a single virtual scroller:

* pagesByType holds one bucket per page_type (summary / entity /
  concept / synthesis / comparison), each with its own items list,
  pagination cursor, KB-wide total, and loading flag. Initial load
  fires one 50-item request per type in parallel.
* Index and Log are fetched directly via getWikiIndex / getWikiLog
  instead of scanning the full page list to find them.
* Groups are surfaced as a horizontal tab bar; switching tabs swaps
  the RecycleScroller's items. Only one bucket is visible at a time,
  so we never stack multiple virtual viewports.
* The scroller runs in page-mode with the outer .wiki-page-list as the
  scrollable ancestor; a 1px sentinel after the list is watched by
  IntersectionObserver to trigger the next page fetch. After each
  batch lands we call scroller.updateVisibleItems(true) on the next
  tick so appended rows render in order without requiring the user
  to jiggle the scroll.
* Switching tabs resets scrollTop so the retained position of the
  previous tab can't re-trigger the sentinel and cascade loads on
  the new (shorter) list.
* Search enters a dedicated flat-result mode (searchResults ref);
  clearing the query flips back to the bucketed view without
  refetching the buckets the user already had open.

Adds vue-virtual-scroller dependency and imports its stylesheet.
2026-05-08 15:07:59 +08:00

37 lines
1.2 KiB
TypeScript

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import "./assets/fonts.css";
import TDesign from "tdesign-vue-next";
// 引入组件库的少量全局样式变量
import "tdesign-vue-next/es/style/index.css";
import "@/assets/theme/theme.css";
import "@/assets/dropdown-menu.less";
// vue-virtual-scroller ships its own tiny stylesheet — required for
// RecycleScroller/DynamicScroller to size their viewport correctly.
// Without it the scroller computes 0 height and renders no items.
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import i18n from "./i18n";
import { initTheme } from "@/composables/useTheme";
import { initFont } from "@/composables/useFont";
import { installTDesignIconOfflineGuard } from "@/utils/tdesign-icon-offline";
// 必须在 Vue 组件挂载之前执行,避免 tdesign-icons 运行时请求 tdesign.gtimg.com
installTDesignIconOfflineGuard();
initTheme();
initFont();
const app = createApp(App);
app.use(TDesign);
app.use(createPinia());
app.use(router);
app.use(i18n);
// 等首屏路由(含导航守卫、Lite 自动登录)完成后再挂载,避免先闪默认页再跳转
router.isReady().finally(() => {
app.mount("#app");
});