191 lines
5.0 KiB
TypeScript
191 lines
5.0 KiB
TypeScript
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,
|
||
},
|
||
};
|