This commit is contained in:
zzh 2025-12-30 14:35:09 +08:00
parent 512440a2c1
commit 8133faf65f
18 changed files with 1950 additions and 571 deletions

View File

@ -830,7 +830,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.filter-actions{display:flex;gap:8px;align-items:flex-end}
.filter-actions .btn{padding:10px 20px;font-size:14px}
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:20px}
.table-container{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:auto;margin-bottom:20px;max-height:calc(100vh - 280px)}
.data-table{width:100%;border-collapse:collapse;font-size:13px}
.data-table thead{background:var(--surface-2);position:sticky;top:0;z-index:1}
.data-table th{padding:12px 16px;text-align:left;font-weight:600;color:var(--text);border-bottom:2px solid var(--border);white-space:nowrap}
@ -949,8 +949,9 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow-x: auto;
overflow: auto;
margin-bottom: 20px;
max-height: calc(100vh - 250px);
}
.data-table {
@ -1555,7 +1556,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.topnav-item{position:relative;display:flex;align-items:center;gap:4px;padding:8px 14px;color:var(--text-2);text-decoration:none;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all 0.2s ease;white-space:nowrap}
.topnav-item:hover,.topnav-item.active{background:rgba(79,140,255,0.1);color:var(--text)}
.topnav-item.active{color:var(--primary);background:rgba(79,140,255,0.15)}
.topnav-caret{font-size:10px;transition:transform 0.2s ease}
.topnav-caret{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;font-size:14px;line-height:1;transition:transform 0.2s ease;opacity:.85}
.topnav-item.has-dropdown:hover .topnav-caret,.topnav-item.has-dropdown.open .topnav-caret{transform:rotate(180deg)}
.topnav-dropdown{position:absolute;top:100%;left:0;min-width:180px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);padding:6px 0;opacity:0;visibility:hidden;transform:translateY(8px);transition:all 0.2s ease;z-index:1000}
.topnav-item.has-dropdown:hover .topnav-dropdown,.topnav-item.has-dropdown.open .topnav-dropdown{opacity:1;visibility:visible;transform:translateY(0)}
@ -1571,7 +1572,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.topbar .user-avatar-btn:hover{background:rgba(79,140,255,0.1)}
.topbar .user-avatar-img{width:32px;height:32px;border-radius:50%;object-fit:cover}
.topbar .user-name-display{color:var(--text);font-size:14px;font-weight:500}
.topbar .user-dropdown{position:absolute;top:100%;right:0;margin-top:8px;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000}
.topbar .user-dropdown{position:absolute;top:100%;right:0;margin-top:8px;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000;max-height:calc(100vh - 80px);overflow-y:auto}
.topbar .user-menu-container{position:relative}
.topbar-icon-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;color:var(--text-2);text-decoration:none;transition:all 0.2s ease}
.topbar-icon-btn:hover{background:rgba(79,140,255,0.1);color:var(--text)}
@ -1603,3 +1604,178 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.spec-label{font-size:14px;color:var(--text-2)}
.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}}
[data-theme="light"] body{background:#f1f5f9}
#app.trackit-layout{margin:16px;height:calc(100vh - 32px);border-radius:40px;overflow:hidden;background:#ffffff;border:1px solid rgba(255,255,255,.4);box-shadow:0 24px 64px rgba(15,23,42,.14);display:flex;flex-direction:row}
#app.trackit-layout .content{background:rgba(248,250,252,.6);height:100%}
#app.trackit-layout .content.topbar-layout{height:100%;display:flex;flex-direction:column}
#app.trackit-layout .content.topbar-layout .view{padding:24px;height:100%;overflow:hidden}
#app.trackit-layout > #sidebar.sidebar{border-right:1px solid rgba(226,232,240,.9);background:rgba(255,255,255,.75);backdrop-filter:blur(10px);height:100%}
[data-theme="dark"] #app.trackit-layout > #sidebar.sidebar{background:linear-gradient(180deg,var(--surface),var(--surface-2));border-right:1px solid var(--border);backdrop-filter:none}
#app.trackit-layout > #sidebar.topbar{height:auto;padding:18px 16px;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:18px}
#app.trackit-layout > #sidebar .topbar-left{flex-direction:column;align-items:stretch;gap:16px;flex:1;min-height:0}
#app.trackit-layout > #sidebar .brand{width:100%;justify-content:space-between}
#app.trackit-layout > #sidebar .brand-name{font-size:18px;font-weight:800;color:var(--text)}
#app.trackit-layout > #sidebar .sidebar-search{position:relative;padding:0 4px}
#app.trackit-layout > #sidebar .sidebar-search .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:rgba(100,116,139,.8);display:inline-flex;align-items:center;justify-content:center}
#app.trackit-layout > #sidebar .sidebar-search .search-icon svg{width:16px;height:16px}
#app.trackit-layout > #sidebar .sidebar-search-input{width:100%;height:38px;border-radius:14px;border:1px solid transparent;background:rgba(248,250,252,1);padding:0 12px 0 40px;font-size:13px;color:var(--text);outline:none;transition:all .15s ease}
#app.trackit-layout > #sidebar .sidebar-search-input:focus{border-color:rgba(79,140,255,.25);background:#ffffff;box-shadow:0 10px 18px rgba(15,23,42,.08)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-search-input{background:rgba(15,23,42,.25);border-color:rgba(255,255,255,.06)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-search-input:focus{background:rgba(15,23,42,.35);border-color:rgba(79,140,255,.35)}
#app.trackit-layout > #sidebar .topnav{flex-direction:column;align-items:stretch;gap:2px}
#app.trackit-layout > #sidebar .topnav-item{width:100%;justify-content:space-between;border-radius:14px;padding:10px 5px}
#app.trackit-layout > #sidebar .topnav-item:hover,#app.trackit-layout > #sidebar .topnav-item.active{background:rgba(255,255,255,.85)}
[data-theme="dark"] #app.trackit-layout > #sidebar .topnav-item:hover,[data-theme="dark"] #app.trackit-layout > #sidebar .topnav-item.active{background:rgba(79,140,255,.12)}
#app.trackit-layout > #sidebar .topnav{flex:1;min-height:0;overflow:visible;padding-right:2px}
#app.trackit-layout > #sidebar .sidebar-footer{margin-top:auto;display:flex;flex-direction:column;gap:12px}
#app.trackit-layout > #sidebar .sidebar-actions{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:12px 6px 2px;border-top:1px solid rgba(226,232,240,.85)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-actions{border-top:1px solid rgba(255,255,255,.08)}
#app.trackit-layout > #sidebar .sidebar-actions .user-avatar-btn{padding:0;opacity:1}
#app.trackit-layout > #sidebar .sidebar-actions .user-avatar-img{width:36px;height:36px}
#app.trackit-layout > #sidebar .sidebar-actions .user-name-display{display:none}
#app.trackit-layout > #sidebar .sidebar-actions .user-dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:8px}
#app.trackit-layout > #sidebar .sidebar-notification-card{position:relative;background:rgba(255,255,255,.78);border:1px solid rgba(226,232,240,.9);border-radius:18px;padding:12px 12px 10px;box-shadow:0 10px 30px rgba(15,23,42,.06);height:220px;display:flex;flex-direction:column;cursor:default}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-notification-card{background:rgba(15,23,42,.25);border-color:rgba(255,255,255,.08);box-shadow:none}
#app.trackit-layout > #sidebar .sidebar-notification-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:10px;cursor:pointer}
#app.trackit-layout > #sidebar .sidebar-notification-title{font-size:12px;font-weight:800;color:var(--text);letter-spacing:.02em}
#app.trackit-layout > #sidebar .sidebar-notification-right{display:flex;align-items:center;gap:10px}
#app.trackit-layout > #sidebar .sidebar-notification-card .notification-badge{position:absolute;top:-8px;right:-8px;transform:none}
#app.trackit-layout > #sidebar .sidebar-notification-actions{display:flex;align-items:center;gap:8px}
#app.trackit-layout > #sidebar .sidebar-notification-actions .btn-text{padding:2px 6px;border-radius:10px;font-size:11px}
#app.trackit-layout > #sidebar .sidebar-notification-actions .btn-text{position:relative;z-index:1}
#app.trackit-layout > #sidebar .sidebar-notification-list{flex:1;min-height:0;max-height:150px;overflow-y:auto;overflow-x:hidden;padding-right:2px;-webkit-overflow-scrolling:touch;cursor:auto}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-item{padding:8px 10px;border-radius:14px;border:1px solid transparent;background:rgba(248,250,252,.85);margin-bottom:8px;cursor:pointer}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-notification-list .notification-item{background:rgba(2,6,23,.35)}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-item:last-child{margin-bottom:0}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-action,#app.trackit-layout > #sidebar .sidebar-notification-list .notification-detail{word-break:break-word}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-username{font-size:12px;font-weight:700;color:var(--text)}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-action{font-size:12px;color:var(--text);margin-top:4px}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-detail{font-size:11px;color:var(--text-2);margin-top:4px}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-time{font-size:11px;color:var(--text-2);margin-top:6px}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-item.unread{border-color:rgba(79,140,255,.22);border-left-width:3px;padding-left:18px}
#app.trackit-layout > #sidebar .sidebar-notification-list .notification-empty{color:var(--text-2);font-size:12px;padding:10px 6px}
#app.trackit-layout > #sidebar .nav-left{display:flex;align-items:center;gap:10px;min-width:0;flex:1}
#app.trackit-layout > #sidebar .nav-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;color:rgba(100,116,139,.85)}
#app.trackit-layout > #sidebar .nav-icon svg{width:20px;height:20px}
#app.trackit-layout > #sidebar .topnav-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#app.trackit-layout > #sidebar .topnav-caret{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;font-size:14px;line-height:1;transform-origin:center}
#app.trackit-layout > #sidebar .topnav-item.active .nav-icon{color:var(--primary)}
#app.trackit-layout > #sidebar .topnav-item:hover .nav-icon{color:var(--primary)}
#app.trackit-layout > #sidebar .topnav-dropdown{top:0;left:100%;margin-left:10px;transform:translateX(8px);border-radius:14px}
#app.trackit-layout > #sidebar .topnav-item.has-dropdown:hover .topnav-dropdown,#app.trackit-layout > #sidebar .topnav-item.has-dropdown.open .topnav-dropdown{transform:translateX(0)}
#app.trackit-layout > #sidebar.sidebar.collapsed .topnav-text{display:none}
#app.trackit-layout > #sidebar.sidebar.collapsed .topnav-caret{display:none}
#app.trackit-layout > #sidebar.sidebar.collapsed.topbar{padding:14px 6px}
#app.trackit-layout > #sidebar.sidebar.collapsed .topnav-item{justify-content:center;padding:10px 0;margin:4px 0}
#app.trackit-layout > #sidebar.sidebar.collapsed .nav-left{justify-content:center}
#app.trackit-layout > #sidebar.sidebar.collapsed .nav-icon{margin-left:0}
#app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-search{display:none}
#app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-footer{border-top:0;padding-top:0}
#app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-actions{flex-direction:column;gap:12px;justify-content:flex-start;padding:0 0 2px}
#app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-notification-card{display:none !important}
#app.trackit-layout > #sidebar.sidebar.collapsed .brand-name{display:none}
#app.trackit-layout > #sidebar.sidebar.collapsed .brand{position:relative;justify-content:center;flex-direction:column;align-items:center;gap:10px}
#app.trackit-layout > #sidebar.sidebar.collapsed .coin-wrapper{margin:0 auto}
#app.trackit-layout > #sidebar.sidebar.collapsed .sidebar-toggle{position:static;right:auto;top:auto;transform:none;margin-top:2px}
#app.trackit-layout .content-header.trackit-content-header{background:transparent !important;border-bottom:0;padding:18px 24px 8px}
#app.trackit-layout .trackit-header-left{display:flex;align-items:center;gap:12px}
#app.trackit-layout .trackit-header-badge{width:40px;height:40px;border-radius:14px;background:rgba(99,102,241,.15);display:flex;align-items:center;justify-content:center;color:#4f46e5;font-weight:800}
#app.trackit-layout .trackit-header-title{font-size:14px;font-weight:800;color:var(--text);line-height:1}
#app.trackit-layout .trackit-breadcrumb{font-size:11px;color:var(--text-2);margin-top:4px}
#app.trackit-layout .trackit-content-header .topbar-right{display:flex;align-items:center;gap:22px}
#app.trackit-layout .trackit-content-header .notification-bell{position:relative;background:none;border:none;cursor:pointer;font-size:34px;padding:2px;line-height:1}
#app.trackit-layout .trackit-content-header .notification-badge{top:-6px;right:-6px;padding:3px 7px;font-size:12px;min-width:20px}
#app.trackit-layout .trackit-content-header .user-avatar-btn{display:flex;align-items:center;gap:0;background:none;border:none;cursor:pointer;padding:0;border-radius:999px;transition:opacity 0.2s}
#app.trackit-layout .trackit-content-header .user-avatar-btn:hover{background:transparent;opacity:.92}
#app.trackit-layout .trackit-content-header .user-avatar-img{width:40px;height:40px;border-radius:999px}
#app.trackit-layout .trackit-content-header .user-name-display{display:none}
/* Theme Switch (Uiverse) */
#app.trackit-layout .switch{
font-size:13px;
position:relative;
display:inline-block;
width:4em;
height:2.2em;
border-radius:30px;
box-shadow:0 0 10px rgba(0,0,0,0.1);
}
#app.trackit-layout .switch input{opacity:0;width:0;height:0}
#app.trackit-layout .switch .slider{
position:absolute;
cursor:pointer;
top:0;left:0;right:0;bottom:0;
background-color:#00a6ff;
transition:0.4s;
border-radius:30px;
overflow:hidden;
}
#app.trackit-layout .switch .slider:before{
position:absolute;
content:"";
height:1.2em;
width:1.2em;
border-radius:20px;
left:0.5em;
bottom:0.5em;
transition:0.4s;
transition-timing-function:cubic-bezier(0.81,-0.04,0.38,1.5);
transform:translateX(1.8em);
box-shadow:inset 15px -4px 0px 15px #ffcf48;
}
#app.trackit-layout .switch input:checked + .slider{background-color:#2a2a2a}
#app.trackit-layout .switch input:checked + .slider:before{
transform:translateX(0);
box-shadow:inset 8px -4px 0px 0px #fff;
}
#app.trackit-layout .switch .star{
background-color:#fff;
border-radius:50%;
position:absolute;
width:5px;
height:5px;
transition:all 0.4s;
opacity:0;
}
#app.trackit-layout .switch .star_1{left:2.5em;top:0.5em}
#app.trackit-layout .switch .star_2{left:2.2em;top:1.2em}
#app.trackit-layout .switch .star_3{left:3em;top:0.9em}
#app.trackit-layout .switch input:checked ~ .slider .star{opacity:1}
#app.trackit-layout .switch .cloud{
width:3.2em;
position:absolute;
bottom:-1.4em;
left:-1.1em;
opacity:1;
transition:all 0.4s;
}
#app.trackit-layout .switch input:checked ~ .slider .cloud{opacity:0}
.notification-modal{position:fixed;inset:0;z-index:2000;display:none}
.notification-modal .notification-modal-backdrop{position:absolute;inset:0;background:rgba(2,6,23,.38);backdrop-filter:blur(6px)}
.notification-modal .notification-modal-content{position:relative;width:min(720px,calc(100vw - 32px));max-height:min(80vh,680px);margin:10vh auto 0;background:rgba(255,255,255,.92);border:1px solid rgba(226,232,240,.95);border-radius:22px;box-shadow:0 30px 90px rgba(15,23,42,.25);overflow:hidden;display:flex;flex-direction:column}
[data-theme="dark"] .notification-modal .notification-modal-content{background:rgba(15,23,42,.88);border-color:rgba(255,255,255,.12);box-shadow:none}
.notification-modal .notification-modal-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid rgba(226,232,240,.9)}
[data-theme="dark"] .notification-modal .notification-modal-header{border-bottom:1px solid rgba(255,255,255,.10)}
.notification-modal .notification-modal-title{font-size:14px;font-weight:800;color:var(--text)}
.notification-modal .notification-modal-close{background:none;border:0;cursor:pointer;font-size:22px;line-height:1;color:var(--text);width:34px;height:34px;border-radius:10px}
.notification-modal .notification-modal-close:hover{background:rgba(79,140,255,.12)}
.notification-modal .notification-modal-list{padding:14px 16px;overflow:auto}
.notification-modal .notification-modal-list .notification-item{padding:12px 12px;border-radius:16px;border:1px solid transparent;background:rgba(248,250,252,.9);margin-bottom:10px;cursor:pointer}
[data-theme="dark"] .notification-modal .notification-modal-list .notification-item{background:rgba(2,6,23,.35)}
.notification-modal .notification-modal-list .notification-item.unread{border-color:rgba(79,140,255,.22)}

