Files
Fulfilled-Knowledge/wishfulfilled-wiki/05_需求文档/20260504_USER后台ERP_MVP管理员首页高保真原型_v7.html
2026-05-27 15:40:32 +08:00

3299 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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>