fix: remove nested button element in NextUI Card component

This commit is contained in:
pompurin404 2025-02-03 07:30:33 +08:00
parent e746ab4241
commit 77b67849ed
7 changed files with 1477 additions and 952 deletions

View File

@ -26,6 +26,7 @@
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@mihomo-party/sysproxy": "^2.0.7", "@mihomo-party/sysproxy": "^2.0.7",
"@mihomo-party/sysproxy-darwin-arm64": "^2.0.7",
"@nextui-org/react": "2.6.10", "@nextui-org/react": "2.6.10",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
@ -57,7 +58,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cron-validator": "^1.3.1", "cron-validator": "^1.3.1",
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"electron": "^33.1.0", "electron": "^33.3.2",
"electron-builder": "25.0.4", "electron-builder": "25.0.4",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
@ -90,5 +91,6 @@
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^6.0.7", "vite": "^6.0.7",
"vite-plugin-monaco-editor": "^1.1.0" "vite-plugin-monaco-editor": "^1.1.0"
} },
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
} }

1777
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -221,7 +221,8 @@ export async function createWindow(): Promise<void> {
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
spellcheck: false, spellcheck: false,
sandbox: false sandbox: false,
devTools: true
} }
}) })
mainWindowState.manage(mainWindow) mainWindowState.manage(mainWindow)
@ -282,6 +283,12 @@ export async function createWindow(): Promise<void> {
shell.openExternal(details.url) shell.openExternal(details.url)
return { action: 'deny' } return { action: 'deny' }
}) })
// 在开发模式下自动打开 DevTools
if (is.dev) {
mainWindow.webContents.openDevTools()
}
// HMR for renderer base on electron-vite cli. // HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production. // Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) { if (is.dev && process.env['ELECTRON_RENDERER_URL']) {

View File

