Update App.jsx via upload script - 2026-08-07 15:50:41
This commit is contained in:
parent
0d308d5224
commit
ebf85a5932
179
App.jsx
179
App.jsx
|
|
@ -318,6 +318,10 @@ const TRANSLATION_DICTIONARY = {
|
|||
'控制管理': { en: 'Interface', fr: 'Interface', ru: 'Интерфейс', es: 'Interfaz', ar: 'الواجهة' },
|
||||
'湿度': { en: 'Humidity', fr: 'Humidité', ru: 'Влажность', es: 'Humedad', ar: 'الرطوبة' },
|
||||
'噪音': { en: 'Noise Level', fr: 'Niveau sonore', ru: 'Уровень шума', es: 'Nivel de ruido', ar: 'مستوى الضوضاء' },
|
||||
'应用场景': { en: 'Application', fr: 'Application', ru: 'Применение', es: 'Aplicación', ar: 'التطبيق' },
|
||||
'20尺柜装箱数量': { en: '20ft Container Qty', fr: 'Qté conteneur 20 pieds', ru: 'Кол-во в 20-фут контейнере', es: 'Cant. contenedor 20 pies', ar: 'الكمية في حاوية 20 قدم' },
|
||||
'40尺柜装箱数量': { en: '40ft Container Qty', fr: 'Qté conteneur 40 pieds', ru: 'Кол-во в 40-фут контейнере', es: 'Cant. contenedor 40 pies', ar: 'الكمية في حاوية 40 قدم' },
|
||||
'40高柜装箱数量': { en: '40HQ Container Qty', fr: 'Qté conteneur 40HQ', ru: 'Кол-во в контейнере 40HQ', es: 'Cant. contenedor 40HQ', ar: 'الكمية في حاوية 40HQ' },
|
||||
'主要特点': { en: 'Main Features', fr: 'Caractéristiques principales', ru: 'Основные характеристики', es: 'Características principales', ar: 'الميزات الرئيسية' },
|
||||
'相数': { en: 'Phase', fr: 'Phase', ru: 'Фаза', es: 'Fase', ar: 'الطور' },
|
||||
'输入功率因数': { en: 'Input Power Factor', fr: "Facteur de puissance d'entrée", ru: 'Входной коэффициент мощности', es: 'Factor de potencia de entrada', ar: 'معامل قدرة الدخل' },
|
||||
|
|
@ -592,11 +596,10 @@ function productExportRows(p, categories) {
|
|||
if (p.powerOrCapacity) rows.push({ labelKey: '功率 / 容量', value: p.powerOrCapacity });
|
||||
if (p.voltage) rows.push({ labelKey: '电压', value: p.voltage });
|
||||
}
|
||||
// 通用兜底:不属于上面任何固定字段的"其他参数"(AI识别原文时发现的、schema里没有对应字段的
|
||||
// 参数),不管品类都追加在最后,保证原文规格表里的信息不会因为不在预定义字段列表里而丢失
|
||||
(p.extraSpecs || []).forEach(item => {
|
||||
if (item && item.label && item.value) rows.push({ labelKey: item.label, value: item.value });
|
||||
});
|
||||
if (p.applicationScenarios) rows.push({ labelKey: '应用场景', value: p.applicationScenarios });
|
||||
if (p.container20ft) rows.push({ labelKey: '20尺柜装箱数量', value: p.container20ft });
|
||||
if (p.container40ft) rows.push({ labelKey: '40尺柜装箱数量', value: p.container40ft });
|
||||
if (p.container40hq) rows.push({ labelKey: '40高柜装箱数量', value: p.container40hq });
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
|
@ -776,11 +779,10 @@ const blankProduct = () => ({
|
|||
batterySpec: '', // 电池型号和数量,如 "12V4.5AH*1"(迷你UPS/离线式/在线互动式UPS共用)
|
||||
dimensions: '', // 尺寸(mm)(迷你UPS/离线式/在线互动式UPS共用)
|
||||
netWeight: '', // 净重(kg)(迷你UPS/离线式/在线互动式UPS共用)
|
||||
applicationScenarios: '', // 应用场景文本,如 "电脑、投影仪、打印机、摄像机、收银机"
|
||||
container20ft: '', container40ft: '', container40hq: '', // 装箱数量(20尺柜/40尺柜/40高柜,无栈板台数)
|
||||
productImageFileName: '', productImageOriginalName: '', // 产品主图,导出多语言PDF时展示
|
||||
sellingPoints: '', // 对客卖点/主要特点,每行一条,专门给客户看的(和下面的costPrice/notes内部信息彻底分开,不会一起导出)
|
||||
extraSpecs: [], // 不在上面固定字段列表里的"其他参数",如 [{label:'防雷等级', value:'Type II'}];
|
||||
// AI批量导入时用来兜底承接原文规格表里有、但当前schema没有对应字段的参数,
|
||||
// 保证不会因为字段没预定义就丢信息;也支持手动在产品详情页里增删
|
||||
costPrice: '', notes: '',
|
||||
});
|
||||
|
||||
|
|
@ -841,9 +843,6 @@ function powerInfoItems(p, categories) {
|
|||
if (p.safetyStandard) items.push({ key: 'safetystd', label: '安规/EMC标准', value: p.safetyStandard });
|
||||
if (p.gridStandard) items.push({ key: 'gridstd', label: '并网标准', value: p.gridStandard });
|
||||
if (p.otherStandard) items.push({ key: 'otherstd', label: '其他标准', value: p.otherStandard });
|
||||
(p.extraSpecs || []).forEach((item, idx) => {
|
||||
if (item && item.label && item.value) items.push({ key: 'extraspec' + idx, label: item.label, value: item.value });
|
||||
});
|
||||
return items;
|
||||
}
|
||||
if (isBatteryCategory(cat) && p.type === 'UPS配套锂电池系统') {
|
||||
|
|
@ -870,9 +869,6 @@ function powerInfoItems(p, categories) {
|
|||
if (p.cabinetWeight) items.push({ key: 'cabinetweight', label: '系统柜重量(kg)', value: p.cabinetWeight });
|
||||
if (p.altitude) items.push({ key: 'altitude', label: '海拔(m)', value: p.altitude });
|
||||
if (p.selfDischargeRate) items.push({ key: 'selfdischarge', label: '自放电率', value: p.selfDischargeRate });
|
||||
(p.extraSpecs || []).forEach((item, idx) => {
|
||||
if (item && item.label && item.value) items.push({ key: 'extraspec' + idx, label: item.label, value: item.value });
|
||||
});
|
||||
return items;
|
||||
}
|
||||
if (isUpsCategory(cat) && (p.type === '离线式UPS' || p.type === '在线互动式UPS')) {
|
||||
|
|
@ -899,9 +895,10 @@ function powerInfoItems(p, categories) {
|
|||
if (p.noiseLevel) items.push({ key: 'noiselevel', label: '噪音', value: p.noiseLevel });
|
||||
if (p.dimensions) items.push({ key: 'dim', label: '尺寸(mm)', value: p.dimensions });
|
||||
if (p.netWeight) items.push({ key: 'weight', label: '净重(kg)', value: p.netWeight });
|
||||
(p.extraSpecs || []).forEach((item, idx) => {
|
||||
if (item && item.label && item.value) items.push({ key: 'extraspec' + idx, label: item.label, value: item.value });
|
||||
});
|
||||
if (p.applicationScenarios) items.push({ key: 'appscenario', label: '应用场景', value: p.applicationScenarios });
|
||||
if (p.container20ft) items.push({ key: 'container20', label: '20尺柜装箱数量', value: p.container20ft });
|
||||
if (p.container40ft) items.push({ key: 'container40', label: '40尺柜装箱数量', value: p.container40ft });
|
||||
if (p.container40hq) items.push({ key: 'container40hq', label: '40高柜装箱数量', value: p.container40hq });
|
||||
return items;
|
||||
}
|
||||
if (isUpsCategory(cat) && (p.type === '机架式UPS' || p.type === '在线式高频UPS')) {
|
||||
|
|
@ -937,9 +934,10 @@ function powerInfoItems(p, categories) {
|
|||
if (p.noiseLevel) items.push({ key: 'noiselevel', label: '噪音', value: p.noiseLevel });
|
||||
if (p.interfacePort) items.push({ key: 'interfaceport', label: '控制管理', value: p.interfacePort });
|
||||
if (p.optionalSnmp) items.push({ key: 'snmp', label: '可选SNMP', value: p.optionalSnmp });
|
||||
(p.extraSpecs || []).forEach((item, idx) => {
|
||||
if (item && item.label && item.value) items.push({ key: 'extraspec' + idx, label: item.label, value: item.value });
|
||||
});
|
||||
if (p.applicationScenarios) items.push({ key: 'appscenario', label: '应用场景', value: p.applicationScenarios });
|
||||
if (p.container20ft) items.push({ key: 'container20', label: '20尺柜装箱数量', value: p.container20ft });
|
||||
if (p.container40ft) items.push({ key: 'container40', label: '40尺柜装箱数量', value: p.container40ft });
|
||||
if (p.container40hq) items.push({ key: 'container40hq', label: '40高柜装箱数量', value: p.container40hq });
|
||||
return items;
|
||||
}
|
||||
if (isUpsCategory(cat) && p.type !== '迷你UPS') {
|
||||
|
|
@ -963,9 +961,10 @@ function powerInfoItems(p, categories) {
|
|||
if (p.batterySpec) items.push({ key: 'battery', label: '电池数量和容量', value: p.batterySpec });
|
||||
if (p.dimensions) items.push({ key: 'dim', label: '尺寸(mm)', value: p.dimensions });
|
||||
if (p.netWeight) items.push({ key: 'weight', label: '净重(kg)', value: p.netWeight });
|
||||
(p.extraSpecs || []).forEach((item, idx) => {
|
||||
if (item && item.label && item.value) items.push({ key: 'extraspec' + idx, label: item.label, value: item.value });
|
||||
});
|
||||
if (p.applicationScenarios) items.push({ key: 'appscenario', label: '应用场景', value: p.applicationScenarios });
|
||||
if (p.container20ft) items.push({ key: 'container20', label: '20尺柜装箱数量', value: p.container20ft });
|
||||
if (p.container40ft) items.push({ key: 'container40', label: '40尺柜装箱数量', value: p.container40ft });
|
||||
if (p.container40hq) items.push({ key: 'container40hq', label: '40高柜装箱数量', value: p.container40hq });
|
||||
return items;
|
||||
}
|
||||
return [{ key: 'power', label: '功率 / 容量', value: p.powerOrCapacity || '—' }];
|
||||
|
|
@ -1925,7 +1924,6 @@ function MainApp({ username, onLogout }) {
|
|||
|
||||
const [dragCustomerIdx, setDragCustomerIdx] = useState(null);
|
||||
const [dragGroupState, setDragGroupState] = useState(null); // { category, index }
|
||||
const [dragJmsIdx, setDragJmsIdx] = useState(null);
|
||||
|
||||
const [pwCurrent, setPwCurrent] = useState('');
|
||||
const [pwNew, setPwNew] = useState('');
|
||||
|
|
@ -2091,11 +2089,6 @@ function MainApp({ username, onLogout }) {
|
|||
setCustomers(prev => reorderArray(prev, dragCustomerIdx, dropIdx));
|
||||
setDragCustomerIdx(null);
|
||||
}
|
||||
function handleJmsDrop(dropIdx) {
|
||||
if (dragJmsIdx === null || dragJmsIdx === dropIdx) { setDragJmsIdx(null); return; }
|
||||
setJmsLines(prev => reorderArray(prev, dragJmsIdx, dropIdx));
|
||||
setDragJmsIdx(null);
|
||||
}
|
||||
async function handleAddCatalogFile(file) {
|
||||
if (!file) return;
|
||||
const tempId = 'catdoc_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
|
|
@ -2420,7 +2413,7 @@ function MainApp({ username, onLogout }) {
|
|||
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} 检测异常${res.detail ? ':' + res.detail : ''}`);
|
||||
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)));
|
||||
|
|
@ -2555,9 +2548,6 @@ function MainApp({ username, onLogout }) {
|
|||
otherStandard: p.otherStandard || '',
|
||||
sellingPoints: p.sellingPoints || '', // AI如果在原文里认出了营销卖点/主要特点,会自动填这里
|
||||
batterySpec: p.batterySpec || '', dimensions: p.dimensions || '', netWeight: p.netWeight || '', notes: p.notes || '',
|
||||
extraSpecs: Array.isArray(p.extraSpecs)
|
||||
? p.extraSpecs.filter(it => it && (it.label || it.value)).map(it => ({ label: it.label || '', value: it.value || '' }))
|
||||
: [], // 不在固定schema里的"其他参数",原文有什么就带什么,型号之间可以完全不一样
|
||||
}));
|
||||
setAiImportDrafts(drafts);
|
||||
} catch (e) {
|
||||
|
|
@ -2635,7 +2625,6 @@ function MainApp({ username, onLogout }) {
|
|||
otherStandard: d.otherStandard || '',
|
||||
sellingPoints: d.sellingPoints || '',
|
||||
batterySpec: d.batterySpec || '', dimensions: d.dimensions || '', netWeight: d.netWeight || '', notes: d.notes || '',
|
||||
extraSpecs: Array.isArray(d.extraSpecs) ? d.extraSpecs.filter(it => it && (it.label || it.value)) : [],
|
||||
};
|
||||
}
|
||||
function confirmAiImport() {
|
||||
|
|
@ -3175,10 +3164,6 @@ function MainApp({ username, onLogout }) {
|
|||
onDelete={deleteJmsLine}
|
||||
onCheck={checkJmsLineNow}
|
||||
onNotify={notify}
|
||||
dragIdx={dragJmsIdx}
|
||||
onDragStart={setDragJmsIdx}
|
||||
onDrop={handleJmsDrop}
|
||||
onDragEnd={() => setDragJmsIdx(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -3853,11 +3838,9 @@ function MainApp({ username, onLogout }) {
|
|||
{!aiImportDrafts ? (
|
||||
<>
|
||||
<div className="disclaimer">
|
||||
支持上传PDF、图片、HTML、或Word/PPT/Excel(.docx/.pptx/.xlsx)形式的产品目录,可以一次选多个文件(比如一份多页PDF,或者拍的多张照片)。
|
||||
支持上传PDF、图片、或HTML形式的产品目录,可以一次选多个文件(比如一份多页PDF,或者拍的多张照片)。
|
||||
AI会自动识别里面有几个产品型号、并提取型号/品牌/规格参数,之后可以在预览里检查修改,确认无误再批量建档。
|
||||
<br />
|
||||
PDF会先自动判断有没有文字层:正式排版、非扫描件的PDF会直接读取文字(更快更准),只有扫描件/纯图片排版的PDF才会走OCR。
|
||||
<br />
|
||||
如果原始文件是水印很重、排版复杂、OCR容易识别错的图片,可以先用其他AI工具(如Gemini)把表格转换成结构清晰的HTML文件再上传——HTML会跳过OCR直接读取,准确率通常明显更高。
|
||||
</div>
|
||||
<div className="grid-2" style={{ marginBottom: 4 }}>
|
||||
|
|
@ -3897,9 +3880,7 @@ function MainApp({ username, onLogout }) {
|
|||
</div>
|
||||
<label className="tax-add-category" style={{ display: 'block', cursor: aiImportBusy ? 'default' : 'pointer' }}>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf,image/*,.html,.htm,text/html,.docx,.pptx,.xlsx,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
multiple style={{ display: 'none' }}
|
||||
type="file" accept="application/pdf,image/*,.html,.htm,text/html" multiple style={{ display: 'none' }}
|
||||
disabled={aiImportBusy}
|
||||
onChange={e => { handleAiImportFiles(e.target.files); e.target.value = ''; }}
|
||||
/>
|
||||
|
|
@ -3909,7 +3890,7 @@ function MainApp({ username, onLogout }) {
|
|||
正在识别中,视文件大小可能需要几十秒…
|
||||
</>
|
||||
) : (
|
||||
<><Plus size={14} style={{ verticalAlign: -2, marginRight: 4 }} />点击选择PDF、图片、HTML或Word/PPT/Excel(可多选)</>
|
||||
<><Plus size={14} style={{ verticalAlign: -2, marginRight: 4 }} />点击选择PDF、图片或HTML(可多选)</>
|
||||
)}
|
||||
</label>
|
||||
{aiImportError && <div style={{ color: 'var(--led-red)', fontSize: 12.5, marginTop: 10 }}>{aiImportError}</div>}
|
||||
|
|
@ -3953,7 +3934,6 @@ function MainApp({ username, onLogout }) {
|
|||
d.netWeight && `净重:${d.netWeight}`,
|
||||
(d.outputPortTypes && d.outputPortTypes.length) ? `输出口:${d.outputPortTypes.join('/')}` : '',
|
||||
d.sellingPoints ? `✓已识别对客卖点(${d.sellingPoints.split('\n').filter(Boolean).length}条)` : '',
|
||||
(d.extraSpecs && d.extraSpecs.length) ? `✓其他参数(${d.extraSpecs.length}项:${d.extraSpecs.map(it => it.label).filter(Boolean).join('、')})` : '',
|
||||
].filter(Boolean).join(' · ') || '(无更多规格信息)'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -4270,7 +4250,7 @@ const JMS_PROTO_META = {
|
|||
'vless-reality': { label: 'VLESS Reality' },
|
||||
'vless-cdn': { label: 'VLESS CDN' },
|
||||
};
|
||||
const JMS_PROTOCOLS = ['ss', 'vless-reality'];
|
||||
const JMS_PROTOCOLS = ['ss', 'vmess', 'vless-reality'];
|
||||
|
||||
function jmsTimeAgo(ts) {
|
||||
if (!ts) return '尚未检测';
|
||||
|
|
@ -4321,35 +4301,21 @@ async function jmsResolveIp(domain) {
|
|||
return answer ? answer.data : null;
|
||||
}
|
||||
|
||||
function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate, onEdit, onDelete, onNotify, dragging, onCardDragStart, onCardDragOver, onCardDrop, onCardDragEnd }) {
|
||||
function copyAddr(addr) {
|
||||
navigator.clipboard.writeText(addr)
|
||||
.then(() => onNotify && onNotify('域名已复制'))
|
||||
.catch(() => onNotify && onNotify('复制失败,请手动选择'));
|
||||
}
|
||||
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
|
||||
? '检测方式:TCP端口连通性测试(HTTP状态码/WS握手结果仅作为附加参考,不参与判定)'
|
||||
? '检测方式:请求WS路径看是否返回520-524源站不可达错误,并做真实WebSocket握手(不是单纯测TCP)'
|
||||
: '检测方式:TCP端口连通性测试';
|
||||
const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '试试解析当前IP');
|
||||
const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '点击右侧刷新解析');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`jms-card ${dragging ? 'jms-card-dragging' : ''}`}
|
||||
style={{ opacity: dragging ? 0.5 : 1 }}
|
||||
draggable
|
||||
onDragStart={onCardDragStart}
|
||||
onDragOver={onCardDragOver}
|
||||
onDrop={onCardDrop}
|
||||
onDragEnd={onCardDragEnd}
|
||||
>
|
||||
<div className="jms-card">
|
||||
<div className="jms-card-top">
|
||||
<div className="jms-card-title">
|
||||
<GripVertical size={14} className="drag-handle" />
|
||||
<span className={`jms-dot ${dotClass}`} />
|
||||
<span className="jms-card-name">{line.name || line.addr}</span>
|
||||
</div>
|
||||
|
|
@ -4363,13 +4329,7 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate
|
|||
</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>
|
||||
<button className="jms-refresh-btn" onClick={() => copyAddr(line.addr)} title="复制域名">
|
||||
<Copy size={13} />
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
|
@ -4412,6 +4372,14 @@ function JmsLineFormFields({ group, protocol, draft, setDraft }) {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
if (protocol === 'vless-reality') {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -4431,7 +4399,7 @@ function JmsLineFormFields({ group, protocol, draft, setDraft }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragStart, onDrop, onDragEnd }) {
|
||||
function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
|
||||
const [ipMap, setIpMap] = useState({});
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
|
|
@ -4443,6 +4411,7 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
|
||||
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;
|
||||
|
||||
async function refreshIp(id, addr) {
|
||||
|
|
@ -4455,14 +4424,6 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
}
|
||||
}
|
||||
|
||||
// 打开线路管理页面时自动解析所有线路当前DNS IP(只对还没解析过的线路发请求,
|
||||
// 避免拖拽排序等操作触发的重渲染重复请求已解析过的域名)
|
||||
useEffect(() => {
|
||||
lines.forEach(line => {
|
||||
if (!(line.id in ipMap)) refreshIp(line.id, line.addr);
|
||||
});
|
||||
}, [lines]);
|
||||
|
||||
function copyLink(line) {
|
||||
const link = jmsBuildLink(line);
|
||||
navigator.clipboard.writeText(link).then(() => onNotify('订阅链接已复制')).catch(() => onNotify('复制失败,请手动选择'));
|
||||
|
|
@ -4511,27 +4472,15 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
setGroup(g);
|
||||
if (g === 'selfbuilt') {
|
||||
setProtocol('vless-cdn');
|
||||
setDraft(d => ({ ...d, addr: d.addr || 'pb.impromx.com', port: d.port || '443', ws_path: d.ws_path || '/stickrouter' }));
|
||||
setDraft(d => ({ name: d.name, addr: d.addr || 'pb.impromx.com', port: d.port || '443', ws_path: d.ws_path || '/stickrouter' }));
|
||||
} else {
|
||||
setProtocol(p => JMS_PROTOCOLS.includes(p) ? p : 'ss');
|
||||
setDraft(d => ({ ...d }));
|
||||
setProtocol('ss');
|
||||
setDraft(d => ({ name: d.name, addr: d.addr, port: d.port }));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!draft.addr || !draft.port) { onNotify('请填写服务器地址和端口'); return; }
|
||||
const missing = [];
|
||||
if (group === 'selfbuilt') {
|
||||
if (!draft.uuid) missing.push('UUID');
|
||||
} else if (protocol === 'ss') {
|
||||
if (!draft.password) missing.push('密码');
|
||||
} else if (protocol === 'vless-reality') {
|
||||
if (!draft.uuid) missing.push('UUID');
|
||||
if (!draft.publicKey) missing.push('Public Key');
|
||||
if (!draft.shortId) missing.push('Short ID');
|
||||
if (!draft.sni) missing.push('SNI');
|
||||
}
|
||||
if (missing.length) { onNotify(`请填写:${missing.join('、')}`); return; }
|
||||
onSave({ ...draft, group, protocol }, editingId);
|
||||
closeModal();
|
||||
}
|
||||
|
|
@ -4541,6 +4490,7 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
<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>
|
||||
<button className="btn btn-primary" onClick={openAddModal}><Plus size={14} />新增线路</button>
|
||||
</div>
|
||||
|
|
@ -4548,12 +4498,13 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
<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>
|
||||
|
||||
<div className="jms-grid">
|
||||
{lines.length === 0 && <div className="empty-state" style={{ gridColumn: '1/-1' }}><Wifi size={28} />还没有线路,点右上角"新增线路"添加</div>}
|
||||
{lines.map((line, idx) => (
|
||||
{lines.map(line => (
|
||||
<JmsLineCard
|
||||
key={line.id}
|
||||
line={line}
|
||||
|
|
@ -4565,12 +4516,6 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
|
|||
onDuplicate={duplicateLine}
|
||||
onEdit={openEditModal}
|
||||
onDelete={onDelete}
|
||||
onNotify={onNotify}
|
||||
dragging={dragIdx === idx}
|
||||
onCardDragStart={() => onDragStart(idx)}
|
||||
onCardDragOver={(e) => e.preventDefault()}
|
||||
onCardDrop={() => onDrop(idx)}
|
||||
onCardDragEnd={onDragEnd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -4669,10 +4614,6 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
|
|||
function addExtraName() { set({ extraNames: [...extraNames, ''] }); }
|
||||
function updateExtraName(idx, val) { const arr = [...extraNames]; arr[idx] = val; set({ extraNames: arr }); }
|
||||
function removeExtraName(idx) { set({ extraNames: extraNames.filter((_, i) => i !== idx) }); }
|
||||
const extraSpecs = draft.extraSpecs || [];
|
||||
function addExtraSpec() { set({ extraSpecs: [...extraSpecs, { label: '', value: '' }] }); }
|
||||
function updateExtraSpec(idx, patch) { set({ extraSpecs: extraSpecs.map((it, i) => (i === idx ? { ...it, ...patch } : it)) }); }
|
||||
function removeExtraSpec(idx) { set({ extraSpecs: extraSpecs.filter((_, i) => i !== idx) }); }
|
||||
function toggleOutputPortType(t) {
|
||||
const list = draft.outputPortTypes || [];
|
||||
if (list.includes(t)) {
|
||||
|
|
@ -4968,6 +4909,12 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
|
|||
</div>
|
||||
<div className="field"><span className="field-label">控制管理 / 接口</span><input className="input" value={draft.interfacePort} onChange={e => set({ interfacePort: e.target.value })} placeholder="如:Smart RS-232/USB, supports Windows 2000/2003/XP/Vista/2008, 7/8, Linux, Unix, MAC" /></div>
|
||||
<div className="field"><span className="field-label">可选SNMP</span><input className="input" value={draft.optionalSnmp} onChange={e => set({ optionalSnmp: e.target.value })} placeholder="如:Power management from SNMP manager and web browser" /></div>
|
||||
<div className="field"><span className="field-label">应用场景</span><input className="input" value={draft.applicationScenarios} onChange={e => set({ applicationScenarios: e.target.value })} placeholder="如:电脑、投影仪、打印机、摄像机、收银机" /></div>
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">20尺柜装箱数量</span><input className="input" value={draft.container20ft} onChange={e => set({ container20ft: e.target.value })} placeholder="如:1000PCS(无栈板)" /></div>
|
||||
<div className="field"><span className="field-label">40尺柜装箱数量</span><input className="input" value={draft.container40ft} onChange={e => set({ container40ft: e.target.value })} placeholder="如:2080PCS(无栈板)" /></div>
|
||||
</div>
|
||||
<div className="field"><span className="field-label">40高柜装箱数量</span><input className="input" value={draft.container40hq} onChange={e => set({ container40hq: e.target.value })} placeholder="如:2340PCS(无栈板)" /></div>
|
||||
</>
|
||||
) : isUps && isOfflineOrLI ? (
|
||||
<>
|
||||
|
|
@ -5009,6 +4956,12 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
|
|||
<div className="field"><span className="field-label">湿度</span><input className="input" value={draft.humidity} onChange={e => set({ humidity: e.target.value })} placeholder="如:0-90% RH@0-40℃(Non-condensing)" /></div>
|
||||
<div className="field"><span className="field-label">噪音</span><input className="input" value={draft.noiseLevel} onChange={e => set({ noiseLevel: e.target.value })} placeholder="如:Less than 40dB(1m spacing)" /></div>
|
||||
</div>
|
||||
<div className="field"><span className="field-label">应用场景</span><input className="input" value={draft.applicationScenarios} onChange={e => set({ applicationScenarios: e.target.value })} placeholder="如:电脑、投影仪、打印机、摄像机、收银机" /></div>
|
||||
<div className="grid-2">
|
||||
<div className="field"><span className="field-label">20尺柜装箱数量</span><input className="input" value={draft.container20ft} onChange={e => set({ container20ft: e.target.value })} placeholder="如:1000PCS(无栈板)" /></div>
|
||||
<div className="field"><span className="field-label">40尺柜装箱数量</span><input className="input" value={draft.container40ft} onChange={e => set({ container40ft: e.target.value })} placeholder="如:2080PCS(无栈板)" /></div>
|
||||
</div>
|
||||
<div className="field"><span className="field-label">40高柜装箱数量</span><input className="input" value={draft.container40hq} onChange={e => set({ container40hq: e.target.value })} placeholder="如:2340PCS(无栈板)" /></div>
|
||||
</>
|
||||
) : isUps && !isMiniUps ? (
|
||||
<div className="grid-2">
|
||||
|
|
@ -5113,18 +5066,6 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<span className="field-label">其他参数(可选——原文规格表里有、但上面没有专属字段的参数,AI批量导入时会自动补充在这里,也可以手动增删;会一起显示在产品详情和多语言PDF导出里)</span>
|
||||
{extraSpecs.map((item, idx) => (
|
||||
<div key={idx} style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
|
||||
<input className="input" style={{ flex: 1 }} value={item.label} onChange={e => updateExtraSpec(idx, { label: e.target.value })} placeholder="参数名,如:防雷等级" />
|
||||
<input className="input" style={{ flex: 1 }} value={item.value} onChange={e => updateExtraSpec(idx, { value: e.target.value })} placeholder="参数值,如:Type II" />
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={() => removeExtraSpec(idx)}><Trash2 size={13} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={addExtraSpec}><Plus size={13} />添加参数</button>
|
||||
</div>
|
||||
|
||||
<div className="field"><span className="field-label">正本价格(元)</span><input className="input" value={draft.costPrice} onChange={e => set({ costPrice: e.target.value })} placeholder="采购/成本价,人民币" /></div>
|
||||
<div className="field">
|
||||
<span className="field-label">内部备注(仅自己可见,不会出现在任何导出的文档里)</span>
|
||||
|
|
|
|||
Loading…
Reference in New Issue