365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
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)
|