优化
This commit is contained in:
parent
512440a2c1
commit
8133faf65f
@ -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)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">×</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);
|
||||
})();
|
||||
327
frontend/js/components/menu-search.js
Normal file
327
frontend/js/components/menu-search.js
Normal 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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@ -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] 已清理');
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 '—';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)">×</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';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 初始加载最新记录
|
||||
|
||||
@ -73,6 +73,7 @@ const Router = (() => {
|
||||
stats: '良/不良统计',
|
||||
defects: '不良明细',
|
||||
repairs: '返修记录',
|
||||
'repairs-history': '返修历史',
|
||||
shipments: '发货',
|
||||
sop: 'SOP',
|
||||
query: '详细记录查询',
|
||||
|
||||
@ -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 |
Loading…
Reference in New Issue
Block a user