diff --git a/App.jsx b/App.jsx index dafa293..c4123d9 100644 --- a/App.jsx +++ b/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 ? ( <>
- 支持上传PDF、图片、HTML、或Word/PPT/Excel(.docx/.pptx/.xlsx)形式的产品目录,可以一次选多个文件(比如一份多页PDF,或者拍的多张照片)。 + 支持上传PDF、图片、或HTML形式的产品目录,可以一次选多个文件(比如一份多页PDF,或者拍的多张照片)。 AI会自动识别里面有几个产品型号、并提取型号/品牌/规格参数,之后可以在预览里检查修改,确认无误再批量建档。
- PDF会先自动判断有没有文字层:正式排版、非扫描件的PDF会直接读取文字(更快更准),只有扫描件/纯图片排版的PDF才会走OCR。 -
如果原始文件是水印很重、排版复杂、OCR容易识别错的图片,可以先用其他AI工具(如Gemini)把表格转换成结构清晰的HTML文件再上传——HTML会跳过OCR直接读取,准确率通常明显更高。
@@ -3897,9 +3880,7 @@ function MainApp({ username, onLogout }) {
{aiImportError &&
{aiImportError}
} @@ -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(' · ') || '(无更多规格信息)'} @@ -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 ( -
+
- {line.name || line.addr}
@@ -4363,13 +4329,7 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate
{checkNote}
-
- 域名 - {line.addr} - -
+
域名{line.addr}
解析IP {ipDisplay} @@ -4412,6 +4372,14 @@ function JmsLineFormFields({ group, protocol, draft, setDraft }) {
); } + if (protocol === 'vmess') { + return ( +
+
UUID set({ uuid: e.target.value })} placeholder="用户UUID" />
+
AlterId set({ alterId: e.target.value })} />
+
+ ); + } 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

Just My Socks 节点管理

+
管理自建VLESS-CDN线路和 justmysocks2.net 订阅线路 · DNS解析、订阅链接、二维码和连通性监控
@@ -4548,12 +4498,13 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify, dragIdx, onDragSt
{total}
线路总数
{ssCount}
SS
+
{vmessCount}
VMess
{vlessCount}
VLESS
{lines.length === 0 &&
还没有线路,点右上角"新增线路"添加
} - {lines.map((line, idx) => ( + {lines.map(line => ( onDragStart(idx)} - onCardDragOver={(e) => e.preventDefault()} - onCardDrop={() => onDrop(idx)} - onCardDragEnd={onDragEnd} /> ))}
@@ -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,
控制管理 / 接口 set({ interfacePort: e.target.value })} placeholder="如:Smart RS-232/USB, supports Windows 2000/2003/XP/Vista/2008, 7/8, Linux, Unix, MAC" />
可选SNMP set({ optionalSnmp: e.target.value })} placeholder="如:Power management from SNMP manager and web browser" />
+
应用场景 set({ applicationScenarios: e.target.value })} placeholder="如:电脑、投影仪、打印机、摄像机、收银机" />
+
+
20尺柜装箱数量 set({ container20ft: e.target.value })} placeholder="如:1000PCS(无栈板)" />
+
40尺柜装箱数量 set({ container40ft: e.target.value })} placeholder="如:2080PCS(无栈板)" />
+
+
40高柜装箱数量 set({ container40hq: e.target.value })} placeholder="如:2340PCS(无栈板)" />
) : isUps && isOfflineOrLI ? ( <> @@ -5009,6 +4956,12 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage,
湿度 set({ humidity: e.target.value })} placeholder="如:0-90% RH@0-40℃(Non-condensing)" />
噪音 set({ noiseLevel: e.target.value })} placeholder="如:Less than 40dB(1m spacing)" />
+
应用场景 set({ applicationScenarios: e.target.value })} placeholder="如:电脑、投影仪、打印机、摄像机、收银机" />
+
+
20尺柜装箱数量 set({ container20ft: e.target.value })} placeholder="如:1000PCS(无栈板)" />
+
40尺柜装箱数量 set({ container40ft: e.target.value })} placeholder="如:2080PCS(无栈板)" />
+
+
40高柜装箱数量 set({ container40hq: e.target.value })} placeholder="如:2340PCS(无栈板)" />
) : isUps && !isMiniUps ? (
@@ -5113,18 +5066,6 @@ function ProductForm({ draft, setDraft, onSave, onCancel, categories, onManage, />
-
- 其他参数(可选——原文规格表里有、但上面没有专属字段的参数,AI批量导入时会自动补充在这里,也可以手动增删;会一起显示在产品详情和多语言PDF导出里) - {extraSpecs.map((item, idx) => ( -
- updateExtraSpec(idx, { label: e.target.value })} placeholder="参数名,如:防雷等级" /> - updateExtraSpec(idx, { value: e.target.value })} placeholder="参数值,如:Type II" /> - -
- ))} - -
-
正本价格(元) set({ costPrice: e.target.value })} placeholder="采购/成本价,人民币" />
内部备注(仅自己可见,不会出现在任何导出的文档里)