feat: clash/ss suport

This commit is contained in:
2026-04-13 22:19:30 +08:00
parent 699b038a6c
commit 35e95ee777
11 changed files with 665 additions and 129 deletions

View File

@@ -4,10 +4,14 @@ import { config as configApi } from '../api';
export default function Output() {
const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false);
const [copied, setCopied] = 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 {
@@ -17,7 +21,7 @@ export default function Output() {
};
const handleRegenerate = async () => {
if (!confirm('重新生成后,旧的订阅链接将失效Surge 客户端需要更新订阅地址。确定继续?')) return;
if (!confirm('重新生成后,旧的订阅链接将失效Surge / Clash / SSR 三条链接同时更新)。确定继续?')) return;
try {
const data = await configApi.regenerateSurgeToken();
setSurgeToken(data.token);
@@ -42,93 +46,88 @@ export default function Output() {
loadPreview();
}, []);
const handleCopy = () => {
navigator.clipboard.writeText(surgeUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
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 </p>
<p style={styles.subtitle}>Surge / Clash / SSR </p>
{/* Subscription URL */}
<div style={{
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius)',
padding: 16,
marginBottom: 20,
}}>
<div style={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: 8,
}}>
Surge
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<code style={{
flex: 1,
fontFamily: 'var(--font-mono)',
fontSize: 13,
color: 'var(--accent)',
userSelect: 'all',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{surgeUrl || '加载中...'}
</code>
<button className="small" onClick={handleCopy} disabled={!surgeUrl}>
{copied ? '已复制' : '复制'}
</button>
<button className="small" onClick={handleRegenerate} disabled={!surgeUrl}>
</button>
</div>
</div>
{/* 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={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
}}>
</span>
<span style={styles.sectionLabel}>SURGE </span>
<button className="small" onClick={loadPreview} disabled={loading}>
{loading ? '加载中...' : '刷新预览'}
</button>
</div>
<pre style={{
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius)',
padding: 16,
fontFamily: 'var(--font-mono)',
fontSize: 11,
lineHeight: 1.6,
color: 'var(--text-secondary)',
overflow: 'auto',
maxHeight: 'calc(100vh - 360px)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}}>
<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,
@@ -142,4 +141,50 @@ const styles = {
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,
},
};