orztv commited on
Commit
584434c
·
1 Parent(s): b7f25bd
Files changed (2) hide show
  1. src/sshx.tsx +161 -160
  2. src/startup.sh +3 -1
src/sshx.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { useState, useEffect, useCallback } from 'react';
2
  import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node';
3
- import { useLoaderData, Form, useSubmit, useNavigation } from '@remix-run/react';
4
  import { spawn, type ChildProcess } from 'child_process';
5
 
6
  type SshxStatus = 'running' | 'stopped';
@@ -15,6 +15,20 @@ interface LoaderData {
15
  let sshxProcess: ChildProcess | null = null;
16
  let sshxOutput = '';
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export const loader: LoaderFunction = async () => {
19
  const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
20
  const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
@@ -38,23 +52,9 @@ export const action: ActionFunction = async ({ request }) => {
38
  return json({ status: 'stopped' });
39
  }
40
 
41
- return json({ error: 'Invalid action' }, { status: 400 });
42
  };
43
 
44
- function handleProcessOutput(data: Buffer) {
45
- sshxOutput += data.toString();
46
- }
47
-
48
- function handleProcessClose() {
49
- sshxProcess = null;
50
- sshxOutput += '\nSSHX process has ended.';
51
- }
52
-
53
- function extractFromOutput(output: string, regex: RegExp): string | null {
54
- const match = output.match(regex);
55
- return match ? match[1] : null;
56
- }
57
-
58
  export default function Sshx() {
59
  const { status, output, link, shell } = useLoaderData<LoaderData>();
60
  const submit = useSubmit();
@@ -66,9 +66,14 @@ export default function Sshx() {
66
  }, [submit]);
67
 
68
  useEffect(() => {
69
- const interval = setInterval(refreshData, 1000);
70
- return () => clearInterval(interval);
71
- }, [refreshData]);
 
 
 
 
 
72
 
73
  useEffect(() => {
74
  setLocalOutput(output);
@@ -76,168 +81,164 @@ export default function Sshx() {
76
 
77
  const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
78
 
 
 
 
 
 
79
  return (
80
  <div className="container">
81
- <h1 className="title">SSHX Control</h1>
82
  <p className="status">
83
- Status: <span className={status === 'running' ? 'status-running' : 'status-stopped'}>{status}</span>
84
  </p>
85
  <div className="button-group">
86
- <Form method="post">
87
- <button
88
- type="submit"
89
- name="action"
90
- value="start"
91
- className={`button button-start ${status === 'running' || isLoading ? 'button-disabled' : ''}`}
92
- disabled={status === 'running' || isLoading}
93
- >
94
- Start SSHX
95
- </button>
96
- </Form>
97
- <Form method="post">
98
- <button
99
- type="submit"
100
- name="action"
101
- value="stop"
102
- className={`button button-stop ${status === 'stopped' || isLoading ? 'button-disabled' : ''}`}
103
- disabled={status === 'stopped' || isLoading}
104
- >
105
- Stop SSHX
106
- </button>
107
- </Form>
108
  </div>
109
  {status === 'running' && (
110
  <div className="info-box">
111
  <p>
112
- <strong>Link:</strong>{' '}
113
  {link ? (
114
  <a href={link} target="_blank" rel="noopener noreferrer" className="link">
115
  {link}
116
  </a>
117
  ) : (
118
- 'Not available'
119
  )}
120
  </p>
121
  <p>
122
- <strong>Shell:</strong> {shell || 'Not available'}
123
  </p>
124
  </div>
125
  )}
126
- <h2 className="subtitle">Output:</h2>
127
  <pre className="output">{localOutput}</pre>
128
 
129
- <style dangerouslySetInnerHTML={{
130
- __html: `
131
- :root {
132
- --primary-color: #4a90e2;
133
- --secondary-color: #f5a623;
134
- --background-color: #f9f9f9;
135
- --text-color: #333;
136
- --border-color: #e0e0e0;
137
- }
138
-
139
- body {
140
- font-family: 'Arial', sans-serif;
141
- line-height: 1.6;
142
- color: var(--text-color);
143
- background-color: var(--background-color);
144
- margin: 0;
145
- padding: 0;
146
- }
147
-
148
- .container {
149
- max-width: 800px;
150
- margin: 0 auto;
151
- padding: 20px;
152
- }
153
-
154
- .title {
155
- color: var(--primary-color);
156
- border-bottom: 2px solid var(--primary-color);
157
- padding-bottom: 10px;
158
- margin-bottom: 20px;
159
- }
160
-
161
- .subtitle {
162
- color: var(--secondary-color);
163
- border-bottom: 1px solid var(--secondary-color);
164
- padding-bottom: 5px;
165
- margin-top: 20px;
166
- }
167
-
168
- .status {
169
- font-size: 18px;
170
- font-weight: bold;
171
- margin-bottom: 20px;
172
- }
173
-
174
- .status-running {
175
- color: #4CAF50;
176
- }
177
-
178
- .status-stopped {
179
- color: #f44336;
180
- }
181
-
182
- .button-group {
183
- display: flex;
184
- gap: 10px;
185
- margin-bottom: 20px;
186
- }
187
-
188
- .button {
189
- padding: 10px 20px;
190
- font-size: 16px;
191
- color: white;
192
- border: none;
193
- border-radius: 5px;
194
- cursor: pointer;
195
- transition: background-color 0.3s ease;
196
- }
197
-
198
- .button-start {
199
- background-color: #4CAF50;
200
- }
201
-
202
- .button-stop {
203
- background-color: #f44336;
204
- }
205
-
206
- .button-disabled {
207
- opacity: 0.5;
208
- cursor: not-allowed;
209
- }
210
-
211
- .info-box {
212
- background-color: #e8f4fd;
213
- padding: 15px;
214
- border-radius: 5px;
215
- margin-bottom: 20px;
216
- border: 1px solid var(--border-color);
217
- }
218
-
219
- .link {
220
- color: var(--primary-color);
221
- text-decoration: none;
222
- }
223
-
224
- .link:hover {
225
- text-decoration: underline;
226
- }
227
-
228
- .output {
229
- background-color: #f0f0f0;
230
- padding: 15px;
231
- border-radius: 5px;
232
- white-space: pre-wrap;
233
- word-wrap: break-word;
234
- border: 1px solid var(--border-color);
235
- font-family: 'Courier New', monospace;
236
- font-size: 14px;
237
- max-height: 400px;
238
- overflow-y: auto;
239
- }
240
- `}} />
241
  </div>
242
  );
243
- }
 
1
  import { useState, useEffect, useCallback } from 'react';
2
  import { json, type ActionFunction, type LoaderFunction } from '@remix-run/node';
3
+ import { useLoaderData, useSubmit, useNavigation } from '@remix-run/react';
4
  import { spawn, type ChildProcess } from 'child_process';
5
 
6
  type SshxStatus = 'running' | 'stopped';
 
15
  let sshxProcess: ChildProcess | null = null;
16
  let sshxOutput = '';
17
 
18
+ const extractFromOutput = (output: string, regex: RegExp): string | null => {
19
+ const match = output.match(regex);
20
+ return match ? match[1] : null;
21
+ };
22
+
23
+ const handleProcessOutput = (data: Buffer) => {
24
+ sshxOutput += data.toString();
25
+ };
26
+
27
+ const handleProcessClose = () => {
28
+ sshxProcess = null;
29
+ sshxOutput += '\nSSHX进程已结束。';
30
+ };
31
+
32
  export const loader: LoaderFunction = async () => {
33
  const status: SshxStatus = sshxProcess ? 'running' : 'stopped';
34
  const link = extractFromOutput(sshxOutput, /Link:\s+(https:\/\/sshx\.io\/s\/[^\s]+)/);
 
52
  return json({ status: 'stopped' });
53
  }
54
 
55
+ return json({ error: '无效操作' }, { status: 400 });
56
  };
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  export default function Sshx() {
59
  const { status, output, link, shell } = useLoaderData<LoaderData>();
60
  const submit = useSubmit();
 
66
  }, [submit]);
67
 
68
  useEffect(() => {
69
+ let interval: NodeJS.Timeout | null = null;
70
+ if (status === 'running') {
71
+ interval = setInterval(refreshData, 60000); // 每60秒更新一次
72
+ }
73
+ return () => {
74
+ if (interval) clearInterval(interval);
75
+ };
76
+ }, [refreshData, status]);
77
 
78
  useEffect(() => {
79
  setLocalOutput(output);
 
81
 
82
  const isLoading = navigation.state === 'submitting' || navigation.state === 'loading';
83
 
84
+ const handleAction = (action: 'start' | 'stop') => {
85
+ submit({ action }, { method: 'post' });
86
+ setTimeout(refreshData, 100);
87
+ };
88
+
89
  return (
90
  <div className="container">
91
+ <h1 className="title">SSHX控制面板</h1>
92
  <p className="status">
93
+ 状态: <span className={status === 'running' ? 'status-running' : 'status-stopped'}>{status === 'running' ? '运行中' : '已停止'}</span>
94
  </p>
95
  <div className="button-group">
96
+ <button
97
+ onClick={() => handleAction('start')}
98
+ className={`button button-start ${status === 'running' || isLoading ? 'button-disabled' : ''}`}
99
+ disabled={status === 'running' || isLoading}
100
+ >
101
+ 启动SSHX
102
+ </button>
103
+ <button
104
+ onClick={() => handleAction('stop')}
105
+ className={`button button-stop ${status === 'stopped' || isLoading ? 'button-disabled' : ''}`}
106
+ disabled={status === 'stopped' || isLoading}
107
+ >
108
+ 停止SSHX
109
+ </button>
 
 
 
 
 
 
 
 
110
  </div>
111
  {status === 'running' && (
112
  <div className="info-box">
113
  <p>
114
+ <strong>链接:</strong>{' '}
115
  {link ? (
116
  <a href={link} target="_blank" rel="noopener noreferrer" className="link">
117
  {link}
118
  </a>
119
  ) : (
120
+ '暂不可用'
121
  )}
122
  </p>
123
  <p>
124
+ <strong>Shell:</strong> {shell || '暂不可用'}
125
  </p>
126
  </div>
127
  )}
128
+ <h2 className="subtitle">输出:</h2>
129
  <pre className="output">{localOutput}</pre>
130
 
131
+ <style jsx>{`
132
+ :root {
133
+ --primary-color: #4a90e2;
134
+ --secondary-color: #f5a623;
135
+ --background-color: #f9f9f9;
136
+ --text-color: #333;
137
+ --border-color: #e0e0e0;
138
+ }
139
+
140
+ body {
141
+ font-family: 'Arial', sans-serif;
142
+ line-height: 1.6;
143
+ color: var(--text-color);
144
+ background-color: var(--background-color);
145
+ margin: 0;
146
+ padding: 0;
147
+ }
148
+
149
+ .container {
150
+ max-width: 800px;
151
+ margin: 0 auto;
152
+ padding: 20px;
153
+ }
154
+
155
+ .title {
156
+ color: var(--primary-color);
157
+ border-bottom: 2px solid var(--primary-color);
158
+ padding-bottom: 10px;
159
+ margin-bottom: 20px;
160
+ }
161
+
162
+ .subtitle {
163
+ color: var(--secondary-color);
164
+ border-bottom: 1px solid var(--secondary-color);
165
+ padding-bottom: 5px;
166
+ margin-top: 20px;
167
+ }
168
+
169
+ .status {
170
+ font-size: 18px;
171
+ font-weight: bold;
172
+ margin-bottom: 20px;
173
+ }
174
+
175
+ .status-running {
176
+ color: #4CAF50;
177
+ }
178
+
179
+ .status-stopped {
180
+ color: #f44336;
181
+ }
182
+
183
+ .button-group {
184
+ display: flex;
185
+ gap: 10px;
186
+ margin-bottom: 20px;
187
+ }
188
+
189
+ .button {
190
+ padding: 10px 20px;
191
+ font-size: 16px;
192
+ color: white;
193
+ border: none;
194
+ border-radius: 5px;
195
+ cursor: pointer;
196
+ transition: background-color 0.3s ease;
197
+ }
198
+
199
+ .button-start {
200
+ background-color: #4CAF50;
201
+ }
202
+
203
+ .button-stop {
204
+ background-color: #f44336;
205
+ }
206
+
207
+ .button-disabled {
208
+ opacity: 0.5;
209
+ cursor: not-allowed;
210
+ }
211
+
212
+ .info-box {
213
+ background-color: #e8f4fd;
214
+ padding: 15px;
215
+ border-radius: 5px;
216
+ margin-bottom: 20px;
217
+ border: 1px solid var(--border-color);
218
+ }
219
+
220
+ .link {
221
+ color: var(--primary-color);
222
+ text-decoration: none;
223
+ }
224
+
225
+ .link:hover {
226
+ text-decoration: underline;
227
+ }
228
+
229
+ .output {
230
+ background-color: #f0f0f0;
231
+ padding: 15px;
232
+ border-radius: 5px;
233
+ white-space: pre-wrap;
234
+ word-wrap: break-word;
235
+ border: 1px solid var(--border-color);
236
+ font-family: 'Courier New', monospace;
237
+ font-size: 14px;
238
+ max-height: 400px;
239
+ overflow-y: auto;
240
+ }
241
+ `}</style>
 
242
  </div>
243
  );
244
+ }
src/startup.sh CHANGED
@@ -4,6 +4,8 @@
4
  cd ${HOMEDIR}/${REMIX_NAME}
5
  pm2 start ecosystem.config.cjs
6
  pm2 save
7
- pm2 logs my-remix-app
 
 
8
 
9
  cd ${HOMEDIR}
 
4
  cd ${HOMEDIR}/${REMIX_NAME}
5
  pm2 start ecosystem.config.cjs
6
  pm2 save
7
+
8
+ # 只输出日志
9
+ pm2 logs my-remix-app --lines 50
10
 
11
  cd ${HOMEDIR}