更新前端组件和样式

This commit is contained in:
zzh 2026-03-26 10:16:19 +08:00
parent ed49636d44
commit 0d682a826a
7 changed files with 897 additions and 459 deletions

26
frontend/assets/styles.css Normal file → Executable file
View File

@ -2747,6 +2747,32 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.spec-value{font-size:14px;color:var(--text);font-weight:500}
@media(max-width:768px){.product-card{flex-direction:column}.product-image-wrapper{flex:none;width:100%}.product-features{grid-template-columns:1fr}}
/* 历史记录弹窗响应式优化 */
@media (max-width: 480px) {
.notification-modal-content {
width: 95%;
max-width: none;
}
#mac-history-modal .notification-modal-content {
max-width: 95%;
}
#mac-history-modal-list {
max-height: 50vh;
}
#mac-history-modal-list li {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
#mac-history-modal-list .badge {
align-self: flex-start;
}
}
[data-theme="light"] body{background:#f1f5f9}
#app.trackit-layout{margin:0;height:100vh;border-radius:0;overflow:hidden;background:#ffffff;border:none;box-shadow:none;display:flex;flex-direction:row}
#app.trackit-layout .content{background:rgba(248,250,252,.6);height:100%}

11
frontend/index.html Normal file → Executable file
View File

@ -470,6 +470,17 @@
</div>
</div>
<div id="mac-history-modal" class="notification-modal" style="display:none;">
<div class="notification-modal-backdrop" data-close="mac-history"></div>
<div class="notification-modal-content" role="dialog" aria-modal="true" aria-label="📋" style="max-width:800px">
<div class="notification-modal-header">
<div class="notification-modal-title">📋 MAC与批次上传历史记录</div>
<button id="mac-history-modal-close" class="notification-modal-close" type="button" aria-label="关闭">×</button>
</div>
<div id="mac-history-modal-list" class="notification-modal-list" style="max-height:60vh;overflow-y:auto"></div>
</div>
</div>
<script src="./js/router.js" defer></script>
<script src="./js/api.js" defer></script>
<script src="./js/utils/memory-monitor.js" defer></script>

124
frontend/js/components/shipment-audit.js Normal file → Executable file
View File

@ -124,44 +124,94 @@ Router.register('/shipments/audit', async () => {
});
}, 0);
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">审计查询</div>
return `
<style>
#audit-query-page {
padding: 20px;
background: var(--bg);
}
#audit-query-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#audit-query-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#audit-query-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#audit-query-page .query-section {
padding: 20px;
border-bottom: 1px solid var(--border);
}
#audit-query-page .result-section {
padding: 20px;
}
#audit-query-page .info-box {
padding: 12px;
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
color: var(--text);
}
</style>
<div style="margin-bottom:16px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px">
<div style="color:var(--text-2);font-size:14px">
<strong>说明</strong> Redis MAC
<div id="audit-query-page">
<div class="page-header">
<h1>审计查询</h1>
</div>
<div class="content-area">
<div class="query-section">
<div class="info-box">
<strong>说明</strong> Redis MAC
</div>
<div class="field" style="margin-bottom:16px">
<label>选择平台</label>
<select id="platform-select" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
<option value="mt">美团</option>
</select>
</div>
<div class="field" style="margin-bottom:16px">
<label>查询类型</label>
<select id="query-type" class="input">
<option value="mac"> MAC 地址查询</option>
<option value="batch">按批次号查询</option>
</select>
</div>
<div class="field" style="margin-bottom:16px">
<label id="input-label">输入 MAC 地址</label>
<input
id="query-input"
class="input"
placeholder="可输入带或不带冒号的MAC地址001122AABBCC 或 00:11:22:AA:BB:CC"
style="font-family: monospace"
/>
</div>
<div class="actions">
<button class="btn btn-primary" id="query-btn">查询</button>
</div>
</div>
<div class="result-section">
<div id="query-result"></div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div class="field">
<label>选择平台</label>
<select id="platform-select" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
<option value="mt">美团</option>
</select>
</div>
<div class="field">
<label>查询类型</label>
<select id="query-type" class="input">
<option value="mac"> MAC 地址查询</option>
<option value="batch">按批次号查询</option>
</select>
</div>
<div class="field">
<label id="input-label">输入 MAC 地址</label>
<input
id="query-input"
class="input"
placeholder="可输入带或不带冒号的MAC地址001122AABBCC 或 00:11:22:AA:BB:CC"
style="font-family: monospace"
/>
</div>
<button class="btn btn-primary" id="query-btn">查询</button>
</div>
<div id="query-result"></div>
</div>`;
`;
});

109
frontend/js/components/shipment-query.js Normal file → Executable file
View File

@ -187,38 +187,85 @@ Router.register('/shipments/query', async () => {
// 根据用户角色决定是否显示清空按钮
const showClearButton = userRole === 'superadmin';
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">发货详细记录查询</div>
return `
<style>
#shipment-query-page {
padding: 20px;
background: var(--bg);
}
#shipment-query-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#shipment-query-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#shipment-query-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#shipment-query-page .query-section {
padding: 20px;
border-bottom: 1px solid var(--border);
}
#shipment-query-page .result-section {
padding: 20px;
}
#shipment-query-page .stats-box {
padding: 12px;
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
margin-bottom: 16px;
}
</style>
<div style="margin-bottom:20px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px">
<div id="redis-stats" style="display:flex;align-items:center;justify-content:space-between">
<div>加载中...</div>
<div id="shipment-query-page">
<div class="page-header">
<h1>发货详细记录查询</h1>
${showClearButton ? '<button class="btn" id="clear-redis-btn" style="background:#dc2626;color:white">清空所有记录</button>' : ''}
</div>
<div class="content-area">
<div class="query-section">
<div class="stats-box">
<div id="redis-stats">
<div>加载中...</div>
</div>
</div>
<div class="field" style="margin-bottom:16px">
<label>查询类型</label>
<select id="query-type" class="input">
<option value="sn"> SN/MAC 查询</option>
<option value="box">按箱号查询</option>
</select>
</div>
<div class="field" style="margin-bottom:16px">
<label id="input-label">输入 SN/MAC </label>
<input
id="query-input"
class="input"
placeholder="输入 SN 或 MAC 地址"
style="font-family: monospace"
/>
</div>
<div class="actions">
<button class="btn btn-primary" id="query-btn">查询</button>
</div>
</div>
<div class="result-section">
<div id="query-result"></div>
</div>
</div>
</div>
<div style="margin-bottom:16px">
<div class="field">
<label>查询类型</label>
<select id="query-type" class="input">
<option value="sn"> SN/MAC 查询</option>
<option value="box">按箱号查询</option>
</select>
</div>
<div class="field">
<label id="input-label">输入 SN/MAC </label>
<input
id="query-input"
class="input"
placeholder="输入 SN 或 MAC 地址"
style="font-family: monospace"
/>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-primary" id="query-btn">查询</button>
${showClearButton ? '<button class="btn" id="clear-redis-btn" style="background:#dc2626">清空所有记录</button>' : ''}
</div>
</div>
<div id="query-result"></div>
</div>`;
`;
});

