import { LanguageRounded } from '@mui/icons-material' import { alpha, Box, Divider, Menu, MenuItem, styled } from '@mui/material' import { useLockFn } from 'ahooks' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { BaseLoading } from '@/components/base' import { useIconCache } from '@/hooks/use-icon-cache' import { cmdTestDelay } from '@/services/cmds' import delayManager from '@/services/delay' import { showNotice } from '@/services/notice-service' import { useTestAllCounter } from '@/store/app-store-context' import { TestBox } from './test-box' interface Props { itemData: IVergeTestItem onEdit: () => void onDelete: (uid: string) => void } export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => { const { t } = useTranslation() const [anchorEl, setAnchorEl] = useState(null) const [position, setPosition] = useState({ left: 0, top: 0 }) const [delay, setDelay] = useState(-1) const { uid, name, icon, url } = itemData const iconCachePath = useIconCache({ icon: icon!, cacheKey: uid! }) const onDelay = useCallback(async () => { setDelay(-2) try { const result = await cmdTestDelay(url!) setDelay(result) } catch (err) { // A failed test must not leave the item stuck in the loading state. setDelay(-1) showNotice.error(err) } }, [url]) const onEditTest = () => { setAnchorEl(null) onEdit() } const onDelete = useLockFn(async () => { setAnchorEl(null) try { removeTest(uid!) } catch (err: any) { showNotice.error(err) } }) const menu = [ { label: t('shared.actions.edit'), handler: onEditTest }, { label: t('shared.actions.delete'), handler: onDelete }, ] // Store bump replaces the per-item event subscription; skip the initial // mount value and call through a ref so url changes do not re-trigger tests. const testAllCounter = useTestAllCounter() const onDelayRef = useRef(onDelay) onDelayRef.current = onDelay const mountedRef = useRef(false) useEffect(() => { if (!mountedRef.current) { mountedRef.current = true return } void onDelayRef.current() }, [testAllCounter]) return ( { const { clientX, clientY } = event setPosition({ top: clientY, left: clientX }) setAnchorEl(event.currentTarget) event.preventDefault() }} > {icon && icon.trim() !== '' ? ( {icon.trim().startsWith('http') && ( {name!} )} {icon.trim().startsWith('data') && ( {name!} )} {icon.trim().startsWith(' )} ) : ( )} {name} {delay === -2 && ( )} {delay === -1 && ( { e.preventDefault() e.stopPropagation() onDelay() }} sx={({ palette }) => ({ ':hover': { bgcolor: alpha(palette.primary.main, 0.15) }, })} > {t('tests.components.item.actions.test')} )} {delay >= 0 && ( // 显示延迟 { e.preventDefault() e.stopPropagation() onDelay() }} sx={({ palette }) => ({ color: delayManager.formatDelayColor(delay), ':hover': { bgcolor: alpha(palette.primary.main, 0.15), }, })} > {delayManager.formatDelay(delay)} )} setAnchorEl(null)} anchorPosition={position} anchorReference="anchorPosition" transitionDuration={225} slotProps={{ list: { sx: { py: 0.5 } } }} onContextMenu={(e) => { setAnchorEl(null) e.preventDefault() }} > {menu.map((item) => ( {item.label} ))} ) } const Widget = styled(Box)(({ theme: { typography } }) => ({ padding: '3px 6px', fontSize: 14, fontFamily: typography.fontFamily, borderRadius: '4px', }))