View File

@ -12,9 +12,9 @@
<link rel="stylesheet" href="./assets/mod.css" />
</head>
<body>
<div id="app">
<div id="app" class="trackit-layout">
<!-- 顶部导航栏 -->
<header class="topbar" id="topbar">
<header class="topbar sidebar" id="sidebar">
<div class="topbar-left">
<div class="brand">
<div class="coin-wrapper">
@ -34,11 +34,46 @@
</div>
</div>
<span class="brand-name">韬智生产管理</span>
<button id="sidebar-toggle" class="sidebar-toggle" type="button" aria-label="切换侧边栏">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
</div>
<div class="sidebar-search">
<span class="search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" />
</svg>
</span>
<input class="sidebar-search-input" type="text" placeholder="搜索菜单 (Ctrl+K)" autocomplete="off" />
</div>
<nav class="topnav" id="topnav">
<a href="#/dashboard" class="topnav-item" data-route="dashboard">仪表盘</a>
<a href="#/dashboard" class="topnav-item" data-route="dashboard">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="2" />
<rect x="14" y="3" width="7" height="7" rx="2" />
<rect x="14" y="14" width="7" height="7" rx="2" />
<rect x="3" y="14" width="7" height="7" rx="2" />
</svg>
</span>
<span class="topnav-text">仪表盘</span>
</span>
</a>
<div class="topnav-item has-dropdown" data-menu="upload">
<span class="topnav-text">上传</span>
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 16.5a4.5 4.5 0 0 0-1.8-8.7A6 6 0 0 0 6 9a4 4 0 0 0 0 8h14" />
<path d="M12 12v9" />
<path d="M8 16l4-4 4 4" />
</svg>
</span>
<span class="topnav-text">上传</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/upload/mac" class="dropdown-item" data-route="upload-mac">MAC与批次</a>
@ -49,7 +84,15 @@
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="query">
<span class="topnav-text">出货查询</span>
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" />
</svg>
</span>
<span class="topnav-text">出货查询</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/shipments/query" class="dropdown-item" data-route="shipments-query">详细记录查询</a>
@ -57,14 +100,32 @@
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="production">
<span class="topnav-text">生产管理</span>
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21V9l6 3V9l6 3V9l6 3v9" />
<path d="M3 21h18" />
</svg>
</span>
<span class="topnav-text">生产管理</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/production-mgmt/work-order" class="dropdown-item" data-route="production-mgmt-work-order">生产工单下发中心</a>
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="plan">
<span class="topnav-text">计划管理</span>
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" />
<path d="M16 2v4" />
<path d="M8 2v4" />
<path d="M3 10h18" />
</svg>
</span>
<span class="topnav-text">计划管理</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/plan-mgmt/bom" class="dropdown-item" data-route="plan-mgmt-bom">BOM物料清单</a>
@ -75,7 +136,16 @@
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="collect">
<span class="topnav-text">采集</span>
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 19V5" />
<path d="M4 15c2-2 4-3 8-3s6-1 8-3" />
<path d="M20 19V5" />
</svg>
</span>
<span class="topnav-text">采集</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/devices" class="dropdown-item" data-route="devices">设备状态</a>
@ -85,63 +155,90 @@
<a href="#/production" class="dropdown-item" data-route="production">时间记录</a>
</div>
</div>
<a href="#/export" class="topnav-item" data-route="export">导出</a>
<a href="#/product-intro" class="topnav-item" data-route="product-intro">产品</a>
<a href="#/system/operations-log" class="topnav-item superadmin-only" data-route="system-operations-log" style="display:none;">操作日志</a>
<a href="#/export" class="topnav-item" data-route="export">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v10" />
<path d="M8 9l4 4 4-4" />
<path d="M4 17v3h16v-3" />
</svg>
</span>
<span class="topnav-text">导出</span>
</span>
</a>
<a href="#/product-intro" class="topnav-item" data-route="product-intro">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l7-4a2 2 0 0 0 1-1.7z" />
<path d="M3.3 7.5L12 12l8.7-4.5" />
<path d="M12 22V12" />
</svg>
</span>
<span class="topnav-text">产品</span>
</span>
</a>
<a href="#/system/operations-log" class="topnav-item superadmin-only" data-route="system-operations-log" style="display:none;">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
<path d="M16 13H8" />
<path d="M16 17H8" />
</svg>
</span>
<span class="topnav-text">操作日志</span>
</span>
</a>
</nav>
</div>
<div class="topbar-right">
<label class="bb8-toggle">
<input id="theme-toggle-checkbox" class="bb8-toggle__checkbox" type="checkbox">
<div class="bb8-toggle__container">
<div class="bb8-toggle__scenery">
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="bb8-toggle__star"></div>
<div class="tatto-1"></div>
<div class="tatto-2"></div>
<div class="gomrassen"></div>
<div class="hermes"></div>
<div class="chenini"></div>
<div class="bb8-toggle__cloud"></div>
<div class="bb8-toggle__cloud"></div>
<div class="bb8-toggle__cloud"></div>
</div>
<div class="bb8">
<div class="bb8__head-container">
<div class="bb8__antenna"></div>
<div class="bb8__antenna"></div>
<div class="bb8__head"></div>
<div class="sidebar-footer">
<div id="sidebar-notification-card" class="sidebar-notification-card" style="display:none;">
<span id="notification-badge" class="notification-badge" style="display:none;">0</span>
<div class="sidebar-notification-header">
<div class="sidebar-notification-title"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg> </div>
<div class="sidebar-notification-actions">
<button id="mark-all-read" class="btn-text" type="button">全部已读</button>
<button id="delete-read" class="btn-text btn-danger" type="button">删除已读</button>
</div>
<div class="bb8__body"></div>
</div>
<div class="artificial__hidden">
<div class="bb8__shadow"></div>
<div id="notification-list" class="sidebar-notification-list">
<div class="notification-empty">暂无消息通知</div>
</div>
</div>
</label>
<button id="notification-bell" class="notification-bell" style="display:none;">
<span class="bell-icon">🔔</span>
<span id="notification-badge" class="notification-badge" style="display:none;">0</span>
</button>
<div class="user-menu-container">
<button id="user-avatar-btn" class="user-avatar-btn">
<img id="user-avatar-img" src="./assets/user-avatar.svg" alt="用户头像" class="user-avatar-img" />
<span id="user-name-display" class="user-name-display">未登录</span>
</button>
<div id="user-dropdown" class="user-dropdown" style="display:none;">
<a href="#/settings" class="user-dropdown-item" data-route="settings">
<span class="dropdown-icon">⚙️</span>
<span>设置</span>
</a>
<div class="user-dropdown-item" id="user-dropdown-logout">
<span class="dropdown-icon">🚪</span>
<span>退出登录</span>
<div class="sidebar-actions">
<label class="switch">
<input id="theme-toggle-checkbox" type="checkbox" />
<span class="slider">
<div class="star star_1"></div>
<div class="star star_2"></div>
<div class="star star_3"></div>
<svg viewBox="0 0 16 16" class="cloud_1 cloud">
<path
transform="matrix(.77976 0 0 .78395-299.99-418.63)"
fill="#fff"
d="m391.84 540.91c-.421-.329-.949-.524-1.523-.524-1.351 0-2.451 1.084-2.485 2.435-1.395.526-2.388 1.88-2.388 3.466 0 1.874 1.385 3.423 3.182 3.667v.034h12.73v-.006c1.775-.104 3.182-1.584 3.182-3.395 0-1.747-1.309-3.186-2.994-3.379.007-.106.011-.214.011-.322 0-2.707-2.271-4.901-5.072-4.901-2.073 0-3.856 1.202-4.643 2.925"
></path>
</svg>
</span>
</label>
<div class="user-menu-container">
<button id="user-avatar-btn" class="user-avatar-btn">
<img id="user-avatar-img" src="./assets/user-avatar.svg" alt="用户头像" class="user-avatar-img" />
<span id="user-name-display" class="user-name-display">未登录</span>
</button>
<div id="user-dropdown" class="user-dropdown" style="display:none;">
<a href="#/settings" class="user-dropdown-item" data-route="settings">
<span class="dropdown-icon">⚙️</span>
<span>设置</span>
</a>
<div class="user-dropdown-item" id="user-dropdown-logout">
<span class="dropdown-icon">🚪</span>
<span>退出登录</span>
</div>
</div>
</div>
</div>
</div>
@ -150,20 +247,6 @@
<main class="content topbar-layout">
<section id="view" class="view"></section>
</main>
<!-- 通知面板 -->
<div id="notification-panel" class="notification-panel" style="display:none;">
<div class="notification-header">
<h3>消息通知</h3>
<div class="notification-actions">
<button id="mark-all-read" class="btn-text">全部已读</button>
<button id="delete-read" class="btn-text btn-danger">删除已读</button>
</div>
</div>
<div id="notification-list" class="notification-list">
<!-- 通知列表将在这里动态生成 -->
</div>
</div>
</div>
<div id="toast" class="toast"></div>
<div id="overlay" class="overlay hidden">
@ -176,10 +259,23 @@
<div></div>
</div>
</div>
<div id="notification-modal" class="notification-modal" style="display:none;">
<div class="notification-modal-backdrop" data-close="1"></div>
<div class="notification-modal-content" role="dialog" aria-modal="true" aria-label="🔔">
<div class="notification-modal-header">
<div class="notification-modal-title">🔔</div>
<button id="notification-modal-close" class="notification-modal-close" type="button" aria-label="关闭">×</button>
</div>
<div id="notification-modal-list" class="notification-modal-list"></div>
</div>
</div>
<script src="./js/router.js"></script>
<script src="./js/api.js"></script>
<script src="./js/utils/memory-monitor.js"></script>
<script src="./js/components/sidebar.js"></script>
<script src="./js/components/menu-search.js"></script>
<script src="./js/components/dashboard.js"></script>
<script src="./js/components/upload.js"></script>
<script src="./js/components/shipments.js"></script>

View File