96
frontend/js/components/shipment-summary.js Normal file → Executable file
View File

@ -95,31 +95,83 @@ Router.register('/shipments/summary', async () => {
performQuery();
}, 0);
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">发货汇总信息查询</div>
return `
<style>
#shipment-summary-page {
padding: 20px;
background: var(--bg);
}
#shipment-summary-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#shipment-summary-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#shipment-summary-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#shipment-summary-page .query-section {
padding: 20px;
border-bottom: 1px solid var(--border);
}
#shipment-summary-page .result-section {
padding: 20px;
}
#shipment-summary-page .date-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
#shipment-summary-page .date-row {
grid-template-columns: 1fr;
}
}
</style>
<div style="margin-bottom:16px">
<div class="grid cols-2" style="margin-bottom:12px">
<div class="field">
<label>开始日期</label>
<input
id="start-date"
type="date"
class="input"
/>
<div id="shipment-summary-page">
<div class="page-header">
<h1>发货汇总信息查询</h1>
</div>
<div class="content-area">
<div class="query-section">
<div class="date-row">
<div class="field">
<label>开始日期</label>
<input
id="start-date"
type="date"
class="input"
/>
</div>
<div class="field">
<label>结束日期</label>
<input
id="end-date"
type="date"
class="input"
/>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" id="summary-query-btn">查询</button>
</div>
</div>
<div class="field">
<label>结束日期</label>
<input
id="end-date"
type="date"
class="input"
/>
<div class="result-section">
<div id="summary-result"></div>
</div>
</div>
<button class="btn" id="summary-query-btn">查询</button>
</div>
<div id="summary-result"></div>
</div>`;
`;
});

196
frontend/js/components/shipments.js Normal file → Executable file
View File

@ -170,25 +170,133 @@ Router.register('/upload/shipments', async () => {
}
});
}, 0);
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">发货记录</div>
return `
<style>
#shipments-page {
padding: 20px;
background: var(--bg);
}
#shipments-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#shipments-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#shipments-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#shipments-page .section {
padding: 20px;
}
#shipments-page .section + .section {
border-top: 1px solid var(--border);
}
#shipments-page .section-title {
font-weight: 600;
font-size: 16px;
margin-bottom: 12px;
color: var(--text);
}
#shipments-page .section-desc {
font-size: 13px;
color: var(--text-2);
margin-bottom: 16px;
}
#shipments-page .format-requirements {
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 13px;
line-height: 1.6;
}
#shipments-page .row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
#shipments-page .row {
grid-template-columns: 1fr;
}
}
</style>
<div style="margin-bottom:24px;padding-bottom:24px;border-bottom:1px solid var(--border)">
<div style="font-weight:500;margin-bottom:12px">手动录入</div>
<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">
用于快速录入发货汇总信息不含详细SN
<div id="shipments-page">
<div class="page-header">
<h1>发货记录</h1>
</div>
<div class="row">
<div class="col">
<div class="field">
<label>发货日期 <span style="color:var(--danger)">*</span></label>
<input id="ship-date" type="date" class="input" />
<div class="content-area">
<div class="section">
<div class="section-title">手动录入</div>
<div class="section-desc">用于快速录入发货汇总信息不含详细SN</div>
<div class="row">
<div class="field">
<label>发货日期 <span style="color:var(--danger)">*</span></label>
<input id="ship-date" type="date" class="input" />
</div>
<div class="field">
<label>机种类型 <span style="color:var(--danger)">*</span></label>
<select id="ship-manual-platform" class="input">
<option value="">请选择机种</option>
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
<option value="mt">美团</option>
<option value="drf">大润发</option>
<option value="std">标准版</option>
</select>
</div>
</div>
<div class="row">
<div class="field">
<label>接收方 <span style="color:var(--danger)">*</span></label>
<input id="ship-to" class="input" placeholder="客户名称" />
</div>
<div class="field">
<label>数量 <span style="color:var(--danger)">*</span></label>
<input id="ship-qty" type="number" min="1" class="input" placeholder="发货数量" />
</div>
</div>
<div class="field" style="margin-bottom:16px">
<label>箱号可选</label>
<input id="ship-box-no" class="input" placeholder="例如BOX001" />
</div>
<div id="ship-manual-status" style="margin:8px 0;font-size:13px"></div>
<div class="actions">
<button class="btn btn-primary" id="ship-upload">提交录入</button>
</div>
</div>
<div class="col">
<div class="field">
<div class="section">
<div class="section-title">详细记录批量导入</div>
<div class="format-requirements">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📋 文件格式要求</div>
<div style="color:var(--text)">
<div> 必需列出货日期箱号SN1SN2...SN20</div>
<div> 出货日期列支持合并单元格</div>
<div> 支持格式Excel (.xlsx, .xls) CSV</div>
</div>
</div>
<div class="field" style="margin-bottom:16px">
<label>机种类型 <span style="color:var(--danger)">*</span></label>
<select id="ship-manual-platform" class="input">
<select id="ship-platform" class="input">
<option value="">请选择机种</option>
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
@ -198,59 +306,19 @@ Router.register('/upload/shipments', async () => {
<option value="std">标准版</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="field">
<label>接收方 <span style="color:var(--danger)">*</span></label>
<input id="ship-to" class="input" placeholder="客户名称" />
<div class="field" style="margin-bottom:16px">
<label>选择文件</label>
<input type="file" id="ship-file" accept=".xlsx,.xls,.csv" class="input" style="padding:6px" />
</div>
</div>
<div class="col">
<div class="field">
<label>数量 <span style="color:var(--danger)">*</span></label>
<input id="ship-qty" type="number" min="1" class="input" placeholder="发货数量" />
<div id="ship-file-status" style="margin:8px 0;font-size:13px"></div>
<div class="actions">
<button class="btn btn-secondary" id="ship-validate">验证文件</button>
<button class="btn btn-primary" id="ship-upload-file">导入数据</button>
</div>
</div>
</div>
<div class="field">
<label>箱号可选</label>
<input id="ship-box-no" class="input" placeholder="例如BOX001" />
</div>
<div id="ship-manual-status" style="margin:8px 0;font-size:13px"></div>
<div class="actions"><button class="btn" id="ship-upload">提交录入</button></div>
</div>
<div>
<div style="font-weight:500;margin-bottom:12px">详细记录批量导入</div>
<div class="format-requirements">
<div style="font-weight:500;margin-bottom:4px">文件格式要求</div>
<div> 必需列出货日期箱号SN1SN2...SN20</div>
<div> 出货日期列支持合并单元格</div>
<div> 支持格式Excel (.xlsx, .xls) CSV</div>
</div>
<div class="field">
<label>机种类型 <span style="color:var(--danger)">*</span></label>
<select id="ship-platform" class="input">
<option value="">请选择机种</option>
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
<option value="mt">美团</option>
<option value="drf">大润发</option>
<option value="std">标准版</option>
</select>
</div>
<div class="field">
<label>选择文件</label>
<input type="file" id="ship-file" accept=".xlsx,.xls,.csv" class="input" style="padding:6px" />
</div>
<div id="ship-file-status" style="margin:8px 0;font-size:13px"></div>
<div class="actions">
<button class="btn" id="ship-validate" style="background:#6c757d">验证文件</button>
<button class="btn" id="ship-upload-file">导入数据</button>
</div>
</div>
</div>`;
`;
});

