paw-gui/src/pages/home/index.tsx
2025-04-21 19:32:32 +08:00

365 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useDispatch, useSelector } from 'react-redux'
import { resourceDir, join } from '@tauri-apps/api/path'
import { convertFileSrc } from '@tauri-apps/api/core'
import { v4 as uuidv4 } from 'uuid'
import { cn } from '@/lib/utils'
import { bytesToMbps } from '@/utils/tools'
import { errorToast } from '@/components/GlobalToast'
import { toast } from '@/components/ui/use-toast'
import { commands } from '@/bindings'
import {
// installService,
// uninstallService,
// startCore,
// stopCore,
enableProxy,
disableProxy,
} from '@/store/serviceSlice'
import { WebSocketClient } from '@/utils/webSocketClient'
import { ServiceControlPanel } from '@/components/ServiceControlPanel'
import { useCoreConfig } from '@/hooks/useCoreConfig'
import LineChart from '@/pages/home/components/LineChart'
import Links from '@/pages/home/components/Links'
import UnionSvg from '@/assets/svg/home/union.svg?react'
import ArrowDownSvg from '@/assets/svg/home/arrow-down.svg?react'
import ArrowUpSvg from '@/assets/svg/home/arrow-up.svg?react'
import type { AppDispatch, RootState } from '@/store'
import './index.scss'
const AgentOpenBtn = () => {
return (
<div className="proxy-btn w-[220px] h-[54px] p-1.5 bg-emerald-700 rounded-[10px] justify-start items-center gap-2.5 inline-flex">
<div className="w-[74px] h-[46px] bg-emerald-200 rounded-[8.93px] justify-center items-center gap-2 inline-flex">
<UnionSvg className="animate-fade-in-medium" />
<UnionSvg className="animate-fade-in-strong" />
</div>
<div className=" justify-center items-center flex flex-1">
<div className="text-white text-xl font-semibold leading-7 ">
</div>
</div>
</div>
)
}
const AgentCloseBtn = ({ className }: { className?: string }) => {
return (
<div
className={cn(
'proxy-btn w-[220px] h-[54px] p-1.5 bg-emerald-700 rounded-[10px] justify-end items-center gap-2.5 inline-flex',
className,
)}
>
<div className=" justify-center items-center flex flex-1">
<div className="text-white text-xl font-semibold leading-7">
</div>
</div>
<div className="w-[74px] h-[46px] bg-emerald-200 rounded-[8.93px] justify-center items-center gap-2 inline-flex">
<UnionSvg className="animate-fade-in-medium transform rotate-180" />
<UnionSvg className="animate-fade-in-strong transform rotate-180" />
</div>
</div>
)
}
const AgentInBtn = ({
className,
isProxyEnabled,
}: { className?: string; isProxyEnabled: boolean }) => {
let timer: NodeJS.Timeout | null = null
let index = 0
let [opacityList, setOpacityList] = useState([
'opacity-100',
'opacity-50',
'opacity-25',
])
// todo 后期使用gasp 或者 formwork 替代定时器
useEffect(() => {
timer = setInterval(() => {
switch (index) {
case 0:
opacityList = ['opacity-50', 'opacity-100', 'opacity-50']
break
case 1:
opacityList = ['opacity-25', 'opacity-50', 'opacity-100']
break
case 2:
opacityList = ['opacity-100', 'opacity-50', 'opacity-25']
break
default:
}
index++
setOpacityList(opacityList)
if (index === 3) {
index = 0
}
}, 300)
return () => {
timer && clearInterval(timer)
}
}, [])
return (
<div
className={cn(
'proxy-btn-loading w-[220px] h-[54px] p-1 bg-emerald-700 rounded-[10px] justify-start items-center gap-2.5 inline-flex',
className,
)}
>
<div className="w-full h-full p-[4.46px] bg-emerald-200 rounded-lg justify-center items-center gap-[4.46px] inline-flex">
<div className="text-[#232b54] text-xl font-semibold leading-7 mr-[10px]">
{isProxyEnabled ? '关闭中' : '代理中'}
</div>
<UnionSvg className={cn(opacityList[0])} />
<UnionSvg className={cn(opacityList[1])} />
<UnionSvg className={cn(opacityList[2])} />
</div>
</div>
)
}
function Home() {
let trafficWs: WebSocketClient | null = null
const dispatch = useDispatch<AppDispatch>()
const { loadCoreConfig } = useCoreConfig()
const [videoPath, setVideoPath] = useState<string>('')
const { countries, exitCountries } = useSelector(
(state: RootState) => state.nodesReducer,
)
const { circuits, fallbackCircuit } = useSelector(
(state: RootState) => state.circuitReducer,
)
const { isProxyEnabled, isCoreRunning } = useSelector(
(state: RootState) => state.serviceReducer,
)
const [isProxyLoading, setIsProxyLoading] = useState(false)
const [traffic, setTraffic] = useState<{
send_bytes: number
recv_bytes: number
}>({ recv_bytes: 0, send_bytes: 0 })
const isCircuitReady = useMemo(() => {
// 判断是否有有链路并且至少有一个能用
const circuitReady = circuits.some((circuit) => circuit?.state !== 'failed')
if (
circuitReady ||
(fallbackCircuit && fallbackCircuit?.state !== 'failed')
) {
return true
} else {
return false
}
}, [circuits, fallbackCircuit])
// 处理代理开关
const handleProxyToggle = async (
isProxyEnabled: boolean,
isCircuitReady: boolean,
isCoreRunning: boolean,
) => {
try {
// 如果核心未运行,先启动核心
if (!isCoreRunning) {
await commands.startCore()
}
// 这里要先轮询去判断是否启动成功,如果启动失败,则抛出异常
let isGetNodesResult = await commands.getNodes()
let count = 0
while (isGetNodesResult.status !== 'ok') {
isGetNodesResult = await commands.getNodes()
count++
// 如果50次都没有开启成功核心那么启动失败
if (count > 50) {
throw new Error('启动核心失败')
}
}
// 如果代理未启用且链路未就绪,创建默认链路
// if (!isProxyEnabled && !isCircuitReady) {
// await dispatch(
// createCircuit({
// uid: uuidv4(),
// name: '系统默认链路',
// inbound: countries[0],
// outbound: exitCountries[exitCountries.length - 1],
// multi_hop: 3,
// fallback: true,
// rule_path: null,
// is_prefix: false,
// }),
// ).unwrap()
// }
setIsProxyLoading(true)
// 切换代理状态
await dispatch(isProxyEnabled ? disableProxy() : enableProxy()).unwrap()
} catch (error) {
console.log(error, 'error')
const errorMessage = isProxyEnabled
? '关闭代理失败!'
: '开启代理失败,请检查节点配置、或重新尝试开启'
errorToast(errorMessage, toast)
console.error('Proxy toggle failed:', error)
} finally {
setIsProxyLoading(false)
}
}
const openWsTraffic = async () => {
if (trafficWs) return
const { api_port } = await loadCoreConfig()
// todo! 后面会把二级制文件启动的参数作为配置项,这里暂时写死
trafficWs = new WebSocketClient(`ws://127.0.0.1:${api_port}/traffic`, {
headers: {
Authorization: 'Bearer secret',
},
})
// 执行 WebSocket 操作
await trafficWs.connect()
trafficWs.addListener((msg) => {
if (msg.type === 'Text') {
const result = JSON.parse(msg.data)
setTraffic({
recv_bytes: bytesToMbps(result.data.recv_bytes),
send_bytes: bytesToMbps(result.data.send_bytes),
})
}
})
}
const closeWsTraffic = async () => {
if (trafficWs) {
await trafficWs.disconnect()
trafficWs = null
}
}
const getVideoPath = async () => {
const resourceDirPath = await resourceDir()
// tauri 读取视频和音频不能直接像前端那样import video from 'xxx.mp4' 需要用convertFileSrc并且开启相应的权限不然打包出来的引用会非常卡媒体文件越大越卡
// https://v2.tauri.app/zh-cn/reference/javascript/api/namespacecore/#convertfilesrc
const filePath = await join(
resourceDirPath,
'assets/video/crystal-ball.mp4',
)
const assetUrl = convertFileSrc(filePath)
setVideoPath(assetUrl)
}
useEffect(() => {
getVideoPath()
}, [])
useEffect(() => {
if (isProxyEnabled) {
openWsTraffic()
} else {
closeWsTraffic()
}
return () => {
closeWsTraffic()
}
}, [isProxyEnabled])
return (
<main className="home w-full h-[100%] flex flex-col">
<div className="w-full h-[217.81px] py-8 relative flex flex-shrink-0 overflow-hidden">
<video
id="my-video"
src={videoPath}
className="absolute top-0 -right-40 w-full h-[309px] object-cover transform -translate-y-[80px] pointer-events-none"
autoPlay
loop
muted
preload="auto"
></video>
{/* <video src={BgVideo} className='absolute top-0 right-0 w-full h-[309px] object-none transform -translate-y-[80px] translate-x-[0px]' autoPlay loop muted></video> */}
<div className="my-gradient"></div>
<div className="w-fit h-full flex">
<div className="relative px-12 pr-6 z-10 cursor-default no-select">
<div className="text-zinc-900 text-3xl font-semibold leading-9">
使
</div>
<div className="mt-[11px] text-zinc-900 text-lg font-light leading-relaxed">
</div>
<div
className="w-fit cursor-pointer mt-5 flex items-center gap-x-4"
onClick={() =>
handleProxyToggle(isProxyEnabled, isCircuitReady, isCoreRunning)
}
>
{isProxyLoading ? (
<div className="group relative">
<AgentInBtn
className="group-hover:hidden"
isProxyEnabled={isProxyEnabled}
/>
<AgentCloseBtn className="hidden group-hover:flex" />
</div>
) : isProxyEnabled ? (
<AgentCloseBtn />
) : (
<AgentOpenBtn />
)}
</div>
</div>
{isProxyEnabled && (
<div className="relative z-10 flex flex-col gap-y-[16px]">
<div className="h-[58.1px] rounded-xl justify-center items-center gap-4 flex">
<div>
<div className="text-zinc-900 text-3xl font-semibold leading-9">
{traffic.send_bytes === 0
? '0.000'
: traffic.send_bytes.toFixed(3)}
</div>
<div className="mt-2 flex items-center gap-1 text-zinc-600 text-sm font-normal leading-tight">
<ArrowDownSvg /> <span>/Mbps</span>
</div>
</div>
<LineChart data={traffic.send_bytes} lineColor="#2563eb" />
</div>
<div className="h-[58.1px] rounded-xl justify-center items-center gap-4 flex">
<div>
<div className="text-zinc-900 text-3xl font-semibold leading-9">
{traffic.recv_bytes === 0
? '0.000'
: traffic.recv_bytes.toFixed(3)}
</div>
<div className="mt-2 flex items-center gap-1 text-zinc-600 text-sm font-normal leading-tight">
<ArrowUpSvg /> <span>/Mbps</span>
</div>
</div>
<LineChart data={traffic.recv_bytes} lineColor="#14b8a6" />
</div>
</div>
)}
</div>
</div>
<div className="flex flex-col flex-1 px-6 my-5">
<Links
isDefault={true}
label="默认链路"
className="h-[156px] flex-shrink-0"
/>
<Links
label="应用分流链路"
className="mt-5 flex-1 flex flex-col"
tableClassName="h-[100%]"
/>
</div>
{import.meta.env.DEV && <ServiceControlPanel />}
</main>
)
}
export default memo(Home)