feat: 优化知识库分块操作区与滚动导航

- 将向量化操作上移到分块预览前的统一操作区

- 增加页顶页末双气泡并完善边界禁用状态
This commit is contained in:
2026-07-31 14:38:05 +08:00
parent a051dbacba
commit f92707dac8
8 changed files with 128 additions and 44 deletions

View File

@@ -1,5 +1,6 @@
export {
ArrowDown,
ArrowDownToLine,
ArrowLeft,
ArrowLeftToLine,
ArrowRightLeft,

View File

@@ -3,7 +3,7 @@ import type { BacktopProps } from './backtop';
import { computed } from 'vue';
import { ArrowUpToLine } from '@easyflow-core/icons';
import { ArrowDownToLine, ArrowUpToLine } from '@easyflow-core/icons';
import { EasyFlowButton } from '../button';
import { useBackTop } from './use-backtop';
@@ -14,9 +14,11 @@ defineOptions({ name: 'BackTop' });
const props = withDefaults(defineProps<Props>(), {
bottom: 20,
bottomLabel: 'Scroll to bottom',
isGroup: false,
right: 24,
target: '',
topLabel: 'Back to top',
visibilityHeight: 200,
});
@@ -25,19 +27,45 @@ const backTopStyle = computed(() => ({
right: `${props.right}px`,
}));
const { handleClick, visible } = useBackTop(props);
const {
canScrollDown,
canScrollUp,
handleBottomClick,
handleClick,
scrollable,
visible,
} = useBackTop(props);
</script>
<template>
<transition name="fade-down">
<EasyFlowButton
v-if="visible"
<div
v-if="isGroup ? scrollable : visible"
:style="backTopStyle"
class="dark:bg-accent dark:hover:bg-heavy bg-background hover:bg-heavy data shadow-float z-popup fixed bottom-10 size-10 rounded-full duration-500"
size="icon"
variant="icon"
@click="handleClick"
class="z-popup fixed flex flex-col gap-2"
>
<ArrowUpToLine class="size-4" />
</EasyFlowButton>
<EasyFlowButton
:aria-label="topLabel"
:disabled="isGroup && !canScrollUp"
:title="topLabel"
class="dark:bg-accent dark:hover:bg-heavy bg-background hover:bg-heavy shadow-float size-10 rounded-full duration-150"
size="icon"
variant="icon"
@click="handleClick"
>
<ArrowUpToLine class="size-4" />
</EasyFlowButton>
<EasyFlowButton
v-if="isGroup"
:aria-label="bottomLabel"
:disabled="!canScrollDown"
:title="bottomLabel"
class="dark:bg-accent dark:hover:bg-heavy bg-background hover:bg-heavy shadow-float size-10 rounded-full duration-150"
size="icon"
variant="icon"
@click="handleBottomClick"
>
<ArrowDownToLine class="size-4" />
</EasyFlowButton>
</div>
</transition>
</template>

View File

@@ -6,6 +6,20 @@ export const backtopProps = {
default: 40,
type: Number,
},
/**
* @zh_CN scroll-to-bottom button label.
*/
bottomLabel: {
default: 'Scroll to bottom',
type: String,
},
/**
* @zh_CN whether to show top and bottom controls as a group.
*/
isGroup: {
default: false,
type: Boolean,
},
/**
* @zh_CN right distance.
*/
@@ -20,6 +34,13 @@ export const backtopProps = {
default: '',
type: String,
},
/**
* @zh_CN back-to-top button label.
*/
topLabel: {
default: 'Back to top',
type: String,
},
/**
* @zh_CN the button will not show until the scroll height reaches this value.
*/
@@ -31,8 +52,10 @@ export const backtopProps = {
export interface BacktopProps {
bottom?: number;
bottomLabel?: string;
isGroup?: boolean;
right?: number;
target?: string;
topLabel?: string;
visibilityHeight?: number;
}

View File

@@ -1,16 +1,31 @@
import type { BacktopProps } from './backtop';
import { onMounted, ref, shallowRef } from 'vue';
import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { useEventListener, useThrottleFn } from '@vueuse/core';
import {
useEventListener,
useResizeObserver,
useThrottleFn,
} from '@vueuse/core';
export const useBackTop = (props: BacktopProps) => {
const el = shallowRef<HTMLElement>();
const container = shallowRef<Document | HTMLElement>();
const resizeTarget = shallowRef<HTMLElement>();
const canScrollDown = ref(false);
const canScrollUp = ref(false);
const scrollable = ref(false);
const visible = ref(false);
const handleScroll = () => {
if (el.value) {
const maxScrollTop = Math.max(
el.value.scrollHeight - el.value.clientHeight,
0,
);
scrollable.value = maxScrollTop > 1;
canScrollUp.value = el.value.scrollTop > 1;
canScrollDown.value = el.value.scrollTop < maxScrollTop - 1;
visible.value = el.value.scrollTop >= (props?.visibilityHeight ?? 0);
}
};
@@ -19,12 +34,24 @@ export const useBackTop = (props: BacktopProps) => {
el.value?.scrollTo({ behavior: 'smooth', top: 0 });
};
const handleBottomClick = () => {
if (!el.value) {
return;
}
el.value.scrollTo({
behavior: 'smooth',
top: Math.max(el.value.scrollHeight - el.value.clientHeight, 0),
});
};
const handleScrollThrottled = useThrottleFn(handleScroll, 300, true);
useEventListener(container, 'scroll', handleScrollThrottled);
useResizeObserver(resizeTarget, handleScrollThrottled);
onMounted(() => {
container.value = document;
el.value = document.documentElement;
resizeTarget.value = document.body;
if (props.target) {
el.value = document.querySelector<HTMLElement>(props.target) ?? undefined;
@@ -33,13 +60,17 @@ export const useBackTop = (props: BacktopProps) => {
throw new Error(`target does not exist: ${props.target}`);
}
container.value = el.value;
resizeTarget.value = el.value;
}
// Give visible an initial value, fix #13066
handleScroll();
void nextTick(handleScroll);
});
return {
canScrollDown,
canScrollUp,
handleBottomClick,
handleClick,
scrollable,
visible,
};
};

View File

@@ -425,7 +425,11 @@ const headerSlots = computed(() => {
@clear-preferences-and-logout="clearPreferencesAndLogout"
/>
</template>
<EasyFlowBackTop />
<EasyFlowBackTop
:bottom-label="$t('button.goToBottom')"
is-group
:top-label="$t('button.backToTop')"
/>
</template>
</EasyFlowAdminLayout>
</template>