794
frontend/js/components/upload.js Normal file → Executable file
View File

@ -68,100 +68,289 @@ const Upload = (() => {
function textarea(id,label,placeholder=''){return `<div class="field"><label>${label}</label><textarea id="${id}" class="input" rows="4" placeholder="${placeholder}"></textarea></div>`}
async function renderMac(){
return section('MAC与批次MAC与批次对应关系表',`
<div style="background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📋 Excel文件格式要求</div>
<div style="color:var(--text)">
<div> Excel文件必须包含以下列头按顺序</div>
<div style="margin-top:4px;padding:8px;background:var(--bg);border-radius:4px;font-family:monospace;font-size:12px">
拼多多/圆通<br>
第1列: MAC格式90A9F7300000<br>
第2列: 批次号<br><br>
兔喜<br>
第1列: SN_MAC格式TJ251639510533:90A9F73007D0<br>
第2列: 批次号
return `
<style>
#mac-batch-page {
padding: 20px;
background: var(--bg);
}
#mac-batch-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#mac-batch-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#mac-batch-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#mac-batch-page .upload-section {
padding: 20px;
border-bottom: 1px solid var(--border);
}
#mac-batch-page .help-toggle {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: var(--primary);
font-weight: 600;
margin-bottom: 16px;
}
#mac-batch-page .help-toggle:hover {
background: rgba(79,140,255,0.12);
border-color: rgba(79,140,255,0.3);
}
#mac-batch-page .help-content {
margin-bottom: 16px;
padding: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
}
#mac-batch-page .form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
#mac-batch-page .field {
margin-bottom: 0;
}
#mac-batch-page .actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
#mac-batch-page .log-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
@media (max-width: 768px) {
#mac-batch-page .form-row {
grid-template-columns: 1fr;
}
#mac-batch-page .actions {
flex-direction: column;
}
#mac-batch-page .actions button {
width: 100%;
}
}
</style>
<div id="mac-batch-page">
<div class="page-header">
<h1>MAC与批次MAC与批次对应关系表</h1>
<div class="page-actions">
<button class="btn btn-secondary" id="mac-view-history" type="button">📋 查看历史记录</button>
</div>
<div style="margin-top:6px;color:var(--text-2)">
示例数据<br>
拼多多/圆通90A9F7300001 | D20250000000001<br>
兔喜TJ251639510533:90A9F73007D0 | D20250000000002
</div>
<div style="margin-top:6px;color:var(--warning)">
注意拼多多和圆通的MAC地址为12位十六进制字符不包含冒号<br>
兔喜的SN_MAC格式为SN号:MAC地址
</div>
<div class="content-area">
<div class="upload-section">
<!-- 可折叠的格式说明 -->
<button id="mac-toggle-help" class="help-toggle" type="button">
<span>📋 Excel文件格式要求</span>
<svg id="mac-help-icon" style="width:20px;height:20px;transition:transform 0.2s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div id="mac-help-content" class="help-content" style="display:none">
<div style="color:var(--text)">
<div> Excel文件必须包含以下列头按顺序</div>
<div style="margin-top:8px;padding:8px;background:var(--surface);border-radius:4px;font-family:monospace;font-size:12px">
拼多多/圆通<br>
第1列: MAC格式90A9F7300000<br>
第2列: 批次号<br><br>
兔喜<br>
第1列: SN_MAC格式TJ251639510533:90A9F73007D0<br>
第2列: 批次号
</div>
<div style="margin-top:8px;color:var(--text-2)">
示例数据<br>
拼多多/圆通90A9F7300001 | D20250000000001<br>
兔喜TJ251639510533:90A9F73007D0 | D20250000000002
</div>
<div style="margin-top:8px;color:var(--warning)">
注意拼多多和圆通的MAC地址为12位十六进制字符不包含冒号<br>
兔喜的SN_MAC格式为SN号:MAC地址
</div>
</div>
</div>
<div class="form-row">
<div class="field">
<label>上传机种 <span style="color:#ff4444">*</span></label>
<select id="mac-type" class="input">
<option value="">请选择上传机种</option>
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
<div class="field">
<label>批量导入(Excel)</label>
<input id="mac-file" type="file" class="input" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" />
</div>
</div>
<div class="actions">
<button class="btn btn-primary" id="mac-upload">上传</button>
</div>
<div id="upload-log" class="log-section" style="display:none">
<div style="font-weight:600;margin-bottom:8px;color:var(--text)">上传日志</div>
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-size:12px;color:var(--text);white-space:pre-wrap"></pre>
</div>
</div>
</div>
</div>
<div class="field">
<label>上传机种 <span style="color:#ff4444">*</span></label>
<select id="mac-type" class="input">
<option value="">请选择上传机种</option>
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
${filePicker('mac-file','批量导入(Excel)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
<div class="actions"><button class="btn" id="mac-upload">上传</button></div>
<div id="upload-log" style="margin-top:12px;display:none">
<div style="font-weight:600;margin-bottom:8px">上传日志</div>
<pre style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-size:12px;color:var(--text);white-space:pre-wrap"></pre>
</div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600">历史记录</span>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="mac-show-history" style="font-size:12px;padding:4px 8px">刷新</button>
<button class="btn btn-secondary" id="mac-clear-display" style="font-size:12px;padding:4px 8px">清空显示</button>
</div>
</div>
<ul id="mac-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
</div>
`);
`;
}
async function renderStats(){
return section('良/不良统计',`
<div style="background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📊 数据说明</div>
<div style="color:var(--text)">
<div><strong>直通良品数</strong></div>
<div><strong>良品数</strong> + </div>
<div><strong>不良品数</strong></div>
<div style="margin-top:6px;padding-top:6px;border-top:1px solid rgba(79,140,255,0.2);color:var(--text-2)">
💡 <strong>计算公式</strong><br>
直通良品率 = 直通良品数 / (良品数 + 不良品数) × 100%<br>
总良品率 = 良品数 / (良品数 + 不良品数) × 100%
return `
<style>
#stats-page {
padding: 20px;
background: var(--bg);
}
#stats-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#stats-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#stats-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#stats-page .upload-section {
padding: 20px;
}
#stats-page .info-box {
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 13px;
line-height: 1.6;
}
#stats-page .form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
#stats-page .form-row {
grid-template-columns: 1fr;
}
}
</style>
<div id="stats-page">
<div class="page-header">
<h1>/不良统计</h1>
<div class="page-actions">
<button class="btn btn-secondary" id="stats-show-history">查看历史</button>
</div>
</div>
<div class="content-area">
<div class="upload-section">
<div class="info-box">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📊 数据说明</div>
<div style="color:var(--text)">
<div><strong>直通良品数</strong></div>
<div><strong>良品数</strong> + </div>
<div><strong>不良品数</strong></div>
<div style="margin-top:6px;padding-top:6px;border-top:1px solid rgba(79,140,255,0.2);color:var(--text-2)">
💡 <strong>计算公式</strong><br>
直通良品率 = 直通良品数 / (良品数 + 不良品数) × 100%<br>
总良品率 = 良品数 / (良品数 + 不良品数) × 100%
</div>
</div>
</div>
<div class="form-row">
<div class="field">
<label>平台类型</label>
<select id="stats-platform" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
<div class="field">
<label>直通良品数量一次检测通过</label>
<input id="fpy-good-count" type="number" class="input" />
</div>
</div>
<div class="form-row">
<div class="field">
<label>良品数量最终通过检测的总数</label>
<input id="good-count" type="number" class="input" />
</div>
<div class="field">
<label>不良品数量最终未通过</label>
<input id="bad-count" type="number" class="input" />
</div>
</div>
<div class="field" style="margin-bottom:16px">
<label>不良明细可选</label>
<textarea id="bad-details" class="input" rows="4" placeholder="每行一个不良记录格式MAC地址,批次号&#10;例如90:A9:F7:DD:EE:FF,D20250000000001"></textarea>
</div>
<div class="actions">
<button class="btn btn-primary" id="stats-upload">上传</button>
</div>
</div>
</div>
</div>
<div class="field">
<label>平台类型</label>
<select id="stats-platform" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
</select>
</div>
${numberInput('fpy-good-count','直通良品数量(一次检测通过)')}
${numberInput('good-count','良品数量(最终通过检测的总数)')}
${numberInput('bad-count','不良品数量(最终未通过)')}
<div class="field">
<label>不良明细可选</label>
<textarea id="bad-details" class="input" rows="4" placeholder="每行一个不良记录格式MAC地址,批次号&#10;例如90:A9:F7:DD:EE:FF,D20250000000001"></textarea>
</div>
<div class="actions"><button class="btn" id="stats-upload">上传</button></div>
<div style="margin-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-weight:600">最新记录</span>
<div style="display:flex;gap:8px">
<button class="btn btn-secondary" id="stats-show-history" style="font-size:12px;padding:4px 8px">查看历史</button>
<button class="btn btn-secondary" id="stats-clear-display" style="font-size:12px;padding:4px 8px">清空显示</button>
</div>
</div>
<ul id="stats-list" class="list" style="max-height:300px;overflow-y:auto"></ul>
</div>
`);
`;
}
// 不良原因选项
@ -187,11 +376,46 @@ const Upload = (() => {
];
async function renderRepairs(){
return section('返修记录上传',`
<div style="margin-bottom:16px;display:flex;justify-content:flex-start">
<a href="#/upload/repairs-history" class="btn btn-secondary" style="font-size:12px;padding:6px 10px">📋 查看历史记录</a>
</div>
<div id="repairs-upload-form" style="max-height:calc(100vh - 200px);overflow-y:auto;padding-right:8px">
return `
<style>
#repairs-page {
padding: 20px;
background: var(--bg);
}
#repairs-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#repairs-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#repairs-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#repairs-page .upload-section {
padding: 20px;
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
#repair-image-dropzone:hover { background:rgba(79,140,255,0.12); border-color:var(--primary); }
#repair-image-dropzone.dragover { background:rgba(79,140,255,0.15); border-color:var(--primary); border-style:solid; }
</style>
<div id="repairs-page">
<div class="page-header">
<h1>返修记录上传</h1>
<div class="page-actions">
<a href="#/upload/repairs-history" class="btn btn-secondary">📋 查看历史记录</a>
</div>
</div>
<div class="content-area">
<div class="upload-section">
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">
设备SN <span style="color:#ef4444">*</span>
@ -267,17 +491,10 @@ const Upload = (() => {
<button class="btn btn-secondary" id="repairs-clear" style="flex:0 0 auto">清空</button>
</div>
<style>
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
#repair-image-dropzone:hover { background:rgba(79,140,255,0.12); border-color:var(--primary); }
#repair-image-dropzone.dragover { background:rgba(79,140,255,0.15); border-color:var(--primary); border-style:solid; }
#repairs-upload-form::-webkit-scrollbar { width: 8px; }
#repairs-upload-form::-webkit-scrollbar-track { background: var(--bg); border-radius: 4px; }
#repairs-upload-form::-webkit-scrollbar-thumb { background: rgba(79,140,255,0.3); border-radius: 4px; }
#repairs-upload-form::-webkit-scrollbar-thumb:hover { background: rgba(79,140,255,0.5); }
</style>
</div>
</div>
</div>
`);
`;
}
async function renderRepairsHistory(){
@ -395,39 +612,98 @@ const Upload = (() => {
});
async function renderSop(){
return section('SOP 文件管理',`
<div style="background:rgba(79,140,255,0.08);border:1px solid rgba(79,140,255,0.2);border-radius:8px;padding:12px;margin-bottom:12px;font-size:13px;line-height:1.6">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📄 SOP 说明</div>
<div style="color:var(--text)">
<div> 所有用户均可在线查看和下载 SOP 文件</div>
<div> 管理员可以上传新的 SOP 文件支持 Excel Word 格式</div>
<div> 建议为每个 SOP 文件添加清晰的描述说明</div>
return `
<style>
#sop-page {
padding: 20px;
background: var(--bg);
}
#sop-page .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
#sop-page h1 {
margin: 0;
font-size: 24px;
color: var(--text);
}
#sop-page .content-area {
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#sop-page .upload-section {
padding: 20px;
border-bottom: 1px solid var(--border);
}
#sop-page .list-section {
padding: 20px;
}
#sop-page .info-box {
background: rgba(79,140,255,0.08);
border: 1px solid rgba(79,140,255,0.2);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 13px;
line-height: 1.6;
}
</style>
<div id="sop-page">
<div class="page-header">
<h1>SOP 文件管理</h1>
</div>
<div class="content-area">
<div class="upload-section">
<div class="info-box">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">📄 SOP 说明</div>
<div style="color:var(--text)">
<div> 所有用户均可在线查看和下载 SOP 文件</div>
<div> 管理员可以上传新的 SOP 文件支持 Excel Word 格式</div>
<div> 建议为每个 SOP 文件添加清晰的描述说明</div>
</div>
</div>
<div class="field" style="margin-bottom:16px">
<label>上传 SOP 文件 (Excel/Word)</label>
<input id="sop-file" type="file" class="input" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword" />
</div>
<div class="field" style="margin-bottom:16px">
<label>文件描述可选</label>
<textarea id="sop-description" class="input" rows="2" placeholder="例如:拼多多生产流程 SOP v1.0"></textarea>
</div>
<div class="actions">
<button class="btn btn-primary" id="sop-upload">上传</button>
</div>
<div id="sop-upload-progress" style="display:none;margin-top:16px;padding:12px;background:var(--bg);border-radius:8px;border:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span style="font-weight:500">上传进度</span>
<span id="sop-progress-percent">0%</span>
</div>
<div style="width:100%;height:8px;background:var(--surface);border-radius:4px;overflow:hidden">
<div id="sop-progress-bar" style="width:0%;height:100%;background:var(--primary);transition:width 0.3s"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:8px;font-size:12px;color:var(--text-2)">
<span id="sop-progress-size">0 MB / 0 MB</span>
<span id="sop-progress-speed">0 KB/s</span>
</div>
</div>
</div>
<div class="list-section">
<div style="font-weight:600;margin-bottom:12px;color:var(--text)">SOP 文件列表</div>
<ul id="sop-list" class="list" style="max-height:400px;overflow-y:auto"></ul>
</div>
</div>
</div>
${filePicker('sop-file','上传 SOP 文件 (Excel/Word)','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,text/csv,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/msword')}
<div class="field">
<label>文件描述可选</label>
<textarea id="sop-description" class="input" rows="2" placeholder="例如:拼多多生产流程 SOP v1.0"></textarea>
</div>
<div class="actions"><button class="btn" id="sop-upload">上传</button></div>
<div id="sop-upload-progress" style="display:none;margin-top:12px;padding:12px;background:var(--surface);border-radius:8px;border:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
<span style="font-weight:500">上传进度</span>
<span id="sop-progress-percent">0%</span>
</div>
<div style="width:100%;height:8px;background:var(--bg);border-radius:4px;overflow:hidden">
<div id="sop-progress-bar" style="width:0%;height:100%;background:var(--primary);transition:width 0.3s"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:8px;font-size:12px;color:var(--text-2)">
<span id="sop-progress-size">0 MB / 0 MB</span>
<span id="sop-progress-speed">0 KB/s</span>
</div>
</div>
<div style="margin-top:12px">
<div style="font-weight:600;margin-bottom:8px">SOP 文件列表</div>
<ul id="sop-list" class="list" style="max-height:400px;overflow-y:auto"></ul>
</div>
`);
`;
}
Router.register('/upload/sop', async () => {
@ -590,70 +866,98 @@ const Upload = (() => {
async function bindMacEvents(){
const fileEl=document.getElementById('mac-file');
const btn = document.getElementById('mac-upload');
const showHistoryBtn = document.getElementById('mac-show-history');
const clearDisplayBtn = document.getElementById('mac-clear-display');
const viewHistoryBtn = document.getElementById('mac-view-history');
const toggleHelpBtn = document.getElementById('mac-toggle-help');
const helpContent = document.getElementById('mac-help-content');
const helpIcon = document.getElementById('mac-help-icon');
// 显示历史记录的通用函数
const displayHistory = async (showAll = false) => {
const listEl = document.getElementById('mac-list');
// 格式说明折叠/展开功能
addListener(toggleHelpBtn, 'click', () => {
const isHidden = helpContent.style.display === 'none';
helpContent.style.display = isHidden ? 'block' : 'none';
helpIcon.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';
});
// 历史记录弹窗功能
const showHistoryModal = async () => {
const modal = document.getElementById('mac-history-modal');
const listEl = document.getElementById('mac-history-modal-list');
if(!modal || !listEl) return;
// 显示弹窗
modal.style.display = 'block';
listEl.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-2)">加载中...</div>';
try {
const data = await API.listMac();
if(listEl){
if(data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
if(data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
// 按时间和机种分组统计
const uploadGroups = {};
data.list.forEach(r => {
const date = new Date(r.ts).toLocaleDateString('zh-CN');
const platform = platformNames[r.platform] || r.platform || '未知';
const key = `${date}_${platform}`;
// 按时间和机种分组统计
const uploadGroups = {};
data.list.forEach(r => {
const date = new Date(r.ts).toLocaleDateString('zh-CN');
const platform = platformNames[r.platform] || r.platform || '未知';
const key = `${date}_${platform}`;
if (!uploadGroups[key]) {
uploadGroups[key] = {
date: date,
platform: platform,
count: 0,
firstTime: r.ts
};
}
uploadGroups[key].count++;
});
// 转换为数组并按时间排序
const groupArray = Object.values(uploadGroups)
.sort((a, b) => new Date(b.firstTime) - new Date(a.firstTime));
listEl.innerHTML = groupArray.map(g => {
return `<li style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--surface)">
<div>
<div style="font-weight:600;color:var(--primary)">${g.date}</div>
<div style="margin-top:4px">上传 <span style="color:var(--success);font-weight:600">${g.count}</span> </div>
</div>
<div>
<span class="badge" style="background:var(--primary-light);color:var(--primary)">${g.platform}</span>
</div>
</li>`;
}).join('');
if(showAll) {
const totalGroups = groupArray.length;
const totalRecords = data.list.length;
API.toast(`显示 ${totalGroups} 次上传记录,共 ${totalRecords} 条数据`);
if (!uploadGroups[key]) {
uploadGroups[key] = {
date: date,
platform: platform,
count: 0,
firstTime: r.ts
};
}
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
uploadGroups[key].count++;
});
// 转换为数组并按时间排序
const groupArray = Object.values(uploadGroups)
.sort((a, b) => new Date(b.firstTime) - new Date(a.firstTime));
listEl.innerHTML = `
<div style="padding:12px;background:var(--info-bg);border-radius:8px;margin-bottom:12px">
<div style="font-size:14px;color:var(--text)"> <strong>${groupArray.length}</strong> <strong>${data.list.length}</strong> </div>
</div>
<ul class="list" style="margin:0">
${groupArray.map(g => {
return `<li style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--surface);margin-bottom:8px;border-radius:8px">
<div style="flex:1">
<div style="font-weight:600;color:var(--primary);font-size:15px">${g.date}</div>
<div style="margin-top:6px;color:var(--text-2);font-size:13px">上传 <span style="color:var(--success);font-weight:600">${g.count}</span> </div>
</div>
<div>
<span class="badge" style="background:var(--primary);color:white;padding:6px 12px;border-radius:6px;font-size:12px">${g.platform}</span>
</div>
</li>`;
}).join('')}
</ul>
`;
} else {
listEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-2)">暂无历史记录</div>';
}
} catch(e) {
listEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger)">加载失败,请重试</div>';
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
};
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
await displayHistory(true);
// 查看历史记录按钮
addListener(viewHistoryBtn, 'click', showHistoryModal);
// 关闭弹窗
const closeBtn = document.getElementById('mac-history-modal-close');
const backdrop = document.querySelector('#mac-history-modal .notification-modal-backdrop');
addListener(closeBtn, 'click', () => {
const modal = document.getElementById('mac-history-modal');
if(modal) modal.style.display = 'none';
});
addListener(backdrop, 'click', () => {
const modal = document.getElementById('mac-history-modal');
if(modal) modal.style.display = 'none';
});
// 文件选择后立即验证
@ -683,77 +987,6 @@ const Upload = (() => {
}
});
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
const listEl = document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl){
if(data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
// 按时间和机种分组统计
const uploadGroups = {};
data.list.forEach(r => {
const date = new Date(r.ts).toLocaleDateString('zh-CN');
const platform = platformNames[r.platform] || r.platform || '未知';
const key = `${date}_${platform}`;
if (!uploadGroups[key]) {
uploadGroups[key] = {
date: date,
platform: platform,
count: 0,
firstTime: r.ts
};
}
uploadGroups[key].count++;
});
// 转换为数组并按时间排序
const groupArray = Object.values(uploadGroups)
.sort((a, b) => new Date(b.firstTime) - new Date(a.firstTime));
listEl.innerHTML = groupArray.map(g => {
const time = new Date(g.firstTime).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return `<li style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--surface)">
<div>
<div style="font-weight:600;color:var(--primary)">${g.date}</div>
<div style="margin-top:4px">上传 <span style="color:var(--success);font-weight:600">${g.count}</span> </div>
</div>
<div>
<span class="badge" style="background:var(--primary-light);color:var(--primary)">${g.platform}</span>
</div>
</li>`;
}).join('');
const totalGroups = groupArray.length;
const totalRecords = data.list.length;
API.toast(`显示 ${totalGroups} 次上传记录,共 ${totalRecords} 条数据`);
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
});
// 清空显示按钮
addListener(clearDisplayBtn, 'click', ()=>{
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = '<li>已清空显示</li>';
API.toast('已清空显示(历史记录仍保留)');
}
});
addListener(btn, 'click', async ()=>{
const file = fileEl.files[0];
if(!file){
@ -804,64 +1037,15 @@ const Upload = (() => {
if(result.ok){
API.toast('上传成功');
// 解析并显示成功上传的记录
// 解析成功上传的记录
const output = result.output || '';
const jsonMatch = output.match(/=== 成功导入的数据 ===\n([\s\S]*?)\n=== 数据输出结束 ===/);
if(jsonMatch && jsonMatch[1]){
try{
const records = JSON.parse(jsonMatch[1].trim());
if(records.length > 0){
// 保存到本地历史记录(仅用于本地显示)
saveToHistory(records);
// 重新从服务器加载数据并显示
const listEl = document.getElementById('mac-list');
if(listEl){
try {
const serverData = await API.listMac();
if(serverData.list && serverData.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
// 按时间和机种分组统计
const uploadGroups = {};
serverData.list.forEach(r => {
const date = new Date(r.ts).toLocaleDateString('zh-CN');
const platform = platformNames[r.platform] || r.platform || '未知';
const key = `${date}_${platform}`;
if (!uploadGroups[key]) {
uploadGroups[key] = {
date: date,
platform: platform,
count: 0,
firstTime: r.ts
};
}
uploadGroups[key].count++;
});
// 转换为数组并按时间排序
const groupArray = Object.values(uploadGroups)
.sort((a, b) => new Date(b.firstTime) - new Date(a.firstTime));
listEl.innerHTML = groupArray.map(g => {
return `<li style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--surface)">
<div>
<div style="font-weight:600;color:var(--primary)">${g.date}</div>
<div style="margin-top:4px">上传 <span style="color:var(--success);font-weight:600">${g.count}</span> </div>
</div>
<div>
<span class="badge" style="background:var(--primary-light);color:var(--primary)">${g.platform}</span>
</div>
</li>`;
}).join('');
API.toast(`成功上传 ${records.length} 条记录`);
}
} catch(e) {
console.error('刷新列表失败:', e);
}
}
API.toast(`成功上传 ${records.length} 条记录`);
}
}catch(e){
console.error('解析上传记录失败:', e);