fix: calc scrollheight dynamically for tabs change

This commit is contained in:
jialin
2025-11-10 20:40:15 +08:00
parent 285e274818
commit 9eb1dba4a1
14 changed files with 283 additions and 77 deletions
+32 -8
View File
@@ -217,12 +217,6 @@ export default function useOverlayScroller(data?: {
// const currentScroll = instanceRef.current?.scroll().position.y; // const currentScroll = instanceRef.current?.scroll().position.y;
const targetPos = targetRect.top - containerRect.top + currentScroll; const targetPos = targetRect.top - containerRect.top + currentScroll;
console.log(
'target=======',
currentScroll,
targetPos,
instanceRef.current?.options()
);
scrollEventElement.current.scroll({ scrollEventElement.current.scroll({
y: targetPos - offset, y: targetPos - offset,
@@ -231,6 +225,35 @@ export default function useOverlayScroller(data?: {
instanceRef.current?.update?.(); instanceRef.current?.update?.();
}; };
const getScrollElementScrollableHeight = () => {
if (!instanceRef.current || !scrollEventElement.current) {
instanceRef.current = instance?.();
scrollEventElement.current =
instanceRef.current?.elements()?.scrollEventElement;
}
const scrollerState = instanceRef.current?.state();
const overflowAmount = scrollerState?.overflowAmount;
const scrollOffsetElement = instanceRef.current?.elements().viewport;
console.log('osInstance==========', overflowAmount, scrollOffsetElement, {
top:
Math.round(
(overflowAmount?.y - scrollOffsetElement?.scrollTop || 0) /
overflowAmount?.y
) * overflowAmount?.y,
scrollTop: scrollOffsetElement?.scrollTop,
scrollLeft: scrollOffsetElement?.scrollLeft,
scrollHeight:
scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight
});
return {
scrollTop: scrollOffsetElement?.scrollTop,
scrollHeight:
scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight
};
};
useEffect(() => { useEffect(() => {
return () => { return () => {
instanceRef.current?.destroy?.(); instanceRef.current?.destroy?.();
@@ -240,9 +263,10 @@ export default function useOverlayScroller(data?: {
return { return {
initialize: createInstance, initialize: createInstance,
instance: instanceRef.current, instance: instanceRef,
scrollEventElement: scrollEventElement.current, scrollEventElement: scrollEventElement,
initialized: initialized.current, initialized: initialized.current,
getScrollElementScrollableHeight,
generateInstance, generateInstance,
destroyInstance: destroyInstance, destroyInstance: destroyInstance,
updateScrollerPosition: throttledUpdateScrollerPosition, updateScrollerPosition: throttledUpdateScrollerPosition,
+4 -2
View File
@@ -45,7 +45,8 @@ export default {
'models.form.scheduletype': 'Schedule Type', 'models.form.scheduletype': 'Schedule Type',
'models.form.categories': 'Model Category', 'models.form.categories': 'Model Category',
'models.form.scheduletype.auto': 'Auto', 'models.form.scheduletype.auto': 'Auto',
'models.form.scheduletype.manual': 'Specify GPU', 'models.form.scheduletype.manual': 'Manual',
'models.form.scheduletype.gpu': 'Specify GPU',
'models.form.scheduletype.gpuType': 'Specify GPU Type', 'models.form.scheduletype.gpuType': 'Specify GPU Type',
'models.form.scheduletype.auto.tips': 'models.form.scheduletype.auto.tips':
'Automatically deploys model instances to appropriate GPUs based on current resource conditions.', 'Automatically deploys model instances to appropriate GPUs based on current resource conditions.',
@@ -242,5 +243,6 @@ export default {
'models.form.rules.name': 'models.form.rules.name':
'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.',
'models.catalog.button.explore': 'Explore Other Models', 'models.catalog.button.explore': 'Explore Other Models',
'models.catalog.precision': 'Precision' 'models.catalog.precision': 'Precision',
'models.form.gpuPerReplica.tips': 'Enter a custom number'
}; };
+7 -3
View File
@@ -47,7 +47,8 @@ export default {
'models.form.scheduletype': 'スケジュールタイプ', 'models.form.scheduletype': 'スケジュールタイプ',
'models.form.categories': 'モデルカテゴリ', 'models.form.categories': 'モデルカテゴリ',
'models.form.scheduletype.auto': '自動', 'models.form.scheduletype.auto': '自動',
'models.form.scheduletype.manual': 'GPUを指定', 'models.form.scheduletype.manual': '手動',
'models.form.scheduletype.gpu': 'GPUを指定',
'models.form.scheduletype.gpuType': 'GPUタイプを指定', 'models.form.scheduletype.gpuType': 'GPUタイプを指定',
'models.form.scheduletype.auto.tips': 'models.form.scheduletype.auto.tips':
'現在のリソース状況に基づいて、モデルインスタンスを適切なGPUに自動的にデプロイします。', '現在のリソース状況に基づいて、モデルインスタンスを適切なGPUに自動的にデプロイします。',
@@ -242,7 +243,8 @@ export default {
'models.form.rules.name': 'models.form.rules.name':
'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.',
'models.catalog.button.explore': 'Explore Other Models', 'models.catalog.button.explore': 'Explore Other Models',
'models.catalog.precision': 'Precision' 'models.catalog.precision': 'Precision',
'models.form.gpuPerReplica.tips': 'Enter a custom number'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -313,5 +315,7 @@ export default {
// 48. 'models.form.backend.custom': 'User Defined', // 48. 'models.form.backend.custom': 'User Defined',
// 49. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', // 49. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.',
// 50. 'models.catalog.button.explore': 'Explore Other Models', // 50. 'models.catalog.button.explore': 'Explore Other Models',
// 51. 'models.catalog.precision': 'Precision' // 51. 'models.catalog.precision': 'Precision',
// 52. 'models.form.gpuPerReplica.tips': 'Enter a custom number'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+6 -3
View File
@@ -45,7 +45,8 @@ export default {
'models.form.scheduletype': 'Тип планирования', 'models.form.scheduletype': 'Тип планирования',
'models.form.categories': 'Категория модели', 'models.form.categories': 'Категория модели',
'models.form.scheduletype.auto': 'Авто', 'models.form.scheduletype.auto': 'Авто',
'models.form.scheduletype.manual': 'Указать GPU', 'models.form.scheduletype.manual': 'Ручной',
'models.form.scheduletype.gpu': 'Указать GPU',
'models.form.scheduletype.gpuType': 'Указать тип GPU', 'models.form.scheduletype.gpuType': 'Указать тип GPU',
'models.form.scheduletype.auto.tips': 'models.form.scheduletype.auto.tips':
'Автоматическое развертывание инстансов модели на подходящие GPU в зависимости от текущих ресурсов.', 'Автоматическое развертывание инстансов модели на подходящие GPU в зависимости от текущих ресурсов.',
@@ -243,9 +244,11 @@ export default {
'models.form.rules.name': 'models.form.rules.name':
'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.',
'models.catalog.button.explore': 'Изучить другие модели', 'models.catalog.button.explore': 'Изучить другие модели',
'models.catalog.precision': 'Точность' 'models.catalog.precision': 'Точность',
'models.form.gpuPerReplica.tips': 'Enter a custom number'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.' // 1. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.',
// 2. 'models.form.gpuPerReplica.tips': 'Enter a custom number'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+4 -2
View File
@@ -46,7 +46,8 @@ export default {
'models.form.categories': '模型类别', 'models.form.categories': '模型类别',
'models.form.scheduletype': '调度方式', 'models.form.scheduletype': '调度方式',
'models.form.scheduletype.auto': '自动', 'models.form.scheduletype.auto': '自动',
'models.form.scheduletype.manual': '指定 GPU', 'models.form.scheduletype.manual': '手动',
'models.form.scheduletype.gpu': '指定 GPU',
'models.form.scheduletype.gpuType': '指定 GPU 类型', 'models.form.scheduletype.gpuType': '指定 GPU 类型',
'models.form.scheduletype.auto.tips': 'models.form.scheduletype.auto.tips':
'自动根据当前资源情况部署模型实例到合适的 GPU 上。', '自动根据当前资源情况部署模型实例到合适的 GPU 上。',
@@ -229,5 +230,6 @@ export default {
'models.form.rules.name': 'models.form.rules.name':
'长度不超过 63 个字符,只能包含字母、数字、点(.)、下划线(_)和连字符(-),且必须以字母或数字开头和结尾。', '长度不超过 63 个字符,只能包含字母、数字、点(.)、下划线(_)和连字符(-),且必须以字母或数字开头和结尾。',
'models.catalog.button.explore': '浏览其他模型', 'models.catalog.button.explore': '浏览其他模型',
'models.catalog.precision': '精度' 'models.catalog.precision': '精度',
'models.form.gpuPerReplica.tips': '输入自定义数值'
}; };
+16 -3
View File
@@ -22,8 +22,14 @@ const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
styles = {} styles = {}
}) => { }) => {
const scroller = React.useRef<any>(null); const scroller = React.useRef<any>(null);
const { initialize, instance, scrollToBottom, scrollToTarget } = const {
useOverlayScroller({ initialize,
instance,
scrollEventElement,
scrollToBottom,
scrollToTarget,
getScrollElementScrollableHeight
} = useOverlayScroller({
options: { options: {
scrollbars: { scrollbars: {
autoHide: 'move' autoHide: 'move'
@@ -39,7 +45,14 @@ const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
return ( return (
<WrapperContext.Provider <WrapperContext.Provider
value={{ osInstance: instance, scrollToBottom, scrollToTarget }} value={{
scroller: scroller,
osInstance: instance,
scrollEventElement,
getScrollElementScrollableHeight,
scrollToBottom,
scrollToTarget
}}
> >
<div <div
className="column-wrapper-footer" className="column-wrapper-footer"
@@ -2,8 +2,14 @@ import { createContext, useContext } from 'react';
interface WrapperContextProps { interface WrapperContextProps {
osInstance?: any; osInstance?: any;
scroller?: any;
scrollEventElement?: any;
scrollToBottom?: () => void; scrollToBottom?: () => void;
scrollToTop?: () => void; scrollToTop?: () => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
scrollToTarget?: (target: any, offset?: number) => void; scrollToTarget?: (target: any, offset?: number) => void;
} }
+3 -2
View File
@@ -29,7 +29,7 @@ const BackendFields: React.FC = () => {
const { onValuesChange, backendOptions, onBackendChange } = useFormContext(); const { onValuesChange, backendOptions, onBackendChange } = useFormContext();
const backend = Form.useWatch('backend', form); const backend = Form.useWatch('backend', form);
const handleBackendVersionOnBlur = () => { const handleBackendVersionOnChange = () => {
onValuesChange?.({}, form.getFieldsValue()); onValuesChange?.({}, form.getFieldsValue());
}; };
@@ -137,13 +137,14 @@ const BackendFields: React.FC = () => {
{backendOptionsMap.custom !== backend && ( {backendOptionsMap.custom !== backend && (
<Form.Item name="backend_version"> <Form.Item name="backend_version">
<SealSelect <SealSelect
allowClear
options={backendVersions} options={backendVersions}
optionRender={optionRender} optionRender={optionRender}
labelRender={labelRender} labelRender={labelRender}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'models.form.backendVersion.holder' id: 'models.form.backendVersion.holder'
})} })}
onBlur={handleBackendVersionOnBlur} onChange={handleBackendVersionOnChange}
label={intl.formatMessage({ id: 'models.form.backendVersion' })} label={intl.formatMessage({ id: 'models.form.backendVersion' })}
description={intl.formatMessage( description={intl.formatMessage(
{ {
+4 -15
View File
@@ -4,33 +4,22 @@ import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React from 'react'; import React from 'react';
import { DeployFormKeyMap } from '../config'; import { DeployFormKeyMap } from '../config';
import { useCatalogFormContext, useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
/**
* It can access some props from CatalogFormContext
*/
const CatalogForm: React.FC = () => { const CatalogForm: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const formCtx = useFormContext(); const formCtx = useFormContext();
const catalogFormCtx = useCatalogFormContext();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const { formKey } = formCtx; const { formKey } = formCtx;
const {
sizeOptions,
quantizationOptions,
onSizeChange,
onQuantizationChange
} = catalogFormCtx;
if (formKey !== DeployFormKeyMap.CATALOG) { if (formKey !== DeployFormKeyMap.CATALOG) {
return null; return null;
} }
const handleSizeChange = (val: any) => {
onSizeChange?.(val);
};
const handleOnQuantizationChange = (val: any) => {
onQuantizationChange?.(val);
};
return ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
+27 -17
View File
@@ -1,6 +1,7 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
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 { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form, Segmented } from 'antd'; import { Form, Segmented } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
@@ -72,6 +73,13 @@ interface DataFormProps {
onBackendChange?: (value: string) => void; onBackendChange?: (value: string) => void;
} }
const TABKeysMap = {
BASIC: 'basic',
SCHEDULING: 'scheduling',
PERFORMANCE: 'performance',
ADVANCED: 'advanced'
};
const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => { const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const { const {
action, action,
@@ -89,6 +97,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onValuesChange, onValuesChange,
onOk onOk
} = props; } = props;
const { getScrollElementScrollableHeight } = useWrapperContext();
const { backendOptions, getBackendOptions } = useQueryBackends(); const { backendOptions, getBackendOptions } = useQueryBackends();
const { getGPUOptionList, gpuOptions, workerLabelOptions } = const { getGPUOptionList, gpuOptions, workerLabelOptions } =
useGenerateGPUOptions(); useGenerateGPUOptions();
@@ -96,14 +105,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]); const [activeKey, setActiveKey] = React.useState<string[]>([]);
const scheduleType = Form.useWatch('scheduleType', form); const scheduleType = Form.useWatch('scheduleType', form);
const [target, setTarget] = React.useState<string>('basic'); const [target, setTarget] = React.useState<string>(TABKeysMap.BASIC);
const TABKeysMap = {
BASIC: 'basic',
SCHEDULING: 'scheduling',
PERFORMANCE: 'performance',
ADVANCED: 'advanced'
};
const segmentOptions = [ const segmentOptions = [
{ {
@@ -128,14 +130,12 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
]; ];
const { scrollToSegment } = useFieldScroll({ console.log(
form, 'getScrollElementScrollableHeight',
activeKey, getScrollElementScrollableHeight?.()
setActiveKey, );
segmentOptions
});
const SegmentedTop = useMemo(() => { const segmentedTop = useMemo(() => {
if ( if (
modelSourceMap.local_path_value === source || modelSourceMap.local_path_value === source ||
action === PageAction.EDIT || action === PageAction.EDIT ||
@@ -153,6 +153,15 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
}; };
}, [source, formKey, action]); }, [source, formKey, action]);
const { scrollToSegment, holderHeight } = useFieldScroll({
form,
activeKey,
setActiveKey,
segmentOptions,
segmentedTop: segmentedTop,
getScrollElementScrollableHeight: getScrollElementScrollableHeight
});
const handleSumit = () => { const handleSumit = () => {
form.submit(); form.submit();
}; };
@@ -288,7 +297,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const handleTargetChange = async (val: any) => { const handleTargetChange = async (val: any) => {
setTarget(val); setTarget(val);
await scrollToSegment(val, { offsetTop: SegmentedTop.offsetTop }); await scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
}; };
const handleOnFinishFailed = (errorInfo: any) => { const handleOnFinishFailed = (errorInfo: any) => {
@@ -389,7 +398,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onBackendChange: handleBackendChange onBackendChange: handleBackendChange
}} }}
> >
<SegmentedHeader $top={SegmentedTop.top}> <SegmentedHeader $top={segmentedTop.top}>
<SegmentedInner <SegmentedInner
defaultValue="basic" defaultValue="basic"
value={target} value={target}
@@ -465,6 +474,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
} }
]} ]}
></CollapsePanel> ></CollapsePanel>
<div className="holder" style={{ height: holderHeight }}></div>
</Form> </Form>
</FormContext.Provider> </FormContext.Provider>
); );
+41 -6
View File
@@ -5,9 +5,10 @@ import SealSelect from '@/components/seal-form/seal-select';
import TooltipList from '@/components/tooltip-list'; import TooltipList from '@/components/tooltip-list';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form, InputNumber } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import GPUCard from '../components/gpu-card'; import GPUCard from '../components/gpu-card';
import { import {
placementStrategyOptions, placementStrategyOptions,
@@ -18,6 +19,10 @@ import { backendOptionsMap } from '../config/backend-parameters';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
const InputWrapper = styled.div`
padding: 8px 4px;
`;
const placementStrategyTips = [ const placementStrategyTips = [
{ {
title: 'Spread', title: 'Spread',
@@ -70,6 +75,10 @@ const ScheduleTypeForm: React.FC = () => {
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const scheduleType = Form.useWatch('scheduleType', form); const scheduleType = Form.useWatch('scheduleType', form);
const workerSelector = Form.useWatch('worker_selector', form); const workerSelector = Form.useWatch('worker_selector', form);
const GPUsPerReplicas = Form.useWatch(
['gpu_selector', 'gpus_per_replica'],
form
);
const handleScheduleTypeChange = async (value: string) => { const handleScheduleTypeChange = async (value: string) => {
if (value === ScheduleValueMap.Auto) { if (value === ScheduleValueMap.Auto) {
@@ -81,6 +90,15 @@ const ScheduleTypeForm: React.FC = () => {
form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null); form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null);
} }
}; };
const handleGpusPerReplicasChange = (val: string | number | null) => {
if (val === null) {
form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null);
} else {
form.setFieldValue(['gpu_selector', 'gpus_per_replica'], val);
}
onValuesChange?.({}, form.getFieldsValue());
};
const handleGpuSelectorChange = (value: any[]) => { const handleGpuSelectorChange = (value: any[]) => {
if (value.length > 0) { if (value.length > 0) {
@@ -191,15 +209,32 @@ const ScheduleTypeForm: React.FC = () => {
{ label: '2', value: 2 }, { label: '2', value: 2 },
{ label: '4', value: 4 }, { label: '4', value: 4 },
{ label: '8', value: 8 }, { label: '8', value: 8 },
{ label: '16', value: 16 }, { label: '16', value: 16 }
{ label: '32', value: 32 },
{ label: '64', value: 64 },
{ label: '128', value: 128 },
{ label: '256', value: 256 }
]} ]}
description={ description={
<TooltipList list={GPUsPerReplicaTips}></TooltipList> <TooltipList list={GPUsPerReplicaTips}></TooltipList>
} }
popupRender={(originNode) => (
<div>
{originNode}
<InputWrapper>
<InputNumber
min={1}
max={16}
step={1}
style={{ width: '100%' }}
defaultValue={
GPUsPerReplicas === -1 ? null : GPUsPerReplicas
}
placeholder={intl.formatMessage({
id: 'models.form.gpuPerReplica.tips'
})}
value={GPUsPerReplicas === -1 ? null : GPUsPerReplicas}
onChange={handleGpusPerReplicasChange}
/>
</InputWrapper>
</div>
)}
/> />
</Form.Item> </Form.Item>
</> </>
+56 -9
View File
@@ -1,4 +1,5 @@
import { useCallback } from 'react'; import { useMemoizedFn } from 'ahooks';
import { useCallback, useRef, useState } from 'react';
interface ScrollOptions { interface ScrollOptions {
wait?: number; wait?: number;
@@ -12,14 +13,27 @@ export default function useScrollAfterExpand({
activeKey, activeKey,
setActiveKey, setActiveKey,
segmentOptions, segmentOptions,
defaultWait = 300 defaultWait = 300,
segmentedTop = { top: 0, offsetTop: 96 },
getScrollElementScrollableHeight
}: { }: {
form: any; form: any;
activeKey: string[]; activeKey: string[];
setActiveKey: React.Dispatch<React.SetStateAction<string[]>>; setActiveKey: React.Dispatch<React.SetStateAction<string[]>>;
segmentOptions: { value: string; field: string }[]; segmentOptions: { value: string; field: string }[];
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
defaultWait?: number; defaultWait?: number;
segmentedTop: {
top: number;
offsetTop: number;
};
}) { }) {
const [holderHeight, setHolderHeight] = useState<number>(0);
const boxHeightRef = useRef<number>(0);
const scrollToElement = useCallback( const scrollToElement = useCallback(
( (
el: HTMLElement, el: HTMLElement,
@@ -46,7 +60,11 @@ export default function useScrollAfterExpand({
[] []
); );
const scrollToSegment = useCallback( /**
* due to the scrollheight changes after expanding the segment and including the holder height.
*
*/
const scrollToSegment = useMemoizedFn(
async (val: string, options?: ScrollOptions) => { async (val: string, options?: ScrollOptions) => {
if (!activeKey.includes(val)) { if (!activeKey.includes(val)) {
setActiveKey((prev) => [...prev, val]); setActiveKey((prev) => [...prev, val]);
@@ -55,19 +73,48 @@ export default function useScrollAfterExpand({
}); });
} }
await new Promise(requestAnimationFrame);
const current = segmentOptions.find((item) => item.value === val); const current = segmentOptions.find((item) => item.value === val);
if (!current?.field) return; if (!current?.field) return;
const el = document.querySelector( await new Promise(requestAnimationFrame);
const el: HTMLElement | null = document.querySelector(
`[data-field="${current.field}"]` `[data-field="${current.field}"]`
) as HTMLElement | null; ) as HTMLElement | null;
const targetRectTop = el?.getBoundingClientRect().top || 0;
const scroller = getScrollElementScrollableHeight?.() || {
scrollHeight: 0,
scrollTop: 0
};
// remaining scroll height
const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop;
// total distance from the top of the scroller to the target element
const offsetDistance =
targetRectTop - segmentedTop.offsetTop - segmentedTop.top;
let boxHeight = 0;
// verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight
if (offsetDistance <= 0) {
boxHeight = boxHeightRef.current;
} else {
boxHeight =
offsetDistance - remainingScrollHeight + boxHeightRef.current;
}
// update boxHeightRef
boxHeightRef.current = boxHeight;
setHolderHeight(boxHeight);
await new Promise(requestAnimationFrame);
if (el) scrollToElement(el, options); if (el) scrollToElement(el, options);
}, }
[activeKey, setActiveKey, segmentOptions, defaultWait, scrollToElement]
); );
return { scrollToSegment }; return { scrollToSegment, holderHeight };
} }
@@ -17,11 +17,11 @@ export default function useQueryBackends() {
value: item.backend_name, value: item.backend_name,
label: label:
item.backend_name === backendOptionsMap.custom item.backend_name === backendOptionsMap.custom
? intl.formatMessage({ id: 'backend.quickConfig' }) ? intl.formatMessage({ id: 'backend.custom' })
: item.backend_name, : item.backend_name,
title: title:
item.backend_name === backendOptionsMap.custom item.backend_name === backendOptionsMap.custom
? intl.formatMessage({ id: 'backend.quickConfig' }) ? intl.formatMessage({ id: 'backend.custom' })
: item.backend_name.replace(/-custom$/, ''), : item.backend_name.replace(/-custom$/, ''),
default_backend_param: item.default_backend_param || [], default_backend_param: item.default_backend_param || [],
default_version: item.default_version, default_version: item.default_version,
@@ -0,0 +1,70 @@
import HighlightCode from '@/components/highlight-code';
import { useIntl } from '@umijs/max';
import React, { useMemo } from 'react';
import { addWorkerGuide } from '../config';
type ViewModalProps = { token: string };
const AddWorker: React.FC<ViewModalProps> = (props) => {
const intl = useIntl();
const origin = window.location.origin;
const labels = useMemo(
() => ({
step1: intl.formatMessage({ id: 'resources.worker.add.step1' }),
step2: intl.formatMessage({ id: 'resources.worker.add.step2' }),
step2Tips: intl.formatMessage({ id: 'resources.worker.add.step2.tips' }),
step3: intl.formatMessage({ id: 'resources.worker.add.step3' }),
linuxOrMac: intl.formatMessage({ id: 'resources.worker.linuxormaxos' })
}),
[intl]
);
return (
<div className="script-install">
<h3 className="font-size-14 font-600">
1. <span dangerouslySetInnerHTML={{ __html: labels.step1 }}></span>
</h3>
<h4 className="font-size-13">{labels.linuxOrMac}</h4>
<HighlightCode
code={addWorkerGuide.mac.getToken}
theme="dark"
></HighlightCode>
<h4 className="m-t-6 font-size-13">Windows </h4>
<HighlightCode
code={addWorkerGuide.win.getToken}
theme="dark"
></HighlightCode>
<h3 className="m-t-10 font-size-14 font-600">
2. {labels.step2}{' '}
<span
className="font-size-12"
style={{ color: 'var(--ant-color-text-tertiary)' }}
dangerouslySetInnerHTML={{
__html: `${labels.step2Tips}`
}}
></span>
</h3>
<h4 className="font-size-13">{labels.linuxOrMac}</h4>
<HighlightCode
code={addWorkerGuide.mac.registerWorker({
server: origin,
token: '${token}'
})}
theme="dark"
></HighlightCode>
<h4 className="m-t-6 font-size-13">Windows</h4>
<HighlightCode
theme="dark"
code={addWorkerGuide.win.registerWorker({
server: origin,
token: '${token}'
})}
></HighlightCode>
<h3 className="m-b-0 m-t-10 font-size-14 font-600">3. {labels.step3}</h3>
</div>
);
};
export default AddWorker;