style: scroll to field

This commit is contained in:
jialin
2025-10-21 19:59:49 +08:00
parent 1e4018350f
commit fc7350b7e2
6 changed files with 166 additions and 34 deletions
+7
View File
@@ -129,6 +129,12 @@ export default function useTableFetch<T>(
..._.pickBy(query || queryParams, (val: any) => !!val)
};
const res = await fetchAPI(params);
if (!dataSource.loadend) {
// add a delay to avoid flash
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
}
if (
!res.items.length &&
params.page > res.pagination.totalPage &&
@@ -139,6 +145,7 @@ export default function useTableFetch<T>(
page: res.pagination.totalPage
};
const newRes = await fetchAPI(newParams);
setDataSource({
dataList: loadmore
? [...dataSource.dataList, ...(newRes.items || [])]
+7 -1
View File
@@ -81,7 +81,13 @@ const AdvanceConfig = () => {
return (
<>
<Form.Item<FormData> name="categories">
<Form.Item<FormData>
name="categories"
data-field="categories"
style={{
scrollMarginTop: 200
}}
>
<SealSelect
allowNull
label={intl.formatMessage({
+1
View File
@@ -38,6 +38,7 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
return (
<>
<Form.Item<FormData>
data-field="name"
name="name"
rules={[
{
+77 -32
View File
@@ -1,9 +1,10 @@
import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { Form, Segmented } from 'antd';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import { excludeFields, gpusCountTypeMap, ScheduleValueMap } from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import { FormContext } from '../config/form-context';
@@ -14,13 +15,35 @@ import {
SourceType
} from '../config/types';
import { generateGPUIds } from '../config/utils';
import useFieldScroll from '../hooks/use-field-scroll';
import { useGenerateGPUOptions } from '../hooks/use-form-initial-values';
import useQueryBackends from '../hooks/use-query-backends';
import AdvanceConfig from './advance-config';
import BasicForm from './basic';
import Performance from './performance';
const requiredFields = ['gpu_selector', 'backend'];
const advancedRequiredFields = [
'gpu_selector',
'backend',
'image_name',
'run_command'
];
const performanceRequiredFields = ['speculative_config'];
const SegmentedInner = styled(Segmented)`
width: 100%;
border-radius: 0;
.ant-segmented-item {
flex: 1;
}
`;
const SegmentedHeader = styled.div`
position: sticky;
top: 0;
z-index: 10;
margin-bottom: 16px;
`;
interface DataFormProps {
initialValues?: any;
@@ -64,6 +87,31 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const performanceRef = React.useRef<HTMLDivElement>(null);
const advanceRef = React.useRef<HTMLDivElement>(null);
const segmentOptions = [
{
value: 'basic',
label: intl.formatMessage({ id: 'common.title.basicInfo' }),
field: 'name'
},
{
value: 'performance',
label: intl.formatMessage({ id: 'models.form.performance' }),
field: 'extended_kv_cache.enabled'
},
{
value: 'advanced',
label: intl.formatMessage({ id: 'resources.form.advanced' }),
field: 'categories'
}
];
const { scrollToSegment } = useFieldScroll({
form,
activeKey,
setActiveKey,
segmentOptions
});
const handleSumit = () => {
form.submit();
};
@@ -155,21 +203,34 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const handleOnFinishFailed = (errorInfo: any) => {
const { errorFields } = errorInfo;
if (errorFields && errorFields.length > 0) {
const collapseKeys: string[] = [];
const names = errorFields.map((item: any) => item.name[0]);
const isRequired = names.some((name: string) =>
requiredFields.includes(name)
const isAdvancedRequired = names.some((name: string) =>
advancedRequiredFields.includes(name)
);
if (isRequired) {
setActiveKey(['advanced']);
const isPerformanceRequired = names.some((name: string) =>
performanceRequiredFields.includes(name)
);
if (isPerformanceRequired) {
collapseKeys.push('performance');
}
if (isAdvancedRequired) {
collapseKeys.push('advanced');
}
setActiveKey((prev: string[]) => [
...new Set([...prev, ...collapseKeys])
]);
}
};
const handleTargetChange = (val: string) => {
form.scrollToField(val, {
behavior: 'smooth',
block: 'center'
});
const handleTargetChange = async (val: any) => {
setTarget(val);
await scrollToSegment(val, { offsetTop: 96 });
};
useImperativeHandle(ref, () => {
@@ -214,30 +275,14 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onBackendChange: handleBackendChange
}}
>
{/* <div
className="m-b-8"
style={{ position: 'sticky', top: 0, zIndex: 100 }}
>
<Segmented
<SegmentedHeader>
<SegmentedInner
defaultValue="basic"
value={target}
defaultValue="Basic"
onChange={handleTargetChange}
options={[
{
value: 'name',
label: 'Basic'
},
{
value: 'categories',
label: 'Performance'
},
{
value: 'categories',
label: 'Advanced'
}
]}
options={segmentOptions}
/>
</div> */}
</SegmentedHeader>
<Form
name="deployModel"
form={form}
+1 -1
View File
@@ -14,10 +14,10 @@ const KVCacheForm = () => {
<>
<div style={{ paddingBottom: 22 }}>
<Form.Item<FormData>
data-field="extended_kv_cache.enabled"
name={['extended_kv_cache', 'enabled']}
valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
>
<CheckboxField
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })}
@@ -0,0 +1,73 @@
import { useCallback } from 'react';
interface ScrollOptions {
wait?: number;
behavior?: 'smooth' | 'auto';
block?: 'start' | 'end' | 'center';
offsetTop?: number;
}
export default function useScrollAfterExpand({
form,
activeKey,
setActiveKey,
segmentOptions,
defaultWait = 300
}: {
form: any;
activeKey: string[];
setActiveKey: React.Dispatch<React.SetStateAction<string[]>>;
segmentOptions: { value: string; field: string }[];
defaultWait?: number;
}) {
const scrollToElement = useCallback(
(
el: HTMLElement,
{ behavior = 'smooth', offsetTop = 0 }: ScrollOptions = {}
) => {
// find the nearest scrollable parent
const scrollParent = (() => {
let node: HTMLElement | null = el;
while (node) {
const { overflowY } = getComputedStyle(node);
if (overflowY === 'auto' || overflowY === 'scroll') return node;
node = node.parentElement;
}
return document.scrollingElement || document.documentElement;
})();
const parentRect = scrollParent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const top =
elRect.top - parentRect.top + scrollParent.scrollTop - offsetTop;
scrollParent.scrollTo({ top, behavior });
},
[]
);
const scrollToSegment = useCallback(
async (val: string, options?: ScrollOptions) => {
if (!activeKey.includes(val)) {
setActiveKey((prev) => [...prev, val]);
await new Promise((r) => {
setTimeout(r, options?.wait ?? defaultWait);
});
}
await new Promise(requestAnimationFrame);
const current = segmentOptions.find((item) => item.value === val);
if (!current?.field) return;
const el = document.querySelector(
`[data-field="${current.field}"]`
) as HTMLElement | null;
if (el) scrollToElement(el, options);
},
[activeKey, setActiveKey, segmentOptions, defaultWait, scrollToElement]
);
return { scrollToSegment };
}