Files
sub-router/web/src/components/Output.tsx
2026-04-13 22:19:30 +08:00

191 lines
5.0 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 { useState, useEffect } from 'react';
import { config as configApi } from '../api';
export default function Output() {
const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false);
const [copiedSurge, setCopiedSurge] = useState(false);
const [copiedClash, setCopiedClash] = useState(false);
const [copiedSsr, setCopiedSsr] = useState(false);
const [surgeToken, setSurgeToken] = useState('');
const surgeUrl = surgeToken ? `${window.location.origin}/surge/${surgeToken}` : '';
const clashUrl = surgeToken ? `${window.location.origin}/clash/${surgeToken}` : '';
const ssrUrl = surgeToken ? `${window.location.origin}/ssr/${surgeToken}` : '';
const loadToken = async () => {
try {
const data = await configApi.getSurgeToken();
setSurgeToken(data.token || '');
} catch {}
};
const handleRegenerate = async () => {
if (!confirm('重新生成后旧的订阅链接将失效Surge / Clash / SSR 三条链接同时更新)。确定继续?')) return;
try {
const data = await configApi.regenerateSurgeToken();
setSurgeToken(data.token);
loadPreview();
} catch {}
};
const loadPreview = async () => {
setLoading(true);
try {
const data = await configApi.preview();
setPreview(data.config);
} catch (err: any) {
setPreview(`# Error: ${err.message}`);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadToken();
loadPreview();
}, []);
const makeCopyHandler = (url: string, setter: (v: boolean) => void) => () => {
navigator.clipboard.writeText(url);
setter(true);
setTimeout(() => setter(false), 2000);
};
return (
<div>
<h2 style={styles.title}></h2>
<p style={styles.subtitle}>Surge / Clash / SSR </p>
{/* Surge URL */}
<UrlCard
label="SURGE 订阅链接"
url={surgeUrl}
copied={copiedSurge}
onCopy={makeCopyHandler(surgeUrl, setCopiedSurge)}
onRegenerate={handleRegenerate}
/>
{/* Clash URL */}
<UrlCard
label="CLASH / STASH 订阅链接"
url={clashUrl}
copied={copiedClash}
onCopy={makeCopyHandler(clashUrl, setCopiedClash)}
/>
{/* SSR URL */}
<UrlCard
label="SSR / QX / 小火箭 订阅链接"
url={ssrUrl}
copied={copiedSsr}
onCopy={makeCopyHandler(ssrUrl, setCopiedSsr)}
/>
{/* Preview */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<span style={styles.sectionLabel}>SURGE </span>
<button className="small" onClick={loadPreview} disabled={loading}>
{loading ? '加载中...' : '刷新预览'}
</button>
</div>
<pre style={styles.pre}>
{preview || '(empty)'}
</pre>
</div>
);
}
function UrlCard({
label,
url,
copied,
onCopy,
onRegenerate,
}: {
label: string;
url: string;
copied: boolean;
onCopy: () => void;
onRegenerate?: () => void;
}) {
return (
<div style={styles.card}>
<div style={styles.cardLabel}>{label}</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<code style={styles.urlCode}>{url || '加载中...'}</code>
<button className="small" onClick={onCopy} disabled={!url}>
{copied ? '已复制' : '复制'}
</button>
{onRegenerate && (
<button className="small" onClick={onRegenerate} disabled={!url}>
</button>
)}
</div>
</div>
);
}
const styles = {
title: {
fontFamily: 'var(--font-mono)' as const,
fontSize: 16,
fontWeight: 600 as const,
color: 'var(--text-primary)',
marginBottom: 4,
},
subtitle: {
fontSize: 12,
color: 'var(--text-secondary)',
marginBottom: 20,
},
card: {
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius)',
padding: 16,
marginBottom: 12,
},
cardLabel: {
fontSize: 10,
fontFamily: 'var(--font-mono)' as const,
color: 'var(--text-muted)',
textTransform: 'uppercase' as const,
letterSpacing: '0.1em',
marginBottom: 8,
},
urlCode: {
flex: 1,
fontFamily: 'var(--font-mono)' as const,
fontSize: 13,
color: 'var(--accent)',
userSelect: 'all' as const,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap' as const,
},
sectionLabel: {
fontSize: 10,
fontFamily: 'var(--font-mono)' as const,
color: 'var(--text-muted)',
textTransform: 'uppercase' as const,
letterSpacing: '0.1em',
},
pre: {
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius)',
padding: 16,
fontFamily: 'var(--font-mono)' as const,
fontSize: 11,
lineHeight: 1.6,
color: 'var(--text-secondary)',
overflow: 'auto',
maxHeight: 'calc(100vh - 480px)',
whiteSpace: 'pre-wrap' as const,
wordBreak: 'break-all' as const,
},
};