@ -73,13 +73,11 @@ const App: React.FC = () => {
const location = useLocation() const location = useLocation()
const page = useRoutes(routes) const page = useRoutes(routes)
const setTitlebar = (): void => { const setTitlebar = (): void => {
if (!useWindowFrame) { if (!useWindowFrame && platform !== 'darwin') {
const options = { height: 48 } as TitleBarOverlayOptions const options = { height: 48 } as TitleBarOverlayOptions
try { try {
if (platform !== 'darwin') { options.color = window.getComputedStyle(document.documentElement).backgroundColor
options.color = window.getComputedStyle(document.documentElement).backgroundColor options.symbolColor = window.getComputedStyle(document.documentElement).color
options.symbolColor = window.getComputedStyle(document.documentElement).color
}
setTitleBarOverlay(options) setTitleBarOverlay(options)
} catch (e) { } catch (e) {
// ignore // ignore

View File

@ -173,141 +173,144 @@ const ProfileItem: React.FC<Props> = (props) => {
)} )}
<Card <Card
fullWidth fullWidth
isPressable
onPress={() => {
if (disableSelect) return
setSelecting(true)
onClick().finally(() => {
setSelecting(false)
})
}}
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`} className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
> >
<div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full"> <div
<CardBody className="pb-1"> className="w-full h-full cursor-pointer"
<div className="flex justify-between h-[32px]"> onClick={() => {
<h3 if (disableSelect) return
title={info?.name} setSelecting(true)
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`} onClick().finally(() => {
> setSelecting(false)
{info?.name} })
</h3> }}
<div className="flex"> >
{info.type === 'remote' && ( <div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full">
<Tooltip placement="left" content={dayjs(info.updated).fromNow()}> <CardBody className="pb-1">
<div className="flex justify-between h-[32px]">
<h3
title={info?.name}
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
>
{info?.name}
</h3>
<div className="flex">
{info.type === 'remote' && (
<Tooltip placement="left" content={dayjs(info.updated).fromNow()}>
<Button
isIconOnly
size="sm"
variant="light"
color="default"
disabled={updating}
onPress={async () => {
setUpdating(true)
await addProfileItem(info)
setUpdating(false)
}}
>
<IoMdRefresh
color="default"
className={`${isCurrent ? 'text-primary-foreground' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
/>
</Button>
</Tooltip>
)}
<Dropdown>
<DropdownTrigger>
<Button isIconOnly size="sm" variant="light" color="default">
<IoMdMore
color="default"
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
/>
</Button>
</DropdownTrigger>
<DropdownMenu onAction={onMenuAction}>
{menuItems.map((item) => (
<DropdownItem
showDivider={item.showDivider}
key={item.key}
color={item.color}
className={item.className}
>
{item.label}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
</div>
{info.type === 'remote' && extra && (
<div
className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
>
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
{profileDisplayDate === 'expire' ? (
<Button <Button
isIconOnly
size="sm" size="sm"
variant="light" variant="light"
color="default" className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
disabled={updating}
onPress={async () => { onPress={async () => {
setUpdating(true) await patchAppConfig({ profileDisplayDate: 'update' })
await addProfileItem(info)
setUpdating(false)
}} }}
> >
<IoMdRefresh {extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
color="default"
className={`${isCurrent ? 'text-primary-foreground' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
/>
</Button> </Button>
</Tooltip> ) : (
)} <Button
size="sm"
<Dropdown> variant="light"
<DropdownTrigger> className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
<Button isIconOnly size="sm" variant="light" color="default"> onPress={async () => {
<IoMdMore await patchAppConfig({ profileDisplayDate: 'expire' })
color="default" }}
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`} >
/> {dayjs(info.updated).fromNow()}
</Button> </Button>
</DropdownTrigger> )}
<DropdownMenu onAction={onMenuAction}> </div>
{menuItems.map((item) => ( )}
<DropdownItem </CardBody>
showDivider={item.showDivider} <CardFooter className="pt-0">
key={item.key} {info.type === 'remote' && !extra && (
color={item.color} <div
className={item.className} className={`w-full mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
>
{item.label}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
</div>
{info.type === 'remote' && extra && (
<div
className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
>
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
{profileDisplayDate === 'expire' ? (
<Button
size="sm"
variant="light"
className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
onPress={async () => {
await patchAppConfig({ profileDisplayDate: 'update' })
}}
>
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
</Button>
) : (
<Button
size="sm"
variant="light"
className={`h-[20px] p-1 m-0 ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
onPress={async () => {
await patchAppConfig({ profileDisplayDate: 'expire' })
}}
>
{dayjs(info.updated).fromNow()}
</Button>
)}
</div>
)}
</CardBody>
<CardFooter className="pt-0">
{info.type === 'remote' && !extra && (
<div
className={`w-full mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
>
<Chip
size="sm"
variant="bordered"
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
> >
<Chip
</Chip> size="sm"
<small>{dayjs(info.updated).fromNow()}</small> variant="bordered"
</div> className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
)} >
{info.type === 'local' && (
<div </Chip>
className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`} <small>{dayjs(info.updated).fromNow()}</small>
> </div>
<Chip )}
size="sm" {info.type === 'local' && (
variant="bordered" <div
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`} className={`mt-2 flex justify-between ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
> >
<Chip
</Chip> size="sm"
</div> variant="bordered"
)} className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
{extra && ( >
<Progress
className="w-full" </Chip>
classNames={{ </div>
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground' )}
}} {extra && (
value={calcPercent(extra?.upload, extra?.download, extra?.total)} <Progress
/> className="w-full"
)} classNames={{
</CardFooter> indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
}}
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
/>
)}
</CardFooter>
</div>
</div> </div>
</Card> </Card>
</div> </div>

View File

@ -49,8 +49,6 @@ const ProxyItem: React.FC<Props> = (props) => {
return ( return (
<Card <Card
onPress={() => onSelect(group.name, proxy.name)}
isPressable
fullWidth fullWidth
shadow="sm" shadow="sm"
className={`${ className={`${
@ -62,93 +60,98 @@ const ProxyItem: React.FC<Props> = (props) => {
}`} }`}
radius="sm" radius="sm"
> >
<CardBody className="p-1"> <div
{proxyDisplayMode === 'full' ? ( onClick={() => onSelect(group.name, proxy.name)}
<div className="flex flex-col gap-1"> className="cursor-pointer"
<div className="flex justify-between items-center pl-1"> >
<div className="text-ellipsis overflow-hidden whitespace-nowrap" title={proxy.name}> <CardBody className="p-1">
{proxy.name} {proxyDisplayMode === 'full' ? (
</div> <div className="flex flex-col gap-1">
{fixed && ( <div className="flex justify-between items-center pl-1">
<Button <div className="text-ellipsis overflow-hidden whitespace-nowrap" title={proxy.name}>
isIconOnly {proxy.name}
title="取消固定"
color="danger"
onPress={async () => {
await mihomoUnfixedProxy(group.name)
mutateProxies()
}}
variant="light"
className="h-[20px] p-0 text-sm"
>
<FaMapPin className="text-md le" />
</Button>
)}
</div>
<div className="flex justify-between items-center pl-1">
<div className="flex gap-1 items-center">
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
{proxy.type}
</div> </div>
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(protocol => {fixed && (
proxy[protocol as keyof IMihomoProxy] && ( <Button
<div key={protocol} className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md"> isIconOnly
{protocol} title="取消固定"
</div> color="danger"
) onPress={async () => {
await mihomoUnfixedProxy(group.name)
mutateProxies()
}}
variant="light"
className="h-[20px] p-0 text-sm"
>
<FaMapPin className="text-md le" />
</Button>
)} )}
</div> </div>
<Button <div className="flex justify-between items-center pl-1">
isIconOnly <div className="flex gap-1 items-center">
title={proxy.type} <div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
isLoading={loading} {proxy.type}
color={delayColor(delay)} </div>
onPress={onDelay} {['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(protocol =>
variant="light" proxy[protocol as keyof IMihomoProxy] && (
className="h-full text-sm ml-auto -mt-0.5" <div key={protocol} className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
> {protocol}
{delayText(delay)} </div>
</Button> )
)}
</div>
<Button
isIconOnly
title={proxy.type}
isLoading={loading}
color={delayColor(delay)}
onPress={onDelay}
variant="light"
className="h-full text-sm ml-auto -mt-0.5"
>
{delayText(delay)}
</Button>
</div>
</div> </div>
</div> ) : (
) : ( <div className="flex justify-between items-center pl-1 p-1">
<div className="flex justify-between items-center pl-1 p-1"> <div className="text-ellipsis overflow-hidden whitespace-nowrap">
<div className="text-ellipsis overflow-hidden whitespace-nowrap"> <div className="flag-emoji inline" title={proxy.name}>
<div className="flag-emoji inline" title={proxy.name}> {proxy.name}
{proxy.name} </div>
</div>
<div className="flex justify-end">
{fixed && (
<Button
isIconOnly
title="取消固定"
color="danger"
onPress={async () => {
await mihomoUnfixedProxy(group.name)
mutateProxies()
}}
variant="light"
className="h-[20px] p-0 text-sm"
>
<FaMapPin className="text-md le" />
</Button>
)}
<Button
isIconOnly
title={proxy.type}
isLoading={loading}
color={delayColor(delay)}
onPress={onDelay}
variant="light"
className="h-full p-0 text-sm"
>
{delayText(delay)}
</Button>
</div>
</div> </div>
</div> )}
<div className="flex justify-end"> </CardBody>
{fixed && ( </div>
<Button
isIconOnly
title="取消固定"
color="danger"
onPress={async () => {
await mihomoUnfixedProxy(group.name)
mutateProxies()
}}
variant="light"
className="h-[20px] p-0 text-sm"
>
<FaMapPin className="text-md le" />
</Button>
)}
<Button
isIconOnly
title={proxy.type}
isLoading={loading}
color={delayColor(delay)}
onPress={onDelay}
variant="light"
className="h-full p-0 text-sm"
>
{delayText(delay)}
</Button>
</div>
</div>
)}
</CardBody>
</Card> </Card>
) )
} }

View File

@ -327,118 +327,121 @@ const Proxies: React.FC = () => {
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`} className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
> >
<Card <Card
isPressable
fullWidth fullWidth
onClick={() => {
setIsOpen((prev) => {
const newOpen = [...prev]
newOpen[index] = !prev[index]
return newOpen
})
}}
> >
<CardBody className="w-full"> <div
<div className="flex justify-between"> onClick={(): void => {
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap"> setIsOpen((prev) => {
{groups[index].icon ? ( const newOpen = [...prev]
<Avatar newOpen[index] = !prev[index]
className="bg-transparent mr-2" return newOpen
size="sm" })
radius="sm" }}
src={ className="cursor-pointer"
groups[index].icon.startsWith('<svg') >
? `data:image/svg+xml;utf8,${groups[index].icon}` <CardBody className="w-full">
: localStorage.getItem(groups[index].icon) || groups[index].icon <div className="flex justify-between">
} <div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
/> {groups[index].icon ? (
) : null} <Avatar
<div className="text-ellipsis overflow-hidden whitespace-nowrap"> className="bg-transparent mr-2"
<div size="sm"
title={groups[index].name} radius="sm"
className="inline flag-emoji h-[32px] text-md leading-[32px]" src={
> groups[index].icon.startsWith('<svg')
{groups[index].name} ? `data:image/svg+xml;utf8,${groups[index].icon}`
</div> : localStorage.getItem(groups[index].icon) || groups[index].icon
{proxyDisplayMode === 'full' && ( }
/>
) : null}
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
<div <div
title={groups[index].type} title={groups[index].name}
className="inline ml-2 text-sm text-foreground-500" className="inline flag-emoji h-[32px] text-md leading-[32px]"
> >
{groups[index].type} {groups[index].name}
</div> </div>
)} {proxyDisplayMode === 'full' && (
<div
title={groups[index].type}
className="inline ml-2 text-sm text-foreground-500"
>
{groups[index].type}
</div>
)}
{proxyDisplayMode === 'full' && (
<div className="inline flag-emoji ml-2 text-sm text-foreground-500">
{groups[index].now}
</div>
)}
</div>
</div>
<div className="flex">
{proxyDisplayMode === 'full' && ( {proxyDisplayMode === 'full' && (
<div className="inline flag-emoji ml-2 text-sm text-foreground-500"> <Chip size="sm" className="my-1 mr-2">
{groups[index].now} {groups[index].all.length}
</div> </Chip>
)} )}
<CollapseInput
title="搜索节点"
value={searchValue[index]}
onValueChange={(v) => {
setSearchValue((prev) => {
const newSearchValue = [...prev]
newSearchValue[index] = v
return newSearchValue
})
}}
/>
<Button
title="定位到当前节点"
variant="light"
size="sm"
isIconOnly
onPress={() => {
if (!isOpen[index]) {
setIsOpen((prev) => {
const newOpen = [...prev]
newOpen[index] = true
return newOpen
})
}
let i = 0
for (let j = 0; j < index; j++) {
i += groupCounts[j]
}
i += Math.floor(
allProxies[index].findIndex(
(proxy) => proxy.name === groups[index].now
) / cols
)
virtuosoRef.current?.scrollToIndex({
index: Math.floor(i),
align: 'start'
})
}}
>
<FaLocationCrosshairs className="text-lg text-foreground-500" />
</Button>
<Button
title="延迟测试"
variant="light"
isLoading={delaying[index]}
size="sm"
isIconOnly
onPress={() => {
onGroupDelay(index)
}}
>
<MdOutlineSpeed className="text-lg text-foreground-500" />
</Button>
<IoIosArrowBack
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
/>
</div> </div>
</div> </div>
<div className="flex"> </CardBody>
{proxyDisplayMode === 'full' && ( </div>
<Chip size="sm" className="my-1 mr-2">
{groups[index].all.length}
</Chip>
)}
<CollapseInput
title="搜索节点"
value={searchValue[index]}
onValueChange={(v) => {
setSearchValue((prev) => {
const newSearchValue = [...prev]
newSearchValue[index] = v
return newSearchValue
})
}}
/>
<Button
title="定位到当前节点"
variant="light"
size="sm"
isIconOnly
onPress={() => {
if (!isOpen[index]) {
setIsOpen((prev) => {
const newOpen = [...prev]
newOpen[index] = true
return newOpen
})
}
let i = 0
for (let j = 0; j < index; j++) {
i += groupCounts[j]
}
i += Math.floor(
allProxies[index].findIndex(
(proxy) => proxy.name === groups[index].now
) / cols
)
virtuosoRef.current?.scrollToIndex({
index: Math.floor(i),
align: 'start'
})
}}
>
<FaLocationCrosshairs className="text-lg text-foreground-500" />
</Button>
<Button
title="延迟测试"
variant="light"
isLoading={delaying[index]}
size="sm"
isIconOnly
onPress={() => {
onGroupDelay(index)
}}
>
<MdOutlineSpeed className="text-lg text-foreground-500" />
</Button>
<IoIosArrowBack
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
/>
</div>
</div>
</CardBody>
</Card> </Card>
</div> </div>
) : ( ) : (