Spaces:
Paused
Paused
| import { useState, useEffect, useCallback } from 'react'; | |
| import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node'; | |
| import { useLoaderData, useSubmit, useNavigation } from '@remix-run/react'; | |
| import { spawn, type ChildProcess } from 'child_process'; | |
| type SshxStatus = 'running' | 'stopped'; | |
| interface LoaderData { | |
| status: SshxStatus; | |
| output: string; | |
| link: string | null; | |
| shell: string | null; | |
| } | |
| let sshxProcess: ChildProcess | null = null; | |
| let sshxOutput = ''; | |
| const extractFromOutput = (output: string, regex: RegExp): string | null => { | |
| const match = output.match(regex); | |
| return match ? match[1] : null; | |
| }; | |
| const handleProcessOutput = (data: Buffer) => { | |
| sshxOutput += data.toString(); | |
| }; | |
| const handleProcessClose = () => { | |
| sshxProcess = null; | |
| sshxOutput += '\nSSHX进程已结束。'; | |
| }; | |
| export const loader: LoaderFunction = async () => { | |
| const status: SshxStatus = sshxProcess ? 'running' : 'stopped'; | |
| const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/); | |
| const shell = extractFromOutput(sshxOutput, /Shell:\s+([^\n]+)/); | |
| return json<LoaderData>({ status, output: sshxOutput, link, shell }); | |
| }; | |
| export const action: ActionFunction = async ({ request }) => { | |
| const formData = await request.formData(); | |
| const action = formData.get('action'); | |
| if (action === 'start' && !sshxProcess) { | |
| sshxProcess = spawn('/home/pn/sshx/sshx', ['-q']); // 修改这里,添加 '-q' 参数 | |
| sshxProcess.stdout?.on('data', handleProcessOutput); | |
| sshxProcess.stderr?.on('data', handleProcessOutput); | |
| sshxProcess.on('close', handleProcessClose); | |
| return json({ status: 'started' }); | |
| } else if (action === 'stop' && sshxProcess) { | |
| sshxProcess.kill(); | |
| handleProcessClose(); | |
| return json({ status: 'stopped' }); | |
| } | |
| return json({ error: '无效操作' }, { status: 400 }); | |
| }; | |
| export default function Sshx() { | |
| const { status, output, link, shell } = useLoaderData<LoaderData>(); | |
| const submit = useSubmit(); | |
| const navigation = useNavigation(); | |
| const [localOutput, setLocalOutput] = useState(output); | |
| const [startTime, setStartTime] = useState<number | null>(null); | |
| const refreshData = useCallback(() => { | |
| submit(null, { method: 'get', replace: true }); | |
| }, [submit]); | |
| const stopSshx = useCallback(() => { | |
| submit({ action: 'stop' }, { method: 'post' }); | |
| setStartTime(null); | |
| }, [submit]); | |
| useEffect(() => { | |
| let interval: NodeJS.Timeout | null = null; | |
| let timer: NodeJS.Timeout | null = null; | |
| if (status === 'running') { | |
| interval = setInterval(refreshData, 60000); // 每60秒更新一次 | |
| if (startTime === null) { | |
| setStartTime(Date.now()); | |
| } else { | |
| const elapsedTime = Date.now() - startTime; | |
| const remainingTime = 600000 - elapsedTime; // 10分钟 = 600000毫秒 | |
| if (remainingTime > 0) { | |
| timer = setTimeout(stopSshx, remainingTime); | |
| } else { | |
| stopSshx(); | |
| } | |
| } | |
| } else { | |
| setStartTime(null); | |
| } | |
| return () => { | |
| if (interval) clearInterval(interval); | |
| if (timer) clearTimeout(timer); | |
| }; | |
| }, [refreshData, status, startTime, stopSshx]); | |
| useEffect(() => { | |
| setLocalOutput(output); | |
| }, [output]); | |
| const isLoading = navigation.state === 'submitting' || navigation.state === 'loading'; | |
| const handleAction = (action: 'start' | 'stop') => { | |
| if (action === 'start') { | |
| setStartTime(Date.now()); | |
| } else { | |
| setStartTime(null); | |
| } | |
| submit({ action }, { method: 'post' }); | |
| setTimeout(refreshData, 100); | |
| }; | |
| // 计算剩余时间 | |
| const remainingTime = startTime ? Math.max(0, 600 - Math.floor((Date.now() - startTime) / 1000)) : 0; | |
| return ( | |
| <div className="container mx-auto px-4 py-8 max-w-3xl"> | |
| <h1 className="text-3xl font-bold text-primary mb-6 pb-2 border-b-2 border-primary">SSHX控制面板</h1> | |
| <div className="mb-6"> | |
| <p className="text-lg font-semibold"> | |
| 状态: <span className={`${status === 'running' ? 'text-green-600' : 'text-red-600'} font-bold`}> | |
| {status === 'running' ? '运行中' : '已停止'} | |
| </span> | |
| </p> | |
| </div> | |
| <div className="flex space-x-4 mb-6"> | |
| <button | |
| onClick={() => handleAction('start')} | |
| className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${ | |
| status === 'running' || isLoading | |
| ? 'bg-gray-400 cursor-not-allowed' | |
| : 'bg-green-500 hover:bg-green-600' | |
| }`} | |
| disabled={status === 'running' || isLoading} | |
| > | |
| 启动SSHX | |
| </button> | |
| <button | |
| onClick={() => handleAction('stop')} | |
| className={`px-4 py-2 rounded-md text-white transition-colors duration-300 ${ | |
| status === 'stopped' || isLoading | |
| ? 'bg-gray-400 cursor-not-allowed' | |
| : 'bg-red-500 hover:bg-red-600' | |
| }`} | |
| disabled={status === 'stopped' || isLoading} | |
| > | |
| 停止SSHX | |
| </button> | |
| </div> | |
| {status === 'running' && ( | |
| <div className="bg-blue-100 border border-blue-300 rounded-md p-4 mb-6"> | |
| <p className="mb-2 text-blue-800"> | |
| <strong className="font-semibold">链接:</strong>{' '} | |
| {link ? ( | |
| <a href={link} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline font-medium"> | |
| {link} | |
| </a> | |
| ) : ( | |
| <span className="text-gray-600">暂不可用</span> | |
| )} | |
| </p> | |
| <p className="text-blue-800"> | |
| <strong className="font-semibold">Shell:</strong> <span className="font-medium">{shell || '暂不可用'}</span> | |
| </p> | |
| <p className="text-blue-800"> | |
| <strong className="font-semibold">剩余时间:</strong> <span className="font-medium">{remainingTime} 秒</span> | |
| </p> | |
| </div> | |
| )} | |
| <h2 className="text-2xl font-semibold text-secondary mb-4">输出:</h2> | |
| <pre className="bg-gray-800 text-blue-600 p-4 rounded-md border border-gray-600 font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96 overflow-y-auto"> | |
| {localOutput} | |
| </pre> | |
| </div> | |
| ); | |
| } | |