ahooks#useMemoizedFn TypeScript Examples
The following examples show how to use
ahooks#useMemoizedFn.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: use-url-state.ts From bext with MIT License | 7 votes |
useUrlState = <S extends UrlState = UrlState>(
initialState?: S | (() => S),
options?: Options,
) => {
type State = Partial<{ [key in keyof S]: any }>;
const { navigateMode = 'push' } = options || {};
const history = useHistory();
const update = useUpdate();
const initialStateRef = useRef(
typeof initialState === 'function'
? (initialState as () => S)()
: initialState || {},
);
const queryFromUrl = useMemo(() => {
return parse(location.search, parseConfig);
}, [location.search]);
const targetQuery: State = useMemo(
() => ({
...initialStateRef.current,
...queryFromUrl,
}),
[queryFromUrl],
);
const setState = (s: React.SetStateAction<State>) => {
const newQuery = typeof s === 'function' ? s(targetQuery) : s;
update();
history[navigateMode]({
hash: location.hash,
search: stringify({ ...queryFromUrl, ...newQuery }, parseConfig) || '?',
});
};
return [targetQuery, useMemoizedFn(setState)] as const;
}
Example #2
Source File: BatchExecutorPageEx.tsx From yakit with GNU Affero General Public License v3.0 | 5 votes |
BatchExecutorPageEx: React.FC<BatchExecutorPageExProp> = (props) => {
const [simpleQuery, setSimpleQuery] = useState<SimpleQueryYakScriptSchema>({
exclude: [],
include: [],
tags: "",
type: "mitm,port-scan,nuclei"
})
const [loading, setLoading] = useState<boolean>(false)
const [allTag, setAllTag] = useState<FieldName[]>([])
const [isAll, setIsAll] = useState<boolean>(false)
const [historyTask, setHistoryTask] = useState<string>("")
useEffect(() => updateAllTag(), [])
const updateAllTag = () => {
setLoading(true)
ipcRenderer.invoke("GetAvailableYakScriptTags", {}).then((data: Fields) => {
if(data && data.Values) setAllTag(data.Values)
}).catch(e => console.info(e))
.finally(() => setTimeout(() => setLoading(false), 300))
}
const forceUpdateAllTag = () => {
setLoading(true)
ipcRenderer.invoke("ForceUpdateAvailableYakScriptTags", {}).then((data: Fields) => {
if(data && data.Values) setAllTag(data.Values)
}).catch(e => console.info(e))
.finally(() => setTimeout(() => setLoading(false), 300))
}
const executeHistory = useMemoizedFn((info: NewTaskHistoryProps) => {
setLoading(true)
setSimpleQuery(info.simpleQuery)
setIsAll(info.isAll)
setHistoryTask(info.simpleQuery.tags)
setTimeout(() => setLoading(false), 300);
})
return (
<ResizeBox
firstNode={
<>
<QueryYakScriptParamSelector
params={simpleQuery}
onParams={(param) => {
setSimpleQuery({...param})
}}
loading={loading}
allTag={allTag}
onAllTag={forceUpdateAllTag}
isAll={isAll}
onIsAll={setIsAll}
historyTask={historyTask}
/>
</>
}
firstMinSize={300}
firstRatio={"300px"}
secondNode={<BatchExecuteByFilter
simpleQuery={simpleQuery}
allTag={allTag}
isAll={isAll}
executeHistory={executeHistory}
/>}
></ResizeBox>
)
}
Example #3
Source File: rich-editor.tsx From bext with MIT License | 5 votes |
RichEditor: FC<{
defaultHtml?: string;
defaultReadOnly?: boolean;
onChange?: (html: string) => void;
className?: string;
}> = (props) => {
const ref = useRef<HTMLDivElement>(null);
const getProps = useMemoizedFn(() => props);
useEffect(() => {
const { defaultHtml, defaultReadOnly } = getProps();
if (ref.current) {
ref.current.innerHTML = defaultHtml || '';
const quill = new Quill(ref.current, {
theme: 'snow',
modules: {
toolbar: defaultReadOnly
? false
: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ size: ['small', false, 'large', 'huge'] }],
[
{ align: [] },
'bold',
'italic',
'underline',
'strike',
'blockquote',
'code-block',
],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ script: 'sub' },
{ script: 'super' },
],
[{ indent: '-1' }, { indent: '+1' }],
[{ color: [] }, { background: [] }],
['link', 'image'],
['clean'],
],
},
readOnly: defaultReadOnly,
bounds: ref.current,
});
const toolbar = quill.getModule('toolbar');
if (toolbar) {
const originImageHandler = toolbar.handlers.image;
toolbar.handlers.image = function (...args: any[]) {
if (confirm('选择文件(确定)/输入图片链接(取消)')) {
originImageHandler?.call(this, ...args);
return;
}
const url = prompt('请输入图片链接');
const range = quill.getSelection();
if (url && range) {
quill.insertEmbed(range.index, 'image', url, Quill.sources.USER);
}
};
}
const handler = () => {
const el = ref.current?.querySelector('.ql-editor') as HTMLDivElement;
getProps()?.onChange?.(el.innerHTML || '');
};
quill.on('text-change', handler);
return () => quill.off('text-change', handler);
}
return noop;
}, []);
return <div ref={ref} className={props.className} />;
}
Example #4
Source File: SimplePluginList.tsx From yakit with GNU Affero General Public License v3.0 | 5 votes |
SimplePluginList: React.FC<SimplePluginListProp> = React.memo((props) => {
const [scripts, setScripts, getScripts] = useGetState<YakScript[]>([])
const [total, setTotal] = useState(0)
const [listNames, setListNames] = useState<string[]>([...(props.initialSelected || [])]);
// const [params, setParams] = useState<{ ScriptNames: string[] }>({ScriptNames: [...props.initialSelected || []]})
const [pluginLoading, setPluginLoading] = useState<boolean>(false)
const allSelectYakScript = useMemoizedFn((flag: boolean) => {
if (flag) {
const newSelected = [...scripts.map((i) => i.ScriptName), ...listNames]
setListNames([...newSelected.filter((e, index) => newSelected.indexOf(e) === index)])
} else {
setListNames([])
}
})
const selectYakScript = useMemoizedFn((y: YakScript) => {
listNames.push(y.ScriptName)
setListNames([...listNames])
})
const unselectYakScript = useMemoizedFn((y: YakScript) => {
const names = listNames.splice(listNames.indexOf(y.ScriptName), 1);
setListNames([...listNames])
})
useEffect(() => {
if (props.onSelected) {
props.onSelected([...listNames])
}
}, [listNames])
const search = useMemoizedFn((searchParams?: { limit: number; keyword: string }) => {
const {limit, keyword} = searchParams || {}
setPluginLoading(true)
queryYakScriptList(
props.pluginTypes ? props.pluginTypes : "",
(data, total) => {
setTotal(total || 0)
setScripts(data)
setListNames([...(data || []).filter(i => i.IsGeneralModule).map(i => i.ScriptName)])
},
() => setTimeout(() => setPluginLoading(false), 300),
limit || 200,
undefined,
keyword || "",
props.initialQuery,
)
})
useEffect(() => {
search()
}, [useDebounce(props.initialQuery, {wait: 500})])
return <PluginList
readOnly={props.readOnly}
bordered={props.bordered}
loading={pluginLoading}
lists={(scripts || []).sort((a: YakScript, b: YakScript) => {
return (b.IsGeneralModule ? 1 : 0) - (a.IsGeneralModule ? 1 : 0)
})}
disabled={props.disabled}
getLists={getScripts}
total={total}
selected={listNames}
allSelectScript={allSelectYakScript}
selectScript={selectYakScript}
unSelectScript={unselectYakScript}
search={search}
title={props?.verbose || "选择插件"}
bodyStyle={{
padding: "0 4px",
overflow: "hidden"
}}
/>
})
Example #5
Source File: use-history.ts From bext with MIT License | 5 votes |
useHistoryPush = () => {
const history = useHistory();
const [query] = useUrlState();
return useMemoizedFn((path: string, newQuery: Record<string, any> = {}) =>
history.push(`${path}?${queryStr(query, newQuery)}`),
);
}
Example #6
Source File: PluginExecutor.tsx From yakit with GNU Affero General Public License v3.0 | 5 votes |
PluginExecutor: React.FC<PluginExecutorProp> = (props) => {
const {script, settingShow, settingNode} = props;
const [token, setToken] = useState(randomString(40));
const [loading, setLoading] = useState(false);
const [infoState, {reset, setXtermRef}, xtermRef] = useHoldingIPCRStream(
script.ScriptName,
"exec-yak-script",
token,
() => {
setTimeout(() => setLoading(false), 300)
}
)
const executeByParams = useMemoizedFn((p: YakExecutorParam[]) => {
setLoading(true)
setTimeout(() => {
ipcRenderer.invoke("exec-yak-script", {
Params: [...p, ...(props.extraYakExecutorParams || [])],
YakScriptId: props.script.Id,
}, token)
}, 300);
})
return <div style={{height: "100%", display: "flex", flexFlow: "column"}}>
<PageHeader
title={script.ScriptName} style={{marginBottom: 0, paddingBottom: 0}}
subTitle={props.subTitle}
extra={props.extraNode}
>
{!!settingShow && settingNode}
<YakScriptParamsSetter
{...script}
loading={loading}
onParamsConfirm={executeByParams}
onClearData={() => {
xtermClear(xtermRef)
reset()
}}
onCanceled={() => {
ipcRenderer.invoke("cancel-exec-yak-script", token)
}}
styleSize={props.size}
submitVerbose={"开始执行"}
primaryParamsOnly={true}
/>
</PageHeader>
<Divider/>
<PluginResultUI
script={script} loading={loading} progress={infoState.processState} results={infoState.messageState}
featureType={infoState.featureTypeState}
feature={infoState.featureMessageState}
statusCards={infoState.statusState} onXtermRef={setXtermRef}
/>
</div>
}
Example #7
Source File: index.tsx From ui with MIT License | 4 votes |
LicenseKeyBoard: React.FC<LicenseKeyBoardProps> = p => {
const props = mergeProps(defaultProps, p);
const {
visible,
title,
value,
confirmText,
showCloseButton,
onInput,
} = props;
const keyboardRef = useRef<HTMLDivElement | null>(null);
const keys = useMemo(() => {
const defaultKeys = '京沪粤津浙苏湘渝云豫皖陕桂新青琼闽蒙辽宁鲁晋吉冀黑甘鄂赣贵川藏民使领警学港澳'.split(
'',
);
defaultKeys.push('BACKSPACE');
return defaultKeys;
}, []);
const smallVehicleNewEnergy = '1234567890';
const newEnergyLetter = 'ABCDEFGHJK';
const newEnergyLetterReg = new RegExp(`[${newEnergyLetter}]`);
/**
新能源车牌号规则:
https://zh.wikipedia.org/wiki/中华人民共和国民用机动车号牌#新能源汽车号牌
*/
const isNewEnergyPlate = (plate: string): false | string => {
if (isNewEnergyBigVehicle(plate)) {
return newEnergyLetter;
} else if (isNewEnergySmallVehicle(plate)) {
return smallVehicleNewEnergy;
}
return false;
};
const isNewEnergySmallVehicle = (plate: string) =>
newEnergyLetterReg.test(plate[2]) && /^[0-9]+$/.test(plate.slice(4, 7));
const isNewEnergyBigVehicle = (plate: string) =>
/^[0-9]+$/.test(plate.slice(2, 7));
const numberKeys = smallVehicleNewEnergy.split('');
const letterKeys = 'QWERTYUIOPASDFGHJKLZXCVBNM'.split('');
letterKeys.push('OK');
const specialKeys = '港澳警领应学挂'.split('');
specialKeys.push('BACKSPACE');
const timeoutRef = useRef(-1);
const intervalRef = useRef(-1);
const onDelete = useMemoizedFn(() => {
props.onDelete?.();
});
const onBackspacePressStart = () => {
timeoutRef.current = window.setTimeout(() => {
onDelete();
intervalRef.current = window.setInterval(onDelete, 150);
}, 700);
};
const onBackspacePressEnd = () => {
clearTimeout(timeoutRef.current);
clearInterval(intervalRef.current);
};
// 点击键盘按键
const onKeyPress = (
e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>,
key: string,
) => {
e.preventDefault();
switch (key) {
case 'BACKSPACE':
onDelete?.();
break;
case 'OK':
props.onConfirm?.();
if (props.closeOnConfirm) {
props.onClose?.();
}
break;
default:
if (key !== '') onInput?.(key);
break;
}
};
// 渲染 title 和 close button
const renderHeader = () => {
if (!showCloseButton && !title) return null;
return (
<div
className={classNames(`${classPrefix}-header`, {
'with-title': !!title,
})}
>
{title && <div className={`${classPrefix}-title`}>{title}</div>}
{showCloseButton && (
<span
className={`${classPrefix}-header-close-button`}
onClick={() => {
props.onClose?.();
}}
role="button"
title="CLOSE"
>
<Rotate angle={90}>
<Icon name={'kq-right'} size={32} color={'#999999'} />
</Rotate>
</span>
)}
</div>
);
};
// 渲染基础键盘按键
const renderKey = (key: string) => {
const isNumberKey = /^\d$/.test(key);
const isSpecialCharacters = /[港澳警领应学挂]/.test(key);
const isDisabledKey = /^(I|O)$/.test(key);
const className = classNames(`${classPrefix}-key`, {
'number-key': isNumberKey,
'sign-key': !isNumberKey && key,
'a-key': key === 'A',
'l-key': key === 'L',
'z-key': key === 'Z',
'm-key': key === 'M',
'ok-key': key === 'OK',
'del-key': key === 'BACKSPACE',
'disabled-key':
(value.length < 2 && (isNumberKey || isSpecialCharacters)) ||
isDisabledKey ||
((isNewEnergyPlate(value) ? value.length >= 8 : value.length >= 7) &&
key !== 'OK' &&
key !== 'BACKSPACE'),
});
return (
<div
key={key}
className={className}
onTouchStart={() => {
if (key === 'BACKSPACE') {
onBackspacePressStart();
}
}}
onTouchEnd={e => {
if (
(value.length < 2 && (isNumberKey || isSpecialCharacters)) ||
isDisabledKey ||
((isNewEnergyPlate(value)
? value.length >= 8
: value.length >= 7) &&
key !== 'OK' &&
key !== 'BACKSPACE')
)
return;
onKeyPress(e, key);
if (key === 'BACKSPACE') {
onBackspacePressEnd();
}
}}
onMouseUp={e => {
if (
(value.length < 2 && (isNumberKey || isSpecialCharacters)) ||
isDisabledKey ||
((isNewEnergyPlate(value)
? value.length >= 8
: value.length >= 7) &&
key !== 'OK' &&
key !== 'BACKSPACE')
)
return;
onKeyPress(e, key);
}}
title={key}
role="button"
>
{key === 'BACKSPACE' ? (
<Icon name={'kq-shanchu'} size={36} />
) : key === 'OK' ? (
confirmText ? (
confirmText
) : (
key
)
) : (
key
)}
</div>
);
};
return (
<Popup
visible={visible}
mask={false}
afterClose={props.afterClose}
afterShow={props.afterShow}
className={`${classPrefix}-popup`}
stopPropagation={props.stopPropagation}
>
{withNativeProps(
props,
<div
ref={keyboardRef}
className={classPrefix}
onMouseDown={e => {
e.preventDefault();
}}
>
{renderHeader()}
<div className={`${classPrefix}-wrapper`}>
{value.length === 0 ? (
<div
className={classNames(`${classPrefix}-main`, {
'confirmed-style': !!confirmText,
})}
>
{keys.map(renderKey)}
</div>
) : (
<div
className={classNames(`${classPrefix}-main`, {
'confirmed-style': !!confirmText,
})}
>
{numberKeys.map(renderKey)}
{letterKeys.map(renderKey)}
{specialKeys.map(renderKey)}
</div>
)}
</div>
{props.safeArea && (
<div className={`${classPrefix}-footer`}>
<SafeArea position="bottom" />
</div>
)}
</div>,
)}
</Popup>
);
}
Example #8
Source File: config-install.tsx From bext with MIT License | 4 votes |
ConfigInstall: FC<{
onInstall: (build: string) => void;
hide?: () => void;
}> = ({ onInstall, hide }) => {
const { currentMeta } = useMetaDetail();
const [formData, setFormData] = useState(
() => currentMeta?.defaultConfig || {},
);
const [hasError, setHasError] = useState(false);
const { notify } = useNotifications();
const updateConfigCache = useMemoizedFn(() => setConfigCache(formData));
const { run: install, loading } = useRequest(
async (config?: any) => {
const { id, name, version, source, defaultConfig } = currentMeta!;
onInstall(
await excuteCompile({
meta: {
id,
name,
version,
source,
defaultConfig: config ?? defaultConfig,
},
}),
);
hide?.();
},
{
manual: true,
onError: () =>
notify({
message: '编译失败,请点击“更多” -> “报告问题”',
status: 'error',
}),
onSuccess: updateConfigCache,
},
);
const [configCache, setConfigCache] = useLocalStorageState(
bextConfigCacheKey(currentMeta?.id || 'unknown'),
);
const validate = useMemo(() => {
try {
return ajv.compile(currentMeta?.configSchema);
} catch (error) {
console.error(error);
}
}, [currentMeta?.configSchema]);
const restoreCache = () => {
if (validate?.(configCache)) {
setFormData(configCache);
notify({
message: '已填充上次安装选项',
status: 'success',
dismissAfter: 1000,
});
} else {
notify({
message: '选项缓存已过期,请重新编辑配置',
status: 'error',
});
}
};
return (
<Dialog
hidden={false}
onDismiss={hide}
dialogContentProps={{ type: DialogType.normal, title: '安装选项' }}
minWidth={400}
modalProps={{ layerProps: { hostId: LAYER_HOST_ID } }}
>
已为你填充默认选项
{configCache ? (
<>
,或者点击<Link onClick={restoreCache}>恢复之前的安装配置</Link>
</>
) : null}
,你可以直接安装或者修改下方选项后安装。
<Separator />
<JsonSchemaForm
schema={currentMeta?.configSchema}
formData={formData}
onChange={({ formData, errors }) => {
setFormData(formData);
setHasError(!!errors.length);
}}
omitExtraData
liveOmit
liveValidate
>
<></>
</JsonSchemaForm>
<DialogFooter>
<PrimaryButton
onClick={() => install(formData)}
text={loading ? '处理中...' : '安装'}
disabled={hasError || loading}
/>
<DefaultButton onClick={hide} text="取消" />
</DialogFooter>
</Dialog>
);
}
Example #9
Source File: HTTPFlowTable.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFlowTable: React.FC<HTTPFlowTableProp> = (props) => {
const [data, setData, getData] = useGetState<HTTPFlow[]>([])
const [params, setParams] = useState<YakQueryHTTPFlowRequest>(
props.params || {SourceType: "mitm"}
)
const [pagination, setPagination] = useState<PaginationSchema>({
Limit: OFFSET_LIMIT,
Order: "desc",
OrderBy: "created_at",
Page: 1
});
// const [autoReload, setAutoReload, getAutoReload] = useGetState(false);
const autoReloadRef = useRef<boolean>(false);
const autoReload = autoReloadRef.current;
const setAutoReload = (b: boolean) => {
autoReloadRef.current = b
};
const getAutoReload = () => autoReloadRef.current;
const [total, setTotal] = useState<number>(0)
const [loading, setLoading] = useState(false)
const [selected, setSelected, getSelected] = useGetState<HTTPFlow>()
const [_lastSelected, setLastSelected, getLastSelected] = useGetState<HTTPFlow>()
const [compareLeft, setCompareLeft] = useState<CompateData>({content: '', language: 'http'})
const [compareRight, setCompareRight] = useState<CompateData>({content: '', language: 'http'})
const [compareState, setCompareState] = useState(0)
const [tableContentHeight, setTableContentHeight, getTableContentHeight] = useGetState<number>(0);
// 用于记录适合
const [_scrollY, setScrollYRaw, getScrollY] = useGetState(0)
const setScrollY = useThrottleFn(setScrollYRaw, {wait: 300}).run
// 如果这个大于等于 0 ,就 Lock 住,否则忽略
const [_trigger, setLockedScroll, getLockedScroll] = useGetState(-1);
const lockScrollTimeout = (size: number, timeout: number) => {
setLockedScroll(size)
setTimeout(() => setLockedScroll(-1), timeout)
}
const tableRef = useRef(null)
const ref = useHotkeys('ctrl+r, enter', e => {
const selected = getSelected()
if (selected) {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: selected?.IsHTTPS,
request: new Buffer(selected.Request).toString()
}
})
}
})
// 使用上下箭头
useHotkeys("up", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
const expected = parseInt(`${parseInt(`${(getSelected()?.Id as number)}`) + 1}`);
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
let current = parseInt(`${data[i]?.Id}`);
if (current === expected) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
useHotkeys("down", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
if (data[i]?.Id == (getSelected()?.Id as number) - 1) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
// 向主页发送对比数据
useEffect(() => {
if (compareLeft.content) {
const params = {info: compareLeft, type: 1}
setCompareState(compareState === 0 ? 1 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareLeft])
useEffect(() => {
if (compareRight.content) {
const params = {info: compareRight, type: 2}
setCompareState(compareState === 0 ? 2 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareRight])
const update = useMemoizedFn((
page?: number,
limit?: number,
order?: string,
orderBy?: string,
sourceType?: string,
noLoading?: boolean
) => {
const paginationProps = {
Page: page || 1,
Limit: limit || pagination.Limit,
Order: order || "desc",
OrderBy: orderBy || "id"
}
if (!noLoading) {
setLoading(true)
// setAutoReload(false)
}
// yakQueryHTTPFlow({
// SourceType: sourceType, ...params,
// Pagination: {...paginationProps},
// })
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: sourceType,
...params,
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
setData((rsp?.Data || []))
setPagination(rsp.Pagination)
setTotal(rsp.Total)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 300))
})
const getNewestId = useMemoizedFn(() => {
let max = 0;
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id >= max) {
max = id
}
})
return max
})
const getOldestId = useMemoizedFn(() => {
if (getData().length <= 0) {
return 0
}
let min = parseInt(`${getData()[0].Id}`);
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id <= min) {
min = id
}
})
return min
})
// 第一次启动的时候加载一下
useEffect(() => {
update(1)
}, [])
const scrollTableTo = useMemoizedFn((size: number) => {
if (!tableRef || !tableRef.current) return
const table = tableRef.current as unknown as {
scrollTop: (number) => any,
scrollLeft: (number) => any,
}
table.scrollTop(size)
})
const scrollUpdateTop = useDebounceFn(useMemoizedFn(() => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getNewestId()
console.info("触顶:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
AfterId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
let offsetData = offsetDeltaData.concat(data);
if (offsetData.length > MAX_ROW_COUNT) {
offsetData = offsetData.splice(0, MAX_ROW_COUNT)
}
setData(offsetData);
scrollTableTo((offsetDeltaData.length + 1) * ROW_HEIGHT)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 200))
}), {wait: 600, leading: true, trailing: false}).run
const scrollUpdateButt = useDebounceFn(useMemoizedFn((tableClientHeight: number) => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getOldestId();
console.info("触底:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
BeforeId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
const originDataLength = data.length;
let offsetData = data.concat(offsetDeltaData);
let metMax = false
const originOffsetLength = offsetData.length;
if (originOffsetLength > MAX_ROW_COUNT) {
metMax = true
offsetData = offsetData.splice(originOffsetLength - MAX_ROW_COUNT, MAX_ROW_COUNT)
}
setData(offsetData);
setTimeout(() => {
if (!metMax) {
// 没有丢结果的裁剪问题
scrollTableTo((originDataLength + 1) * ROW_HEIGHT - tableClientHeight)
} else {
// 丢了结果之后的裁剪计算
const a = originOffsetLength - offsetDeltaData.length;
scrollTableTo((originDataLength + 1 + MAX_ROW_COUNT - originOffsetLength) * ROW_HEIGHT - tableClientHeight)
}
}, 50)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
}).finally(() => setTimeout(() => setLoading(false), 60))
}), {wait: 600, leading: true, trailing: false}).run
const sortFilter = useMemoizedFn((column: string, type: any) => {
const keyRelation: any = {
UpdatedAt: "updated_at",
BodyLength: "body_length",
StatusCode: "status_code"
}
if (column && type) {
update(1, OFFSET_LIMIT, type, keyRelation[column])
} else {
update(1, OFFSET_LIMIT)
}
})
// 这是用来设置选中坐标的,不需要做防抖
useEffect(() => {
if (!getLastSelected() || !getSelected()) {
return
}
const lastSelected = getLastSelected() as HTTPFlow;
const up = parseInt(`${lastSelected?.Id}`) < parseInt(`${selected?.Id}`)
// if (up) {
// console.info("up")
// } else {
// console.info("down")
// }
// console.info(lastSelected.Id, selected?.Id)
const screenRowCount = Math.floor(getTableContentHeight() / ROW_HEIGHT) - 1
if (!autoReload) {
let count = 0;
const data = getData();
for (let i = 0; i < data.length; i++) {
if (data[i].Id != getSelected()?.Id) {
count++
} else {
break
}
}
let minCount = count
if (minCount < 0) {
minCount = 0
}
const viewHeightMin = getScrollY() + tableContentHeight
const viewHeightMax = getScrollY() + tableContentHeight * 2
const minHeight = minCount * ROW_HEIGHT;
const maxHeight = minHeight + tableContentHeight
const maxHeightBottom = minHeight + tableContentHeight + 3 * ROW_HEIGHT
// console.info("top: ", minHeight, "maxHeight: ", maxHeight, "maxHeightBottom: ", maxHeightBottom)
// console.info("viewTop: ", viewHeightMin, "viewButtom: ", viewHeightMax)
if (maxHeight < viewHeightMin) {
// 往下滚动
scrollTableTo(minHeight)
return
}
if (maxHeightBottom > viewHeightMax) {
// 上滚动
const offset = minHeight - (screenRowCount - 2) * ROW_HEIGHT;
// console.info(screenRowCount, minHeight, minHeight - (screenRowCount - 1) * ROW_HEIGHT)
if (offset > 0) {
scrollTableTo(offset)
}
return
}
}
}, [selected])
// 给设置做防抖
useDebounceEffect(() => {
props.onSelected && props.onSelected(selected)
}, [selected], {wait: 400, trailing: true, leading: true})
useEffect(() => {
if (autoReload) {
const id = setInterval(() => {
update(1, undefined, "desc", undefined, undefined, true)
}, 1000)
return () => {
clearInterval(id)
}
}
}, [autoReload])
return (
// <AutoCard bodyStyle={{padding: 0, margin: 0}} bordered={false}>
<div ref={ref as Ref<any>} tabIndex={-1}
style={{width: "100%", height: "100%", overflow: "hidden"}}
>
<ReactResizeDetector
onResize={(width, height) => {
if (!width || !height) {
return
}
setTableContentHeight(height - 38)
}}
handleWidth={true} handleHeight={true} refreshMode={"debounce"} refreshRate={50}/>
{!props.noHeader && (
<PageHeader
title={"HTTP History"}
subTitle={
<Space>
{"所有相关请求都在这里"}
<Button
icon={<ReloadOutlined/>}
type={"link"}
onClick={(e) => {
update(1)
}}
/>
</Space>
}
extra={[
<Space>
<Form.Item label={"选择 HTTP History 类型"} style={{marginBottom: 0}}>
<Select
mode={"multiple"}
value={params.SourceType}
style={{minWidth: 200}}
onChange={(e) => {
setParams({...params, SourceType: e})
setLoading(true)
setTimeout(() => {
update(1, undefined, undefined, undefined, e)
}, 200)
}}
>
<Select.Option value={"mitm"}>mitm: 中间人劫持</Select.Option>
<Select.Option value={"fuzzer"}>
fuzzer: 模糊测试分析
</Select.Option>
</Select>
</Form.Item>
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true}>清除全部历史记录?</Button>
</Popconfirm>
</Space>
]}
/>
)}
<Row style={{margin: "5px 0 5px 5px"}}>
<Col span={12}>
<Space>
<span>HTTP History</span>
<Button
icon={<ReloadOutlined/>}
type={"link"}
size={"small"}
onClick={(e) => {
update(1, undefined, "desc")
}}
/>
{/* <Space>
自动刷新:
<Switch size={"small"} checked={autoReload} onChange={setAutoReload}/>
</Space> */}
<Input.Search
placeholder={"URL关键字"}
enterButton={true}
size={"small"}
style={{width: 170}}
value={params.SearchURL}
onChange={(e) => {
setParams({...params, SearchURL: e.target.value})
}}
onSearch={(v) => {
update(1)
}}
/>
{props.noHeader && (
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setCompareLeft({content: '', language: 'http'})
setCompareRight({content: '', language: 'http'})
setCompareState(0)
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true} size={"small"}>
删除历史记录
</Button>
</Popconfirm>
)}
{/*{autoReload && <Tag color={"green"}>自动刷新中...</Tag>}*/}
</Space>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Tag>{total} Records</Tag>
</Col>
</Row>
<TableResizableColumn
tableRef={tableRef}
virtualized={true}
className={"httpFlowTable"}
loading={loading}
columns={[
{
dataKey: "Id",
width: 80,
headRender: () => "序号",
cellRender: ({rowData, dataKey, ...props}: any) => {
return `${rowData[dataKey] <= 0 ? "..." : rowData[dataKey]}`
}
},
{
dataKey: "Method",
width: 70,
headRender: (params1: any) => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
方法
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索方法"}
params={params}
setParams={setParams}
filterName={"Methods"}
autoCompletions={["GET", "POST", "HEAD"]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.Methods ? undefined : "gray",
}}
type={!!params.Methods ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
// return (
// <Tag color={"geekblue"} style={{marginRight: 20}}>
// {rowData[dataKey]}
// </Tag>
// )
return rowData[dataKey]
}
},
{
dataKey: "StatusCode",
width: 100,
sortable: true,
headRender: () => {
return (
<div
style={{display: "inline-flex"}}
>
状态码
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索状态码"}
params={params}
setParams={setParams}
filterName={"StatusCode"}
autoCompletions={[
"200",
"300-305",
"400-404",
"500-502",
"200-299",
"300-399",
"400-499"
]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.StatusCode ? undefined : "gray",
}}
type={!!params.StatusCode ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{color: StatusCodeToColor(rowData[dataKey])}}>
{rowData[dataKey] === 0 ? "" : rowData[dataKey]}
</div>
)
}
},
{
dataKey: "Url",
resizable: true,
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
URL
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索URL关键字"}
params={params}
setParams={setParams}
filterName={"SearchURL"}
pureString={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.SearchURL ? undefined : "gray",
}}
type={!!params.SearchURL ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
if (rowData.IsPlaceholder) {
return <div style={{color: "#888585"}}>{"滚轮上滑刷新..."}</div>
}
return (
<div style={{width: "100%", display: "flex"}}>
<div className='resize-ellipsis' title={rowData.Url}>
{!params.SearchURL ? (
rowData.Url
) : (
rowData.Url
)}
</div>
</div>
)
},
width: 600
},
{
dataKey: "HtmlTitle",
width: 120,
resizable: true,
headRender: () => {
return "Title"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "Tags",
width: 120,
resizable: true,
headRender: () => {
return "Tags"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? (
`${rowData[dataKey]}`.split("|").filter(i => !i.startsWith("YAKIT_COLOR_")).join(", ")
) : ""
}
},
{
dataKey: "IPAddress",
width: 140, resizable: true,
headRender: () => {
return "IP"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "BodyLength",
width: 120,
sortable: true,
headRender: () => {
return (
<div style={{display: "inline-block", position: "relative"}}>
响应长度
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"是否存在Body?"}
params={params}
setParams={setParams}
filterName={"HaveBody"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveBody ? undefined : "gray",
}}
type={!!params.HaveBody ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{width: 100}}>
{/* 1M 以上的话,是红色*/}
{rowData.BodyLength !== -1 ?
(<div style={{color: rowData.BodyLength > 1000000 ? "red" : undefined}}>
{rowData.BodySizeVerbose
? rowData.BodySizeVerbose
: rowData.BodyLength}
</div>)
:
(<div></div>)
}
</div>
)
}
},
// {
// dataKey: "UrlLength",
// width: 90,
// headRender: () => {
// return "URL 长度"
// },
// cellRender: ({rowData, dataKey, ...props}: any) => {
// const len = (rowData.Url || "").length
// return len > 0 ? <div>{len}</div> : "-"
// }
// },
{
dataKey: "GetParamsTotal",
width: 65,
align: "center",
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
参数
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"过滤是否存在基础参数"}
params={params}
setParams={setParams}
filterName={"HaveCommonParams"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveCommonParams ? undefined : "gray",
}}
type={!!params.HaveCommonParams ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<Space>
{(rowData.GetParamsTotal > 0 ||
rowData.PostParamsTotal > 0) && <CheckOutlined/>}
</Space>
)
}
},
{
dataKey: "ContentType",
resizable: true, width: 80,
headRender: () => {
return "响应类型"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
let contentTypeFixed = rowData.ContentType.split(";")
.map((el: any) => el.trim())
.filter((i: any) => !i.startsWith("charset"))
.join(",") || "-"
if (contentTypeFixed.includes("/")) {
const contentTypeFixedNew = contentTypeFixed.split("/").pop()
if (!!contentTypeFixedNew) {
contentTypeFixed = contentTypeFixedNew
}
}
return (
<div>
{contentTypeFixed === "null" ? "" : contentTypeFixed}
</div>
)
}
},
{
dataKey: "UpdatedAt",
sortable: true,
width: 110,
headRender: () => {
return "请求时间"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return <Tooltip
title={rowData[dataKey] === 0 ? "" : formatTimestamp(rowData[dataKey])}
>
{rowData[dataKey] === 0 ? "" : formatTime(rowData[dataKey])}
</Tooltip>
}
},
{
dataKey: "operate",
width: 90,
headRender: () => "操作",
cellRender: ({rowData}: any) => {
if (!rowData.Hash) return <></>
return (
<a
onClick={(e) => {
let m = showDrawer({
width: "80%",
content: onExpandHTTPFlow(
rowData,
() => m.destroy()
)
})
}}
>
详情
</a>
)
}
}
]}
data={autoReload ? data : [TableFirstLinePlaceholder].concat(data)}
autoHeight={tableContentHeight <= 0}
height={tableContentHeight}
sortFilter={sortFilter}
renderRow={(children: ReactNode, rowData: any) => {
if (rowData)
return (
<div
id='http-flow-row'
ref={(node) => {
const color =
rowData.Hash === selected?.Hash ?
"rgba(78, 164, 255, 0.4)" :
rowData.Tags.indexOf("YAKIT_COLOR") > -1 ?
TableRowColor(rowData.Tags.split("|").pop().split('_').pop().toUpperCase()) :
"#ffffff"
if (node) {
if (color) node.style.setProperty("background-color", color, "important")
else node.style.setProperty("background-color", "#ffffff")
}
}}
style={{height: "100%"}}
>
{children}
</div>
)
return children
}}
onRowContextMenu={(rowData: HTTPFlow | any, event: React.MouseEvent) => {
if (rowData) {
setSelected(rowData);
}
showByCursorMenu(
{
content: [
{
title: '发送到 Web Fuzzer',
onClick: () => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: rowData.IsHTTPS,
request: new Buffer(rowData.Request).toString("utf8")
}
})
}
},
{
title: '发送到 数据包扫描',
onClick: () => {
ipcRenderer
.invoke("GetHTTPFlowByHash", {Hash: rowData.Hash})
.then((i: HTTPFlow) => {
ipcRenderer.invoke("send-to-packet-hack", {
request: i.Request,
ishttps: i.IsHTTPS,
response: i.Response
})
})
.catch((e: any) => {
failed(`Query Response failed: ${e}`)
})
}
},
{
title: '复制 URL',
onClick: () => {
callCopyToClipboard(rowData.Url)
},
},
{
title: '复制为 Yak PoC 模版', onClick: () => {
},
subMenuItems: [
{
title: "数据包 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Ordinary)
}
},
{
title: "批量检测 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Batch)
}
},
]
},
{
title: '标注颜色',
subMenuItems: availableColors.map(i => {
return {
title: i.title,
render: i.render,
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) {
return
}
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.push(`YAKIT_COLOR_${i.color.toUpperCase()}`)
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`设置 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = `YAKIT_COLOR_${i.color.toUpperCase()}`
return item
}
return item
}))
}
})
}
}
}),
onClick: () => {
}
},
{
title: '移除颜色',
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) return
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.pop()
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`清除 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = ""
return item
}
return item
}))
}
})
return
},
},
{
title: "发送到对比器", onClick: () => {
},
subMenuItems: [
{
title: '发送到对比器左侧',
onClick: () => {
setCompareLeft({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, true, false][compareState]
},
{
title: '发送到对比器右侧',
onClick: () => {
setCompareRight({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, false, true][compareState]
}
]
},
]
},
event.clientX,
event.clientY
)
}}
onRowClick={(rowDate: any) => {
if (!rowDate.Hash) return
if (rowDate.Hash !== selected?.Hash) {
setSelected(rowDate)
} else {
// setSelected(undefined)
}
}}
onScroll={(scrollX, scrollY) => {
setScrollY(scrollY)
// 防止无数据触发加载
if (data.length === 0 && !getAutoReload()) {
setAutoReload(true)
return
}
// 根据页面展示内容决定是否自动刷新
let contextHeight = (data.length + 1) * ROW_HEIGHT // +1 是要把表 title 算进去
let offsetY = scrollY + tableContentHeight;
if (contextHeight < tableContentHeight) {
setAutoReload(true)
return
}
setAutoReload(false)
// 向下刷新数据
if (contextHeight <= offsetY) {
setAutoReload(false)
scrollUpdateButt(tableContentHeight)
return
}
// 锁住滚轮
if (getLockedScroll() > 0 && getLockedScroll() >= scrollY) {
if (scrollY === getLockedScroll()) {
return
}
// scrollTableTo(getLockedScroll())
return
}
const toTop = scrollY <= 0;
if (toTop) {
lockScrollTimeout(ROW_HEIGHT, 600)
scrollUpdateTop()
}
}}
/>
</div>
// </AutoCard>
)
}
Example #10
Source File: index.tsx From ui with MIT License | 4 votes |
Calendar = ({
className,
current,
renderDot,
onChange: _onChange = () => {},
itemCls,
renderDisable = day => day.isBefore(dayjs(), 'date'),
activeItemCls,
disableItemCls,
activeDotCls,
dotWrapCls,
limit = 14,
renderDate = day => day.get('date'),
listEndDay: _listEndDay,
monthCls,
startDay: _outStartDay,
elderly = useConfig().elderly,
renderItemProps,
dotCls,
range,
weekCls,
rangeActiveCls,
...props
}: Props) => {
const onChange = useMemoizedFn(_onChange);
const listEndDayStr = _listEndDay?.toString();
const listEndDay = useMemo(
() => (listEndDayStr ? dayjs(listEndDayStr) : undefined),
[listEndDayStr],
);
const outStartDayStr = _outStartDay?.toString();
const outStartDay = useMemo(
() => (outStartDayStr ? dayjs(outStartDayStr) : undefined),
[outStartDayStr],
);
const [selected, setSelected] = useEffectState<Current>(
useMemo(() => current || (range ? [dayjs(), undefined] : dayjs()), [
current,
range,
]),
);
const selectedRef = useStateRef(selected);
const startDay = useMemo(() => {
return listEndDay ? dayjs().set('date', 1) : outStartDay || dayjs();
}, [listEndDay, outStartDay]);
limit = useMemo(() => {
return listEndDay ? listEndDay.diff(dayjs(), 'day') + 1 : limit;
}, [limit, listEndDay]);
const days = useMemo(
() =>
new Array(limit).fill(0).map((_, index) =>
dayjs(
startDay
.subtract(startDay.day(), 'day')
.add(index, 'day')
.format('YYYY-MM-DD'),
),
),
[limit, startDay],
);
const getItemArg = useCallback(
(day: dayjs.Dayjs) => {
const selected = selectedRef.current;
const [start, end] = selected instanceof Array ? selected : [];
const isStart = !!(day.isSame(start, 'date') && range);
const isEnd = !!(day.isSame(end, 'date') && range && end);
const inRange = !!(
day.isAfter(start) &&
day.isBefore(end) &&
range &&
end &&
!isEnd
);
const disabled = renderDisable(day);
const active =
selected instanceof Array
? isStart || isEnd || inRange
: day.isSame(selected, 'date');
const renderProps = renderItemProps?.(day);
return {
renderProps,
end,
isEnd,
isStart,
active,
inRange,
disabled,
};
},
[range, renderDisable, renderItemProps, selectedRef],
);
const getItemNativeData = useCallback(
(day: dayjs.Dayjs) => {
const {
renderProps,
end,
isEnd,
isStart,
active,
inRange,
disabled,
} = getItemArg(day);
return {
style: {
marginRight:
day.weekday() === 6
? '0PX'
: getPlatform === 'native'
? rpxToPx(30)
: undefined,
...renderProps?.style,
},
className: classNames(
styles.item,
itemCls,
renderProps?.className,
disabled && classNames(styles.disable, disableItemCls),
active && classNames(styles.active, activeItemCls),
inRange && classNames(styles.rangeActive, rangeActiveCls),
isEnd && classNames(styles.end),
isStart && end && classNames(styles.start),
),
};
},
[activeItemCls, disableItemCls, getItemArg, itemCls, rangeActiveCls],
);
const nativeRefArrRef = useRef<
{ day: dayjs.Dayjs; native: NativeInstance | null }[]
>([]);
return (
<View
className={classNames(
styles.calendar,
className,
elderly && styles.elderly,
)}
{...props}
>
{weeks.map((item, index) => (
<View
className={classNames(styles.item, itemCls, styles.week, weekCls)}
key={item}
style={{
marginRight:
index === 6
? 0
: getPlatform === 'native'
? rpxToPx(30)
: undefined,
}}
>
{item}
</View>
))}
{useMemo(() => {
return days.map((day, index) => {
const dot = renderDot?.(day, index);
const { renderProps, active, disabled } = getItemArg(day);
const renderEmpty = (before = false) => {
const length = before ? day.weekday() : 7 - day.weekday() - 1;
return new Array(length).fill(0).map((_, i) => (
<View
className={classNames(itemCls, styles.item, styles.empty)}
key={i}
style={{
marginRight:
!before && i === length - 1
? 0
: getPlatform === 'native'
? rpxToPx(30)
: undefined,
}}
/>
));
};
if (listEndDay && day.month() === startDay.month() - 1) {
return null;
}
return (
<React.Fragment key={index}>
{day.date() === 1 && listEndDay && (
<>
<View className={classNames(styles.month, monthCls)}>
{day.format('YYYY年MM月')}
</View>
{renderEmpty(true)}
</>
)}
<Native
{...renderProps}
onTap={() => {
if (disabled) return;
if (range) {
const [start, end] = selectedRef.current as any;
let current: Current;
if (start && end) {
current = [day, undefined];
} else if (start && dayjs(start).isBefore(day)) {
current = [start, day];
} else {
current = [day, undefined];
}
setSelected(current);
selectedRef.current = current;
onChange?.(current);
} else {
setSelected(day);
selectedRef.current = day;
onChange?.(day);
}
nativeRefArrRef.current.forEach(({ day, native }) => {
native?.setData(getItemNativeData(day));
});
}}
initData={getItemNativeData(day)}
ref={native => {
nativeRefArrRef.current[index] = { day, native };
native?.setData(getItemNativeData(day));
}}
>
{renderDate(day)}
<View className={classNames(styles.dotWrap, dotWrapCls)}>
{dot === true ? (
<View
className={classNames(
styles.dot,
dotCls,
active && classNames(styles.activeDot, activeDotCls),
)}
>
{dot}
</View>
) : (
dot
)}
</View>
</Native>
{listEndDay &&
(day.month() !==
dayjs(day)
.add(1, 'day')
.month() ||
days.length === index + 1) &&
renderEmpty(false)}
</React.Fragment>
);
});
}, [
activeDotCls,
days,
dotCls,
dotWrapCls,
getItemArg,
getItemNativeData,
itemCls,
listEndDay,
monthCls,
onChange,
range,
renderDate,
renderDot,
selectedRef,
setSelected,
startDay,
])}
</View>
);
}
Example #11
Source File: YakScriptCreator.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
CreateYakScriptParamForm: React.FC<CreateYakScriptParamFormProp> = (props) => {
const [params, setParams] = useState<YakScriptParam>(
props.modifiedParam || {
DefaultValue: "",
Field: "",
FieldVerbose: "",
Help: "",
TypeVerbose: ""
}
)
const [extraSetting, setExtraSetting] = useState<{ [key: string]: any }>(
props.modifiedParam?.ExtraSetting ? JSON.parse(props.modifiedParam.ExtraSetting) : {}
)
// 选择类型时的转换
const typeChange = useMemoizedFn((type: string) => {
switch (type) {
case "select":
setExtraSetting({
double: false,
data: []
})
break
case "upload-path":
setExtraSetting({isTextArea: false})
break
default:
setExtraSetting({})
break
}
setParams({...params, TypeVerbose: type})
})
// 提交参数信息的验证
const verify = useMemoizedFn(() => {
const type = params.TypeVerbose
switch (type) {
case "select":
if (extraSetting.data.length === 0) {
failed("下拉框类型时,请最少添加一个选项数据")
return false
}
return true
default:
return true
}
})
// 提交参数信息的转换
const convert = useMemoizedFn(() => {
const type = params.TypeVerbose
const setting: YakScriptParam = cloneDeep(params)
const extra = cloneDeep(extraSetting)
const extraStr = JSON.stringify(extraSetting)
switch (type) {
case "select":
const dataObj = {}
extra.data.map(item => {
if (item.value in dataObj && item.key) dataObj[item.value] = item.key
if (!(item.value in dataObj)) dataObj[item.value] = item.key
})
const data: any = []
for (let item in dataObj) data.push({key: dataObj[item], value: item})
extra.data = data
setting.ExtraSetting = JSON.stringify(extra)
return setting
case "upload-path":
extra.isTextArea = setting.Required ? extra.isTextArea : false
setting.ExtraSetting = JSON.stringify(extra)
return setting
default:
setting.ExtraSetting = extraStr === "{}" ? undefined : extraStr
return setting
}
})
const updateExtraSetting = useMemoizedFn((type: string, kind: string, key: string, value: any, index?: number) => {
const extra = cloneDeep(extraSetting)
switch (type) {
case "select":
if (Array.isArray(extra.data) && kind === "update" && index !== undefined) {
extra.data[index][key] = value
setExtraSetting({...extra})
}
if (Array.isArray(extra.data) && kind === "del" && index !== undefined) {
extra.data.splice(index, 1)
setExtraSetting({...extra})
}
return
default:
return
}
})
const selectOptSetting = (item: { key: string; value: string }, index: number) => {
return (
<div key={index} className="select-type-opt">
<span className="opt-hint-title">
选项名称
</span>
<Input
className="opt-hint-input"
size='small'
value={item.key}
onChange={(e) => updateExtraSetting("select", "update", "key", e.target.value, index)}
/>
<span className="opt-hint-title">
<span className="form-item-required-title">*</span>选项值
</span>
<Input
className="opt-hint-input"
required
size='small'
value={item.value}
placeholder="必填项"
onChange={(e) => updateExtraSetting("select", "update", "value", e.target.value, index)}
/>
<Button
type='link'
danger
icon={<DeleteOutlined/>}
onClick={() => updateExtraSetting("select", "del", "", "", index)}
/>
</div>
)
}
const extraSettingComponent = useMemoizedFn((type: string) => {
switch (type) {
case "select":
return (
<div>
<SwitchItem
label={"是否支持多选"}
setValue={(value) => setExtraSetting({...extraSetting, double: value})}
value={!!extraSetting.double}
help={"多选状态时,用户选中数据保存格式为数组类型"}
/>
<Form.Item label='下拉框选项数据' className="creator-form-item-margin">
<Button type='link' onClick={() => {
(extraSetting.data || []).push({key: "", value: ""})
setExtraSetting({...extraSetting})
}}>
新增选项 <PlusOutlined/>
</Button>
</Form.Item>
<Form.Item label={" "} colon={false} className="creator-form-item-margin">
{(extraSetting.data || []).map((item, index) => selectOptSetting(item, index))}
</Form.Item>
</div>
)
case "upload-path":
if (!params.Required) {
return <></>
}
return (
<div>
<SwitchItem
label={"是否以文本域展示"}
setValue={(value) => setExtraSetting({...extraSetting, isTextArea: value})}
value={!!extraSetting.isTextArea}
/>
</div>
)
default:
break
}
})
return <>
<Form
onSubmitCapture={e => {
e.preventDefault()
if (!verify()) return false
props.onCreated(convert())
}}
labelCol={{span: 5}} wrapperCol={{span: 14}}
>
<InputItem
disable={!!props.modifiedParam}
label={"参数名(英文)"} required={true} placeholder={"填入想要增加的参数名"}
setValue={Field => setParams({...params, Field})} value={params.Field}
help={"参数名应该避免特殊符号,只允许英文 / '-' 等"}
/>
<InputItem
label={"参数显示名称(可中文)"} placeholder={"输入想要显示的参数名"}
setValue={FieldVerbose => setParams({...params, FieldVerbose})} value={params.FieldVerbose}
/>
<SwitchItem label={"必要参数"} setValue={Required => setParams({...params, Required})} value={params.Required}/>
<ManySelectOne
label={"选择参数类型"}
data={[
{text: "字符串 / string", value: "string"},
{text: "布尔值 / boolean", value: "boolean"},
{text: "HTTP 数据包 / yak", value: "http-packet"},
{text: "Yak 代码块 / yak", value: "yak"},
{text: "文本块 / text", value: "text"},
{text: "整数(大于零) / uint", value: "uint"},
{text: "浮点数 / float", value: "float"},
{text: "上传文件路径 / uploadPath", value: "upload-path"},
{text: "下拉框 / select", value: "select"},
]}
setValue={TypeVerbose => typeChange(TypeVerbose)} value={params.TypeVerbose}
/>
{!["upload-path"].includes(params.TypeVerbose) && <InputItem
label={"默认值"} placeholder={"该参数的默认值"}
setValue={DefaultValue => setParams({...params, DefaultValue})} value={params.DefaultValue}
help={params.TypeVerbose === "select" ? "使用 逗号(,) 作为选项分隔符 " : undefined}
/>}
{extraSettingComponent(params.TypeVerbose)}
<InputItem
label={"参数帮助信息"}
setValue={Help => setParams({...params, Help})} value={params.Help}
textarea={true} textareaRow={4} placeholder={"填写该参数的帮助信息,帮助用户更容易理解该内容"}
/>
{!params.Required && <InputItem
label={"参数组"}
setValue={Group => setParams({...params, Group})} value={params.Group}
placeholder={"参数组,在用户输入界面将会把参数分成组,一般用于设置可选参数`"}
/>}
<Form.Item colon={false} label={" "}>
<Button type="primary" htmlType="submit"> 添加参数 </Button>
</Form.Item>
</Form>
</>
}