@ -17,7 +17,9 @@
// 加载用户头像(如果有设置)
const avatarImg = document.getElementById('user-avatar-img');
if (avatarImg && user && user.avatar) {
avatarImg.src = user.avatar;
// 添加时间戳参数防止缓存
const timestamp = user.avatar.includes('?') ? '' : `?t=${Date.now()}`;
avatarImg.src = user.avatar + timestamp;
} else if (avatarImg) {
avatarImg.src = './assets/user-avatar.svg';
}

View File

@ -3,17 +3,99 @@
Router.register('/plan-mgmt/customer-order', async () => {
// 先返回 HTML
const html = `
<div class="page-header">
<h1>客户订单管理</h1>
<div class="page-actions">
<button id="add-order-btn" class="btn btn-primary">新增订单</button>
</div>
</div>
<style>
/* 完全重新设计页面布局 */
.view:has(#customer-order-page) {
padding: 0 !important;
display: flex;
flex-direction: column;
height: 100vh;
}
#customer-order-page {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
}
#customer-order-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#customer-order-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#customer-order-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#customer-order-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#customer-order-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#customer-order-page th {
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
font-size: 14px;
}
#customer-order-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#customer-order-page tbody tr:last-child td {
border-bottom: none;
}
#customer-order-page tbody tr:hover {
background: var(--hover);
}
#customer-order-page .text-center {
text-align: center;
color: var(--text-2);
}
</style>
<div class="card">
<div class="card-body">
<div class="table-container">
<table class="data-table">
<div id="customer-order-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">客户订单管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-order-btn" class="btn btn-primary">新增订单</button>
</div>
</div>
<div class="content-area">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>下单时间</th>

View File

@ -43,7 +43,7 @@ const Dashboard = (() => {
const platformName = isPdd ? '拼多多' : '圆通';
const platformIcon = isPdd ? '<img src="assets/pdd.svg" style="width:20px;height:20px" />' : '<img src="assets/yt.svg" style="width:20px;height:20px" />';
const value = isPdd ? todayPdd : todayYt;
const color = isPdd ? { bg: '#f6af3dff', text: '#f6af3dff' } : { bg: '#1a6dd6', text: '#1a6dd6' };
const color = isPdd ? { bg: '#3B82F6', text: '#3B82F6' } : { bg: '#10B981', text: '#10B981' };
const fillWidth = Math.min((value / 10000) * 100, 100);
return `<div class="metrics-card" id="today-production-card" data-platform="${activePlatform}">
@ -102,6 +102,160 @@ const Dashboard = (() => {
</div>`;
}
// 显示审计数据模态框
const showAuditModal = async () => {
try {
// 创建加载中的模态框
const modal = document.createElement('div');
modal.id = 'audit-data-modal';
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn 0.2s';
modal.innerHTML = `
<div style="background:var(--surface);border-radius:16px;width:90%;max-width:900px;max-height:85vh;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.3);animation:slideUp 0.3s">
<div style="padding:20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<h3 style="margin:0;font-size:18px;font-weight:600;display:flex;align-items:center;gap:8px">📊 审计数据明细</h3>
<button id="close-audit-modal" style="background:none;border:none;font-size:24px;cursor:pointer;color:var(--text-secondary);line-height:1">&times;</button>
</div>
<div style="padding:20px;overflow-y:auto;max-height:calc(85vh - 80px)">
<div style="text-align:center;padding:40px 0">
<div style="display:inline-block;width:24px;height:24px;border:3px solid var(--primary);border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite"></div>
<div style="margin-top:12px;color:var(--text-2)">正在加载审计数据...</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// 绑定关闭事件
document.getElementById('close-audit-modal').onclick = () => modal.remove();
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
document.addEventListener('keydown', function escHandler(e) {
if(e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', escHandler); }
});
// 加载全部审计数据
const [pddRes, ytRes] = await Promise.all([
API.auditPddQuiet().catch(() => ({list: []})),
API.auditYtQuiet().catch(() => ({list: []}))
]);
const pddData = pddRes.list || [];
const ytData = ytRes.list || [];
// 生成数据表格HTML
const generateTable = (data, platform, icon, displayCount) => {
if(data.length === 0) {
return `
<div style="margin-bottom:24px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;font-weight:600;font-size:16px">
${icon} ${platform}
</div>
<div style="text-align:center;padding:20px;color:var(--text-2);background:var(--bg);border-radius:8px;border:1px solid var(--border)">
暂无数据
</div>
</div>
`;
}
const actualCount = displayCount === 'all' ? data.length : Math.min(displayCount, data.length);
const rows = data.slice(0, actualCount).map(r => `
<tr>
<td style="padding:8px 12px;border-bottom:1px solid var(--border)">${r.ts_cn || '—'}</td>
<td style="padding:8px 12px;border-bottom:1px solid var(--border)">${r.batch || '—'}</td>
<td style="padding:8px 12px;border-bottom:1px solid var(--border)">${r.mac || '—'}</td>
<td style="padding:8px 12px;border-bottom:1px solid var(--border)">${r.note || '—'}</td>
</tr>
`).join('');
const displayText = displayCount === 'all' ? '全部' : `${actualCount}`;
return `
<div style="margin-bottom:24px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;font-weight:600;font-size:16px">
${icon} ${platform} <span style="font-size:12px;color:var(--text-2);font-weight:400">(显示${displayText}共${data.length})</span>
</div>
<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead>
<tr style="background:var(--bg)">
<th style="padding:10px 12px;text-align:left;font-weight:600">时间</th>
<th style="padding:10px 12px;text-align:left;font-weight:600">批次</th>
<th style="padding:10px 12px;text-align:left;font-weight:600">MAC地址</th>
<th style="padding:10px 12px;text-align:left;font-weight:600">备注</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>
`;
};
// 更新模态框内容
const updateModalContent = (displayCount) => {
const contentEl = modal.querySelector('div[style*="max-height:calc(85vh - 80px)"]');
contentEl.innerHTML = `
<div style="margin-bottom:20px;display:flex;justify-content:center;align-items:center;gap:12px">
<span style="font-size:14px;color:var(--text)">显示数量</span>
<select id="audit-display-count" style="padding:6px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;cursor:pointer">
<option value="50" ${displayCount === 50 ? 'selected' : ''}>50</option>
<option value="100" ${displayCount === 100 ? 'selected' : ''}>100</option>
<option value="200" ${displayCount === 200 ? 'selected' : ''}>200</option>
<option value="all" ${displayCount === 'all' ? 'selected' : ''}>全部</option>
</select>
</div>
<div style="margin-bottom:20px">
<div style="display:flex;gap:24px;justify-content:center">
<div style="text-align:center">
<div style="font-size:24px;font-weight:700;color:#3B82F6">${pddData.length.toLocaleString()}</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px">拼多多记录</div>
</div>
<div style="text-align:center">
<div style="font-size:24px;font-weight:700;color:#10B981">${ytData.length.toLocaleString()}</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px">圆通记录</div>
</div>
<div style="text-align:center">
<div style="font-size:24px;font-weight:700;color:var(--primary)">${(pddData.length + ytData.length).toLocaleString()}</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px">总记录数</div>
</div>
</div>
</div>
<div>
${generateTable(pddData, '拼多多', '<img src="assets/pdd.svg" style="width:16px;height:16px" />', displayCount)}
${generateTable(ytData, '圆通', '<img src="assets/yt.svg" style="width:16px;height:16px" />', displayCount)}
</div>
`;
// 绑定选择器事件
const selector = document.getElementById('audit-display-count');
if(selector) {
selector.onchange = (e) => {
const newCount = e.target.value === 'all' ? 'all' : parseInt(e.target.value);
updateModalContent(newCount);
};
}
};
// 默认显示100条
updateModalContent(100);
} catch(e) {
console.error('显示审计数据失败:', e);
const modal = document.getElementById('audit-data-modal');
if(modal) {
const contentEl = modal.querySelector('div[style*="max-height:calc(85vh - 80px)"]');
if(contentEl) {
contentEl.innerHTML = `
<div style="text-align:center;padding:40px 0">
<div style="font-size:48px;margin-bottom:16px">😞</div>
<div style="font-size:16px;color:var(--text);margin-bottom:8px">加载失败</div>
<div style="font-size:14px;color:var(--text-2)">请稍后重试</div>
</div>
`;
}
}
}
};
// 清理函数
const cleanup = () => {
if(window.__auditTimer){
@ -171,8 +325,10 @@ const Dashboard = (() => {
const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' };
const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] };
const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] };
const pddList = (pdd.list||[]).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/pdd.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
const ytList = (yt.list||[]).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/yt.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
// 优化:默认只显示少量数据,完整数据在点击模态框时加载
const pddList = (pdd.list||[]).slice(0, 10).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/pdd.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
const ytList = (yt.list||[]).slice(0, 10).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/yt.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
setTimeout(()=>{
// 清理旧的定时器和事件
if(window.__auditTimer){
@ -261,11 +417,11 @@ const Dashboard = (() => {
text: isLight ? '#94a3b8' : '#64748b',
textLabel: isLight ? '#1e293b' : '#e5e7eb',
// 渐变色(参考图样式:紫色和橙色)
pddGradient: ['#fbbf24', '#f59e0b', '#ea580c'], // 黄-橙
ytGradient: ['#a78bfa', '#8b5cf6', '#7c3aed'], // 紫
pddGradient: ['#93c5fd', '#3B82F6', '#1d4ed8'],
ytGradient: ['#6ee7b7', '#10B981', '#059669'], // 翠绿
// 填充区域渐变(从上到下透明度递减)
pddFill: isLight ? ['rgba(251,191,36,0.3)', 'rgba(251,191,36,0.05)'] : ['rgba(251,191,36,0.25)', 'rgba(251,191,36,0.02)'],
ytFill: isLight ? ['rgba(139,92,246,0.3)', 'rgba(139,92,246,0.05)'] : ['rgba(139,92,246,0.25)', 'rgba(139,92,246,0.02)']
pddFill: isLight ? ['rgba(59,130,246,0.28)', 'rgba(59,130,246,0.05)'] : ['rgba(59,130,246,0.22)', 'rgba(59,130,246,0.02)'],
ytFill: isLight ? ['rgba(16,185,129,0.3)', 'rgba(16,185,129,0.05)'] : ['rgba(16,185,129,0.25)', 'rgba(16,185,129,0.02)']
};
};
@ -622,7 +778,7 @@ const Dashboard = (() => {
let label = point;
if(timeRange === 'week') label = point + ' 周';
else if(timeRange === 'month') label = point + ' 月';
tooltip.innerHTML = `<div style="margin-bottom:2px">${label}</div><div style="color:#f59e0b">拼多多: ${pddCount}</div><div style="color:#3b82f6">圆通: ${ytCount}</div>`;
tooltip.innerHTML = `<div style="margin-bottom:2px">${label}</div><div style="color:#3B82F6">拼多多: ${pddCount}</div><div style="color:#10B981">圆通: ${ytCount}</div>`;
tooltip.style.display = 'block';
// 获取提示框宽度以便放在鼠标左侧
@ -769,8 +925,9 @@ const Dashboard = (() => {
const centerX = size / 2;
const centerY = size / 2;
const radius = 65;
const lineWidth = 18;
const outerRadius = 50;
const innerRadius = 35;
const lineWidth = outerRadius - innerRadius;
// 清空画布
ctx.clearRect(0, 0, size, size);
@ -778,44 +935,77 @@ const Dashboard = (() => {
if(total === 0) {
// 无数据时显示灰色圆环
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = lineWidth;
ctx.stroke();
// 中心显示0
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('0', centerX, centerY - 3);
ctx.font = '10px sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.fillText('总产量', centerX, centerY + 10);
return;
}
// 绘制圆通部分(紫色)
const ytAngle = (ytCount / total) * Math.PI * 2;
// 计算角度和间隙
const paddingAngle = 5 * (Math.PI / 180); // 5度转弧度
const totalPadding = paddingAngle * 2; // 总间隙角度
const availableAngle = Math.PI * 2 - totalPadding;
const ytAngle = total > 0 ? (ytCount / total) * availableAngle : 0;
const pddAngle = total > 0 ? (pddCount / total) * availableAngle : 0;
const startAngle = -Math.PI / 2 + paddingAngle;
// 绘制背景圆环(浅灰色)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + ytAngle);
ctx.strokeStyle = '#8b5cf6';
ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
ctx.strokeStyle = '#f3f4f6';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
// 绘制拼多多部分(橙色)
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2 + ytAngle, -Math.PI / 2 + Math.PI * 2);
ctx.strokeStyle = '#f6af3dff';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
// 绘制圆通部分(绿色)
if(ytCount > 0 && ytAngle > 0.01) {
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius, startAngle, startAngle + ytAngle);
ctx.strokeStyle = '#10B981';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'butt';
ctx.stroke();
}
// 中心显示总数
ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb';
ctx.font = 'bold 24px sans-serif';
// 绘制拼多多部分(蓝色)
if(pddCount > 0 && pddAngle > 0.01) {
ctx.beginPath();
const pddStart = startAngle + ytAngle + paddingAngle;
const pddEnd = pddStart + pddAngle;
ctx.arc(centerX, centerY, outerRadius, pddStart, pddEnd);
ctx.strokeStyle = '#3B82F6';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'butt';
ctx.stroke();
}
// 中心显示总数(优化位置和大小)
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(total.toLocaleString(), centerX, centerY - 8);
ctx.font = '11px sans-serif';
ctx.fillText(total.toLocaleString(), centerX, centerY - 3);
ctx.font = '10px sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.fillText('本月总产量', centerX, centerY + 14);
ctx.fillText('总产量', centerX, centerY + 10);
// 保存数据供鼠标事件使用
canvas.__donutData = {
pddCount, ytCount, total, centerX, centerY, radius, lineWidth,
ytAngle, pddAngle: Math.PI * 2 - ytAngle
pddCount, ytCount, total, centerX, centerY,
outerRadius, innerRadius, lineWidth,
ytAngle, pddAngle, startAngle, paddingAngle
};
};
@ -852,9 +1042,9 @@ const Dashboard = (() => {
const platformColor = (k, fallbackIndex) => {
const fixed = {
pdd: '#f6af3dff',
tx: '#3b82f6',
yt: '#8b5cf6'
pdd: '#3B82F6',
yt: '#10B981',
tx: '#F97316'
};
if(fixed[k]) return fixed[k];
const palette = ['#10b981', '#06b6d4', '#f97316', '#a3e635', '#ef4444', '#f59e0b'];
@ -869,47 +1059,74 @@ const Dashboard = (() => {
const centerX = size / 2;
const centerY = size / 2;
const radius = 65;
const lineWidth = 18;
const outerRadius = 50;
const innerRadius = 35;
const lineWidth = outerRadius - innerRadius;
ctx.clearRect(0, 0, size, size);
if(total === 0 || segments.length === 0) {
// 绘制灰色圆环
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
ctx.strokeStyle = '#e5e7eb';
ctx.lineWidth = lineWidth;
ctx.stroke();
// 中心显示0
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('0', centerX, centerY - 3);
ctx.font = '10px sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.fillText('总发货', centerX, centerY + 10);
if(legendEl) legendEl.innerHTML = '';
return;
}
let acc = 0;
segments.forEach(seg => {
const portion = (seg.count / total) * Math.PI * 2;
const start = acc;
const end = acc + portion;
seg.start = start;
seg.end = end;
// 计算角度和间隙
const paddingAngle = 5 * (Math.PI / 180); // 5度转弧度
const segmentCount = segments.length;
const totalPadding = paddingAngle * segmentCount;
const availableAngle = Math.PI * 2 - totalPadding;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2 + start, -Math.PI / 2 + end);
ctx.strokeStyle = seg.color;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
let currentAngle = -Math.PI / 2; // 从12点钟方向开始
segments.forEach((seg, i) => {
const portion = total > 0 ? (seg.count / total) * availableAngle : 0;
const start = currentAngle;
const end = start + portion;
// 保存原始角度信息
seg.originalStart = start;
seg.originalEnd = end;
// 只绘制有意义的角度
if(portion > 0.01) {
ctx.beginPath();
ctx.arc(centerX, centerY, outerRadius, start, end);
ctx.strokeStyle = seg.color;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'butt';
ctx.stroke();
}
acc = end;
// 下一个扇段的起始位置 = 当前扇段结束位置 + 间隙
currentAngle = end + paddingAngle;
});
ctx.fillStyle = document.documentElement.getAttribute('data-theme') === 'light' ? '#1e293b' : '#e5e7eb';
ctx.font = 'bold 24px sans-serif';
// 中心显示总数(优化位置和大小)
ctx.fillStyle = '#1e293b';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(total.toLocaleString(), centerX, centerY - 8);
ctx.font = '11px sans-serif';
ctx.fillText(total.toLocaleString(), centerX, centerY - 3);
ctx.font = '10px sans-serif';
ctx.fillStyle = '#94a3b8';
ctx.fillText('发货总量', centerX, centerY + 14);
ctx.fillText('发货', centerX, centerY + 10);
if(legendEl) {
legendEl.innerHTML = segments.map(seg => {
@ -918,7 +1135,7 @@ const Dashboard = (() => {
}).join('');
}
canvas.__shipmentDonutData = { total, centerX, centerY, radius, lineWidth, segments };
canvas.__shipmentDonutData = { total, centerX, centerY, outerRadius, innerRadius, lineWidth, segments };
};
const refreshShipmentPlatformStats = async(force=false) => {
@ -986,12 +1203,12 @@ const Dashboard = (() => {
// 圆通区域
platform = '圆通';
count = data.ytCount;
color = '#8b5cf6';
color = '#10B981';
} else {
// 拼多多区域
platform = '拼多多';
count = data.pddCount;
color = '#f6af3dff';
color = '#3B82F6';
}
const pct = ((count / data.total) * 100).toFixed(1);
@ -1065,18 +1282,35 @@ const Dashboard = (() => {
const relativeY = e.clientY - rect.top - data.centerY;
const distance = Math.sqrt(relativeX * relativeX + relativeY * relativeY);
const innerRadius = data.radius - data.lineWidth / 2;
const outerRadius = data.radius + data.lineWidth / 2;
if(distance < innerRadius || distance > outerRadius) {
// 使用新的内外半径
if(distance < data.innerRadius || distance > data.outerRadius) {
shipmentTooltip.style.display = 'none';
return;
}
let angle = Math.atan2(relativeY, relativeX);
if(angle < 0) angle += Math.PI * 2;
angle = (angle + Math.PI / 2) % (Math.PI * 2);
const seg = (data.segments || []).find(s => angle >= s.start && angle <= s.end);
// 查找对应的扇段
const seg = (data.segments || []).find(s => {
// 将扇段角度转换到 0-2π 范围
let segStart = s.originalStart;
let segEnd = s.originalEnd;
// 如果角度是负数,转换到 0-2π 范围
if(segStart < 0) segStart += Math.PI * 2;
if(segEnd < 0) segEnd += Math.PI * 2;
// 处理跨越0度的情况
if(segStart > segEnd) {
// 扇段跨越0度例如从350度到10度
return angle >= segStart || angle <= segEnd;
} else {
// 正常情况
return angle >= segStart && angle <= segEnd;
}
});
if(!seg) {
shipmentTooltip.style.display = 'none';
return;
@ -1091,27 +1325,33 @@ const Dashboard = (() => {
const tooltipWidth = shipmentTooltip.offsetWidth;
const tooltipHeight = shipmentTooltip.offsetHeight;
shipmentTooltip.style.visibility = 'visible';
// 智能定位
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const mousePageX = e.clientX;
const mousePageY = e.clientY;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let tooltipPageX, tooltipPageY;
if(mousePageX + 15 + tooltipWidth <= viewportWidth - 10) {
tooltipPageX = mousePageX + 15;
} else {
tooltipPageX = mousePageX - tooltipWidth - 15;
if(tooltipPageX < 10) tooltipPageX = 10;
}
tooltipPageY = mousePageY - 10;
if(tooltipPageY < 10) tooltipPageY = mousePageY + 15;
if(tooltipPageY + tooltipHeight > viewportHeight - 10) tooltipPageY = viewportHeight - tooltipHeight - 10;
const containerRect = shipmentDonutCanvas.parentElement.getBoundingClientRect();
const left = tooltipPageX - containerRect.left;
const top = tooltipPageY - containerRect.top;
shipmentTooltip.style.left = left + 'px';
shipmentTooltip.style.top = top + 'px';
};
@ -1174,10 +1414,10 @@ const Dashboard = (() => {
let rows = allDates.map(date => {
const pdd = pddMap[date] || 0;
const yt = ytMap[date] || 0;
return `<tr><td style="padding:8px 12px;border-bottom:1px solid var(--border)">${date.slice(5)}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;color:#f59e0b">${pdd}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;color:#8b5cf6">${yt}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;font-weight:600">${pdd+yt}</td></tr>`;
return `<tr><td style="padding:8px 12px;border-bottom:1px solid var(--border)">${date.slice(5)}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;color:#3B82F6">${pdd}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;color:#10B981">${yt}</td><td style="padding:8px 12px;border-bottom:1px solid var(--border);text-align:right;font-weight:600">${pdd+yt}</td></tr>`;
}).join('');
return `<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="background:var(--surface)"><th style="padding:8px 12px;text-align:left;font-weight:600">日期</th><th style="padding:8px 12px;text-align:right;font-weight:600;color:#f59e0b">拼多多</th><th style="padding:8px 12px;text-align:right;font-weight:600;color:#8b5cf6">圆通</th><th style="padding:8px 12px;text-align:right;font-weight:600">合计</th></tr></thead><tbody>${rows}</tbody></table>`;
return `<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="background:var(--surface)"><th style="padding:8px 12px;text-align:left;font-weight:600">日期</th><th style="padding:8px 12px;text-align:right;font-weight:600;color:#3B82F6">拼多多</th><th style="padding:8px 12px;text-align:right;font-weight:600;color:#10B981">圆通</th><th style="padding:8px 12px;text-align:right;font-weight:600">合计</th></tr></thead><tbody>${rows}</tbody></table>`;
};
// 创建模态框
@ -1207,17 +1447,17 @@ const Dashboard = (() => {
<!-- 平台对比 -->
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:20px">
<div style="background:var(--surface);padding:12px;border-radius:8px;display:flex;align-items:center;gap:12px">
<div style="width:40px;height:40px;background:#fef3c7;border-radius:8px;display:flex;align-items:center;justify-content:center"><img src="assets/pdd.svg" style="width:24px;height:24px" /></div>
<div style="width:40px;height:40px;background:#dbeafe;border-radius:8px;display:flex;align-items:center;justify-content:center"><img src="assets/pdd.svg" style="width:24px;height:24px" /></div>
<div>
<div style="font-size:11px;color:var(--text-secondary)">拼多多本月</div>
<div style="font-size:18px;font-weight:600;color:#f59e0b">${pddThisTotal.toLocaleString()}</div>
<div style="font-size:18px;font-weight:600;color:#3B82F6">${pddThisTotal.toLocaleString()}</div>
</div>
</div>
<div style="background:var(--surface);padding:12px;border-radius:8px;display:flex;align-items:center;gap:12px">
<div style="width:40px;height:40px;background:#ede9fe;border-radius:8px;display:flex;align-items:center;justify-content:center"><img src="assets/yt.svg" style="width:24px;height:24px" /></div>
<div>
<div style="font-size:11px;color:var(--text-secondary)">圆通本月</div>
<div style="font-size:18px;font-weight:600;color:#8b5cf6">${ytThisTotal.toLocaleString()}</div>
<div style="font-size:18px;font-weight:600;color:#10B981">${ytThisTotal.toLocaleString()}</div>
</div>
</div>
</div>
@ -1432,7 +1672,7 @@ const Dashboard = (() => {
const isPdd = newPlatform === 'pdd';
const value = isPdd ? (dashRes.todayPdd || 0) : (dashRes.todayYt || 0);
const platformName = isPdd ? '拼多多' : '圆通';
const color = isPdd ? '#f6af3dff' : '#1a6dd6';
const color = isPdd ? '#3B82F6' : '#10B981';
const fillWidth = Math.min((value / 10000) * 100, 100);
// 如果平台切换了,更新图标
@ -1520,20 +1760,27 @@ const Dashboard = (() => {
window.__auditTimer=setInterval(refreshAll, 10000);
},0);
return `
<div>
<!-- 顶部趋势图 + 环形图 -->
<div style="display:flex;gap:12px;margin-bottom:12px">
<div class="dashboard-container" style="display:flex;flex-direction:column;height:100%;overflow:hidden">
<!-- 四个指标卡片 -->
<div class="dashboard-metrics-4col" style="flex-shrink:0">
${metricsCard('直通良品数', data.fpyCount || 0, 'success')}
${rateCard(data.goodRate || '—', data.badRate || '—')}
${metricsCard('发货数量', data.shipments, 'warning')}
${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
</div>
<!-- 趋势图 + 环形图 -->
<div style="display:flex;gap:12px;margin-top:12px;flex-shrink:0">
<!-- 左侧趋势卡片 -->
<div class="card" style="flex:0 0 72%;padding:0;overflow:hidden;border-radius:16px;background:var(--surface)">
<div style="display:flex;flex-wrap:wrap;padding:20px">
<!-- 左侧摘要信息 -->
<div style="flex:0 0 160px;padding-right:16px;border-right:1px solid var(--border);display:flex;flex-direction:column;justify-content:center">
<div style="margin-bottom:12px">
<div id="trend-total-pdd" style="font-size:24px;font-weight:700;color:#f59e0b;line-height:1.2">0</div>
<div id="trend-total-pdd" style="font-size:24px;font-weight:700;color:#3B82F6;line-height:1.2">0</div>
<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">拼多多本月产量</div>
</div>
<div style="margin-bottom:12px">
<div id="trend-total-yt" style="font-size:24px;font-weight:700;color:#8b5cf6;line-height:1.2">0</div>
<div id="trend-total-yt" style="font-size:24px;font-weight:700;color:#10B981;line-height:1.2">0</div>
<div style="font-size:11px;color:var(--text-secondary);margin-top:2px">圆通本月产量</div>
</div>
<button id="trend-summary-btn" style="padding:8px 12px;background:linear-gradient(135deg,#ec4899,#f43f5e);color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;transition:transform 0.2s,box-shadow 0.2s" onmouseover="this.style.transform='translateY(-1px)';this.style.boxShadow='0 4px 12px rgba(236,72,153,0.4)'" onmouseout="this.style.transform='';this.style.boxShadow=''">月度汇总</button>
@ -1545,8 +1792,8 @@ const Dashboard = (() => {
<div style="display:flex;align-items:center;gap:12px">
<!-- 图例 -->
<div style="display:flex;align-items:center;gap:10px;font-size:11px;color:var(--text-secondary)">
<span style="display:flex;align-items:center;gap:3px"><span style="width:6px;height:6px;border-radius:50%;background:#8b5cf6"></span></span>
<span style="display:flex;align-items:center;gap:3px"><span style="width:6px;height:6px;border-radius:50%;background:#f59e0b"></span></span>
<span style="display:flex;align-items:center;gap:3px"><span style="width:6px;height:6px;border-radius:50%;background:#10B981"></span></span>
<span style="display:flex;align-items:center;gap:3px"><span style="width:6px;height:6px;border-radius:50%;background:#3B82F6"></span></span>
</div>
<!-- 时间范围选择 -->
<div id="trend-range-tabs" style="display:flex;background:var(--surface);border-radius:6px;padding:2px;font-size:11px">
@ -1556,7 +1803,7 @@ const Dashboard = (() => {
</div>
</div>
</div>
<div style="height:180px;position:relative">
<div style="height:230px;position:relative">
<canvas id="trend-chart" style="width:100%;height:100%;cursor:crosshair"></canvas>
<div id="chart-tooltip" style="position:absolute;background:rgba(15,22,35,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:12px;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1)"></div>
</div>
@ -1566,11 +1813,11 @@ const Dashboard = (() => {
<div style="display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--border);background:var(--surface)">
<div style="padding:10px 12px;text-align:center;border-right:1px solid var(--border)">
<div style="font-size:10px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px">今日拼多多</div>
<div id="trend-today-pdd" style="font-size:16px;font-weight:600;color:#f59e0b">0</div>
<div id="trend-today-pdd" style="font-size:16px;font-weight:600;color:#3B82F6">0</div>
</div>
<div style="padding:10px 12px;text-align:center;border-right:1px solid var(--border)">
<div style="font-size:10px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px">今日圆通</div>
<div id="trend-today-yt" style="font-size:16px;font-weight:600;color:#8b5cf6">0</div>
<div id="trend-today-yt" style="font-size:16px;font-weight:600;color:#10B981">0</div>
</div>
<div style="padding:10px 12px;text-align:center;border-right:1px solid var(--border)">
<div style="font-size:10px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px">本周合计</div>
@ -1583,7 +1830,7 @@ const Dashboard = (() => {
</div>
</div>
<!-- 右侧环形图卡片 -->
<div class="card" style="flex:1;padding:20px;border-radius:16px;display:flex;flex-direction:column;background:var(--surface)">
<div class="card" style="flex:1;padding:20px;border-radius:16px;display:flex;flex-direction:column;background:var(--surface);min-height:290px">
<div style="font-weight:600;font-size:14px;margin-bottom:12px">产量占比</div>
<div style="flex:1;display:grid;grid-template-columns:repeat(2,1fr);gap:16px;align-items:center">
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;min-width:0">
@ -1593,15 +1840,15 @@ const Dashboard = (() => {
</div>
<div style="display:flex;justify-content:center;gap:24px;margin-top:12px">
<div style="text-align:center">
<div id="donut-pdd-pct" style="font-size:20px;font-weight:700;color:#f6af3dff">0%</div>
<div id="donut-pdd-pct" style="font-size:20px;font-weight:700;color:#3B82F6">0%</div>
<div style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px">
<span style="width:8px;height:8px;border-radius:50%;background:#f6af3dff"></span>
<span style="width:8px;height:8px;border-radius:50%;background:#3B82F6"></span>
</div>
</div>
<div style="text-align:center">
<div id="donut-yt-pct" style="font-size:20px;font-weight:700;color:#8b5cf6">0%</div>
<div id="donut-yt-pct" style="font-size:20px;font-weight:700;color:#10B981">0%</div>
<div style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-secondary);margin-top:2px">
<span style="width:8px;height:8px;border-radius:50%;background:#8b5cf6"></span>
<span style="width:8px;height:8px;border-radius:50%;background:#10B981"></span>
</div>
</div>
</div>
@ -1616,38 +1863,31 @@ const Dashboard = (() => {
</div>
</div>
</div>
<!-- 四个指标卡片 -->
<div class="dashboard-metrics-4col">
${metricsCard('直通良品数', data.fpyCount || 0, 'success')}
${rateCard(data.goodRate || '—', data.badRate || '—')}
${metricsCard('发货数量', data.shipments, 'warning')}
${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
</div>
<div style="margin-top:12px">
<div id="audit-card" class="card" style="display:flex;flex-direction:column;height:460px;background:var(--surface)">
<div style="margin-top:12px;flex:1;min-height:0;display:flex;flex-direction:column">
<div id="audit-card" class="card" style="display:flex;flex-direction:column;flex:1;min-height:0;background:var(--surface);cursor:pointer" onclick="window.Dashboard.showAuditModal()">
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:8px">
<span>审计看板</span>
<div style="display:flex;gap:8px;align-items:center">
<select id="audit-platform-select" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;cursor:pointer">
<span style="display:flex;align-items:center;gap:8px">审计看板 <span style="font-size:11px;color:var(--text-2);font-weight:400">点击查看完整数据</span></span>
<div style="display:flex;gap:8px;align-items:center" onclick="event.stopPropagation()">
<select id="audit-platform-select" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;cursor:pointer" onclick="event.stopPropagation()">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
</select>
<input id="audit-date" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" />
<input id="audit-date" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" onclick="event.stopPropagation()" />
</div>
</div>
<ul id="audit-list" class="list" style="overflow-y:auto;flex:1;min-height:0">${pddList}</ul>
</div>
<div id="audit-pdd-card" class="card" style="display:none;flex-direction:column;height:460px;background:var(--surface)">
<div id="audit-pdd-card" class="card" style="display:none;flex-direction:column;flex:1;min-height:0;background:var(--surface)">
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0">
<span>拼多多审计</span>
<input id="audit-date-pdd" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" />
<input id="audit-date-pdd" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" onclick="event.stopPropagation()" />
</div>
<ul id="audit-pdd" class="list" style="overflow-y:auto;flex:1;min-height:0">${pddList}</ul>
</div>
<div id="audit-yt-card" class="card" style="display:none;flex-direction:column;height:460px;background:var(--surface)">
<div id="audit-yt-card" class="card" style="display:none;flex-direction:column;flex:1;min-height:0;background:var(--surface)">
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0">
<span>圆通审计</span>
<input id="audit-date-yt" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" />
<input id="audit-date-yt" type="date" style="padding:4px 8px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none" onclick="event.stopPropagation()" />
</div>
<ul id="audit-yt" class="list" style="overflow-y:auto;flex:1;min-height:0">${ytList}</ul>
</div>
@ -1656,5 +1896,11 @@ const Dashboard = (() => {
`;
}
// 将方法暴露到全局
window.Dashboard = {
showAuditModal,
cleanup
};
Router.register('/dashboard', render);
})();

View File

@ -0,0 +1,327 @@
(() => {
// 菜单搜索功能
class MenuSearch {
constructor() {
this.searchInput = document.querySelector('.sidebar-search-input');
this.searchIcon = document.querySelector('.search-icon');
this.menuItems = [];
this.searchResults = [];
this.isSearchVisible = false;
this.init();
}
init() {
if (!this.searchInput) return;
// 收集所有菜单项
this.collectMenuItems();
// 监听输入事件
this.searchInput.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
});
// 监听焦点事件
this.searchInput.addEventListener('focus', () => {
if (this.searchInput.value.trim()) {
this.showSearchResults();
}
});
// 监听失焦事件(延迟隐藏,以便点击搜索结果)
this.searchInput.addEventListener('blur', () => {
setTimeout(() => {
this.hideSearchResults();
}, 200);
});
// 监听键盘事件
this.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.navigateFirstResult();
} else if (e.key === 'Escape') {
this.clearSearch();
this.searchInput.blur();
}
});
// 创建搜索结果容器
this.createSearchResultsContainer();
}
collectMenuItems() {
// 收集主导航项
document.querySelectorAll('.topnav-item[data-route]').forEach(item => {
const text = item.querySelector('.topnav-text')?.textContent.trim();
const route = item.dataset.route;
const href = item.getAttribute('href');
if (text && route) {
this.menuItems.push({
text,
route,
href,
type: 'menu',
element: item
});
}
});
// 收集下拉菜单项
document.querySelectorAll('.dropdown-item[data-route]').forEach(item => {
const text = item.textContent.trim();
const route = item.dataset.route;
const href = item.getAttribute('href');
const parentMenu = item.closest('.topnav-item.has-dropdown')?.querySelector('.topnav-text')?.textContent.trim();
if (text && route) {
this.menuItems.push({
text,
route,
href,
type: 'submenu',
parent: parentMenu,
element: item
});
}
});
}
createSearchResultsContainer() {
// 创建搜索结果下拉框
this.resultsContainer = document.createElement('div');
this.resultsContainer.className = 'search-results-container';
this.resultsContainer.innerHTML = `
<div class="search-results-list"></div>
`;
// 插入到搜索框后面
const searchWrapper = this.searchInput.closest('.sidebar-search');
searchWrapper.appendChild(this.resultsContainer);
// 添加样式
const style = document.createElement('style');
style.textContent = `
.sidebar-search {
position: relative;
}
.search-results-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--card-bg, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 300px;
overflow-y: auto;
z-index: 9999;
display: none;
}
[data-theme="dark"] .search-results-container {
background: var(--card-bg, #1a1a1a);
border-color: var(--border-color, #333333);
}
.search-results-container.show {
display: block;
}
.search-results-list {
padding: 4px 0;
}
.search-result-item {
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-color, #333333);
background: transparent;
}
.search-result-item:hover {
background: var(--hover-bg, #f5f5f5);
}
.search-result-item.active {
background: var(--primary-color, #007bff);
color: white !important;
}
.search-result-item .result-icon {
opacity: 0.8;
flex-shrink: 0;
}
.search-result-item .result-text {
flex: 1;
opacity: 1 !important;
}
.search-result-item .result-parent {
font-size: 12px;
opacity: 0.7;
margin-left: 8px;
}
.search-result-item .result-highlight {
font-weight: 600;
color: var(--primary-color, #007bff);
}
.search-result-item.active .result-highlight {
color: inherit !important;
}
.search-empty {
padding: 16px;
text-align: center;
color: var(--text-muted, #666666);
font-size: 14px;
}
.search-shortcut {
padding: 8px 16px;
border-top: 1px solid var(--border-color, #e0e0e0);
font-size: 12px;
color: var(--text-muted, #666666);
}
[data-theme="dark"] .search-result-item {
color: var(--text-color, #e0e0e0);
}
[data-theme="dark"] .search-result-item:hover {
background: var(--hover-bg, #2a2a2a);
}
[data-theme="dark"] .search-empty,
[data-theme="dark"] .search-shortcut {
color: var(--text-muted, #999999);
}
`;
document.head.appendChild(style);
}
handleSearch(query) {
const trimmedQuery = query.trim().toLowerCase();
if (!trimmedQuery) {
this.hideSearchResults();
return;
}
// 搜索匹配的菜单项
this.searchResults = this.menuItems.filter(item => {
return item.text.toLowerCase().includes(trimmedQuery) ||
(item.parent && item.parent.toLowerCase().includes(trimmedQuery));
});
// 按匹配度和类型排序
this.searchResults.sort((a, b) => {
// 优先匹配开头的
const aStarts = a.text.toLowerCase().startsWith(trimmedQuery);
const bStarts = b.text.toLowerCase().startsWith(trimmedQuery);
if (aStarts && !bStarts) return -1;
if (!aStarts && bStarts) return 1;
// 主菜单优先于子菜单
if (a.type === 'menu' && b.type === 'submenu') return -1;
if (a.type === 'submenu' && b.type === 'menu') return 1;
// 按文本排序
return a.text.localeCompare(b.text);
});
this.displaySearchResults(trimmedQuery);
}
displaySearchResults(query) {
const resultsList = this.resultsContainer.querySelector('.search-results-list');
if (this.searchResults.length === 0) {
resultsList.innerHTML = '<div class="search-empty">未找到匹配的菜单项</div>';
} else {
resultsList.innerHTML = this.searchResults.map((item, index) => {
const highlightedText = this.highlightText(item.text, query);
const parentText = item.parent ? `<span class="result-parent">${item.parent}</span>` : '';
return `
<div class="search-result-item ${index === 0 ? 'active' : ''}" data-index="${index}" data-href="${item.href}">
<span class="result-icon">${item.type === 'menu' ? '📄' : '📋'}</span>
<span class="result-text">${highlightedText}</span>
${parentText}
</div>
`;
}).join('');
// 添加点击事件
resultsList.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', () => {
const href = item.dataset.href;
if (href) {
window.location.href = href;
}
});
});
}
// 添加快捷键提示
const shortcut = document.createElement('div');
shortcut.className = 'search-shortcut';
shortcut.innerHTML = '↑↓ 选择 Enter 确认 Esc 取消';
resultsList.appendChild(shortcut);
this.showSearchResults();
}
highlightText(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<span class="result-highlight">$1</span>');
}
showSearchResults() {
this.resultsContainer.classList.add('show');
this.isSearchVisible = true;
}
hideSearchResults() {
this.resultsContainer.classList.remove('show');
this.isSearchVisible = false;
}
clearSearch() {
this.searchInput.value = '';
this.hideSearchResults();
}
navigateFirstResult() {
if (this.searchResults.length > 0) {
const firstResult = this.searchResults[0];
if (firstResult.href) {
window.location.href = firstResult.href;
}
}
}
}
// 初始化搜索功能
const menuSearch = new MenuSearch();
// 全局快捷键 Ctrl+K 或 Cmd+K 聚焦搜索框
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
menuSearch.searchInput?.focus();
}
});
})();

View File

@ -1,7 +1,7 @@
(() => {
let notificationInterval = null;
let isOpen = false;
let isInitialized = false;
let isModalOpen = false;
// 格式化时间
function formatTime(ts) {
@ -58,6 +58,7 @@
try {
const data = await API.getUnreadCount();
const badge = document.getElementById('notification-badge');
if (!badge) return;
const count = data.count || 0;
const previousCount = parseInt(badge.textContent) || 0;
@ -65,8 +66,8 @@
badge.textContent = count > 99 ? '99+' : count;
badge.style.display = 'block';
// 如果有新消息且面板是打开的,自动刷新列表
if (count > previousCount && isOpen) {
// 如果有新消息,自动刷新列表
if (count > previousCount) {
console.log('[Notifications] 检测到新消息,刷新列表');
await loadNotifications();
}
@ -84,13 +85,16 @@
const data = await API.getNotifications();
const list = data.list || [];
const container = document.getElementById('notification-list');
const modalContainer = document.getElementById('notification-modal-list');
if (!container) return;
if (list.length === 0) {
container.innerHTML = '<div class="notification-empty">暂无消息通知</div>';
if (modalContainer) modalContainer.innerHTML = '<div class="notification-empty">暂无消息通知</div>';
return;
}
container.innerHTML = list.map(item => `
const fullHtml = list.map(item => `
<div class="notification-item ${item.read ? '' : 'unread'}" data-id="${item.id}">
<div class="notification-username">${item.username || '用户'}</div>
<div class="notification-action">${getActionText(item.action)}</div>
@ -99,6 +103,9 @@
</div>
`).join('');
container.innerHTML = fullHtml;
if (modalContainer) modalContainer.innerHTML = fullHtml;
// 为每个通知项添加点击事件
container.querySelectorAll('.notification-item').forEach(item => {
item.addEventListener('click', async () => {
@ -107,6 +114,9 @@
try {
await API.markNotificationRead(id);
item.classList.remove('unread');
// 同步 modal 中对应项
const modalItem = document.querySelector(`#notification-modal-list .notification-item[data-id="${id}"]`);
if (modalItem) modalItem.classList.remove('unread');
await updateUnreadCount();
} catch (e) {
console.error('标记已读失败:', e);
@ -114,28 +124,42 @@
}
});
});
if (modalContainer) {
modalContainer.querySelectorAll('.notification-item').forEach(item => {
item.addEventListener('click', async () => {
const id = item.dataset.id;
if (item.classList.contains('unread')) {
try {
await API.markNotificationRead(id);
item.classList.remove('unread');
const sidebarItem = document.querySelector(`#notification-list .notification-item[data-id="${id}"]`);
if (sidebarItem) sidebarItem.classList.remove('unread');
await updateUnreadCount();
} catch (e) {
console.error('标记已读失败:', e);
}
}
});
});
}
} catch (e) {
console.error('加载通知失败:', e);
}
}
// 切换通知面板
function togglePanel() {
const panel = document.getElementById('notification-panel');
if (!panel) {
console.error('[Notifications] 找不到面板元素');
return;
}
isOpen = !isOpen;
console.log('[Notifications] 面板状态:', isOpen ? '打开' : '关闭');
if (isOpen) {
panel.style.display = 'flex';
loadNotifications();
} else {
panel.style.display = 'none';
}
function openModal() {
const modal = document.getElementById('notification-modal');
if (!modal) return;
isModalOpen = true;
modal.style.display = 'block';
}
function closeModal() {
const modal = document.getElementById('notification-modal');
if (!modal) return;
isModalOpen = false;
modal.style.display = 'none';
}
// 初始化通知系统
@ -154,26 +178,35 @@
return; // 超级管理员和管理员才显示通知铃铛
}
// 显示通知铃铛
const bell = document.getElementById('notification-bell');
if (!bell) {
console.error('[Notifications] 找不到铃铛元素');
// 显示侧边栏通知卡片
const card = document.getElementById('sidebar-notification-card');
if (!card) {
console.error('[Notifications] 找不到侧边栏通知卡片元素');
return;
}
bell.style.display = 'inline-flex';
console.log('[Notifications] 铃铛已显示');
card.style.display = 'block';
console.log('[Notifications] 侧边栏通知卡片已显示');
// 移除旧的事件监听器(如果有)
const newBell = bell.cloneNode(true);
bell.parentNode.replaceChild(newBell, bell);
// 点击卡片标题区域打开大弹窗(列表和按钮区域不触发)
card.addEventListener('click', (e) => {
if (e.target.closest('.sidebar-notification-actions')) return;
if (e.target.closest('.sidebar-notification-list')) return;
openModal();
});
// 绑定铃铛点击事件
newBell.addEventListener('click', (e) => {
console.log('[Notifications] 铃铛被点击');
e.preventDefault();
e.stopPropagation();
togglePanel();
// 弹窗关闭
const closeBtn = document.getElementById('notification-modal-close');
if (closeBtn) {
const newClose = closeBtn.cloneNode(true);
closeBtn.parentNode.replaceChild(newClose, closeBtn);
newClose.addEventListener('click', closeModal);
}
const backdrop = document.querySelector('#notification-modal .notification-modal-backdrop');
if (backdrop) {
backdrop.addEventListener('click', closeModal);
}
document.addEventListener('keydown', (e) => {
if (isModalOpen && e.key === 'Escape') closeModal();
});
// 绑定全部标记为已读按钮(移除旧的事件监听器)
@ -183,7 +216,7 @@
markAllBtn.parentNode.replaceChild(newMarkAllBtn, markAllBtn);
newMarkAllBtn.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
try {
await API.markAllNotificationsRead();
await loadNotifications();
@ -202,7 +235,7 @@
deleteBtn.parentNode.replaceChild(newDeleteBtn, deleteBtn);
newDeleteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
if (!confirm('确定要删除所有已读消息吗?')) {
return;
}
@ -217,15 +250,8 @@
});
}
// 点击其他地方关闭面板
document.addEventListener('click', (e) => {
const panel = document.getElementById('notification-panel');
const currentBell = document.getElementById('notification-bell');
if (isOpen && panel && currentBell && !panel.contains(e.target) && !currentBell.contains(e.target)) {
isOpen = false;
panel.style.display = 'none';
}
});
// 初次加载列表
await loadNotifications();
// 初始加载未读数量
await updateUnreadCount();
@ -250,7 +276,6 @@
notificationInterval = null;
}
isInitialized = false;
isOpen = false;
console.log('[Notifications] 已清理');
}

View File

@ -10,6 +10,7 @@
<div class="page-actions">
<input type="text" id="keyword-filter" class="input" placeholder="搜索操作详情..." style="width: 200px; margin-right: 10px;" />
<button id="search-btn" class="btn btn-secondary">搜索</button>
<button id="retry-btn" class="btn btn-secondary" style="margin-left: 10px;">重试</button>
<button id="cleanup-btn" class="btn btn-danger" style="margin-left: 10px;">清理旧日志</button>
</div>
</div>
@ -54,6 +55,10 @@
loadLogs();
});
document.getElementById('retry-btn')?.addEventListener('click', () => {
loadLogs();
});
document.getElementById('keyword-filter')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
currentPage = 1;
@ -124,7 +129,15 @@
'audit_yt_probe': '审核易泰勒探针',
'audit_pdd_cost': '审核拼多多成本',
'audit_pdd_probe': '审核拼多多探针',
'upload_shipment': '上传发货记录'
'upload_shipment': '上传发货记录',
'export_repair_records': '导出返修记录',
'batch_update_tracking': '批量更新物流',
'upload_repair_record': '上传返修记录',
'upload_avatar': '上传头像',
'delete_repair_record': '删除返修记录',
'shipments_summary': '发货汇总',
'add_user': '添加用户',
'export_reconciliations': '导出对账单'
};
function getActionLabel(action) {
@ -134,10 +147,29 @@
async function loadLogs() {
try {
const keyword = document.getElementById('keyword-filter')?.value || '';
const res = await fetch(`/api/operations-log?page=${currentPage}&page_size=${pageSize}&keyword=${encodeURIComponent(keyword)}`);
// 显示加载状态
const tbody = document.getElementById('log-list');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">加载中...</td></tr>';
}
// 设置10秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const res = await fetch(`/api/operations-log?page=${currentPage}&page_size=${pageSize}&keyword=${encodeURIComponent(keyword)}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!res.ok) {
throw new Error(`服务器错误: ${res.status}`);
}
const data = await res.json();
const tbody = document.getElementById('log-list');
if (!tbody) return;
if (!data.list || data.list.length === 0) {
@ -159,7 +191,15 @@
updatePagination(data.total);
} catch (err) {
console.error('加载操作日志失败:', err);
API.toast('加载失败', 'error');
const tbody = document.getElementById('log-list');
if (tbody) {
if (err.name === 'AbortError') {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">加载超时,请稍后重试</td></tr>';
} else {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">加载失败,请重试</td></tr>';
}
}
API.toast('加载失败,请重试', 'error');
}
}

View File

@ -2,24 +2,126 @@
(() => {
Router.register('/plan-mgmt/reconciliation', async () => {
const html = `
<div class="page-header">
<h1>对账单管理</h1>
<div class="page-actions">
<button id="batch-delete-btn" class="btn btn-danger" style="margin-right: 10px; display: none;">批量删除</button>
<button id="export-reconciliation-btn" class="btn btn-secondary" style="margin-right: 10px;">导出对账单</button>
<button id="upload-shipment-btn" class="btn btn-secondary" style="margin-right: 10px;">上传发货单</button>
<button id="add-reconciliation-btn" class="btn btn-primary">新增对账单</button>
<input type="file" id="shipment-file-input" accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" style="display: none;" />
</div>
</div>
<style>
/* 完全重新设计页面布局 */
.view:has(#reconciliation-page) {
padding: 0 !important;
display: flex;
flex-direction: column;
height: 100vh;
}
#reconciliation-page {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
}
#reconciliation-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#reconciliation-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#reconciliation-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#reconciliation-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#reconciliation-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#reconciliation-page th {
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
font-size: 14px;
}
#reconciliation-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#reconciliation-page tbody tr:last-child td {
border-bottom: none;
}
#reconciliation-page tbody tr:hover {
background: var(--hover);
}
#reconciliation-page .text-center {
text-align: center;
color: var(--text-2);
}
#reconciliation-page .btn-text {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
padding: 4px 8px;
margin-right: 8px;
}
#reconciliation-page .btn-text:hover {
text-decoration: underline;
}
#reconciliation-page .btn-danger {
color: var(--danger);
}
</style>
<div class="card">
<div class="card-body">
<div class="table-container" style="max-height: calc(100vh - 200px); overflow-y: auto; padding-bottom: 5px;">
<table class="data-table">
<div id="reconciliation-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">对账单管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="batch-delete-btn" class="btn btn-danger" style="margin-right: 10px; display: none;">批量删除</button>
<button id="export-reconciliation-btn" class="btn btn-secondary" style="margin-right: 10px;">导出对账单</button>
<button id="upload-shipment-btn" class="btn btn-secondary" style="margin-right: 10px;">上传发货单</button>
<button id="add-reconciliation-btn" class="btn btn-primary">新增对账单</button>
<input type="file" id="shipment-file-input" accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" style="display: none;" />
</div>
</div>
<div class="content-area">
<div style="padding: 0 20px 20px 20px; display: flex; gap: 15px; align-items: center;">
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 40px;">
<th style="width: 40px; padding: 8px 10px; font-size: 14px;">
<label class="custom-checkbox"><input type="checkbox" id="select-all-reconciliation" /><span class="checkmark"></span></label>
</th>
<th>序号</th>
@ -138,7 +240,7 @@
</div>
</div>
`;
// DOM 渲染后初始化
setTimeout(() => {
const addBtn = document.getElementById('add-reconciliation-btn');
if (addBtn) {
@ -147,6 +249,8 @@
});
}
// 不需要筛选功能
// 全选/取消全选
const selectAllCheckbox = document.getElementById('select-all-reconciliation');
if (selectAllCheckbox) {
@ -258,19 +362,28 @@
});
async function loadReconciliations() {
console.log('开始加载对账单数据...');
try {
const res = await fetch('/api/reconciliations');
const data = await res.json();
console.log('对账单数据:', data); // 调试信息
const tbody = document.getElementById('reconciliation-list');
if (!tbody) return;
if (!tbody) {
console.error('找不到 reconciliation-list 元素');
return;
}
if (!data.list || data.list.length === 0) {
tbody.innerHTML = '<tr><td colspan="14" class="text-center">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.list.map((item, index) => {
// 不需要筛选数据
let filteredData = data.list;
tbody.innerHTML = filteredData.map((item, index) => {
// 格式化数字:去掉不必要的小数点和尾随零
const formatNumber = (num) => {
if (!num && num !== 0) return '—';

View File

@ -243,7 +243,8 @@ Router.register('/settings', async () => {
// 更新顶部用户头像显示
const avatarImg = document.getElementById('user-avatar-img');
if (avatarImg && data.avatar_url) {
avatarImg.src = data.avatar_url;
// 添加时间戳参数防止缓存
avatarImg.src = data.avatar_url + '?t=' + Date.now();
}
// 如果有全局更新函数,调用它
if (window.updateUserDisplay) {

View File

@ -112,8 +112,8 @@ Router.register('/shipments/query', async () => {
if (data.found) {
const recordsHtml = data.records.map((record, index) => `
<div style="padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;margin-bottom:8px">
<div style="font-weight:600;margin-bottom:8px;color:var(--primary)">记录 ${index + 1}</div>
<div style="padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:4px;margin-bottom:6px">
<div style="font-weight:600;margin-bottom:6px;color:var(--primary)">记录 ${index + 1}</div>
<div class="result-item"><span class="label">SN/MAC:</span> ${record.sn}</div>
<div class="result-item"><span class="label">机种:</span> <span class="badge">${record.platform_name || ''}</span></div>
<div class="result-item"><span class="label">出货日期:</span> ${record.date}</div>
@ -126,7 +126,7 @@ Router.register('/shipments/query', async () => {
<div class="result-title"> 找到出货记录</div>
<div class="result-item"><span class="label">箱号:</span> ${data.box}</div>
<div class="result-item"><span class="label">记录数量:</span> <span style="font-weight:600;color:var(--primary)">${data.count}</span></div>
<div style="margin-top:16px;max-height:500px;overflow-y:auto">
<div style="margin-top:16px;max-height:50vh;overflow-y:auto;padding-right:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg)">
${recordsHtml}
</div>
</div>
@ -188,7 +188,7 @@ Router.register('/shipments/query', async () => {
const showClearButton = userRole === 'superadmin';
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">发货汇总信息查询</div>
<div style="font-weight:600;margin-bottom:16px">发货详细记录查询</div>
<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">

View File

@ -60,9 +60,9 @@ Router.register('/shipments/summary', async () => {
</div>
</div>
<div style="overflow-x:auto">
<table class="table">
<thead>
<div style="max-height:60vh;overflow-y:auto;overflow-x:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface)">
<table class="table" style="margin:0">
<thead style="position:sticky;top:0;z-index:1;background:var(--surface)">
<tr>
<th>发货日期</th>
<th>数量</th>

View File

@ -17,10 +17,17 @@ const Upload = (() => {
eventListeners.length = 0;
};
let currentUploadPath = null;
Router.onBeforeEach((path) => {
if(!path.startsWith('/upload')){
cleanupListeners();
if(path.startsWith('/upload')){
if(currentUploadPath && currentUploadPath !== path){
cleanupListeners();
}
currentUploadPath = path;
return;
}
cleanupListeners();
currentUploadPath = null;
});
function getHistory() {
@ -62,6 +69,29 @@ const Upload = (() => {
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列: 批次号
</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>
</div>
<div class="field">
<label>上传机种 <span style="color:#ff4444">*</span></label>
<select id="mac-type" class="input">
@ -79,9 +109,9 @@ const Upload = (() => {
</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>
<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-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>
@ -158,6 +188,10 @@ 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">
<div class="form-group" style="margin-bottom:16px">
<label style="display:block;font-weight:600;margin-bottom:6px">
设备SN <span style="color:#ef4444">*</span>
@ -238,33 +272,48 @@ const Upload = (() => {
#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 style="margin-top:20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px">
<span style="font-weight:600">历史记录</span>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="repairs-search-sn" class="input" placeholder="输入SN搜索" style="width:150px;font-size:12px;padding:4px 8px" />
<button class="btn btn-secondary" id="repairs-search-btn" style="font-size:12px;padding:4px 8px">搜索</button>
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:4px 8px">全部</button>
<button class="btn btn-secondary" id="repairs-tracking-btn" style="font-size:12px;padding:4px 8px">添加快递单号</button>
<button class="btn btn-secondary" id="repairs-export-btn" style="font-size:12px;padding:4px 8px">导出选中</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div id="repairs-list-info" style="font-size:12px;color:var(--text-2)"></div>
<div style="display:flex;gap:8px;align-items:center">
<label style="font-size:12px;color:var(--text-2);cursor:pointer;display:flex;align-items:center;gap:4px">
<input type="checkbox" id="repairs-select-all" style="cursor:pointer" />
全选
</label>
<span id="repairs-selected-count" style="font-size:12px;color:var(--primary)"></span>
</div>
</div>
<ul id="repairs-list" class="list" style="max-height:500px;overflow-y:auto"></ul>
</div>
`);
}
async function renderRepairsHistory(){
return `
<div style="height:100%;display:flex;flex-direction:column">
<div id="repairs-history-card" class="card" style="flex:1;min-height:0;display:flex;flex-direction:column;margin-bottom:12px">
<div style="font-weight:600;margin-bottom:8px;flex-shrink:0">返修历史记录</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px;flex-shrink:0">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<a href="#/upload/repairs" class="btn btn-secondary" style="font-size:12px;padding:6px 10px">返回上传</a>
<span style="font-weight:600">历史记录</span>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="repairs-search-sn" class="input" placeholder="输入SN搜索" style="width:180px;font-size:12px;padding:6px 10px" />
<button class="btn btn-secondary" id="repairs-search-btn" style="font-size:12px;padding:6px 10px">搜索</button>
<button class="btn btn-secondary" id="repairs-show-all" style="font-size:12px;padding:6px 10px">全部</button>
<button class="btn btn-secondary" id="repairs-tracking-btn" style="font-size:12px;padding:6px 10px">添加快递单号</button>
<button class="btn btn-secondary" id="repairs-export-btn" style="font-size:12px;padding:6px 10px">导出选中</button>
<button class="btn btn-danger" id="repairs-delete-selected" style="font-size:12px;padding:6px 10px;background:var(--danger);color:white;display:none">删除选中</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px;flex-wrap:wrap;flex-shrink:0">
<div id="repairs-list-info" style="font-size:12px;color:var(--text-2)"></div>
<div style="display:flex;gap:8px;align-items:center">
<label style="font-size:12px;color:var(--text-2);cursor:pointer;display:flex;align-items:center;gap:4px">
<input type="checkbox" id="repairs-select-all" style="cursor:pointer" />
全选
</label>
<span id="repairs-selected-count" style="font-size:12px;color:var(--primary)"></span>
</div>
</div>
<ul id="repairs-list" class="list" style="flex:1;min-height:0;overflow-y:auto"></ul>
</div>
</div>
`;
}
async function renderDefects(){
return section('不良明细',`
${filePicker('defects-file','批量导入不良MAC与批次(Excel/CSV)','text/csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel')}
@ -278,21 +327,14 @@ const Upload = (() => {
const html = await renderMac();
setTimeout(bindMacEvents,0);
setTimeout(async ()=>{
// 从服务器获取最新10条记录
const listEl=document.getElementById('mac-list');
try {
const data = await API.listMac();
if(listEl && data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
listEl.innerHTML = data.list.slice(0, 10).map(r=>{
const platformName = platformNames[r.platform] || r.platform || '未知';
return `<li><span>${r.mac}</span><span class="badge">${r.batch}</span><span class="badge" style="background:var(--primary-light);color:var(--primary)">${platformName}</span></li>`;
}).join('');
} else if(listEl) {
listEl.innerHTML = '<li>暂无数据</li>';
// 页面加载时自动显示所有历史记录
const listEl = document.getElementById('mac-list');
if(listEl) {
// 触发查看历史按钮的点击事件
const showHistoryBtn = document.getElementById('mac-show-history');
if(showHistoryBtn) {
showHistoryBtn.click();
}
} catch(e) {
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
},0);
return html;
@ -328,14 +370,12 @@ const Upload = (() => {
Router.register('/upload/repairs', async () => {
const html = await renderRepairs();
setTimeout(bindRepairsEvents,0);
setTimeout(async ()=>{
const listEl=document.getElementById('repairs-list');
const data=await API.listRepairs().catch(()=>({list:[]}));
listEl.innerHTML=(data.list||[]).slice(0,10).map(r=>{
const ts = new Date(r.ts).toLocaleString('zh-CN');
return `<li><span>数量: ${r.qty}</span><span class="badge">${r.note||'无备注'}</span><span style="font-size:11px;color:var(--text-2)">${ts}</span></li>`;
}).join('')||'<li>暂无数据</li>';
},0);
return html;
});
Router.register('/upload/repairs-history', async () => {
const html = await renderRepairsHistory();
setTimeout(bindRepairsEvents,0);
return html;
});
@ -549,6 +589,69 @@ const Upload = (() => {
const showHistoryBtn = document.getElementById('mac-show-history');
const clearDisplayBtn = document.getElementById('mac-clear-display');
// 显示历史记录的通用函数
const displayHistory = async (showAll = false) => {
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 => {
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} 条数据`);
}
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
}
} catch(e) {
API.toast('加载历史记录失败');
if(listEl) listEl.innerHTML = '<li>加载失败</li>';
}
};
// 查看历史按钮 - 从服务器获取所有用户的上传记录
addListener(showHistoryBtn, 'click', async ()=>{
await displayHistory(true);
});
// 文件选择后立即验证
addListener(fileEl, 'change', async ()=>{
const file = fileEl.files[0];
@ -584,11 +687,50 @@ const Upload = (() => {
if(listEl){
if(data.list && data.list.length > 0){
const platformNames = {pdd: '拼多多', yt: '圆通', tx: '兔喜'};
listEl.innerHTML = data.list.map(r=>{
const platformName = platformNames[r.platform] || r.platform || '未知';
return `<li><span>${r.mac}</span><span class="badge">${r.batch}</span><span class="badge" style="background:var(--primary-light);color:var(--primary)">${platformName}</span><span style="font-size:11px;color:var(--text-2);margin-left:8px">${new Date(r.ts).toLocaleString('zh-CN')}</span></li>`;
// 按时间和机种分组统计
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('');
API.toast(`显示全部 ${data.list.length} 条历史记录`);
const totalGroups = groupArray.length;
const totalRecords = data.list.length;
API.toast(`显示 ${totalGroups} 次上传记录,共 ${totalRecords} 条数据`);
} else {
listEl.innerHTML = '<li>暂无历史记录</li>';
}
@ -665,13 +807,56 @@ const Upload = (() => {
try{
const records = JSON.parse(jsonMatch[1].trim());
if(records.length > 0){
// 保存到历史记录
// 保存到本地历史记录(仅用于本地显示)
saveToHistory(records);
// 显示最新记录
// 重新从服务器加载数据并显示
const listEl = document.getElementById('mac-list');
if(listEl){
listEl.innerHTML = records.map(r=>`<li><span>${r.mac}</span><span class="badge">${r.batch}</span></li>`).join('');
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);
}
}
}
}catch(e){
@ -831,157 +1016,266 @@ const Upload = (() => {
const imageError = document.getElementById('repair-image-error');
const uploadText = document.getElementById('repairs-upload-text');
const uploadLoading = document.getElementById('repairs-upload-loading');
const isUploadPage = !!document.getElementById('repairs-upload-form');
const isHistoryPage = !!document.getElementById('repairs-list');
// 存储待上传的图片文件
let pendingImages = [];
// 根据不良原因决定维修方法是否必填
addListener(defectInput, 'input', () => {
// 清除错误提示
defectError.style.display = 'none';
methodError.style.display = 'none';
if(isUploadPage) {
// 存储待上传的图片文件
let pendingImages = [];
const defectValue = defectInput.value.trim();
// 条件性显示维修方法:无不良时可选,其他原因时必填
if(defectValue) {
methodGroup.style.opacity = '1';
methodInput.disabled = false;
// 根据不良原因决定维修方法是否必填
addListener(defectInput, 'input', () => {
// 清除错误提示
defectError.style.display = 'none';
methodError.style.display = 'none';
if(defectValue === '无不良') {
// 无不良时维修方法可选
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法(可选)';
const defectValue = defectInput.value.trim();
// 条件性显示维修方法:无不良时可选,其他原因时必填
if(defectValue) {
methodGroup.style.opacity = '1';
methodInput.disabled = false;
if(defectValue === '无不良') {
// 无不良时维修方法可选
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法(可选)';
} else {
// 其他原因时维修方法必填
methodRequired.style.display = 'inline';
methodInput.placeholder = '请描述维修方法(必填)';
}
} else {
// 其他原因时维修方法必填
methodRequired.style.display = 'inline';
methodInput.placeholder = '请描述维修方法(必填)';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
}
} else {
});
// 初始化维修方法状态
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
// SN输入验证
addListener(snInput, 'input', () => {
snError.style.display = 'none';
});
// 图片上传处理
const validateAndAddImages = (files) => {
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
const maxSize = 10 * 1024 * 1024; // 10MB
const maxCount = 10;
for(const file of files) {
if(pendingImages.length >= maxCount) {
imageError.textContent = `最多上传${maxCount}张图片`;
imageError.style.display = 'block';
return;
}
if(!validTypes.includes(file.type)) {
imageError.textContent = `不支持的格式: ${file.name}仅支持JPG/PNG/GIF`;
imageError.style.display = 'block';
continue;
}
if(file.size > maxSize) {
imageError.textContent = `图片 ${file.name} 超过10MB限制`;
imageError.style.display = 'block';
continue;
}
// 检查是否已存在同名文件
if(pendingImages.some(f => f.name === file.name && f.size === file.size)) {
continue;
}
pendingImages.push(file);
imageError.style.display = 'none';
}
renderImagePreviews();
};
const renderImagePreviews = () => {
previewContainer.innerHTML = pendingImages.map((file, index) => {
const url = URL.createObjectURL(file);
return `
<div style="position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
<img src="${url}" style="width:100%;height:100%;object-fit:cover" />
<button type="button" data-index="${index}" class="remove-image-btn" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.6);color:#fff;border:none;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center">×</button>
</div>
`;
}).join('');
// 绑定删除按钮事件
previewContainer.querySelectorAll('.remove-image-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
pendingImages.splice(index, 1);
renderImagePreviews();
};
});
};
// 点击上传区域
addListener(dropzone, 'click', () => {
imageInput.click();
});
// 文件选择
addListener(imageInput, 'change', () => {
if(imageInput.files.length > 0) {
validateAndAddImages(Array.from(imageInput.files));
imageInput.value = ''; // 清空以便重复选择同一文件
}
});
// 拖拽上传
addListener(dropzone, 'dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
addListener(dropzone, 'dragleave', () => {
dropzone.classList.remove('dragover');
});
addListener(dropzone, 'drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if(e.dataTransfer.files.length > 0) {
validateAndAddImages(Array.from(e.dataTransfer.files));
}
});
// 清空按钮
addListener(clearBtn, 'click', () => {
snInput.value = '';
machineTypeSelect.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
}
});
// 初始化维修方法状态
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
// SN输入验证
addListener(snInput, 'input', () => {
snError.style.display = 'none';
});
// 图片上传处理
const validateAndAddImages = (files) => {
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
const maxSize = 10 * 1024 * 1024; // 10MB
const maxCount = 10;
for(const file of files) {
if(pendingImages.length >= maxCount) {
imageError.textContent = `最多上传${maxCount}张图片`;
imageError.style.display = 'block';
return;
}
if(!validTypes.includes(file.type)) {
imageError.textContent = `不支持的格式: ${file.name}仅支持JPG/PNG/GIF`;
imageError.style.display = 'block';
continue;
}
if(file.size > maxSize) {
imageError.textContent = `图片 ${file.name} 超过10MB限制`;
imageError.style.display = 'block';
continue;
}
// 检查是否已存在同名文件
if(pendingImages.some(f => f.name === file.name && f.size === file.size)) {
continue;
}
pendingImages.push(file);
pendingImages = [];
previewContainer.innerHTML = '';
snError.style.display = 'none';
machineTypeError.style.display = 'none';
defectError.style.display = 'none';
methodError.style.display = 'none';
imageError.style.display = 'none';
}
renderImagePreviews();
};
const renderImagePreviews = () => {
previewContainer.innerHTML = pendingImages.map((file, index) => {
const url = URL.createObjectURL(file);
return `
<div style="position:relative;width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid var(--border)">
<img src="${url}" style="width:100%;height:100%;object-fit:cover" />
<button type="button" data-index="${index}" class="remove-image-btn" style="position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.6);color:#fff;border:none;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center">×</button>
</div>
`;
}).join('');
// 绑定删除按钮事件
previewContainer.querySelectorAll('.remove-image-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const index = parseInt(btn.dataset.index);
pendingImages.splice(index, 1);
renderImagePreviews();
};
API.toast('已清空表单');
});
};
// 点击上传区域
addListener(dropzone, 'click', () => {
imageInput.click();
});
// 文件选择
addListener(imageInput, 'change', () => {
if(imageInput.files.length > 0) {
validateAndAddImages(Array.from(imageInput.files));
imageInput.value = ''; // 清空以便重复选择同一文件
}
});
// 拖拽上传
addListener(dropzone, 'dragover', (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
});
addListener(dropzone, 'dragleave', () => {
dropzone.classList.remove('dragover');
});
addListener(dropzone, 'drop', (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
if(e.dataTransfer.files.length > 0) {
validateAndAddImages(Array.from(e.dataTransfer.files));
}
});
// 清空按钮
addListener(clearBtn, 'click', () => {
snInput.value = '';
machineTypeSelect.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
pendingImages = [];
previewContainer.innerHTML = '';
snError.style.display = 'none';
machineTypeError.style.display = 'none';
defectError.style.display = 'none';
methodError.style.display = 'none';
imageError.style.display = 'none';
API.toast('已清空表单');
});
// 提交按钮
addListener(btn, 'click', async () => {
// 表单验证
let hasError = false;
// 验证SN
const sn = snInput.value.trim();
if(!sn) {
snError.textContent = '请输入设备SN';
snError.style.display = 'block';
hasError = true;
} else if(sn.length < 6) {
snError.textContent = 'SN至少需要6个字符';
snError.style.display = 'block';
hasError = true;
} else if(!/^[A-Za-z0-9\-_:]+$/.test(sn)) {
snError.textContent = 'SN只能包含字母、数字、横线、下划线或冒号';
snError.style.display = 'block';
hasError = true;
}
// 验证机种
const machineType = machineTypeSelect.value;
if(!machineType) {
machineTypeError.textContent = '请选择机种';
machineTypeError.style.display = 'block';
hasError = true;
}
// 验证不良现象
const defectSymptom = symptomInput.value.trim();
if(!defectSymptom) {
symptomError.textContent = '请输入不良现象';
symptomError.style.display = 'block';
hasError = true;
}
// 验证不良原因
const defectReason = defectInput.value.trim();
if(!defectReason) {
defectError.textContent = '请输入不良原因';
defectError.style.display = 'block';
hasError = true;
}
// 验证维修方法:非"无不良"时必填
const repairMethod = methodInput.value.trim();
if(defectReason && defectReason !== '无不良' && !repairMethod) {
methodError.textContent = '选择了不良原因时,维修方法为必填项';
methodError.style.display = 'block';
hasError = true;
}
if(hasError) return;
// 构建FormData
const formData = new FormData();
formData.append('sn', sn);
formData.append('machine_type', machineType);
formData.append('defect_symptom', defectSymptom);
formData.append('defect_reason', defectReason);
formData.append('repair_method', methodInput.value.trim());
// 添加图片
pendingImages.forEach(file => {
formData.append('images', file);
});
// 显示加载状态
btn.disabled = true;
uploadText.style.display = 'none';
uploadLoading.style.display = 'inline-flex';
try {
const result = await API.uploadRepairRecord(formData);
if(result.ok) {
API.toast('提交成功');
// 清空表单
snInput.value = '';
machineTypeSelect.value = '';
symptomInput.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
pendingImages = [];
previewContainer.innerHTML = '';
} else {
API.toast(result.error || '提交失败');
}
} catch(e) {
API.toast('提交失败: ' + (e.message || '网络错误'));
} finally {
btn.disabled = false;
uploadText.style.display = 'inline';
uploadLoading.style.display = 'none';
}
});
}
if(!isHistoryPage) return;
// 获取当前用户角色
let userRole = window.__userRole || localStorage.getItem('userRole') || '';
@ -1007,9 +1301,14 @@ const Upload = (() => {
// 更新选中计数
const updateSelectedCount = () => {
const countEl = document.getElementById('repairs-selected-count');
const deleteBtn = document.getElementById('repairs-delete-selected');
if(countEl) {
countEl.textContent = selectedIds.size > 0 ? `已选 ${selectedIds.size}` : '';
}
// 显示/隐藏删除按钮
if(deleteBtn) {
deleteBtn.style.display = selectedIds.size > 0 ? 'inline-block' : 'none';
}
};
// 渲染单条记录的HTML
@ -1019,7 +1318,7 @@ const Upload = (() => {
const isChecked = selectedIds.has(r.id);
return `
<li style="display:flex;gap:10px;padding:10px 0;border-bottom:1px solid var(--border)" data-id="${r.id}">
<div style="flex:0 0 auto;padding-top:2px">
<div style="flex:0 0 auto;display:flex;align-items:center">
<input type="checkbox" class="repair-checkbox" data-id="${r.id}" ${isChecked ? 'checked' : ''} style="cursor:pointer;width:16px;height:16px" />
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:4px">
@ -1657,109 +1956,63 @@ const Upload = (() => {
}
});
// 提交按钮
addListener(btn, 'click', async () => {
// 表单验证
let hasError = false;
// 验证SN
const sn = snInput.value.trim();
if(!sn) {
snError.textContent = '请输入设备SN';
snError.style.display = 'block';
hasError = true;
} else if(sn.length < 6) {
snError.textContent = 'SN至少需要6个字符';
snError.style.display = 'block';
hasError = true;
} else if(!/^[A-Za-z0-9\-_:]+$/.test(sn)) {
snError.textContent = 'SN只能包含字母、数字、横线、下划线或冒号';
snError.style.display = 'block';
hasError = true;
// 删除选中按钮
const deleteBtn = document.getElementById('repairs-delete-selected');
addListener(deleteBtn, 'click', async () => {
if(selectedIds.size === 0) {
API.toast('请先勾选要删除的记录');
return;
}
// 验证机种
const machineType = machineTypeSelect.value;
if(!machineType) {
machineTypeError.textContent = '请选择机种';
machineTypeError.style.display = 'block';
hasError = true;
}
// 创建确认弹窗
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
modal.innerHTML = `
<div style="background:var(--surface);border-radius:12px;width:90%;max-width:400px;box-shadow:0 4px 20px rgba(0,0,0,0.15)">
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<h3 style="margin:0;font-size:16px;color:var(--danger)">确认删除</h3>
<button id="delete-modal-close" style="background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-2)">&times;</button>
</div>
<div style="padding:20px">
<div style="margin-bottom:16px;font-size:14px;color:var(--text-2)">
确定要删除选中的 <strong>${selectedIds.size}</strong>
</div>
<div style="margin-bottom:16px;font-size:12px;color:var(--warning)">
此操作不可恢复请谨慎操作
</div>
<div style="display:flex;gap:12px">
<button class="btn" id="delete-confirm" style="flex:1;background:var(--danger);color:white">确定删除</button>
<button class="btn btn-secondary" id="delete-cancel">取消</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// 验证不良现象
const defectSymptom = symptomInput.value.trim();
if(!defectSymptom) {
symptomError.textContent = '请输入不良现象';
symptomError.style.display = 'block';
hasError = true;
}
// 验证不良原因
const defectReason = defectInput.value.trim();
if(!defectReason) {
defectError.textContent = '请输入不良原因';
defectError.style.display = 'block';
hasError = true;
}
// 验证维修方法:非"无不良"时必填
const repairMethod = methodInput.value.trim();
if(defectReason && defectReason !== '无不良' && !repairMethod) {
methodError.textContent = '选择了不良原因时,维修方法为必填项';
methodError.style.display = 'block';
hasError = true;
}
if(hasError) return;
// 构建FormData
const formData = new FormData();
formData.append('sn', sn);
formData.append('machine_type', machineType);
formData.append('defect_symptom', defectSymptom);
formData.append('defect_reason', defectReason);
formData.append('repair_method', methodInput.value.trim());
// 添加图片
pendingImages.forEach(file => {
formData.append('images', file);
});
// 显示加载状态
btn.disabled = true;
uploadText.style.display = 'none';
uploadLoading.style.display = 'inline-flex';
try {
const result = await API.uploadRepairRecord(formData);
if(result.ok) {
API.toast('提交成功');
document.getElementById('delete-modal-close').onclick = () => modal.remove();
document.getElementById('delete-cancel').onclick = () => modal.remove();
document.getElementById('delete-confirm').onclick = async () => {
try {
deleteBtn.disabled = true;
deleteBtn.textContent = '删除中...';
// 清空表单
snInput.value = '';
machineTypeSelect.value = '';
symptomInput.value = '';
defectInput.value = '';
methodInput.value = '';
methodGroup.style.opacity = '0.5';
methodInput.disabled = true;
methodRequired.style.display = 'none';
methodInput.placeholder = '请描述维修方法';
pendingImages = [];
previewContainer.innerHTML = '';
// 刷新列表
await refreshRepairsList();
} else {
API.toast(result.error || '提交失败');
const result = await API.batchDeleteRepairRecords(Array.from(selectedIds));
if(result.ok) {
API.toast(`成功删除 ${result.deleted} 条记录`);
modal.remove();
selectedIds.clear();
updateSelectedCount();
refreshRepairsList();
} else {
API.toast(result.error || '删除失败');
}
} catch(e) {
API.toast('删除失败: ' + (e.message || '网络错误'));
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = '删除选中';
}
} catch(e) {
API.toast('提交失败: ' + (e.message || '网络错误'));
} finally {
btn.disabled = false;
uploadText.style.display = 'inline';
uploadLoading.style.display = 'none';
}
};
});
// 初始加载最新记录

View File

@ -73,6 +73,7 @@ const Router = (() => {
stats: '良/不良统计',
defects: '不良明细',
repairs: '返修记录',
'repairs-history': '返修历史',
shipments: '发货',
sop: 'SOP',
query: '详细记录查询',

View File

@ -624,6 +624,23 @@ def index_html():
return send_from_directory(FRONTEND_DIR, 'index.html')
@app.route('/assets/avatars/<filename>')
def serve_avatar(filename):
"""Serve avatar files with cache headers"""
avatars_dir = os.path.join(FRONTEND_DIR, 'assets', 'avatars')
filepath = os.path.join(avatars_dir, secure_filename(filename))
if not os.path.exists(filepath):
return jsonify({'error': 'Avatar not found'}), 404
# Serve with cache headers - cache for 1 day (86400 seconds)
response = send_file(filepath)
response.headers['Cache-Control'] = 'public, max-age=86400'
response.headers['ETag'] = f'"{os.path.getmtime(filepath)}"'
return response
# auth
@app.get('/api/auth/captcha')
def captcha():
@ -2424,7 +2441,7 @@ def export_pdf():
def list_mac():
conn = get_db()
c = conn.cursor()
c.execute('SELECT mac, batch, platform, ts FROM mac_batches ORDER BY id DESC LIMIT 200')
c.execute('SELECT mac, batch, platform, ts FROM mac_batches ORDER BY id DESC')
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'list': rows})

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB