Update App.jsx via upload script - 2026-08-07 15:50:41

This commit is contained in:
mike 2026-08-07 15:51:02 +08:00
parent 0d308d5224
commit ebf85a5932
1 changed files with 60 additions and 119 deletions

179
App.jsx
View File

@ -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 });
}
// ""AIschema
//
(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'}]
// AIschema
//
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>