3299 lines
108 KiB
HTML
3299 lines
108 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>USER 后台 ERP MVP · 管理员总览原型 v7</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f4f6f8;
|
||
--panel: #ffffff;
|
||
--panel-soft: #f8fafc;
|
||
--line: #d9e1e8;
|
||
--line-strong: #b9c6d2;
|
||
--text: #1f2933;
|
||
--muted: #66737f;
|
||
--blue: #2563eb;
|
||
--blue-soft: #eaf1ff;
|
||
--green: #188260;
|
||
--green-soft: #e7f7f0;
|
||
--red: #c93636;
|
||
--red-soft: #fdecec;
|
||
--amber: #b56a16;
|
||
--amber-soft: #fff4df;
|
||
--purple: #725ac1;
|
||
--purple-soft: #f0ecff;
|
||
--shadow: 0 18px 44px rgba(24, 39, 61, 0.12);
|
||
--radius: 8px;
|
||
--sidebar: #17212b;
|
||
--sidebar-soft: #22313f;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
button,
|
||
input,
|
||
select,
|
||
textarea {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.app-shell {
|
||
min-height: 100vh;
|
||
display: grid;
|
||
grid-template-columns: 236px minmax(0, 1fr);
|
||
}
|
||
|
||
.sidebar {
|
||
background: var(--sidebar);
|
||
color: #e8eef5;
|
||
padding: 18px 14px;
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 4px 8px 16px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.brand-mark {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 8px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: #2f6fca;
|
||
color: #fff;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.brand-title {
|
||
font-weight: 800;
|
||
font-size: 15px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
color: #a8b5c2;
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.nav {
|
||
display: grid;
|
||
gap: 6px;
|
||
overflow: auto;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
.nav-button {
|
||
width: 100%;
|
||
height: 42px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #d8e2ec;
|
||
display: grid;
|
||
grid-template-columns: 28px 1fr auto;
|
||
align-items: center;
|
||
text-align: left;
|
||
padding: 0 10px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.nav-button:hover,
|
||
.nav-button.active {
|
||
background: var(--sidebar-soft);
|
||
color: #fff;
|
||
}
|
||
|
||
.nav-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||
border-radius: 6px;
|
||
display: grid;
|
||
place-items: center;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
color: #dce7f2;
|
||
}
|
||
|
||
.nav-count {
|
||
min-width: 22px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
color: #d7e4ef;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
.sidebar-todo {
|
||
margin-top: auto;
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
padding: 10px;
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.sidebar-todo-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
color: #f5f9ff;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.sidebar-todo-head strong {
|
||
min-width: 26px;
|
||
height: 22px;
|
||
border-radius: 999px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--red);
|
||
color: #fff;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sidebar-todo-row {
|
||
min-height: 30px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: rgba(255, 255, 255, 0.07);
|
||
color: #dce7f2;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 8px;
|
||
text-align: left;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sidebar-todo-row:hover,
|
||
.sidebar-todo-row.primary {
|
||
background: rgba(37, 99, 235, 0.34);
|
||
color: #fff;
|
||
}
|
||
|
||
.sidebar-todo-row strong {
|
||
color: #fff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.main {
|
||
min-width: 0;
|
||
display: grid;
|
||
grid-template-rows: 58px minmax(0, 1fr);
|
||
}
|
||
|
||
.topbar {
|
||
background: var(--panel);
|
||
border-bottom: 1px solid var(--line);
|
||
display: grid;
|
||
grid-template-columns: minmax(300px, 1fr) auto;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding: 8px 18px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
min-height: 58px;
|
||
}
|
||
|
||
.top-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.top-title {
|
||
min-width: 138px;
|
||
}
|
||
|
||
.top-title strong {
|
||
display: block;
|
||
font-size: 16px;
|
||
line-height: 1.2;
|
||
color: #152233;
|
||
}
|
||
|
||
.top-title span {
|
||
display: block;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.search {
|
||
width: min(240px, 22vw);
|
||
height: 34px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel-soft);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
gap: 8px;
|
||
color: var(--muted);
|
||
flex-shrink: 1;
|
||
}
|
||
|
||
.search span {
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.search input {
|
||
width: 100%;
|
||
border: 0;
|
||
background: transparent;
|
||
outline: 0;
|
||
color: var(--text);
|
||
}
|
||
|
||
.top-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.role-select,
|
||
.scope-select {
|
||
height: 32px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
border-radius: 8px;
|
||
padding: 0 10px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.scope-select {
|
||
width: 116px;
|
||
}
|
||
|
||
.role-select {
|
||
width: 164px;
|
||
}
|
||
|
||
.top-time {
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 0 6px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.top-time input {
|
||
width: 98px;
|
||
height: 28px;
|
||
border: 0;
|
||
outline: 0;
|
||
color: var(--text);
|
||
background: transparent;
|
||
}
|
||
|
||
.top-period {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
height: 32px;
|
||
padding: 3px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.top-period button {
|
||
height: 24px;
|
||
min-width: 28px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font-weight: 800;
|
||
padding: 0 7px;
|
||
}
|
||
|
||
.top-period button.active {
|
||
background: var(--blue);
|
||
color: #fff;
|
||
}
|
||
|
||
.content {
|
||
padding: 14px 24px 34px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.page-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 18px;
|
||
margin-bottom: 12px;
|
||
padding: 2px 0 4px;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 19px;
|
||
line-height: 1.2;
|
||
color: #182331;
|
||
}
|
||
|
||
.page-note {
|
||
margin-top: 6px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.button-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
min-height: 34px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
color: var(--text);
|
||
border-radius: 8px;
|
||
padding: 0 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn:hover {
|
||
border-color: var(--line-strong);
|
||
background: #f9fbfd;
|
||
}
|
||
|
||
.btn.primary {
|
||
background: var(--blue);
|
||
color: #fff;
|
||
border-color: var(--blue);
|
||
}
|
||
|
||
.btn.danger {
|
||
background: var(--red);
|
||
color: #fff;
|
||
border-color: var(--red);
|
||
}
|
||
|
||
.btn.warning {
|
||
background: var(--amber);
|
||
color: #fff;
|
||
border-color: var(--amber);
|
||
}
|
||
|
||
.btn.ghost {
|
||
background: transparent;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.kpi-grid {
|
||
grid-template-columns: repeat(4, minmax(170px, 1fr));
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.kpi-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
padding: 14px;
|
||
min-height: 118px;
|
||
display: grid;
|
||
gap: 8px;
|
||
text-align: left;
|
||
transition: border-color 0.16s ease, transform 0.16s ease;
|
||
}
|
||
|
||
.kpi-card:hover {
|
||
border-color: var(--blue);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.kpi-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.kpi-title {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 30px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
color: #172331;
|
||
}
|
||
|
||
.kpi-foot {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.layout-dashboard {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.42fr) minmax(320px, 0.7fr);
|
||
gap: 14px;
|
||
align-items: start;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
min-width: 0;
|
||
}
|
||
|
||
.panel-head {
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.panel-title {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
color: #182331;
|
||
}
|
||
|
||
.panel-note {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 14px 16px 16px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab {
|
||
height: 30px;
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
border-radius: 8px;
|
||
padding: 0 10px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.tab.active {
|
||
background: var(--blue-soft);
|
||
color: var(--blue);
|
||
border-color: #bcd0ff;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: grid;
|
||
grid-template-columns: 1.3fr repeat(2, minmax(128px, 0.7fr)) repeat(4, minmax(126px, 0.8fr)) auto;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.filter-bar input,
|
||
.filter-bar select {
|
||
height: 34px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 0 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.time-toolbar {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.time-controls,
|
||
.period-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.time-toolbar input,
|
||
.time-toolbar select,
|
||
.period-toggle button {
|
||
height: 32px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 0 10px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.period-toggle button.active {
|
||
background: var(--blue-soft);
|
||
color: var(--blue);
|
||
border-color: #bcd0ff;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.trend-line {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
display: grid;
|
||
grid-template-columns: repeat(3, max-content);
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.spark {
|
||
display: flex;
|
||
align-items: end;
|
||
gap: 3px;
|
||
height: 24px;
|
||
min-width: 58px;
|
||
}
|
||
|
||
.spark i {
|
||
display: block;
|
||
width: 7px;
|
||
border-radius: 3px 3px 0 0;
|
||
background: #9db5d0;
|
||
}
|
||
|
||
.insight-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.insight-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
display: grid;
|
||
gap: 8px;
|
||
min-height: 112px;
|
||
}
|
||
|
||
.insight-title {
|
||
font-weight: 800;
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.insight-value {
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
}
|
||
|
||
.insight-desc {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.module-charts {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.chart-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chart-title {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.mini-stat-row {
|
||
display: grid;
|
||
grid-template-columns: 78px minmax(0, 1fr) 44px;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.pending-strip {
|
||
background: #fff7ed;
|
||
border: 1px solid #f3cf9b;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
color: #7d4a0b;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.executive-hero {
|
||
display: grid;
|
||
grid-template-columns: minmax(360px, 0.95fr) minmax(0, 1.55fr);
|
||
gap: 14px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.focus-card {
|
||
background: #111827;
|
||
color: #eef5ff;
|
||
border-radius: 8px;
|
||
padding: 18px;
|
||
display: grid;
|
||
gap: 16px;
|
||
min-height: 260px;
|
||
}
|
||
|
||
.focus-eyebrow {
|
||
color: #a8b3c2;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.focus-title {
|
||
font-size: 21px;
|
||
line-height: 1.32;
|
||
font-weight: 800;
|
||
margin: 0;
|
||
}
|
||
|
||
.health-score {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
align-items: end;
|
||
gap: 14px;
|
||
}
|
||
|
||
.health-number {
|
||
font-size: 54px;
|
||
font-weight: 900;
|
||
line-height: 0.95;
|
||
}
|
||
|
||
.health-copy {
|
||
color: #cbd5e1;
|
||
font-size: 13px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.focus-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.focus-metric {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.focus-metric strong {
|
||
display: block;
|
||
font-size: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.focus-metric span {
|
||
color: #aebbc9;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.decision-panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.decision-list {
|
||
display: grid;
|
||
gap: 10px;
|
||
padding: 14px;
|
||
}
|
||
|
||
.decision-row {
|
||
display: grid;
|
||
grid-template-columns: 92px minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 12px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
background: #fff;
|
||
}
|
||
|
||
.decision-row.critical {
|
||
border-color: #f1b5b5;
|
||
background: #fff8f8;
|
||
}
|
||
|
||
.decision-row.warning {
|
||
border-color: #efd39b;
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.decision-title {
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.decision-desc {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.executive-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.executive-card {
|
||
border: 1px solid var(--line);
|
||
background: var(--panel);
|
||
border-radius: 8px;
|
||
padding: 14px;
|
||
min-height: 158px;
|
||
display: grid;
|
||
gap: 10px;
|
||
text-align: left;
|
||
color: var(--text);
|
||
}
|
||
|
||
.executive-card.primary {
|
||
border-color: #b9cdfb;
|
||
background: #f7faff;
|
||
}
|
||
|
||
.executive-card.risk {
|
||
border-color: #efb6b6;
|
||
background: #fff8f8;
|
||
}
|
||
|
||
.executive-card.warn {
|
||
border-color: #efd39b;
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.executive-card.good {
|
||
border-color: #b8e4d0;
|
||
background: #f6fffb;
|
||
}
|
||
|
||
.executive-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.executive-value {
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
line-height: 1.05;
|
||
color: #162335;
|
||
}
|
||
|
||
.period-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 6px;
|
||
}
|
||
|
||
.period-stat {
|
||
border: 1px solid var(--line);
|
||
border-radius: 7px;
|
||
padding: 7px;
|
||
background: rgba(255, 255, 255, 0.62);
|
||
}
|
||
|
||
.period-stat strong {
|
||
display: block;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.period-stat span {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.ops-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.ops-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
display: grid;
|
||
gap: 8px;
|
||
min-height: 112px;
|
||
text-align: left;
|
||
color: var(--text);
|
||
}
|
||
|
||
.ops-name {
|
||
font-weight: 800;
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ops-status {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.drilldown-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.drilldown-button {
|
||
min-height: 44px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
color: var(--text);
|
||
padding: 0 10px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.drilldown-button:hover {
|
||
border-color: #b7c5d3;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.command-board {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
margin-bottom: 8px;
|
||
overflow: hidden;
|
||
display: grid;
|
||
grid-template-columns: 124px minmax(0, 1fr) 104px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.command-board-head {
|
||
min-height: 58px;
|
||
padding: 8px 10px;
|
||
display: grid;
|
||
align-content: center;
|
||
gap: 2px;
|
||
border-right: 1px solid var(--line);
|
||
background: #f9fbfd;
|
||
}
|
||
|
||
.command-board-title {
|
||
display: grid;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.command-board-title h2 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
line-height: 1.15;
|
||
color: #172234;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.command-board-title span {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.command-board-actions {
|
||
min-height: 58px;
|
||
padding: 7px;
|
||
border-left: 1px solid var(--line);
|
||
display: grid;
|
||
align-content: center;
|
||
gap: 5px;
|
||
background: #fbfcfe;
|
||
}
|
||
|
||
.command-status-strip {
|
||
display: grid;
|
||
grid-template-columns: minmax(150px, 1.15fr) minmax(150px, 1.1fr) repeat(4, minmax(112px, 0.78fr));
|
||
gap: 1px;
|
||
background: var(--line);
|
||
}
|
||
|
||
.status-unit {
|
||
min-height: 58px;
|
||
border: 0;
|
||
border-radius: 0;
|
||
background: #fff;
|
||
padding: 8px 10px;
|
||
display: grid;
|
||
gap: 4px;
|
||
text-align: left;
|
||
}
|
||
|
||
.status-unit.alert {
|
||
background: #fff8f8;
|
||
}
|
||
|
||
.status-unit.warning {
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.status-unit.goal {
|
||
background: #f7fbff;
|
||
}
|
||
|
||
.status-unit.featured {
|
||
min-height: 58px;
|
||
}
|
||
|
||
.command-status-strip.is-collapsed .status-unit.optional {
|
||
display: none;
|
||
}
|
||
|
||
.status-name {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.status-value {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
font-size: 20px;
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
color: #162335;
|
||
}
|
||
|
||
.status-value small {
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
text-align: right;
|
||
}
|
||
|
||
.status-progress {
|
||
height: 5px;
|
||
border-radius: 999px;
|
||
background: #e8eef4;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.status-progress span {
|
||
display: block;
|
||
height: 100%;
|
||
width: var(--progress, 0%);
|
||
border-radius: inherit;
|
||
background: var(--blue);
|
||
}
|
||
|
||
.command-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 360px;
|
||
gap: 12px;
|
||
align-items: start;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.panel-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.compact-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.compact-tabs button {
|
||
height: 28px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
color: var(--muted);
|
||
padding: 0 9px;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.compact-tabs button.active {
|
||
background: var(--blue-soft);
|
||
border-color: #bcd0ff;
|
||
color: var(--blue);
|
||
}
|
||
|
||
.priority-table table {
|
||
min-width: 920px;
|
||
}
|
||
|
||
.priority-table th,
|
||
.priority-table td {
|
||
padding: 10px 9px;
|
||
}
|
||
|
||
.row-title {
|
||
font-weight: 800;
|
||
color: #182331;
|
||
}
|
||
|
||
.row-sub {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.side-stack {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.side-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.side-row {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
display: grid;
|
||
gap: 7px;
|
||
background: #fff;
|
||
}
|
||
|
||
.side-row.alert {
|
||
border-color: #efb6b6;
|
||
background: #fff8f8;
|
||
}
|
||
|
||
.side-row.warning {
|
||
border-color: #efd39b;
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.side-row-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
font-weight: 800;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.side-row-meta {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.matrix-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 440px;
|
||
gap: 12px;
|
||
align-items: start;
|
||
}
|
||
|
||
.matrix-table table {
|
||
min-width: 720px;
|
||
}
|
||
|
||
.channel-table table {
|
||
min-width: 520px;
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 1100px;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 11px 10px;
|
||
text-align: left;
|
||
font-size: 13px;
|
||
vertical-align: middle;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
th {
|
||
color: #536170;
|
||
font-weight: 800;
|
||
background: #fbfcfe;
|
||
}
|
||
|
||
tr:hover td {
|
||
background: #fbfdff;
|
||
}
|
||
|
||
.tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 22px;
|
||
border-radius: 999px;
|
||
padding: 0 8px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.tag.blue {
|
||
background: var(--blue-soft);
|
||
color: var(--blue);
|
||
border-color: #c7d8ff;
|
||
}
|
||
|
||
.tag.green {
|
||
background: var(--green-soft);
|
||
color: var(--green);
|
||
border-color: #bde8d5;
|
||
}
|
||
|
||
.tag.red {
|
||
background: var(--red-soft);
|
||
color: var(--red);
|
||
border-color: #f7bdbd;
|
||
}
|
||
|
||
.tag.amber {
|
||
background: var(--amber-soft);
|
||
color: var(--amber);
|
||
border-color: #f1d39b;
|
||
}
|
||
|
||
.tag.purple {
|
||
background: var(--purple-soft);
|
||
color: var(--purple);
|
||
border-color: #d8ccff;
|
||
}
|
||
|
||
.tag.gray {
|
||
background: #f0f3f6;
|
||
color: #61707f;
|
||
border-color: #dbe2e8;
|
||
}
|
||
|
||
.mini-btn {
|
||
height: 28px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--line);
|
||
background: #fff;
|
||
color: var(--text);
|
||
padding: 0 8px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.mini-btn.primary {
|
||
color: var(--blue);
|
||
border-color: #bfd0ff;
|
||
background: var(--blue-soft);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.mini-btn.danger {
|
||
color: var(--red);
|
||
border-color: #f2b8b8;
|
||
background: var(--red-soft);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stack {
|
||
display: grid;
|
||
gap: 14px;
|
||
}
|
||
|
||
.risk-list,
|
||
.activity-list,
|
||
.suggest-list {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
.risk-item,
|
||
.activity-item,
|
||
.suggest-item {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
display: grid;
|
||
gap: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.risk-item.high {
|
||
border-color: #f4b4b4;
|
||
background: #fff8f8;
|
||
}
|
||
|
||
.risk-item.mid {
|
||
border-color: #efd093;
|
||
background: #fffaf0;
|
||
}
|
||
|
||
.item-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
font-weight: 800;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.item-desc {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.chart-row {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.bar-line {
|
||
display: grid;
|
||
grid-template-columns: 72px minmax(0, 1fr) 46px;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.bar-track {
|
||
height: 10px;
|
||
background: #edf1f5;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bar-fill {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
background: var(--blue);
|
||
}
|
||
|
||
.summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.summary-cell {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
padding: 12px;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.summary-label {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.empty {
|
||
padding: 28px;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
border: 1px dashed var(--line);
|
||
border-radius: 8px;
|
||
background: #fbfcfe;
|
||
}
|
||
|
||
.drawer-mask,
|
||
.modal-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(10, 18, 28, 0.36);
|
||
z-index: 80;
|
||
display: none;
|
||
}
|
||
|
||
.drawer-mask.open,
|
||
.modal-mask.open {
|
||
display: block;
|
||
}
|
||
|
||
.drawer {
|
||
position: fixed;
|
||
right: 0;
|
||
top: 0;
|
||
height: 100vh;
|
||
width: min(680px, 92vw);
|
||
background: var(--panel);
|
||
box-shadow: var(--shadow);
|
||
z-index: 90;
|
||
transform: translateX(102%);
|
||
transition: transform 0.18s ease;
|
||
display: grid;
|
||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||
}
|
||
|
||
.drawer.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.drawer-head,
|
||
.drawer-foot {
|
||
padding: 16px 18px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.drawer-foot {
|
||
border-bottom: 0;
|
||
border-top: 1px solid var(--line);
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.drawer-body {
|
||
overflow: auto;
|
||
padding: 16px 18px;
|
||
display: grid;
|
||
gap: 14px;
|
||
align-content: start;
|
||
}
|
||
|
||
.detail-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.detail-cell {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.detail-label {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
.timeline {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.timeline-item {
|
||
border-left: 3px solid var(--blue);
|
||
padding: 4px 0 4px 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.modal {
|
||
position: fixed;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: min(560px, 92vw);
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow);
|
||
z-index: 100;
|
||
display: none;
|
||
}
|
||
|
||
.modal.open {
|
||
display: block;
|
||
}
|
||
|
||
.modal-head,
|
||
.modal-foot {
|
||
padding: 15px 16px;
|
||
border-bottom: 1px solid var(--line);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-foot {
|
||
border-bottom: 0;
|
||
border-top: 1px solid var(--line);
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 16px;
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.form-row label {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.form-row input,
|
||
.form-row select,
|
||
.form-row textarea {
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
min-height: 36px;
|
||
padding: 8px 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
.form-row textarea {
|
||
min-height: 88px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
right: 18px;
|
||
bottom: 18px;
|
||
z-index: 120;
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toast-item {
|
||
width: min(360px, calc(100vw - 36px));
|
||
background: #172331;
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
box-shadow: var(--shadow);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.route-page {
|
||
display: none;
|
||
}
|
||
|
||
.route-page.active {
|
||
display: block;
|
||
}
|
||
|
||
.source-note {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
@media (max-width: 1180px) {
|
||
.app-shell {
|
||
grid-template-columns: 78px minmax(0, 1fr);
|
||
}
|
||
|
||
.brand-title,
|
||
.brand-subtitle,
|
||
.nav-label,
|
||
.nav-count,
|
||
.sidebar-todo {
|
||
display: none;
|
||
}
|
||
|
||
.nav-button {
|
||
grid-template-columns: 1fr;
|
||
justify-items: center;
|
||
padding: 0;
|
||
}
|
||
|
||
.layout-dashboard {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.kpi-grid {
|
||
grid-template-columns: repeat(2, minmax(170px, 1fr));
|
||
}
|
||
|
||
.insight-grid,
|
||
.module-charts,
|
||
.executive-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.executive-hero {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.ops-strip,
|
||
.drilldown-grid,
|
||
.command-status-strip {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.command-board {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.command-board-head,
|
||
.command-board-actions {
|
||
min-height: auto;
|
||
border-right: 0;
|
||
border-left: 0;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.command-grid,
|
||
.matrix-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 780px) {
|
||
.app-shell {
|
||
display: block;
|
||
}
|
||
|
||
.sidebar {
|
||
position: static;
|
||
height: auto;
|
||
}
|
||
|
||
.nav {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.main {
|
||
display: block;
|
||
}
|
||
|
||
.topbar {
|
||
position: static;
|
||
height: auto;
|
||
grid-template-columns: 1fr;
|
||
padding: 12px;
|
||
}
|
||
|
||
.top-left {
|
||
width: 100%;
|
||
display: grid;
|
||
}
|
||
|
||
.search {
|
||
width: 100%;
|
||
}
|
||
|
||
.content {
|
||
padding: 16px 12px 24px;
|
||
}
|
||
|
||
.page-head,
|
||
.top-actions {
|
||
display: grid;
|
||
width: 100%;
|
||
}
|
||
|
||
.kpi-grid,
|
||
.summary-grid,
|
||
.detail-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.filter-bar {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.insight-grid,
|
||
.module-charts,
|
||
.executive-grid,
|
||
.ops-strip,
|
||
.drilldown-grid,
|
||
.command-status-strip,
|
||
.command-grid,
|
||
.matrix-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.decision-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<div class="brand-mark">U</div>
|
||
<div>
|
||
<div class="brand-title">USER 后台 ERP</div>
|
||
<div class="brand-subtitle">MVP 一期 v7 · 模拟数据</div>
|
||
</div>
|
||
</div>
|
||
<nav class="nav" id="nav"></nav>
|
||
<section class="sidebar-todo" aria-label="固定待办提醒">
|
||
<div class="sidebar-todo-head">
|
||
<span>待办提醒</span>
|
||
<strong>21</strong>
|
||
</div>
|
||
<button class="sidebar-todo-row primary" data-route="dashboard" data-tab="all">
|
||
<span>重要事项</span><strong>3</strong>
|
||
</button>
|
||
<button class="sidebar-todo-row" data-route="plans" data-tab="approval">
|
||
<span>审核类</span><strong>4</strong>
|
||
</button>
|
||
<button class="sidebar-todo-row" data-route="listings" data-tab="emergency">
|
||
<span>紧急 Listing</span><strong>7</strong>
|
||
</button>
|
||
<button class="sidebar-todo-row" data-route="reports" data-tab="issue_summary">
|
||
<span>问题总结</span><strong>9</strong>
|
||
</button>
|
||
</section>
|
||
</aside>
|
||
|
||
<main class="main">
|
||
<header class="topbar">
|
||
<div class="top-left">
|
||
<div class="top-title">
|
||
<strong id="topPageTitle">经营总览</strong>
|
||
<span id="topPageSubtitle">系统管理员 · 最高权限 · 全部部门</span>
|
||
</div>
|
||
<div class="search">
|
||
<span>搜索</span>
|
||
<input id="globalSearch" placeholder="ASIN / 需求 / 用户 / 负责人" />
|
||
</div>
|
||
</div>
|
||
<div class="top-actions">
|
||
<div class="top-time" title="时间范围">
|
||
<input type="date" value="2026-05-01" data-time="startDate" />
|
||
<span>至</span>
|
||
<input type="date" value="2026-05-03" data-time="endDate" />
|
||
</div>
|
||
<div class="top-period" title="周期切换">
|
||
<button class="active" data-period="day">日</button>
|
||
<button data-period="week">周</button>
|
||
<button data-period="month">月</button>
|
||
</div>
|
||
<select class="scope-select" id="scopeSelect">
|
||
<option value="all">全部部门</option>
|
||
<option value="amazon">Amazon 运营</option>
|
||
<option value="user_ops">用户运营</option>
|
||
<option value="support">客服</option>
|
||
</select>
|
||
<select class="role-select" id="roleSelect">
|
||
<option>系统管理员(最高权限)</option>
|
||
<option>Amazon 运营总监</option>
|
||
<option>用户运营负责人</option>
|
||
<option>客服负责人</option>
|
||
</select>
|
||
<button class="btn" data-action="open-modal" data-modal="notice">通知 12</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="content" id="content"></section>
|
||
</main>
|
||
</div>
|
||
|
||
<div class="drawer-mask" id="drawerMask"></div>
|
||
<aside class="drawer" id="drawer">
|
||
<div class="drawer-head">
|
||
<div>
|
||
<h2 class="panel-title" id="drawerTitle">详情</h2>
|
||
<div class="panel-note" id="drawerSubtitle">模拟数据,敏感字段默认脱敏</div>
|
||
</div>
|
||
<button class="btn" data-action="close-drawer">关闭</button>
|
||
</div>
|
||
<div class="drawer-body" id="drawerBody"></div>
|
||
<div class="drawer-foot" id="drawerFoot"></div>
|
||
</aside>
|
||
|
||
<div class="modal-mask" id="modalMask"></div>
|
||
<section class="modal" id="modal">
|
||
<div class="modal-head">
|
||
<h2 class="panel-title" id="modalTitle">操作确认</h2>
|
||
<button class="btn" data-action="close-modal">关闭</button>
|
||
</div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
<div class="modal-foot" id="modalFoot"></div>
|
||
</section>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
const DATA_SOURCE = "模拟数据:用于本机高保真原型演示,不代表真实业务数据";
|
||
|
||
const routes = [
|
||
{ id: "dashboard", label: "工作台", icon: "DB", count: 12 },
|
||
{ id: "requests", label: "需求中心", icon: "RQ", count: 18 },
|
||
{ id: "listings", label: "Listing 管理", icon: "LS", count: 7 },
|
||
{ id: "plans", label: "计划中心", icon: "PL", count: 12 },
|
||
{ id: "push", label: "推送中心", icon: "PS", count: 24 },
|
||
{ id: "support", label: "客服中心", icon: "CS", count: 9 },
|
||
{ id: "risk", label: "风险中心", icon: "RK", count: 10 },
|
||
{ id: "data", label: "数据中心", icon: "DT", count: 5 },
|
||
{ id: "reports", label: "报表中心", icon: "RP", count: 6 },
|
||
{ id: "system", label: "系统管理", icon: "SY", count: 4 }
|
||
];
|
||
|
||
const state = {
|
||
route: "dashboard",
|
||
activeTab: "all",
|
||
keyword: "",
|
||
scope: "all",
|
||
period: "day",
|
||
startDate: "2026-05-01",
|
||
endDate: "2026-05-03",
|
||
statusExpanded: false,
|
||
statusPriorityFirst: true
|
||
};
|
||
|
||
const records = {
|
||
kpis: [
|
||
{
|
||
id: "approved_review_requests",
|
||
title: "测评需求审核",
|
||
value: "申请 18 / 已批 8",
|
||
tone: "blue",
|
||
desc: "测评需求由 Amazon 运营提交,已批后用户运营接收",
|
||
route: "plans",
|
||
filter: "amazon_approved",
|
||
trend: { day: 8, week: 31, month: 92, risk: "正常" },
|
||
spark: [4, 7, 5, 8, 9, 8]
|
||
},
|
||
{
|
||
id: "push_risk_yesterday",
|
||
title: "渠道推送风险",
|
||
value: "IM 2 / EDM 1",
|
||
tone: "amber",
|
||
desc: "IM、EDM、TEL、App Push 日周月风险与反馈",
|
||
route: "push",
|
||
filter: "risk",
|
||
trend: { day: 3, week: 9, month: 27, risk: "偏高" },
|
||
spark: [2, 3, 4, 5, 3, 6]
|
||
},
|
||
{
|
||
id: "new_fraud_events",
|
||
title: "新增诈骗事件",
|
||
value: "昨 5 / 周 18",
|
||
tone: "red",
|
||
desc: "昨日、近 7 天、近 30 天新增与待同步",
|
||
route: "risk",
|
||
filter: "fraud",
|
||
trend: { day: 5, week: 18, month: 64, risk: "高" },
|
||
spark: [3, 5, 4, 8, 6, 9]
|
||
},
|
||
{
|
||
id: "emergency_listings",
|
||
title: "紧急 Listing",
|
||
value: "新 3 / 未处理 7",
|
||
tone: "red",
|
||
desc: "按日、周、月展示,并合并之前未处理紧急事件",
|
||
route: "listings",
|
||
filter: "emergency",
|
||
trend: { day: 3, week: 11, month: 29, risk: "高" },
|
||
spark: [2, 4, 4, 5, 7, 7]
|
||
},
|
||
{
|
||
id: "pending_approval",
|
||
title: "推广计划与紧急策略",
|
||
value: "日 12 / 周 38",
|
||
tone: "blue",
|
||
desc: "确认需求后新建的推广计划、推送计划和紧急策略跟踪",
|
||
route: "plans",
|
||
filter: "approval",
|
||
trend: { day: 12, week: 38, month: 116, risk: "注意审核积压" },
|
||
spark: [7, 8, 10, 12, 12, 14]
|
||
},
|
||
{
|
||
id: "review_output_trend",
|
||
title: "评价产出趋势",
|
||
value: "日 18 / 周 96",
|
||
tone: "green",
|
||
desc: "真实消费者回评完成、趋势和异常提示",
|
||
route: "reports",
|
||
filter: "review_done",
|
||
trend: { day: 18, week: 96, month: 384, risk: "稳定" },
|
||
spark: [13, 15, 16, 18, 20, 18]
|
||
},
|
||
{
|
||
id: "blacklist_sync_failed",
|
||
title: "黑名单同步严重度",
|
||
value: "失败 2 / 高危 1",
|
||
tone: "red",
|
||
desc: "系统管理员视角看日周月影响,判断问题是否严重",
|
||
route: "risk",
|
||
filter: "sync_failed",
|
||
trend: { day: 2, week: 7, month: 19, risk: "需复核" },
|
||
spark: [1, 1, 3, 2, 4, 2]
|
||
},
|
||
{
|
||
id: "kol_koc_progress",
|
||
title: "KOC/KOL 对接",
|
||
value: "KOC 21 / KOL 6",
|
||
tone: "purple",
|
||
desc: "PR 对外联系、价格、CODE、返点和提款进度",
|
||
route: "push",
|
||
filter: "kol",
|
||
trend: { day: 6, week: 27, month: 84, risk: "2 个逾期" },
|
||
spark: [3, 4, 6, 5, 7, 8]
|
||
},
|
||
{
|
||
id: "ph_ops",
|
||
title: "菲律宾团队管理",
|
||
value: "风险 2 / 缺口 1",
|
||
tone: "amber",
|
||
desc: "日周月工作时长、请假、缺席、人均产出与关键岗位缺口",
|
||
route: "support",
|
||
filter: "ph",
|
||
trend: { day: 2, week: 6, month: 15, risk: "排班风险" },
|
||
spark: [1, 2, 1, 4, 3, 5]
|
||
},
|
||
{
|
||
id: "workflow_blocked",
|
||
title: "审核积压与风险",
|
||
value: "卡点 4",
|
||
tone: "amber",
|
||
desc: "已发现问题汇总到总页面,避免系统管理员挨个查数据",
|
||
route: "plans",
|
||
filter: "blocked",
|
||
trend: { day: 4, week: 13, month: 33, risk: "影响进度" },
|
||
spark: [2, 3, 3, 6, 5, 4]
|
||
}
|
||
],
|
||
workItems: [
|
||
{
|
||
id: "WK-20260503-001",
|
||
source: "Amazon 总监",
|
||
type: "测评需求",
|
||
asin: "B0TES001",
|
||
site: "US",
|
||
stage: "Amazon 已批准",
|
||
owner: "用户运营负责人",
|
||
risk: "中",
|
||
priority: "P1",
|
||
due: "今日 18:00",
|
||
action: "接收",
|
||
status: "amazon_approved",
|
||
submitter: "Amazon 运营 A",
|
||
reviewer: "Amazon 总监",
|
||
approval: "通过",
|
||
sourceForm: "飞书需求表单 DEMO-001",
|
||
summary: "评分 4.46,低于 4.5,需要生成用户互动与真实评价跟踪计划。"
|
||
},
|
||
{
|
||
id: "WK-20260503-002",
|
||
source: "推送中心",
|
||
type: "昨日推送风险",
|
||
asin: "B0TES009",
|
||
site: "UK",
|
||
stage: "风险复核",
|
||
owner: "用户运营组长",
|
||
risk: "高",
|
||
priority: "P0",
|
||
due: "今日 12:00",
|
||
action: "复核",
|
||
status: "risk_review",
|
||
submitter: "推送系统",
|
||
reviewer: "用户运营组长",
|
||
approval: "待复核",
|
||
sourceForm: "推送风险自动单 DEMO-006",
|
||
summary: "昨日推送退订率高于基线,需复核人群、素材和文案。"
|
||
},
|
||
{
|
||
id: "WK-20260503-003",
|
||
source: "客服中心",
|
||
type: "新增诈骗事件",
|
||
asin: "B0TES003",
|
||
site: "DE",
|
||
stage: "待同步黑名单",
|
||
owner: "风险负责人",
|
||
risk: "高",
|
||
priority: "P0",
|
||
due: "今日 14:00",
|
||
action: "审核",
|
||
status: "fraud",
|
||
submitter: "客服 B",
|
||
reviewer: "风险负责人",
|
||
approval: "待审核",
|
||
sourceForm: "客服升级表单 DEMO-003",
|
||
summary: "同一 JOYHUB ID 与多个 Profile ID 关联异常样品申请,邮箱和设备号已脱敏。"
|
||
},
|
||
{
|
||
id: "WK-20260503-004",
|
||
source: "Listing 管理",
|
||
type: "紧急 Listing",
|
||
asin: "B0TES005",
|
||
site: "JP",
|
||
stage: "紧急策略审批",
|
||
owner: "Amazon 运营总监",
|
||
risk: "紧急",
|
||
priority: "P0",
|
||
due: "今日 11:30",
|
||
action: "审批",
|
||
status: "emergency",
|
||
submitter: "Amazon 运营 C",
|
||
reviewer: "Amazon 运营总监",
|
||
approval: "待系统管理员确认",
|
||
sourceForm: "紧急 Listing 表单 DEMO-004",
|
||
summary: "当前评分 4.21,接近 4.2 紧急阈值,需要 Amazon 与用户运营联合策略。"
|
||
},
|
||
{
|
||
id: "WK-20260503-005",
|
||
source: "客服中心",
|
||
type: "差评跟进",
|
||
asin: "B0TES007",
|
||
site: "US",
|
||
stage: "客服升级",
|
||
owner: "客服负责人",
|
||
risk: "中",
|
||
priority: "P1",
|
||
due: "明日 10:00",
|
||
action: "分配",
|
||
status: "support",
|
||
submitter: "Amazon 运营 A",
|
||
reviewer: "客服负责人",
|
||
approval: "通过",
|
||
sourceForm: "飞书客服需求 DEMO-005",
|
||
summary: "用户反馈产品说明理解偏差,需要客服跟进并回传产品改进建议。"
|
||
}
|
||
],
|
||
listings: [
|
||
{ id: "LS-001", asin: "B0TES005", site: "JP", marketplaces: "JP", rating: 4.21, reviews: 138, negative: 12, health: "紧急", grade: "S", owner: "王五", issue: "评价下滑且差评集中", participants: "Amazon C / 用户运营 B / 客服 A", progress: "紧急策略审批" },
|
||
{ id: "LS-002", asin: "B0TES003", site: "DE", marketplaces: "DE", rating: 4.18, reviews: 93, negative: 9, health: "紧急", grade: "A", owner: "赵六", issue: "诈骗疑似叠加差评", participants: "Amazon D / 风险负责人", progress: "客服与风险介入" },
|
||
{ id: "LS-003", asin: "B0TES001", site: "US", marketplaces: "US / CA", rating: 4.46, reviews: 412, negative: 16, health: "补强", grade: "S", owner: "张三", issue: "美国加拿大同 ASIN 联动", participants: "Amazon A / 用户运营负责人", progress: "待生成计划" },
|
||
{ id: "LS-004", asin: "B0TES009", site: "UK", marketplaces: "UK", rating: 4.51, reviews: 188, negative: 5, health: "观察", grade: "B", owner: "李四", issue: "推送退订偏高", participants: "用户运营组长 / 推送运营", progress: "推送风险复核" }
|
||
],
|
||
plans: [
|
||
{ id: "PL-0503-001", type: "真实评价跟踪", requestId: "WK-20260503-001", asin: "B0TES001", site: "US/CA", target: 30, coverage: "已覆盖", channelMix: "IM 40% / EDM 35% / TEL 25%", status: "Amazon 已批准", approver: "Amazon 总监", owner: "用户运营负责人", risk: "中", simulated: true },
|
||
{ id: "PL-0503-002", type: "紧急 Listing 策略", requestId: "WK-20260503-004", asin: "B0TES005", site: "JP", target: 18, coverage: "已覆盖", channelMix: "TEL 45% / IM 35% / EDM 20%", status: "待系统管理员审批", approver: "系统管理员", owner: "Amazon 运营总监", risk: "紧急", simulated: true },
|
||
{ id: "PL-0503-003", type: "周度推送计划", requestId: "WK-20260503-002", asin: "多 ASIN", site: "US/UK", target: 2400, coverage: "部分覆盖", channelMix: "IM 50% / EDM 30% / App 20%", status: "用户负责人待审", approver: "用户运营负责人", owner: "用户运营组长", risk: "中", simulated: true }
|
||
],
|
||
pushes: [
|
||
{ id: "PS-0503-001", plan: "PL-0503-001", channel: "IM", strategy: "购后真实体验回访", h5: "H5-Review-01", assets: "图片 A / 文案 B", audience: "购后 7-21 天用户", sent: 980, click: 246, reply: 132, optout: "0.8%", risk: "低", status: "执行中", optimization: "维持当前策略" },
|
||
{ id: "PS-0502-006", plan: "PL-0503-003", channel: "EDM", strategy: "老用户内容召回", h5: "H5-Story-03", assets: "图片 C / 文案 D", audience: "高互动老用户", sent: 1600, click: 188, reply: 87, optout: "2.9%", risk: "高", status: "风险复核", optimization: "暂停同策略并复盘" },
|
||
{ id: "PS-0502-009", plan: "PL-0503-003", channel: "TEL", strategy: "客服电话回访", h5: "无", assets: "话术 V2", audience: "活动参与用户", sent: 740, click: 0, reply: 66, optout: "无", risk: "中", status: "暂停待审", optimization: "调整拨打时段" },
|
||
{ id: "PS-0503-010", plan: "KOC-PR-001", channel: "PR/KOC", strategy: "对外合作跟进", h5: "H5-KOC-02", assets: "CODE 图 / 返点说明", audience: "KOC 21 / KOL 6", sent: 27, click: 19, reply: 11, optout: "无", risk: "中", status: "价格待确认", optimization: "补齐提款信息" }
|
||
],
|
||
support: [
|
||
{ id: "CS-0503-001", type: "差评跟进", user: "JH-****-9021", asin: "B0TES007", owner: "客服 A", status: "处理中", risk: "中", sla: "6h", avgResponse: "12m", workHours: "6.5h", attendance: "正常", output: "处理 18 / 完成 12" },
|
||
{ id: "CS-0503-002", type: "诈骗疑似", user: "JH-****-7712", asin: "B0TES003", owner: "客服组长", status: "升级风险", risk: "高", sla: "2h", avgResponse: "8m", workHours: "7.2h", attendance: "关键岗缺 1", output: "处理 9 / 升级 3" },
|
||
{ id: "CS-0503-003", type: "承诺配合用户", user: "JH-****-1180", asin: "B0TES001", owner: "客服 B", status: "待回访", risk: "低", sla: "24h", avgResponse: "18m", workHours: "5.8h", attendance: "请假 0.5 天", output: "处理 16 / 完成 10" }
|
||
],
|
||
risks: [
|
||
{ id: "RK-0503-001", type: "诈骗同步", subject: "JH-****-7712", relation: "Profile / 邮箱 / 设备号", status: "待同步黑名单", risk: "高" },
|
||
{ id: "RK-0503-002", type: "黑名单同步失败", subject: "JH-****-2098", relation: "接口超时", status: "失败待重试", risk: "高" },
|
||
{ id: "RK-0503-003", type: "规则提醒", subject: "PS-0502-006", relation: "退订率高于基线", status: "待复核", risk: "中" }
|
||
],
|
||
syncLogs: [
|
||
{ id: "DT-001", source: "Amazon 订单", interval: "10 分钟", last: "2026-05-03 10:20", status: "正常", rows: 1240 },
|
||
{ id: "DT-002", source: "Amazon 评价", interval: "运营稳定更新", last: "2026-05-03 09:50", status: "正常", rows: 216 },
|
||
{ id: "DT-003", source: "黑名单系统", interval: "待接口确认", last: "2026-05-03 09:42", status: "部分失败", rows: 5 },
|
||
{ id: "DT-004", source: "用户画像标签", interval: "规划中", last: "模拟", status: "模拟", rows: 0 }
|
||
],
|
||
reports: [
|
||
{ id: "RP-000", name: "目标完成度与问题总结", owner: "系统管理员", range: "日/周/月", schedule: "实时入口,周月预生成", upload: "数据分析 / OKR / 项目负责人提交", exportable: "是", masked: "默认脱敏" },
|
||
{ id: "RP-001", name: "Listing 健康日报", owner: "系统管理员 / Amazon 总监", range: "日/周/月", schedule: "每日 08:30", upload: "自动生成", exportable: "是", masked: "默认脱敏" },
|
||
{ id: "RP-002", name: "推送效果与风险复盘", owner: "用户运营负责人", range: "日/周/月", schedule: "每日 09:00,周月预生成", upload: "支持上传补充记录", exportable: "是", masked: "默认脱敏" },
|
||
{ id: "RP-003", name: "诈骗同步审计表", owner: "系统管理员 / 风险负责人", range: "近 7/30 天", schedule: "每日 10:00", upload: "人工复核附件", exportable: "是", masked: "强制脱敏" }
|
||
],
|
||
system: [
|
||
{ id: "SY-001", module: "系统授权", status: "待细化", owner: "系统管理员", note: "主管、组长、组员、客服、外部商家分层权限" },
|
||
{ id: "SY-002", module: "新建账号", status: "MVP 可模拟", owner: "系统管理员", note: "按部门、角色、站点、数据范围开通账号" },
|
||
{ id: "SY-003", module: "离职管理", status: "必需", owner: "系统管理员", note: "停用账号、交接任务、回收敏感权限" },
|
||
{ id: "SY-004", module: "权限分配", status: "必需", owner: "系统管理员", note: "导出、审批、查看敏感信息、黑名单同步独立授权" },
|
||
{ id: "SY-005", module: "审计日志", status: "必需", owner: "系统管理员", note: "导出、查看敏感信息、黑名单同步、审批动作" }
|
||
]
|
||
};
|
||
|
||
const tableSchemas = {
|
||
requests: ["需求ID", "类型", "提交人", "审核人", "审核结果", "来源表单", "ASIN/站点", "当前环节", "负责人", "风险", "截止", "操作"],
|
||
listings: ["Listing", "站点组合", "评分", "等级", "评价数", "差评数", "健康状态", "责任人", "问题所在", "参与人员/进度", "操作"],
|
||
plans: ["计划ID", "类型", "关联需求", "ASIN/站点", "覆盖状态", "资源分配", "目标量", "状态", "审批人", "风险", "操作"],
|
||
push: ["推送ID", "计划", "渠道", "策略", "H5/素材", "人群", "发送", "点击", "回复", "退订", "风险", "状态", "操作"],
|
||
support: ["工单ID", "类型", "用户摘要", "ASIN", "负责人", "平均响应", "工作时长", "出勤", "人均产出", "风险", "SLA", "操作"],
|
||
risk: ["事件ID", "类型", "主体摘要", "关联字段", "状态", "风险", "操作"],
|
||
data: ["日志ID", "来源", "同步频率", "最近同步", "状态", "记录数", "操作"],
|
||
reports: ["报表ID", "报表名称", "可见角色", "周期", "生成计划", "上传/记录", "可导出", "脱敏", "操作"],
|
||
system: ["配置ID", "模块", "状态", "负责人", "说明", "操作"]
|
||
};
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function toneClass(value) {
|
||
if (["高", "紧急", "失败", "部分失败"].includes(value)) return "red";
|
||
if (["中", "待细化", "暂停待审", "风险复核"].includes(value)) return "amber";
|
||
if (["低", "正常", "已完成", "执行中"].includes(value)) return "green";
|
||
if (["模拟", "MVP 可模拟"].includes(value)) return "gray";
|
||
return "blue";
|
||
}
|
||
|
||
function tag(value, tone) {
|
||
return `<span class="tag ${tone || toneClass(value)}">${escapeHtml(value)}</span>`;
|
||
}
|
||
|
||
function setRoute(route, tab = "all") {
|
||
state.route = route;
|
||
state.activeTab = tab;
|
||
window.location.hash = `${route}${tab !== "all" ? `:${tab}` : ""}`;
|
||
render();
|
||
}
|
||
|
||
function renderNav() {
|
||
const nav = document.getElementById("nav");
|
||
nav.innerHTML = routes.map((route) => `
|
||
<button class="nav-button ${state.route === route.id ? "active" : ""}" data-route="${route.id}">
|
||
<span class="nav-icon">${route.icon}</span>
|
||
<span class="nav-label">${route.label}</span>
|
||
<span class="nav-count">${route.count}</span>
|
||
</button>
|
||
`).join("");
|
||
}
|
||
|
||
function renderHeader(title, note, actions = "") {
|
||
return `
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>${title}</h1>
|
||
<div class="page-note">${note}</div>
|
||
</div>
|
||
<div class="button-row">${actions}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSourceNote() {
|
||
return `
|
||
<div class="source-note">
|
||
${tag("模拟数据", "gray")}
|
||
<span>${DATA_SOURCE}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderTimeToolbar() {
|
||
return `
|
||
<div class="time-toolbar">
|
||
<div class="time-controls">
|
||
<strong>时间范围</strong>
|
||
<input type="date" value="${state.startDate}" data-time="startDate" />
|
||
<span>至</span>
|
||
<input type="date" value="${state.endDate}" data-time="endDate" />
|
||
<select data-time="preset">
|
||
<option>最近 7 天</option>
|
||
<option>最近 30 天</option>
|
||
<option>本月</option>
|
||
<option>自定义</option>
|
||
</select>
|
||
</div>
|
||
<div class="period-toggle">
|
||
${["day", "week", "month"].map((period) => `
|
||
<button class="${state.period === period ? "active" : ""}" data-period="${period}">
|
||
${period === "day" ? "日" : period === "week" ? "周" : "月"}
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
<div class="source-note">
|
||
${tag("周/月预生成", "gray")}
|
||
<span>周、月数据可由后台异步预生成,降低页面访问卡顿。</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderSpark(values) {
|
||
const max = Math.max(...values, 1);
|
||
return `
|
||
<span class="spark" aria-hidden="true">
|
||
${values.map((value) => `<i style="height:${Math.max(6, Math.round((value / max) * 24))}px"></i>`).join("")}
|
||
</span>
|
||
`;
|
||
}
|
||
|
||
function renderKpis() {
|
||
return `
|
||
<div class="grid kpi-grid">
|
||
${records.kpis.map((kpi) => `
|
||
<button class="kpi-card" data-route="${kpi.route}" data-tab="${kpi.filter}">
|
||
<div class="kpi-top">
|
||
<div class="kpi-title">${escapeHtml(kpi.title)}</div>
|
||
${tag(kpi.filter, kpi.tone)}
|
||
</div>
|
||
<div class="kpi-value">${kpi.value}</div>
|
||
<div class="kpi-foot">
|
||
<span>${escapeHtml(kpi.desc)}</span>
|
||
<span>查看</span>
|
||
</div>
|
||
<div class="trend-line">
|
||
<span>日 ${kpi.trend.day}</span>
|
||
<span>周 ${kpi.trend.week}</span>
|
||
<span>月 ${kpi.trend.month}</span>
|
||
</div>
|
||
<div class="kpi-foot">
|
||
${renderSpark(kpi.spark)}
|
||
${tag(kpi.trend.risk, kpi.tone)}
|
||
</div>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderDashboard() {
|
||
return `
|
||
${renderCommandStatusStrip()}
|
||
<div class="command-grid">
|
||
${renderPriorityQueue()}
|
||
<div class="side-stack">
|
||
${renderRiskSummaryBoard()}
|
||
${renderApprovalAgingBoard()}
|
||
</div>
|
||
</div>
|
||
<div class="matrix-grid">
|
||
${renderThemeMatrix()}
|
||
${renderChannelMatrix()}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderCommandStatusStrip() {
|
||
const units = [
|
||
{ name: "P0 待处理", value: "3", sub: "最长等待 6h", tone: "alert", route: "plans", tab: "blocked", priority: 3 },
|
||
{ name: "审核卡点", value: "4", sub: "影响 2 个计划", tone: "warning", route: "plans", tab: "approval", priority: 4 },
|
||
{ name: "紧急 Listing", value: "7", sub: "新 3 / 存量 4", tone: "alert", route: "listings", tab: "emergency", priority: 5 },
|
||
{ name: "黑名单失败", value: "2", sub: "高危 1", tone: "alert", route: "risk", tab: "sync_failed", priority: 6 },
|
||
{ name: "推送复核", value: "3", sub: "EDM 风险高", tone: "warning", route: "push", tab: "risk", priority: 7 },
|
||
{ name: "评价产出", value: "96", sub: "近 7 天稳定", tone: "", route: "reports", tab: "review_done", priority: 8 },
|
||
{ name: "目标完成度", value: "76%", sub: "周目标差 6%", tone: "goal warning", route: "reports", tab: "okr", priority: 1, featured: true, progress: 76 },
|
||
{ name: "问题总结", value: "9", sub: "数据 4 / OKR 2 / 项目 3", tone: "alert", route: "reports", tab: "issue_summary", priority: 2, featured: true }
|
||
];
|
||
const coreUnits = units.filter((unit) => unit.featured);
|
||
const restUnits = units.filter((unit) => !unit.featured);
|
||
const orderedRest = state.statusPriorityFirst
|
||
? [...restUnits].sort((a, b) => a.priority - b.priority)
|
||
: restUnits;
|
||
const ordered = [...coreUnits, ...orderedRest];
|
||
return `
|
||
<section class="command-board">
|
||
<div class="command-board-head">
|
||
<div class="command-board-title">
|
||
<h2>核心看板</h2>
|
||
<span>目标 / 问题 / 卡点</span>
|
||
</div>
|
||
</div>
|
||
<div class="command-status-strip ${state.statusExpanded ? "is-expanded" : "is-collapsed"}">
|
||
${ordered.map((unit, index) => `
|
||
<button class="status-unit ${unit.tone} ${unit.featured ? "featured" : ""} ${!unit.featured && index > 5 ? "optional" : ""}" data-route="${unit.route}" data-tab="${unit.tab}">
|
||
<div class="status-name">${unit.name}</div>
|
||
<div class="status-value"><span>${unit.value}</span><small>${unit.sub}</small></div>
|
||
${unit.progress ? `<div class="status-progress" aria-label="目标完成进度"><span style="--progress:${unit.progress}%;"></span></div>` : ""}
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
<div class="command-board-actions">
|
||
<button class="mini-btn ${state.statusPriorityFirst ? "primary" : ""}" data-action="priority-status">
|
||
${state.statusPriorityFirst ? "高风险" : "原序"}
|
||
</button>
|
||
<button class="mini-btn" data-action="toggle-status">
|
||
${state.statusExpanded ? "收起" : "展开"}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderPriorityQueue() {
|
||
const rows = [
|
||
["P0", "紧急 Listing 策略等待确认", "B0TES005 / JP / 评分 4.21", "Amazon 总监", "超 4h", "审批", "critical", "plans", "blocked"],
|
||
["P0", "黑名单同步失败需判断严重度", "JH-****-2098 / 接口超时 / 高危 1", "风险负责人", "2h", "处理", "critical", "risk", "sync_failed"],
|
||
["P1", "EDM 推送复核后再放量", "PS-0502-006 / 退订率 2.9%", "用户运营组长", "今日 12:00", "复核", "warning", "push", "risk"],
|
||
["P1", "Amazon 已批需求待接收", "8 个测评需求 / 最长等待 6h", "用户运营负责人", "今日 18:00", "接收", "warning", "plans", "amazon_approved"],
|
||
["P2", "菲律宾 TEL 排班缺口", "关键岗位缺 1 / 影响回访覆盖", "客服负责人", "明日 10:00", "分配", "", "support", "ph"]
|
||
];
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head panel-toolbar">
|
||
<div>
|
||
<h2 class="panel-title">P0/P1 处理队列</h2>
|
||
<div class="panel-note">首页唯一主任务区:风险、对象、负责人、时限和操作放在同一行。</div>
|
||
</div>
|
||
<div class="compact-tabs">
|
||
<button class="active" data-tab="all">全部</button>
|
||
<button data-route="plans" data-tab="blocked">审核</button>
|
||
<button data-route="listings" data-tab="emergency">Listing</button>
|
||
<button data-route="risk" data-tab="sync_failed">黑名单</button>
|
||
<button data-route="push" data-tab="risk">推送</button>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body priority-table">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>级别</th><th>事项</th><th>对象</th><th>负责人</th><th>时限</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${rows.map(([level, title, object, owner, due, action, cls, route, tab]) => `
|
||
<tr>
|
||
<td>${tag(level, level === "P0" ? "red" : level === "P1" ? "amber" : "blue")}</td>
|
||
<td><div class="row-title">${title}</div><div class="row-sub">${cls === "critical" ? "需要管理层确认" : "责任部门可处理,异常自动升级"}</div></td>
|
||
<td>${object}</td>
|
||
<td>${owner}</td>
|
||
<td>${due}</td>
|
||
<td>
|
||
<button class="mini-btn primary" data-route="${route}" data-tab="${tab}">${action}</button>
|
||
<button class="mini-btn" data-detail="workItems" data-id="WK-20260503-001">详情</button>
|
||
</td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderRiskSummaryBoard() {
|
||
const rows = [
|
||
["数据分析", "Listing 4.2 附近 3 个,目标完成度拖累 6%", "高", "alert", "reports", "issue_summary"],
|
||
["OKR 总结", "周目标完成 76%,测评接收与推送复核是主要缺口", "中", "warning", "reports", "okr"],
|
||
["负责人提交", "EDM 退订超过基线,项目负责人建议暂停放量", "中", "warning", "push", "risk"]
|
||
];
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">问题总结入口</h2>
|
||
<div class="panel-note">数据分析、OKR 总结、项目负责人提交统一进入这里。</div>
|
||
</div>
|
||
</div>
|
||
<div class="side-list">
|
||
${rows.map(([name, meta, risk, cls, route, tab]) => `
|
||
<button class="side-row ${cls}" data-route="${route}" data-tab="${tab}">
|
||
<div class="side-row-title"><span>${name}</span>${tag(risk)}</div>
|
||
<div class="side-row-meta">${meta}</div>
|
||
</button>
|
||
`).join("")}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderApprovalAgingBoard() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">审核时效</h2>
|
||
<div class="panel-note">看是否卡住业务流。</div>
|
||
</div>
|
||
<button class="btn" data-route="plans" data-tab="approval">全部审批</button>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="chart-row">
|
||
${[
|
||
["0-2h", 42, "green"],
|
||
["2-6h", 28, "blue"],
|
||
["6-12h", 18, "amber"],
|
||
["12h+", 12, "red"]
|
||
].map(([name, value, color]) => `
|
||
<div class="bar-line">
|
||
<span>${name}</span>
|
||
<div class="bar-track"><div class="bar-fill" style="width:${value}%; background:var(--${color});"></div></div>
|
||
<span>${value}%</span>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderThemeMatrix() {
|
||
const rows = [
|
||
["目标完成度", "76%", "80%", "88%", "低于周目标 6%", "查看问题总结", "reports", "okr"],
|
||
["测评需求", "18", "74", "236", "已批 8 / 待接收 8", "用户运营接收", "plans", "amazon_approved"],
|
||
["Listing 健康", "新 3", "11", "29", "4.2 附近", "紧急策略审批", "listings", "emergency"],
|
||
["诈骗黑名单", "5", "18", "64", "失败 2 / 高危 1", "复核并同步", "risk", "sync_failed"],
|
||
["评价产出", "18", "96", "384", "稳定", "查看周报", "reports", "review_done"],
|
||
["客服质量", "12m", "14m", "16m", "关键岗缺 1", "调整排班", "support", "quality"]
|
||
];
|
||
return `
|
||
<section class="panel matrix-table">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">经营主题矩阵</h2>
|
||
<div class="panel-note">每组数据有趋势、阈值和下一步动作。</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body table-wrap">
|
||
<table>
|
||
<thead><tr><th>主题</th><th>日</th><th>周</th><th>月</th><th>状态</th><th>下一步</th><th>操作</th></tr></thead>
|
||
<tbody>
|
||
${rows.map(([name, day, week, month, status, next, route, tab]) => `
|
||
<tr>
|
||
<td><strong>${name}</strong></td>
|
||
<td>${day}</td>
|
||
<td>${week}</td>
|
||
<td>${month}</td>
|
||
<td>${tag(status, status.includes("失败") || status.includes("4.2") || status.includes("缺") || status.includes("低于") ? "red" : status.includes("待") ? "amber" : "green")}</td>
|
||
<td>${next}</td>
|
||
<td><button class="mini-btn primary" data-route="${route}" data-tab="${tab}">查看</button></td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderChannelMatrix() {
|
||
const rows = [
|
||
["IM", "正常", "980", "132", "继续"],
|
||
["EDM", "高风险", "1600", "87", "复核"],
|
||
["TEL", "资源紧", "740", "66", "排班"],
|
||
["PR/KOC", "逾期 2", "27", "11", "补资料"],
|
||
["PH 团队", "缺口 1", "-", "-", "调班"]
|
||
];
|
||
return `
|
||
<section class="panel channel-table">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">渠道与团队</h2>
|
||
<div class="panel-note">只看状态和动作,不展开全量过程。</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body table-wrap">
|
||
<table>
|
||
<thead><tr><th>对象</th><th>状态</th><th>触达</th><th>回复</th><th>动作</th></tr></thead>
|
||
<tbody>
|
||
${rows.map(([name, status, sent, reply, action]) => `
|
||
<tr>
|
||
<td><strong>${name}</strong></td>
|
||
<td>${tag(status, status === "正常" ? "green" : status.includes("高") || status.includes("缺") ? "red" : "amber")}</td>
|
||
<td>${sent}</td>
|
||
<td>${reply}</td>
|
||
<td><button class="mini-btn primary" data-route="${name === "PH 团队" ? "support" : "push"}" data-tab="${name === "EDM" ? "risk" : "all"}">${action}</button></td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderExecutiveHero() {
|
||
return `
|
||
<section class="executive-hero">
|
||
<div class="focus-card">
|
||
<div class="focus-eyebrow">今日管理判断</div>
|
||
<h2 class="focus-title">当前不是数据量问题,主要风险集中在审核卡点、紧急 Listing 和推送复核。</h2>
|
||
<div class="health-score">
|
||
<div class="health-number">78</div>
|
||
<div class="health-copy">
|
||
经营健康指数 / 100<br />
|
||
较昨日下降 4 分。需要优先处理 P0 审批和 4.2 附近 Listing。
|
||
</div>
|
||
</div>
|
||
<div class="focus-metrics">
|
||
<div class="focus-metric"><strong>4</strong><span>审核卡点</span></div>
|
||
<div class="focus-metric"><strong>7</strong><span>未处理紧急</span></div>
|
||
<div class="focus-metric"><strong>6h</strong><span>最长等待</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="decision-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">需要处理的 3 件事</h2>
|
||
<div class="panel-note">按影响程度排序,只保留系统管理员需要介入的判断。</div>
|
||
</div>
|
||
<div class="button-row">
|
||
<button class="btn primary" data-route="plans" data-tab="blocked">处理卡点</button>
|
||
<button class="btn warning" data-route="risk" data-tab="risk_summary">风险汇总</button>
|
||
<button class="btn" data-route="reports" data-tab="weekly">周报</button>
|
||
</div>
|
||
</div>
|
||
<div class="decision-list">
|
||
${[
|
||
["P0", "紧急 Listing 策略等待确认", "B0TES005 评分 4.21,策略审批等待超过 4 小时。", "critical", "plans", "blocked"],
|
||
["P0", "黑名单同步失败需判断严重度", "2 条失败记录中 1 条高危,可能影响诈骗拦截。", "critical", "risk", "sync_failed"],
|
||
["P1", "EDM 推送复核后再放量", "退订率高于基线,周计划需要暂停同策略任务。", "warning", "push", "risk"]
|
||
].map(([level, title, desc, cls, route, tab]) => `
|
||
<div class="decision-row ${cls}">
|
||
<div>${tag(level, level === "P0" ? "red" : "amber")}</div>
|
||
<div>
|
||
<div class="decision-title">${title}</div>
|
||
<div class="decision-desc">${desc}</div>
|
||
</div>
|
||
<button class="mini-btn primary" data-route="${route}" data-tab="${tab}">处理</button>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderExecutiveSnapshot() {
|
||
const cards = [
|
||
["测评需求与计划", "18 申请 / 8 已批", "primary", "日 18", "周 74", "月 236", "Amazon 提交后,用户运营接收仍有等待。", "plans", "amazon_approved"],
|
||
["Listing 健康", "7 未处理紧急", "risk", "新 3", "周 11", "月 29", "4.2 附近 Listing 是当前最高风险。", "listings", "emergency"],
|
||
["诈骗与黑名单", "高危 1 / 失败 2", "risk", "昨 5", "周 18", "月 64", "看严重度和同步失败影响,不展示全量明细。", "risk", "sync_failed"],
|
||
["评价与推送产出", "周 96 / 回复 296", "good", "日 18", "周 96", "月 384", "评价产出稳定,但 EDM 风险需要复核。", "reports", "review_done"]
|
||
];
|
||
return `
|
||
<section class="executive-grid">
|
||
${cards.map(([title, value, cls, day, week, month, desc, route, tab]) => `
|
||
<button class="executive-card ${cls}" data-route="${route}" data-tab="${tab}">
|
||
<div class="executive-title">
|
||
<span>${title}</span>
|
||
${tag(cls === "risk" ? "风险" : cls === "good" ? "稳定" : "关注", cls === "risk" ? "red" : cls === "good" ? "green" : "blue")}
|
||
</div>
|
||
<div class="executive-value">${value}</div>
|
||
<div class="period-stats">
|
||
<div class="period-stat"><strong>${day}</strong><span>日</span></div>
|
||
<div class="period-stat"><strong>${week}</strong><span>周</span></div>
|
||
<div class="period-stat"><strong>${month}</strong><span>月</span></div>
|
||
</div>
|
||
<div class="insight-desc">${desc}</div>
|
||
</button>
|
||
`).join("")}
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderOpsStrip() {
|
||
const cards = [
|
||
["IM", "正常", "触达 980,回复 132,继续执行。", "green", "push", "im"],
|
||
["EDM", "需复核", "退订率 2.9%,暂停同策略放量。", "amber", "push", "risk"],
|
||
["TEL", "资源紧", "客服回访排班接近满负载。", "amber", "support", "tel"],
|
||
["PR/KOC/KOL", "2 个逾期", "价格、CODE、返点信息需补齐。", "purple", "push", "kol"],
|
||
["菲律宾团队", "关键岗缺 1", "请假与缺席影响 TEL 覆盖。", "red", "support", "ph"]
|
||
];
|
||
return `
|
||
<section class="ops-strip">
|
||
${cards.map(([name, status, desc, tone, route, tab]) => `
|
||
<button class="ops-card" data-route="${route}" data-tab="${tab}">
|
||
<div class="ops-name"><span>${name}</span>${tag(status, tone)}</div>
|
||
<div class="ops-status">${desc}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:${tone === "green" ? "72" : tone === "red" ? "84" : "58"}%; background:var(--${tone === "purple" ? "purple" : tone});"></div></div>
|
||
</button>
|
||
`).join("")}
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderDrilldownGrid() {
|
||
const buttons = [
|
||
["待审核需求", "requests", "pending_review"],
|
||
["计划覆盖缺口", "plans", "coverage"],
|
||
["紧急 Listing", "listings", "emergency"],
|
||
["客服质量", "support", "quality"],
|
||
["报表生成", "reports", "weekly"]
|
||
];
|
||
return `
|
||
<section class="drilldown-grid">
|
||
${buttons.map(([label, route, tab]) => `
|
||
<button class="drilldown-button" data-route="${route}" data-tab="${tab}">${label}</button>
|
||
`).join("")}
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderWorkPanel() {
|
||
const tabs = [
|
||
["all", "全部"],
|
||
["amazon_approved", "Amazon 已批"],
|
||
["risk_review", "用户运营接收"],
|
||
["support", "客服升级"],
|
||
["fraud", "风险复核"]
|
||
];
|
||
const filtered = state.activeTab === "all"
|
||
? records.workItems
|
||
: records.workItems.filter((item) => item.status === state.activeTab);
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">跨部门待办流</h2>
|
||
<div class="panel-note">把 Amazon 已通过、用户运营接收、客服升级、风险复核放在同一条管理队列。</div>
|
||
</div>
|
||
<div class="tabs">
|
||
${tabs.map(([id, label]) => `<button class="tab ${state.activeTab === id ? "active" : ""}" data-tab="${id}">${label}</button>`).join("")}
|
||
</div>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>来源</th><th>类型</th><th>ASIN/站点</th><th>当前环节</th>
|
||
<th>负责人</th><th>风险</th><th>截止</th><th>动作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${filtered.map((item) => `
|
||
<tr>
|
||
<td>${escapeHtml(item.source)}</td>
|
||
<td>${tag(item.type, "blue")}</td>
|
||
<td>${escapeHtml(item.asin)} / ${escapeHtml(item.site)}</td>
|
||
<td>${escapeHtml(item.stage)}</td>
|
||
<td>${escapeHtml(item.owner)}</td>
|
||
<td>${tag(item.risk)}</td>
|
||
<td>${escapeHtml(item.due)}</td>
|
||
<td>
|
||
<button class="mini-btn primary" data-detail="workItems" data-id="${item.id}">${escapeHtml(item.action)}</button>
|
||
<button class="mini-btn" data-action="open-modal" data-modal="assign" data-target="${item.id}">分配</button>
|
||
</td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderListingPanel() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">Listing 与评价健康</h2>
|
||
<div class="panel-note">4.5 以下进入补强,接近 4.2 进入紧急协同。</div>
|
||
</div>
|
||
<button class="btn" data-route="listings" data-tab="all">更多</button>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="chart-row">
|
||
${[
|
||
["4.7 以上", 62, "green"],
|
||
["4.5 - 4.7", 24, "blue"],
|
||
["4.2 - 4.5", 11, "amber"],
|
||
["接近 4.2", 3, "red"]
|
||
].map(([name, value, color]) => `
|
||
<div class="bar-line">
|
||
<span>${name}</span>
|
||
<div class="bar-track"><div class="bar-fill" style="width:${value}%; background:var(--${color});"></div></div>
|
||
<span>${value}%</span>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
<div class="table-wrap" style="margin-top:12px;">
|
||
<table>
|
||
<thead><tr><th>ASIN</th><th>站点</th><th>评分</th><th>健康</th><th>阶段</th><th>动作</th></tr></thead>
|
||
<tbody>
|
||
${records.listings.slice(0, 4).map((item) => `
|
||
<tr>
|
||
<td>${item.asin}</td><td>${item.site}</td><td>${item.rating}</td>
|
||
<td>${tag(item.health)}</td><td>${item.stage}</td>
|
||
<td><button class="mini-btn primary" data-detail="listings" data-id="${item.id}">查看</button></td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderRiskPanel() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">风险雷达</h2>
|
||
<div class="panel-note">诈骗同步、推送风险、规则提醒、数据同步异常。</div>
|
||
</div>
|
||
<button class="btn" data-route="risk" data-tab="all">进入风险中心</button>
|
||
</div>
|
||
<div class="panel-body risk-list">
|
||
${records.risks.map((risk) => `
|
||
<div class="risk-item ${risk.risk === "高" ? "high" : "mid"}">
|
||
<div class="item-title">
|
||
<span>${escapeHtml(risk.type)}</span>
|
||
${tag(risk.risk)}
|
||
</div>
|
||
<div class="item-desc">${escapeHtml(risk.subject)} · ${escapeHtml(risk.relation)} · ${escapeHtml(risk.status)}</div>
|
||
<div><button class="mini-btn primary" data-detail="risks" data-id="${risk.id}">处理</button></div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderBlockingPanel() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">审核卡点与已发现问题</h2>
|
||
<div class="panel-note">系统管理员页优先展示已经识别的问题,避免逐表查找。</div>
|
||
</div>
|
||
<button class="btn" data-route="plans" data-tab="blocked">处理卡点</button>
|
||
</div>
|
||
<div class="panel-body risk-list">
|
||
${[
|
||
["紧急策略审批超 4h", "PL-0503-002 接近 4.2 Listing,等待系统管理员确认。", "高", "plans", "blocked"],
|
||
["需求已批但未接收", "8 个 Amazon 已批准测评需求待用户运营接收,最长等待 6h。", "中", "plans", "amazon_approved"],
|
||
["推送风险待复核", "EDM 退订高于基线,相关周计划不应继续放量。", "高", "push", "risk"]
|
||
].map(([title, desc, risk, route, tab]) => `
|
||
<div class="risk-item ${risk === "高" ? "high" : "mid"}">
|
||
<div class="item-title"><span>${title}</span>${tag(risk)}</div>
|
||
<div class="item-desc">${desc}</div>
|
||
<div><button class="mini-btn primary" data-route="${route}" data-tab="${tab}">去处理</button></div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
|
||
function renderSuggestPanel() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">管理动作建议</h2>
|
||
<div class="panel-note">按 P0/P1 优先级生成,后期可由算法模块增强。</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel-body suggest-list">
|
||
${[
|
||
["先处理 3 个紧急 Listing", "需要 Amazon 与用户运营共同确认跟进策略,避免评分继续逼近 4.2。", "listings", "emergency"],
|
||
["复核昨日高风险推送", "检查人群、素材、文案与退订反馈,必要时暂停同策略任务。", "push", "risk"],
|
||
["审核新增诈骗事件", "确认后同步黑名单子系统,失败记录进入重试队列和审计。", "risk", "fraud"]
|
||
].map(([title, desc, route, tab]) => `
|
||
<div class="suggest-item">
|
||
<div class="item-title"><span>${title}</span>${tag("建议", "purple")}</div>
|
||
<div class="item-desc">${desc}</div>
|
||
<div><button class="mini-btn primary" data-route="${route}" data-tab="${tab}">去处理</button></div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderSummaryPanel() {
|
||
return `
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">业务复盘与中期趋势</h2>
|
||
<div class="panel-note">日/周/月切换;系统管理员、负责人/总监、组长都能看到趋势与风险提示。</div>
|
||
</div>
|
||
<button class="btn" data-route="reports" data-tab="yesterday">查看报表</button>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="summary-grid">
|
||
${[
|
||
["2,840", "昨日触达用户"],
|
||
["285", "用户回复"],
|
||
["96", "近 7 天评价完成"],
|
||
["4", "审核卡点"]
|
||
].map(([value, label]) => `
|
||
<div class="summary-cell">
|
||
<div class="summary-value">${value}</div>
|
||
<div class="summary-label">${label}</div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function getRows(route) {
|
||
if (route === "requests") return records.workItems;
|
||
if (route === "listings") return records.listings;
|
||
if (route === "plans") return records.plans;
|
||
if (route === "push") return records.pushes;
|
||
if (route === "support") return records.support;
|
||
if (route === "risk") return records.risks;
|
||
if (route === "data") return records.syncLogs;
|
||
if (route === "reports") return records.reports;
|
||
if (route === "system") return records.system;
|
||
return [];
|
||
}
|
||
|
||
function rowToCells(route, item) {
|
||
if (route === "requests") {
|
||
return [item.id, tag(item.type, "blue"), item.submitter, item.reviewer, tag(item.approval), item.sourceForm, `${item.asin}/${item.site}`, item.stage, item.owner, tag(item.risk), item.due];
|
||
}
|
||
if (route === "listings") {
|
||
return [item.asin, item.marketplaces, item.rating, tag(item.grade, item.grade === "S" ? "red" : item.grade === "A" ? "amber" : "blue"), item.reviews, item.negative, tag(item.health), item.owner, item.issue, `${item.participants} / ${item.progress}`];
|
||
}
|
||
if (route === "plans") {
|
||
return [item.id, tag(item.type, "blue"), item.requestId, `${item.asin}/${item.site}`, tag(item.coverage, item.coverage === "已覆盖" ? "green" : "amber"), item.channelMix, item.target, item.status, item.approver, tag(item.risk)];
|
||
}
|
||
if (route === "push") {
|
||
return [item.id, item.plan, item.channel, item.strategy, `${item.h5} / ${item.assets}`, item.audience, item.sent, item.click, item.reply, item.optout, tag(item.risk), item.status];
|
||
}
|
||
if (route === "support") {
|
||
return [item.id, tag(item.type, "blue"), item.user, item.asin, item.owner, item.avgResponse, item.workHours, item.attendance, item.output, tag(item.risk), item.sla];
|
||
}
|
||
if (route === "risk") {
|
||
return [item.id, tag(item.type, "blue"), item.subject, item.relation, item.status, tag(item.risk)];
|
||
}
|
||
if (route === "data") {
|
||
return [item.id, item.source, item.interval, item.last, tag(item.status), item.rows];
|
||
}
|
||
if (route === "reports") {
|
||
return [item.id, item.name, item.owner, item.range, item.schedule, item.upload, tag(item.exportable, "green"), tag(item.masked, "gray")];
|
||
}
|
||
if (route === "system") {
|
||
return [item.id, item.module, tag(item.status), item.owner, item.note];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
function renderListPage(route) {
|
||
const routeMeta = routes.find((item) => item.id === route);
|
||
const rows = getRows(route);
|
||
const schema = tableSchemas[route] || [];
|
||
const actions = listActions(route);
|
||
return `
|
||
${renderHeader(routeMeta.label, pageNotes(route), actions)}
|
||
${renderModuleInsights(route)}
|
||
<section class="panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2 class="panel-title">${routeMeta.label}列表</h2>
|
||
<div class="panel-note">当前筛选:${escapeHtml(state.activeTab)};所有记录为模拟数据。</div>
|
||
</div>
|
||
${renderSourceNote()}
|
||
</div>
|
||
<div class="panel-body">
|
||
${route === "requests" ? renderPendingStrip("待审核需求 6 个,其中测评需求必须由 Amazon 运营提交;飞书表单可作为创建入口并同步到 ERP。", "requests", "pending_review") : ""}
|
||
${renderFilterBar(route)}
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead><tr>${schema.map((col) => `<th>${col}</th>`).join("")}</tr></thead>
|
||
<tbody>
|
||
${rows.map((item) => `
|
||
<tr>
|
||
${rowToCells(route, item).map((cell) => `<td>${cell}</td>`).join("")}
|
||
<td>${renderRowActions(route, item)}</td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
function renderModuleInsights(route) {
|
||
const insightMap = {
|
||
requests: [
|
||
["新增需求量", "日 18 / 周 74 / 月 236", "较上周 +12%,待审核入口需前置。", "blue"],
|
||
["需求满足量", "日 11 / 周 61", "已完成与已覆盖需求比例 82%。", "green"],
|
||
["关键产品绑定率", "S 级 68%", "通过数据库定期更新,低于 60% 标记风险。", "amber"]
|
||
],
|
||
listings: [
|
||
["评分分层", "4.8+ 24 / 4.5+ 81", "4.2-4.5 有 11 个,4.2 以下 2 个。", "red"],
|
||
["S/A 级跟进", "S 级 6 / A 级 14", "重点看责任人、问题和参与人员进度。", "blue"],
|
||
["跨站点 ASIN", "US/CA 3 个", "同一 ASIN 多站点联动,需按站点分别归因。", "purple"]
|
||
],
|
||
plans: [
|
||
["需求覆盖率", "已覆盖 82%", "未覆盖 7 个,部分覆盖 5 个。", "amber"],
|
||
["资源分配", "IM 46% / EDM 29% / TEL 25%", "按计划策略与团队资源匹配。", "blue"],
|
||
["异常计划", "卡点 4", "审批积压、资源不足、风险复核未完成。", "red"]
|
||
],
|
||
push: [
|
||
["渠道分配", "IM 46% / EDM 29% / TEL 18% / PR 7%", "与计划中心统一看资源和执行。", "blue"],
|
||
["反馈效果", "点击 453 / 回复 296", "按 H5、图片、文案和用户画像拆解。", "green"],
|
||
["优化历史", "本周 8 次", "记录暂停、放量、换图、换文案等策略变化。", "purple"]
|
||
],
|
||
support: [
|
||
["平均响应", "12m", "日周月响应时长与 SLA 趋势。", "green"],
|
||
["出勤风险", "请假 2 / 缺席 1", "菲律宾团队关键岗位缺口需提示。", "amber"],
|
||
["人均产出", "16.4 单/日", "统计工单、IM、EDM、TEL 处理与评价产出。", "blue"]
|
||
],
|
||
risk: [
|
||
["新增诈骗", "昨 5 / 周 18 / 月 64", "同步黑名单前需要审核与脱敏。", "red"],
|
||
["同步失败严重度", "失败 2 / 高危 1", "系统管理员只看影响范围与是否严重。", "amber"],
|
||
["规则提醒", "待复核 6", "自动提醒规则和审核风控风险。", "blue"]
|
||
],
|
||
data: [
|
||
["订单同步", "10 分钟", "Amazon 与独立站订单同步状态。", "green"],
|
||
["周月预生成", "已排队 4", "复杂统计异步处理,减少页面卡顿。", "blue"],
|
||
["数据异常", "部分失败 1", "黑名单接口待确认,失败进入重试。", "amber"]
|
||
],
|
||
reports: [
|
||
["目标完成度", "76%", "系统管理员入口优先显示目标差距和影响原因。", "red"],
|
||
["问题总结", "9 条", "来源包括数据分析、OKR 总结、项目负责人提交。", "amber"],
|
||
["指定报表", "12 个", "支持日、周、月生成和下载。", "blue"]
|
||
],
|
||
system: [
|
||
["账号管理", "新增 3 / 离职 1", "新建账号、停用账号、交接任务。", "blue"],
|
||
["授权变更", "待审 4", "导出、审批、敏感信息、黑名单同步独立授权。", "amber"],
|
||
["审计风险", "高风险 2", "查看敏感信息和导出操作需要记录原因。", "red"]
|
||
]
|
||
};
|
||
const items = insightMap[route];
|
||
if (!items) return "";
|
||
return `
|
||
<div class="module-charts">
|
||
${items.map(([title, value, desc, tone]) => `
|
||
<div class="chart-card">
|
||
<div class="chart-title">${escapeHtml(title)} ${tag(state.period === "day" ? "日" : state.period === "week" ? "周" : "月", tone)}</div>
|
||
<div class="insight-value">${escapeHtml(value)}</div>
|
||
<div class="insight-desc">${escapeHtml(desc)}</div>
|
||
<div class="mini-stat-row">
|
||
<span>趋势</span>
|
||
<div class="bar-track"><div class="bar-fill" style="width:${tone === "red" ? "78" : tone === "amber" ? "58" : "68"}%; background:var(--${tone});"></div></div>
|
||
<span>${tone === "red" ? "高" : tone === "amber" ? "注意" : "正常"}</span>
|
||
</div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderPendingStrip(text, route, tab) {
|
||
return `
|
||
<div class="pending-strip">
|
||
<strong>待处理入口</strong>
|
||
<span>${escapeHtml(text)}</span>
|
||
<button class="mini-btn primary" data-route="${route}" data-tab="${tab}">进入处理</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function pageNotes(route) {
|
||
const notes = {
|
||
requests: "承接需求提交与审核记录;测评需求由 Amazon 运营提交,飞书表单可同步进入 ERP。",
|
||
listings: "按评分层级、S/A/B 等级、站点组合、责任人、问题和参与进度管理。",
|
||
plans: "关注计划与需求匹配关系,确认需求后生成推广计划和紧急策略。",
|
||
push: "与计划中心联动,管理 IM、EDM、TEL、PR/KOC/KOL、H5、素材、反馈和优化历史。",
|
||
support: "处理工单沟通,同时关注日周月响应时长、工作时长、出勤、人均产出和菲律宾团队风险。",
|
||
risk: "同步诈骗事件、黑名单状态、规则提醒与审计风险。",
|
||
data: "展示订单、评价、用户、黑名单等同步状态,不作为数据仓库入口。",
|
||
reports: "指定报表生成、下载、上传补充记录和导出审计,支持日/周/月。",
|
||
system: "处理系统授权、新建账号、离职管理、权限分配、审批流和审计日志。"
|
||
};
|
||
return notes[route] || "";
|
||
}
|
||
|
||
function listActions(route) {
|
||
const common = `<button class="btn" data-action="open-modal" data-modal="export">导出</button>`;
|
||
const map = {
|
||
requests: `<button class="btn primary" data-action="open-modal" data-modal="create-request">Amazon 提交测评需求</button><button class="btn" data-route="requests" data-tab="pending_review">待审核入口</button>${common}`,
|
||
listings: `<button class="btn warning" data-action="open-modal" data-modal="emergency">创建紧急策略</button>${common}`,
|
||
plans: `<button class="btn primary" data-action="open-modal" data-modal="generate-plan">生成计划</button><button class="btn" data-action="open-modal" data-modal="approve">批量审批</button>`,
|
||
push: `<button class="btn primary" data-action="open-modal" data-modal="push-plan">计划与推送分配</button><button class="btn" data-action="open-modal" data-modal="risk-review">风险复核</button>`,
|
||
support: `<button class="btn primary" data-action="open-modal" data-modal="assign">分配工单</button>${common}`,
|
||
risk: `<button class="btn danger" data-action="open-modal" data-modal="blacklist">同步黑名单</button><button class="btn" data-action="open-modal" data-modal="risk-review">规则复核</button>`,
|
||
data: `<button class="btn" data-action="open-modal" data-modal="sync">立即同步</button>`,
|
||
reports: `<button class="btn primary" data-action="open-modal" data-modal="export">生成/下载报表</button><button class="btn" data-action="open-modal" data-modal="upload-report">上传记录</button>`,
|
||
system: `<button class="btn primary" data-action="open-modal" data-modal="new-account">新建账号</button><button class="btn" data-action="open-modal" data-modal="offboarding">离职管理</button><button class="btn" data-action="open-modal" data-modal="permission">权限分配</button>`
|
||
};
|
||
return map[route] || common;
|
||
}
|
||
|
||
function renderFilterBar(route) {
|
||
return `
|
||
<div class="filter-bar">
|
||
<input placeholder="关键词:ID / ASIN / 负责人 / 用户摘要" value="${escapeHtml(state.keyword)}" data-filter="keyword" />
|
||
<input type="date" value="${state.startDate}" data-time="startDate" />
|
||
<input type="date" value="${state.endDate}" data-time="endDate" />
|
||
<select><option>全部站点</option><option>US</option><option>CA</option><option>US/CA</option><option>UK</option><option>DE</option><option>JP</option></select>
|
||
<select><option>全部状态</option><option>待审批</option><option>执行中</option><option>风险复核</option><option>已完成</option></select>
|
||
<select><option>全部风险</option><option>紧急</option><option>高</option><option>中</option><option>低</option></select>
|
||
<select><option>全部负责人</option><option>张三</option><option>李四</option><option>王五</option></select>
|
||
<button class="btn primary" data-action="toast" data-message="已按当前条件执行模拟查询">查询</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderRowActions(route, item) {
|
||
const detailGroup = {
|
||
requests: "workItems",
|
||
listings: "listings",
|
||
plans: "plans",
|
||
push: "pushes",
|
||
support: "support",
|
||
risk: "risks",
|
||
data: "syncLogs",
|
||
reports: "reports",
|
||
system: "system"
|
||
}[route];
|
||
const primaryText = route === "risk" ? "处理" : route === "plans" ? "审批" : "查看";
|
||
return `
|
||
<button class="mini-btn primary" data-detail="${detailGroup}" data-id="${item.id}">${primaryText}</button>
|
||
<button class="mini-btn" data-action="open-modal" data-modal="${route === "reports" ? "export" : "approve"}" data-target="${item.id}">${route === "reports" ? "导出" : "流转"}</button>
|
||
`;
|
||
}
|
||
|
||
function openDrawer(group, id) {
|
||
const list = records[group] || [];
|
||
const item = list.find((entry) => entry.id === id);
|
||
if (!item) return;
|
||
document.getElementById("drawerTitle").textContent = `${id} 详情`;
|
||
document.getElementById("drawerSubtitle").textContent = `${DATA_SOURCE};敏感字段默认脱敏。`;
|
||
document.getElementById("drawerBody").innerHTML = `
|
||
<div class="detail-grid">
|
||
${Object.entries(item).map(([key, value]) => `
|
||
<div class="detail-cell">
|
||
<div class="detail-label">${escapeHtml(key)}</div>
|
||
<div class="detail-value">${escapeHtml(value)}</div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
<section class="panel">
|
||
<div class="panel-head"><h3 class="panel-title">状态流转记录</h3></div>
|
||
<div class="panel-body timeline">
|
||
<div class="timeline-item">2026-05-03 09:10 创建记录</div>
|
||
<div class="timeline-item">2026-05-03 09:40 自动规则提醒完成</div>
|
||
<div class="timeline-item">2026-05-03 10:20 等待当前负责人处理</div>
|
||
</div>
|
||
</section>
|
||
<section class="panel">
|
||
<div class="panel-head"><h3 class="panel-title">脱敏与审计</h3></div>
|
||
<div class="panel-body">
|
||
<div class="item-desc">JOYHUB ID、邮箱、电话、设备号、IP、订单号等字段在 MVP 原型中仅展示脱敏摘要。点击“查看完整信息”会记录审计,本原型仅模拟弹窗。</div>
|
||
</div>
|
||
</section>
|
||
`;
|
||
document.getElementById("drawerFoot").innerHTML = `
|
||
<button class="btn" data-action="open-modal" data-modal="audit">查看完整信息</button>
|
||
<button class="btn" data-action="open-modal" data-modal="assign">分配</button>
|
||
<button class="btn primary" data-action="open-modal" data-modal="approve">审批/确认</button>
|
||
`;
|
||
document.getElementById("drawerMask").classList.add("open");
|
||
document.getElementById("drawer").classList.add("open");
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById("drawerMask").classList.remove("open");
|
||
document.getElementById("drawer").classList.remove("open");
|
||
}
|
||
|
||
function openModal(type, target = "") {
|
||
const configs = {
|
||
notice: ["通知中心", "展示待办、审批退回、风险提醒与同步失败通知。", "标记已读"],
|
||
export: ["导出确认", "仅主管和系统管理员可导出。导出字段默认脱敏,并写入审计日志。", "确认导出"],
|
||
assign: ["任务分配", "选择下一个责任部门和负责人,支持设置截止时间。", "确认分配"],
|
||
approve: ["审批/流转", "选择通过、退回、转交、升级或关闭,并填写审批意见。", "提交审批"],
|
||
"create-request": ["Amazon 提交测评需求", "测评需求由 Amazon 运营提交;客服、KOC/KOL、诈骗等其他需求由对应部门或飞书表单入口同步。", "提交审核"],
|
||
emergency: ["紧急 Listing 策略", "用于接近 4.2 的 Listing,需 Amazon 与用户运营共同确认。", "提交策略审批"],
|
||
"generate-plan": ["自动生成计划", "根据 Listing 健康度、站点目标和用户画像生成计划草稿。", "生成草稿"],
|
||
"push-plan": ["计划与推送分配", "按计划资源分配 IM、EDM、TEL、PR/KOC/KOL,绑定 H5 页面、图片、文案实验和发送窗口。", "保存分配"],
|
||
"risk-review": ["规则与风险复核", "复核推送风险、规则提醒、退订异常和客服升级。", "确认复核"],
|
||
blacklist: ["同步黑名单", "确认诈骗事件后同步黑名单子系统,失败时进入重试队列。", "确认同步"],
|
||
sync: ["立即同步", "触发订单、评价、黑名单或推送回执同步。本原型只模拟动作。", "开始同步"],
|
||
permission: ["权限调整", "维护总监、负责人、组长、组员、客服、系统管理员等权限。", "保存权限"],
|
||
"new-account": ["新建账号", "按部门、角色、站点、数据范围创建账号,并记录授权审批。", "创建账号"],
|
||
offboarding: ["离职管理", "停用账号、交接任务、回收导出与敏感信息权限。", "确认离职处理"],
|
||
"upload-report": ["上传报表记录", "上传人工复核附件、外部表单、补充报表或下载记录,进入报表审计。", "保存记录"],
|
||
audit: ["敏感信息审计", "查看完整敏感信息需要填写原因,并记录审计日志。", "确认查看"]
|
||
};
|
||
const [title, desc, submit] = configs[type] || configs.approve;
|
||
document.getElementById("modalTitle").textContent = target ? `${title} · ${target}` : title;
|
||
document.getElementById("modalBody").innerHTML = `
|
||
<div class="item-desc">${desc}</div>
|
||
<div class="form-row">
|
||
<label>动作类型</label>
|
||
<select>
|
||
<option>通过 / 确认</option>
|
||
<option>退回修改</option>
|
||
<option>转交</option>
|
||
<option>升级</option>
|
||
<option>关闭</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>下一负责人</label>
|
||
<select>
|
||
<option>用户运营负责人</option>
|
||
<option>Amazon 运营总监</option>
|
||
<option>客服负责人</option>
|
||
<option>风险负责人</option>
|
||
<option>系统管理员</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>处理意见</label>
|
||
<textarea placeholder="请输入审批意见、分配说明、风险确认或导出原因。"></textarea>
|
||
</div>
|
||
${renderSourceNote()}
|
||
`;
|
||
document.getElementById("modalFoot").innerHTML = `
|
||
<button class="btn" data-action="close-modal">取消</button>
|
||
<button class="btn primary" data-action="submit-modal" data-message="${submit}成功,已写入模拟操作记录">${submit}</button>
|
||
`;
|
||
document.getElementById("modalMask").classList.add("open");
|
||
document.getElementById("modal").classList.add("open");
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById("modalMask").classList.remove("open");
|
||
document.getElementById("modal").classList.remove("open");
|
||
}
|
||
|
||
function showToast(message) {
|
||
const toast = document.getElementById("toast");
|
||
const item = document.createElement("div");
|
||
item.className = "toast-item";
|
||
item.textContent = message;
|
||
toast.appendChild(item);
|
||
window.setTimeout(() => item.remove(), 2600);
|
||
}
|
||
|
||
function render() {
|
||
renderNav();
|
||
const content = document.getElementById("content");
|
||
updateTopContext();
|
||
if (state.route === "dashboard") {
|
||
content.innerHTML = renderDashboard();
|
||
} else {
|
||
content.innerHTML = renderListPage(state.route);
|
||
}
|
||
}
|
||
|
||
function updateTopContext() {
|
||
const routeMeta = routes.find((item) => item.id === state.route);
|
||
const title = document.getElementById("topPageTitle");
|
||
const subtitle = document.getElementById("topPageSubtitle");
|
||
if (title && routeMeta) title.textContent = routeMeta.label === "工作台" ? "经营总览" : routeMeta.label;
|
||
if (subtitle) {
|
||
const scopeText = {
|
||
all: "全部部门",
|
||
amazon: "Amazon 运营",
|
||
user_ops: "用户运营",
|
||
support: "客服"
|
||
}[state.scope] || "全部部门";
|
||
subtitle.textContent = `系统管理员(最高权限) · ${scopeText}`;
|
||
}
|
||
document.querySelectorAll(".top-period button").forEach((button) => {
|
||
button.classList.toggle("active", button.dataset.period === state.period);
|
||
});
|
||
}
|
||
|
||
function bindEvents() {
|
||
document.body.addEventListener("click", (event) => {
|
||
const target = event.target.closest("button");
|
||
if (!target) return;
|
||
const route = target.dataset.route;
|
||
if (route) {
|
||
setRoute(route, target.dataset.tab || "all");
|
||
return;
|
||
}
|
||
if (target.dataset.tab) {
|
||
state.activeTab = target.dataset.tab;
|
||
render();
|
||
return;
|
||
}
|
||
if (target.dataset.detail) {
|
||
openDrawer(target.dataset.detail, target.dataset.id);
|
||
return;
|
||
}
|
||
const action = target.dataset.action;
|
||
if (action === "toggle-status") {
|
||
state.statusExpanded = !state.statusExpanded;
|
||
render();
|
||
showToast(state.statusExpanded ? "核心看板已展开全部事项" : "核心看板已收起次要事项");
|
||
return;
|
||
}
|
||
if (action === "priority-status") {
|
||
state.statusPriorityFirst = !state.statusPriorityFirst;
|
||
render();
|
||
showToast(state.statusPriorityFirst ? "已按重要度优先排序" : "已恢复原始顺序");
|
||
return;
|
||
}
|
||
if (action === "close-drawer") closeDrawer();
|
||
if (action === "close-modal") closeModal();
|
||
if (action === "open-modal") openModal(target.dataset.modal, target.dataset.target);
|
||
if (action === "submit-modal") {
|
||
closeModal();
|
||
showToast(target.dataset.message || "操作成功");
|
||
}
|
||
if (action === "toast") showToast(target.dataset.message || "已执行");
|
||
});
|
||
|
||
document.getElementById("drawerMask").addEventListener("click", closeDrawer);
|
||
document.getElementById("modalMask").addEventListener("click", closeModal);
|
||
document.getElementById("globalSearch").addEventListener("input", (event) => {
|
||
state.keyword = event.target.value;
|
||
});
|
||
document.getElementById("scopeSelect").addEventListener("change", (event) => {
|
||
state.scope = event.target.value;
|
||
updateTopContext();
|
||
showToast(`已切换数据范围:${event.target.options[event.target.selectedIndex].text}`);
|
||
});
|
||
document.body.addEventListener("change", (event) => {
|
||
const input = event.target.closest("[data-time]");
|
||
if (!input) return;
|
||
const key = input.dataset.time;
|
||
if (key === "startDate" || key === "endDate") {
|
||
state[key] = input.value;
|
||
showToast(`时间范围已更新:${state.startDate} 至 ${state.endDate}`);
|
||
}
|
||
});
|
||
document.body.addEventListener("click", (event) => {
|
||
const target = event.target.closest("[data-period]");
|
||
if (!target) return;
|
||
state.period = target.dataset.period;
|
||
render();
|
||
showToast(`已切换为${state.period === "day" ? "日" : state.period === "week" ? "周" : "月"}视角`);
|
||
});
|
||
window.addEventListener("hashchange", readHash);
|
||
}
|
||
|
||
function readHash() {
|
||
const hash = window.location.hash.replace("#", "");
|
||
if (!hash) return;
|
||
const [route, tab] = hash.split(":");
|
||
if (routes.some((item) => item.id === route)) {
|
||
state.route = route;
|
||
state.activeTab = tab || "all";
|
||
render();
|
||
}
|
||
}
|
||
|
||
bindEvents();
|
||
readHash();
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|