Update App.jsx via upload script - 2026-08-13 11:00:02
This commit is contained in:
parent
8ef989ade4
commit
52dd9237ac
292
App.jsx
292
App.jsx
|
|
@ -5,15 +5,13 @@ import {
|
|||
Building2, Globe, Phone, Save, RefreshCw, LayoutDashboard, Users,
|
||||
Package, Layers, Zap, Tag, Inbox, Paperclip,
|
||||
Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Crown, Sparkles, Printer, Image as ImageIcon,
|
||||
ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Send
|
||||
ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
storageGet, storageSet, uploadFile, deleteFile,
|
||||
authCheck, authLogin, authLogout, authChangePassword,
|
||||
getAiConfig, saveAiConfig, deleteAiConfig, testAiConfig, generateAiDraft,
|
||||
parseCatalogFiles, translateTerms,
|
||||
getTelegramConfig, saveTelegramConfig, deleteTelegramConfig, testTelegramConfig,
|
||||
checkJmsLine,
|
||||
} from './storage';
|
||||
|
||||
/* ----------------------------- constants ----------------------------- */
|
||||
|
|
@ -1600,26 +1598,11 @@ const Styles = () => (
|
|||
.jms-card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.jms-card-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.jms-card-name { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.jms-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.jms-dot-normal { background: var(--led-green); box-shadow: 0 0 0 3px var(--led-green-bg); }
|
||||
.jms-dot-abnormal { background: var(--led-red); box-shadow: 0 0 0 3px var(--led-red-bg); animation: jms-pulse-red 1.6s ease-in-out infinite; }
|
||||
.jms-dot-checking { background: var(--led-amber); box-shadow: 0 0 0 3px var(--led-amber-bg); animation: jms-pulse-amber 1s ease-in-out infinite; }
|
||||
@keyframes jms-pulse-red { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
||||
@keyframes jms-pulse-amber { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
|
||||
.jms-proto-chip { font-size: 10px; font-weight: 700; letter-spacing: 0.03em; padding: 3px 9px; border-radius: 100px; text-transform: uppercase; flex-shrink: 0; }
|
||||
.jms-proto-ss { background: var(--led-green-bg); color: var(--led-green); }
|
||||
.jms-proto-vmess { background: var(--led-blue-bg); color: var(--blue); }
|
||||
.jms-proto-vless-cdn { background: var(--copper-soft); color: var(--copper); }
|
||||
.jms-proto-vless-reality { background: var(--led-red-bg); color: var(--led-red); }
|
||||
|
||||
.jms-monitor-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11.5px; color: var(--ink-soft); }
|
||||
.jms-status-abnormal { color: var(--led-red); font-weight: 600; }
|
||||
.jms-check-btn { background: none; border: none; color: var(--copper); font-size: 11.5px; font-weight: 600; cursor: pointer; padding: 2px 4px; font-family: inherit; }
|
||||
.jms-check-btn:hover { text-decoration: underline; }
|
||||
.jms-check-btn:disabled { color: var(--ink-soft); cursor: default; text-decoration: none; }
|
||||
.jms-check-note { font-size: 10.5px; color: var(--ink-soft); background: var(--bg); border: 1px dashed var(--line); border-radius: 6px; padding: 6px 8px; }
|
||||
|
||||
.jms-terminal { background: var(--ink); border-radius: 9px; padding: 12px 14px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; line-height: 1.85; }
|
||||
.jms-terminal-row { display: flex; gap: 10px; align-items: baseline; }
|
||||
.jms-terminal-key { color: rgba(255,255,255,0.5); flex-shrink: 0; width: 56px; }
|
||||
|
|
@ -1634,14 +1617,9 @@ const Styles = () => (
|
|||
.jms-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.jms-card-actions .btn { flex: 1; justify-content: center; }
|
||||
|
||||
.jms-group-select { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.jms-group-opt { border: 1.5px solid var(--line); border-radius: 9px; padding: 10px 8px; text-align: center; font-size: 12.5px; font-weight: 700; cursor: pointer; color: var(--ink-soft); }
|
||||
.jms-group-selected.jms-group-selfbuilt { border-color: var(--copper); background: var(--copper-soft); color: var(--copper); }
|
||||
.jms-group-selected.jms-group-jms { border-color: var(--blue); background: var(--led-blue-bg); color: var(--blue); }
|
||||
.jms-proto-select { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.jms-proto-select { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.jms-proto-opt { border: 1.5px solid var(--line); border-radius: 9px; padding: 9px 4px; text-align: center; font-size: 11px; font-weight: 700; cursor: pointer; letter-spacing: 0.02em; text-transform: uppercase; color: var(--ink-soft); }
|
||||
.jms-proto-selected.jms-proto-opt-ss { border-color: var(--led-green); background: var(--led-green-bg); color: var(--led-green); }
|
||||
.jms-proto-selected.jms-proto-opt-vmess { border-color: var(--blue); background: var(--led-blue-bg); color: var(--blue); }
|
||||
.jms-proto-selected.jms-proto-opt-vless-reality { border-color: var(--led-red); background: var(--led-red-bg); color: var(--led-red); }
|
||||
.jms-qr-link { font-family: 'IBM Plex Mono', monospace; font-size: 11px; word-break: break-all; color: var(--ink-soft); background: var(--bg); padding: 10px; border-radius: 8px; max-height: 90px; overflow-y: auto; text-align: left; margin: 12px 0; }
|
||||
|
||||
|
|
@ -2099,10 +2077,6 @@ function MainApp({ username, onLogout }) {
|
|||
const [aiKeyInput, setAiKeyInput] = useState('');
|
||||
const [aiSaving, setAiSaving] = useState(false);
|
||||
const [aiTesting, setAiTesting] = useState(false);
|
||||
const [tgTokenInput, setTgTokenInput] = useState('');
|
||||
const [tgChatIdInput, setTgChatIdInput] = useState('');
|
||||
const [tgSaving, setTgSaving] = useState(false);
|
||||
const [tgTesting, setTgTesting] = useState(false);
|
||||
const [aiGenerating, setAiGenerating] = useState(false);
|
||||
const [aiDraftModal, setAiDraftModal] = useState(null); // { channel, customerId, subject, body }
|
||||
|
||||
|
|
@ -2134,8 +2108,6 @@ function MainApp({ username, onLogout }) {
|
|||
const [catalogCategoryFilter, setCatalogCategoryFilter] = useState('all');
|
||||
const [jmsLines, setJmsLines] = useState([]);
|
||||
const [backPanelTemplates, setBackPanelTemplates] = useState([]);
|
||||
const [telegramConfigHasToken, setTelegramConfigHasToken] = useState(false);
|
||||
const [telegramConfigChatId, setTelegramConfigChatId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getAiConfig().then((res) => setAiConfigHasKey(!!res.hasKey)).catch(() => {});
|
||||
|
|
@ -2164,12 +2136,6 @@ function MainApp({ username, onLogout }) {
|
|||
setLoaded(true);
|
||||
})();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
getTelegramConfig().then(res => {
|
||||
setTelegramConfigHasToken(!!res.hasToken);
|
||||
setTelegramConfigChatId(res.chatId || '');
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.customers, JSON.stringify(customers)).catch(() => {}); }, [customers, loaded]);
|
||||
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.products, JSON.stringify(products)).catch(() => {}); }, [products, loaded]);
|
||||
useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.settings, JSON.stringify(settings)).catch(() => {}); }, [settings, loaded]);
|
||||
|
|
@ -2401,24 +2367,6 @@ function MainApp({ username, onLogout }) {
|
|||
setAiConfigHasKey(false);
|
||||
notify('已清除');
|
||||
}
|
||||
async function handleSaveTelegram() {
|
||||
if (!tgTokenInput.trim() || !tgChatIdInput.trim()) { notify('Bot Token和Chat ID都要填'); return; }
|
||||
setTgSaving(true);
|
||||
try {
|
||||
await saveTelegramSettings(tgTokenInput.trim(), tgChatIdInput.trim());
|
||||
setTgTokenInput('');
|
||||
} finally {
|
||||
setTgSaving(false);
|
||||
}
|
||||
}
|
||||
async function handleTestTelegram() {
|
||||
setTgTesting(true);
|
||||
try { await testTelegramSettings(); } finally { setTgTesting(false); }
|
||||
}
|
||||
async function handleClearTelegram() {
|
||||
if (!window.confirm('确定清除已保存的Telegram配置?清除后线路异常将不再发送报警。')) return;
|
||||
await removeTelegramSettings();
|
||||
}
|
||||
async function handleGenerateAi(customerId, channel) {
|
||||
if (!aiConfigHasKey) { notify('请先在设置中配置DeepSeek API Key'); return; }
|
||||
setAiGenerating(true);
|
||||
|
|
@ -2575,49 +2523,7 @@ function MainApp({ username, onLogout }) {
|
|||
notify('已删除');
|
||||
}
|
||||
|
||||
async function checkJmsLineNow(id) {
|
||||
const line = jmsLines.find(l => l.id === id);
|
||||
if (!line) return;
|
||||
setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: 'checking' } : l)));
|
||||
try {
|
||||
const res = await checkJmsLine(line);
|
||||
setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: res.status, lastChecked: res.checkedAt } : l)));
|
||||
if (res.status === 'abnormal') notify(`⚠ ${line.name || line.addr} 检测异常`);
|
||||
else notify(`${line.name || line.addr} 检测正常`);
|
||||
} catch (e) {
|
||||
setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: line.monitorStatus } : l)));
|
||||
notify('检测失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTelegramSettings(botToken, chatId) {
|
||||
try {
|
||||
await saveTelegramConfig(botToken, chatId);
|
||||
setTelegramConfigHasToken(true);
|
||||
setTelegramConfigChatId(chatId);
|
||||
notify('Telegram配置已保存');
|
||||
} catch (e) {
|
||||
notify('保存失败:' + e.message);
|
||||
}
|
||||
}
|
||||
async function removeTelegramSettings() {
|
||||
try {
|
||||
await deleteTelegramConfig();
|
||||
setTelegramConfigHasToken(false);
|
||||
setTelegramConfigChatId('');
|
||||
notify('已清除Telegram配置');
|
||||
} catch (e) {
|
||||
notify('清除失败:' + e.message);
|
||||
}
|
||||
}
|
||||
async function testTelegramSettings() {
|
||||
try {
|
||||
await testTelegramConfig();
|
||||
notify('测试消息已发送,去Telegram看看');
|
||||
} catch (e) {
|
||||
notify('测试失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportProductPdf() {
|
||||
if (!selectedProduct) return;
|
||||
|
|
@ -3337,7 +3243,6 @@ function MainApp({ username, onLogout }) {
|
|||
lines={jmsLines}
|
||||
onSave={saveJmsLine}
|
||||
onDelete={deleteJmsLine}
|
||||
onCheck={checkJmsLineNow}
|
||||
onNotify={notify}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -3821,35 +3726,6 @@ function MainApp({ username, onLogout }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panel-header"><div className="panel-title"><Send size={15} />Telegram 报警(Just My Socks线路监控)</div></div>
|
||||
<div className="panel-body" style={{ padding: 18 }}>
|
||||
<div style={{ fontSize: 12.5, color: 'var(--ink-soft)', marginBottom: 12 }}>
|
||||
状态:{telegramConfigHasToken ? <span style={{ color: 'var(--led-green)', fontWeight: 600 }}>已配置(Chat ID: {telegramConfigChatId})</span> : <span>未配置</span>}
|
||||
</div>
|
||||
<div className="grid-2">
|
||||
<div className="field">
|
||||
<span className="field-label">Bot Token</span>
|
||||
<input
|
||||
type="password" className="input" value={tgTokenInput} onChange={e => setTgTokenInput(e.target.value)}
|
||||
placeholder={telegramConfigHasToken ? '已设置,重新填写可覆盖' : '找 @BotFather 申请'}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<span className="field-label">Chat ID</span>
|
||||
<input className="input" value={tgChatIdInput} onChange={e => setTgChatIdInput(e.target.value)} placeholder="接收报警的对话ID" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn btn-primary" disabled={tgSaving} onClick={handleSaveTelegram}><Save size={13} />保存</button>
|
||||
<button className="btn" disabled={!telegramConfigHasToken || tgTesting} onClick={handleTestTelegram}>{tgTesting ? '发送中…' : '发送测试消息'}</button>
|
||||
{telegramConfigHasToken && <button className="btn btn-danger" onClick={handleClearTelegram}>清除</button>}
|
||||
</div>
|
||||
<div className="disclaimer" style={{ marginTop: 14 }}>
|
||||
Just My Socks页面的线路每10分钟自动巡检一次,只在状态发生变化(正常↔异常)时才发消息,不会每次检测都刷屏。Bot Token加密保存在服务器本地。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -4426,21 +4302,23 @@ function CustomerDetail({
|
|||
|
||||
const JMS_PROTO_META = {
|
||||
'ss': { label: 'SS' },
|
||||
'vmess': { label: 'VMess' },
|
||||
'vless-reality': { label: 'VLESS Reality' },
|
||||
'vless-cdn': { label: 'VLESS CDN' },
|
||||
};
|
||||
const JMS_PROTOCOLS = ['ss', 'vmess', 'vless-reality'];
|
||||
const JMS_PROTOCOLS = ['ss', 'vless-reality'];
|
||||
// Shadowsocks加密方式可选项——2022-blake3-*系列是新一代AEAD-2022标准,比老式aes/chacha更安全,
|
||||
// justmysocks2.net等主流服务商目前都已支持。
|
||||
const JMS_SS_METHODS = [
|
||||
'aes-256-gcm',
|
||||
'aes-128-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305',
|
||||
];
|
||||
|
||||
function jmsTimeAgo(ts) {
|
||||
if (!ts) return '尚未检测';
|
||||
const sec = Math.floor((Date.now() - ts) / 1000);
|
||||
if (sec < 60) return `${sec}秒前`;
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)}分钟前`;
|
||||
return `${Math.floor(sec / 3600)}小时前`;
|
||||
}
|
||||
function jmsMask(len = 8) { return '•'.repeat(Math.max(6, Math.min(len, 16))); }
|
||||
|
||||
// 按协议生成真实格式的订阅链接(ss:// / vmess:// / vless://),一键导入和二维码都用这个
|
||||
// 按协议生成真实格式的订阅链接(ss:// / vless://),一键导入和二维码都用这个
|
||||
function jmsBuildLink(line) {
|
||||
const { protocol, addr, port, name } = line;
|
||||
const tag = encodeURIComponent(name || addr);
|
||||
|
|
@ -4448,20 +4326,6 @@ function jmsBuildLink(line) {
|
|||
const userinfo = btoa(`${line.method}:${line.password}`).replace(/=+$/, '');
|
||||
return `ss://${userinfo}@${addr}:${port}#${tag}`;
|
||||
}
|
||||
if (protocol === 'vmess') {
|
||||
const obj = {
|
||||
v: '2', ps: name || addr, add: addr, port: String(port), id: line.uuid, aid: line.alterId || '0',
|
||||
net: 'ws', type: 'none', host: line.host || addr, path: '/', tls: 'tls',
|
||||
};
|
||||
return 'vmess://' + btoa(JSON.stringify(obj));
|
||||
}
|
||||
if (protocol === 'vless-cdn') {
|
||||
const q = new URLSearchParams({
|
||||
encryption: 'none', security: 'tls', type: 'ws', fp: 'chrome',
|
||||
host: line.host || addr, path: line.ws_path || '/stickrouter', sni: line.sni || addr,
|
||||
});
|
||||
return `vless://${line.uuid}@${addr}:${port}?${q.toString()}#${tag}`;
|
||||
}
|
||||
if (protocol === 'vless-reality') {
|
||||
const q = new URLSearchParams({
|
||||
encryption: 'none', security: 'reality', sni: line.sni, fp: line.fingerprint || 'chrome',
|
||||
|
|
@ -4481,35 +4345,34 @@ async function jmsResolveIp(domain) {
|
|||
return answer ? answer.data : null;
|
||||
}
|
||||
|
||||
function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate, onEdit, onDelete }) {
|
||||
const meta = JMS_PROTO_META[line.protocol];
|
||||
const status = line.monitorStatus || 'normal';
|
||||
const dotClass = status === 'abnormal' ? 'jms-dot-abnormal' : status === 'checking' ? 'jms-dot-checking' : 'jms-dot-normal';
|
||||
const statusLabel = status === 'abnormal' ? '异常,无法连接' : status === 'checking' ? '检测中…' : '正常';
|
||||
const isVlessCdn = line.protocol === 'vless-cdn';
|
||||
const checkNote = isVlessCdn
|
||||
? '检测方式:请求WS路径看是否返回520-524源站不可达错误,并做真实WebSocket握手(不是单纯测TCP)'
|
||||
: '检测方式:TCP端口连通性测试';
|
||||
const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '点击右侧刷新解析');
|
||||
function jmsCopyText(text, label, onNotify) {
|
||||
if (!text) { onNotify && onNotify('没有可复制的内容'); return; }
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => onNotify && onNotify(`${label}已复制`))
|
||||
.catch(() => onNotify && onNotify('复制失败,请手动选择'));
|
||||
}
|
||||
|
||||
function JmsLineCard({ line, ip, onRefreshIp, onCopy, onQr, onDuplicate, onEdit, onDelete, onNotify }) {
|
||||
const meta = JMS_PROTO_META[line.protocol] || { label: line.protocol };
|
||||
const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '解析中…');
|
||||
const hasPassword = line.protocol === 'ss' && !!line.password;
|
||||
|
||||
return (
|
||||
<div className="jms-card">
|
||||
<div className="jms-card-top">
|
||||
<div className="jms-card-title">
|
||||
<span className={`jms-dot ${dotClass}`} />
|
||||
<span className="jms-card-name">{line.name || line.addr}</span>
|
||||
</div>
|
||||
<span className={`jms-proto-chip jms-proto-${line.protocol}`}>{meta.label}</span>
|
||||
</div>
|
||||
<div className="jms-monitor-row">
|
||||
<span className={`jms-status-text ${status === 'abnormal' ? 'jms-status-abnormal' : ''}`}>● {statusLabel} · {jmsTimeAgo(line.lastChecked)}</span>
|
||||
<button className="jms-check-btn" disabled={status === 'checking'} onClick={() => onCheck(line.id)}>
|
||||
{status === 'checking' ? '检测中' : '立即检测'}
|
||||
<div className="jms-terminal">
|
||||
<div className="jms-terminal-row">
|
||||
<span className="jms-terminal-key">域名</span>
|
||||
<span className="jms-terminal-val">{line.addr}</span>
|
||||
<button className="jms-refresh-btn" onClick={() => jmsCopyText(line.addr, '域名', onNotify)} title="复制域名">
|
||||
<Copy size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="jms-check-note">{checkNote}</div>
|
||||
<div className="jms-terminal">
|
||||
<div className="jms-terminal-row"><span className="jms-terminal-key">域名</span><span className="jms-terminal-val">{line.addr}</span></div>
|
||||
<div className="jms-terminal-row">
|
||||
<span className="jms-terminal-key">解析IP</span>
|
||||
<span className={`jms-terminal-val jms-ip ${!ip || ip === 'resolving' ? 'jms-ip-pending' : ''}`}>{ipDisplay}</span>
|
||||
|
|
@ -4518,6 +4381,15 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate
|
|||
</button>
|
||||
</div>
|
||||
<div className="jms-terminal-row"><span className="jms-terminal-key">端口</span><span className="jms-terminal-val">{line.port}</span></div>
|
||||
{hasPassword && (
|
||||
<div className="jms-terminal-row">
|
||||
<span className="jms-terminal-key">密码</span>
|
||||
<span className="jms-terminal-val">{jmsMask(line.password.length)}</span>
|
||||
<button className="jms-refresh-btn" onClick={() => jmsCopyText(line.password, '密码', onNotify)} title="复制密码">
|
||||
<Copy size={12} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="jms-card-actions">
|
||||
<button className="btn btn-sm" onClick={() => onCopy(line)}><Copy size={13} />复制链接</button>
|
||||
|
|
@ -4530,33 +4402,18 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate
|
|||
);
|
||||
}
|
||||
|
||||
function JmsLineFormFields({ group, protocol, draft, setDraft }) {
|
||||
function JmsLineFormFields({ protocol, draft, setDraft }) {
|
||||
const set = (patch) => setDraft(d => ({ ...d, ...patch }));
|
||||
if (group === 'selfbuilt') {
|
||||
return (
|
||||
<>
|
||||
<div className="field"><span className="field-label">UUID</span><input className="input" value={draft.uuid || ''} onChange={e => set({ uuid: e.target.value })} placeholder="用户UUID" /></div>
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">WS路径</span><input className="input" value={draft.ws_path || ''} onChange={e => set({ ws_path: e.target.value })} placeholder="/stickrouter" /></div>
|
||||
<div className="field"><span className="field-label">SNI / Host</span><input className="input" value={draft.sni || ''} onChange={e => set({ sni: e.target.value })} placeholder="留空则用服务器地址" /></div>
|
||||
</div>
|
||||
<div className="disclaimer">* vless加密none · 勾选tls · 指纹chrome(固定,不需要手动填)</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (protocol === 'ss') {
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">加密方式</span><input className="input" value={draft.method || 'aes-256-gcm'} onChange={e => set({ method: e.target.value })} /></div>
|
||||
<div className="field"><span className="field-label">密码</span><input className="input" value={draft.password || ''} onChange={e => set({ password: e.target.value })} placeholder="连接密码" /></div>
|
||||
<div className="field">
|
||||
<span className="field-label">加密方式</span>
|
||||
<select className="input" value={draft.method || 'aes-256-gcm'} onChange={e => set({ method: e.target.value })}>
|
||||
{JMS_SS_METHODS.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (protocol === 'vmess') {
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">UUID</span><input className="input" value={draft.uuid || ''} onChange={e => set({ uuid: e.target.value })} placeholder="用户UUID" /></div>
|
||||
<div className="field"><span className="field-label">AlterId</span><input className="input" value={draft.alterId || '0'} onChange={e => set({ alterId: e.target.value })} /></div>
|
||||
<div className="field"><span className="field-label">密码</span><input className="input" value={draft.password || ''} onChange={e => set({ password: e.target.value })} placeholder="连接密码" /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4579,20 +4436,18 @@ function JmsLineFormFields({ group, protocol, draft, setDraft }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
||||
function JmsPage({ lines, onSave, onDelete, onNotify }) {
|
||||
const [ipMap, setIpMap] = useState({});
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [group, setGroup] = useState('selfbuilt');
|
||||
const [protocol, setProtocol] = useState('vless-cdn');
|
||||
const [protocol, setProtocol] = useState('ss');
|
||||
const [draft, setDraft] = useState({});
|
||||
const [qrLine, setQrLine] = useState(null);
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
|
||||
const total = lines.length;
|
||||
const ssCount = lines.filter(l => l.protocol === 'ss').length;
|
||||
const vmessCount = lines.filter(l => l.protocol === 'vmess').length;
|
||||
const vlessCount = lines.filter(l => l.protocol.startsWith('vless')).length;
|
||||
const vlessCount = lines.filter(l => l.protocol === 'vless-reality').length;
|
||||
|
||||
async function refreshIp(id, addr) {
|
||||
setIpMap(m => ({ ...m, [id]: 'resolving' }));
|
||||
|
|
@ -4604,6 +4459,14 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
}
|
||||
}
|
||||
|
||||
// 域名解析IP实时显示:线路列表加载/变化时,自动对还没解析过的线路跑一次DNS解析,不用用户手动点
|
||||
useEffect(() => {
|
||||
lines.forEach(line => {
|
||||
if (line.addr && ipMap[line.id] === undefined) refreshIp(line.id, line.addr);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lines]);
|
||||
|
||||
function copyLink(line) {
|
||||
const link = jmsBuildLink(line);
|
||||
navigator.clipboard.writeText(link).then(() => onNotify('订阅链接已复制')).catch(() => onNotify('复制失败,请手动选择'));
|
||||
|
|
@ -4625,21 +4488,18 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
|
||||
function openAddModal() {
|
||||
setEditingId(null);
|
||||
setGroup('selfbuilt');
|
||||
setProtocol('vless-cdn');
|
||||
setDraft({ name: '', addr: 'pb.impromx.com', port: '443', ws_path: '/stickrouter' });
|
||||
setProtocol('ss');
|
||||
setDraft({ name: '', addr: '', port: '' });
|
||||
setModalOpen(true);
|
||||
}
|
||||
function openEditModal(line) {
|
||||
setEditingId(line.id);
|
||||
setGroup(line.group);
|
||||
setProtocol(line.protocol);
|
||||
setDraft({ ...line });
|
||||
setModalOpen(true);
|
||||
}
|
||||
function duplicateLine(line) {
|
||||
setEditingId(null); // 不带id,保存时会当成全新的一条线路,不会覆盖原线路
|
||||
setGroup(line.group);
|
||||
setProtocol(line.protocol);
|
||||
const { id, monitorStatus, lastChecked, ...rest } = line;
|
||||
setDraft({ ...rest, name: line.name ? `${line.name} 副本` : '' });
|
||||
|
|
@ -4648,20 +4508,14 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
}
|
||||
function closeModal() { setModalOpen(false); }
|
||||
|
||||
function handleGroupChange(g) {
|
||||
setGroup(g);
|
||||
if (g === 'selfbuilt') {
|
||||
setProtocol('vless-cdn');
|
||||
setDraft(d => ({ name: d.name, addr: d.addr || 'pb.impromx.com', port: d.port || '443', ws_path: d.ws_path || '/stickrouter' }));
|
||||
} else {
|
||||
setProtocol('ss');
|
||||
function handleProtocolChange(p) {
|
||||
setProtocol(p);
|
||||
setDraft(d => ({ name: d.name, addr: d.addr, port: d.port }));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!draft.addr || !draft.port) { onNotify('请填写服务器地址和端口'); return; }
|
||||
onSave({ ...draft, group, protocol }, editingId);
|
||||
onSave({ ...draft, protocol }, editingId);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
|
|
@ -4670,7 +4524,7 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
<div className="page-head-row">
|
||||
<div>
|
||||
<h2 className="crm-display" style={{ fontSize: 22, fontWeight: 700, margin: '0 0 4px' }}>Just My Socks 节点管理</h2>
|
||||
<div className="disclaimer" style={{ marginBottom: 0 }}>管理自建VLESS-CDN线路和 justmysocks2.net 订阅线路 · DNS解析、订阅链接、二维码和连通性监控</div>
|
||||
<div className="disclaimer" style={{ marginBottom: 0 }}>管理 justmysocks2.net 订阅线路 · DNS解析、订阅链接、二维码</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={openAddModal}><Plus size={14} />新增线路</button>
|
||||
</div>
|
||||
|
|
@ -4678,7 +4532,6 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
<div className="jms-stats-row">
|
||||
<div className="stat-chip"><div className="stat-num">{total}</div><div className="stat-lbl">线路总数</div></div>
|
||||
<div className="stat-chip"><div className="stat-num">{ssCount}</div><div className="stat-lbl">SS</div></div>
|
||||
<div className="stat-chip"><div className="stat-num">{vmessCount}</div><div className="stat-lbl">VMess</div></div>
|
||||
<div className="stat-chip"><div className="stat-num">{vlessCount}</div><div className="stat-lbl">VLESS</div></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -4690,12 +4543,12 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
line={line}
|
||||
ip={ipMap[line.id]}
|
||||
onRefreshIp={refreshIp}
|
||||
onCheck={onCheck}
|
||||
onCopy={copyLink}
|
||||
onQr={openQr}
|
||||
onDuplicate={duplicateLine}
|
||||
onEdit={openEditModal}
|
||||
onDelete={onDelete}
|
||||
onNotify={onNotify}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -4708,29 +4561,20 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
|||
<button className="btn btn-ghost btn-sm" onClick={closeModal}><X size={14} /></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="field">
|
||||
<span className="field-label">线路分组</span>
|
||||
<div className="jms-group-select">
|
||||
<div className={`jms-group-opt ${group === 'selfbuilt' ? 'jms-group-selected jms-group-selfbuilt' : ''}`} onClick={() => handleGroupChange('selfbuilt')}>自建 VLESS-CDN</div>
|
||||
<div className={`jms-group-opt ${group === 'jms' ? 'jms-group-selected jms-group-jms' : ''}`} onClick={() => handleGroupChange('jms')}>Just My Socks</div>
|
||||
</div>
|
||||
</div>
|
||||
{group === 'jms' && (
|
||||
<div className="field">
|
||||
<span className="field-label">协议</span>
|
||||
<div className="jms-proto-select">
|
||||
{JMS_PROTOCOLS.map(key => (
|
||||
<div key={key} className={`jms-proto-opt jms-proto-opt-${key} ${protocol === key ? 'jms-proto-selected' : ''}`} onClick={() => setProtocol(key)}>{JMS_PROTO_META[key].label}</div>
|
||||
<div key={key} className={`jms-proto-opt jms-proto-opt-${key} ${protocol === key ? 'jms-proto-selected' : ''}`} onClick={() => handleProtocolChange(key)}>{JMS_PROTO_META[key].label}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="field"><span className="field-label">名称备注</span><input className="input" value={draft.name || ''} onChange={e => setDraft(d => ({ ...d, name: e.target.value }))} placeholder="如:SG-01 新加坡" /></div>
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">服务器地址(域名)</span><input className="input" value={draft.addr || ''} onChange={e => setDraft(d => ({ ...d, addr: e.target.value }))} placeholder="如:sg1.jms-relay.net" /></div>
|
||||
<div className="field"><span className="field-label">端口</span><input className="input" value={draft.port || ''} onChange={e => setDraft(d => ({ ...d, port: e.target.value }))} placeholder="8388" /></div>
|
||||
</div>
|
||||
<JmsLineFormFields group={group} protocol={protocol} draft={draft} setDraft={setDraft} />
|
||||
<JmsLineFormFields protocol={protocol} draft={draft} setDraft={setDraft} />
|
||||
</div>
|
||||
<div className="modal-foot"><button className="btn" onClick={closeModal}>取消</button><button className="btn btn-primary" onClick={handleSave}><Save size={13} />保存</button></div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue