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) ..._.pickBy(query || queryParams, (val: any) => !!val)
}; };
const res = await fetchAPI(params); const res = await fetchAPI(params);
if (!dataSource.loadend) {
// add a delay to avoid flash
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
}
if ( if (
!res.items.length && !res.items.length &&
params.page > res.pagination.totalPage && params.page > res.pagination.totalPage &&
@@ -139,6 +145,7 @@ export default function useTableFetch<T>(
page: res.pagination.totalPage page: res.pagination.totalPage
}; };
const newRes = await fetchAPI(newParams); const newRes = await fetchAPI(newParams);
setDataSource({ setDataSource({
dataList: loadmore dataList: loadmore
? [...dataSource.dataList, ...(newRes.items || [])] ? [...dataSource.dataList, ...(newRes.items || [])]
+7 -1
View File
@@ -81,7 +81,13 @@ const AdvanceConfig = () => {
return ( return (
<> <>
<Form.Item<FormData> name="categories"> <Form.Item<FormData>
name="categories"
data-field="categories"
style={{
scrollMarginTop: 200
}}
>
<SealSelect <SealSelect
allowNull allowNull
label={intl.formatMessage({ label={intl.formatMessage({
+1
View File
@@ -38,6 +38,7 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
return ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
data-field="name"
name="name" name="name"
rules={[ rules={[
{ {
+77 -32
View File
@@ -1,9 +1,10 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel'; import CollapsePanel from '@/pages/_components/collapse-panel';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form, Segmented } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import { excludeFields, gpusCountTypeMap, ScheduleValueMap } from '../config'; import { excludeFields, gpusCountTypeMap, ScheduleValueMap } from '../config';
import { backendOptionsMap } from '../config/backend-parameters'; import { backendOptionsMap } from '../config/backend-parameters';
import { FormContext } from '../config/form-context'; import { FormContext } from '../config/form-context';
@@ -14,13 +15,35 @@ import {
SourceType SourceType
} from '../config/types'; } from '../config/types';
import { generateGPUIds } from '../config/utils'; import { generateGPUIds } from '../config/utils';
import useFieldScroll from '../hooks/use-field-scroll';
import { useGenerateGPUOptions } from '../hooks/use-form-initial-values'; import { useGenerateGPUOptions } from '../hooks/use-form-initial-values';
import useQueryBackends from '../hooks/use-query-backends'; import useQueryBackends from '../hooks/use-query-backends';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
import BasicForm from './basic'; import BasicForm from './basic';
import Performance from './performance'; 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 { interface DataFormProps {
initialValues?: any; initialValues?: any;
@@ -64,6 +87,31 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const performanceRef = React.useRef<HTMLDivElement>(null); const performanceRef = React.useRef<HTMLDivElement>(null);
const advanceRef = 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 = () => { const handleSumit = () => {
form.submit(); form.submit();
}; };
@@ -155,21 +203,34 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const handleOnFinishFailed = (errorInfo: any) => { const handleOnFinishFailed = (errorInfo: any) => {
const { errorFields } = errorInfo; const { errorFields } = errorInfo;
if (errorFields && errorFields.length > 0) { if (errorFields && errorFields.length > 0) {
const collapseKeys: string[] = [];
const names = errorFields.map((item: any) => item.name[0]); const names = errorFields.map((item: any) => item.name[0]);
const isRequired = names.some((name: string) => const isAdvancedRequired = names.some((name: string) =>
requiredFields.includes(name) 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) => { const handleTargetChange = async (val: any) => {
form.scrollToField(val, { setTarget(val);
behavior: 'smooth',
block: 'center' await scrollToSegment(val, { offsetTop: 96 });
});
}; };
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
@@ -214,30 +275,14 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onBackendChange: handleBackendChange onBackendChange: handleBackendChange
}} }}
> >
{/* <div <SegmentedHeader>
className="m-b-8" <SegmentedInner
style={{ position: 'sticky', top: 0, zIndex: 100 }} defaultValue="basic"
>
<Segmented
value={target} value={target}
defaultValue="Basic"
onChange={handleTargetChange} onChange={handleTargetChange}
options={[ options={segmentOptions}
{
value: 'name',
label: 'Basic'
},
{
value: 'categories',
label: 'Performance'
},
{
value: 'categories',
label: 'Advanced'
}
]}
/> />
</div> */} </SegmentedHeader>
<Form <Form
name="deployModel" name="deployModel"
form={form} form={form}
+1 -1
View File
@@ -14,10 +14,10 @@ const KVCacheForm = () => {
<> <>
<div style={{ paddingBottom: 22 }}> <div style={{ paddingBottom: 22 }}>
<Form.Item<FormData> <Form.Item<FormData>
data-field="extended_kv_cache.enabled"
name={['extended_kv_cache', 'enabled']} name={['extended_kv_cache', 'enabled']}
valuePropName="checked" valuePropName="checked"
style={{ padding: '0 10px', marginBottom: 0 }} style={{ padding: '0 10px', marginBottom: 0 }}
noStyle
> >
<CheckboxField <CheckboxField
label={intl.formatMessage({ id: 'models.form.extendedkvcache' })} 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 };
}