You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
13414 lines
664 KiB
13414 lines
664 KiB
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>后台管理系统</title>
|
|
<script src="node_modules/chart.js/dist/chart.umd.js"></script>
|
|
<style>
|
|
/* 粒子特效容器 */
|
|
#particles-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: -1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 全局样式重置 */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* 根变量定义 */
|
|
:root {
|
|
--primary-color: #367ee9;
|
|
--primary-light: #4c8dff;
|
|
--primary-dark: #2563eb;
|
|
--secondary-color: #7f56d9;
|
|
--success-color: #10b981;
|
|
--warning-color: #f59e0b;
|
|
--danger-color: #ef4444;
|
|
--info-color: #3b82f6;
|
|
--bg-color: #f8fafc;
|
|
--card-bg: rgba(255, 255, 255, 0.85);
|
|
--text-primary: #1e293b;
|
|
--text-secondary: #64748b;
|
|
--text-muted: #94a3b8;
|
|
--border-color: #e2e8f0;
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
--border-radius-sm: 0.375rem;
|
|
--border-radius-md: 0.5rem;
|
|
--border-radius-lg: 0.75rem;
|
|
--border-radius-xl: 1rem;
|
|
--transition: all 0.3s ease;
|
|
}
|
|
|
|
/* 图表容器样式 */
|
|
.chart-container {
|
|
position: relative;
|
|
height: 350px;
|
|
width: 100%;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
/* 虚拟滚动容器样式 */
|
|
.virtual-scroll-container {
|
|
height: 600px;
|
|
width: 100%;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
overflow: hidden;
|
|
background-color: white;
|
|
}
|
|
|
|
/* 虚拟滚动行样式 */
|
|
.virtual-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.virtual-row:hover {
|
|
background-color: rgba(54, 126, 233, 0.05);
|
|
}
|
|
|
|
.virtual-row-cell {
|
|
flex: 1;
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.virtual-row-cell:first-child {
|
|
flex: 0 0 80px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.virtual-row-cell:nth-child(2) {
|
|
flex: 0 0 150px;
|
|
}
|
|
|
|
.virtual-row-cell:nth-child(3) {
|
|
flex: 0 0 180px;
|
|
}
|
|
|
|
.virtual-row-cell:nth-child(4) {
|
|
flex: 0 0 120px;
|
|
}
|
|
|
|
.virtual-row-cell:nth-child(5) {
|
|
flex: 0 0 180px;
|
|
}
|
|
|
|
.virtual-row-cell:last-child {
|
|
flex: 2;
|
|
white-space: normal;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
/* 客户统计卡片网格 */
|
|
.client-stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
/* 客户统计卡片 */
|
|
.client-stat-card {
|
|
background-color: var(--card-bg);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border-radius: var(--border-radius-lg);
|
|
box-shadow: var(--shadow-md);
|
|
padding: 1.5rem;
|
|
transition: var(--transition);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.client-stat-card:hover {
|
|
box-shadow: var(--shadow-lg);
|
|
transform: translateY(-2px);
|
|
border-color: rgba(54, 126, 233, 0.2);
|
|
}
|
|
|
|
/* 客户统计标题 */
|
|
.client-stat-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 1rem;
|
|
text-align: center;
|
|
}
|
|
|
|
/* 客户统计数字 */
|
|
.client-stat-number {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--primary-color);
|
|
text-align: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
/* 客户统计标签 */
|
|
.client-stat-label {
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
text-align: center;
|
|
}
|
|
|
|
/* 业务员客户列表表格样式 */
|
|
#agentClientsTable {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
background-color: rgba(255, 255, 255, 0.9);
|
|
border-radius: var(--border-radius-md);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
#agentClientsTable th,
|
|
#agentClientsTable td {
|
|
padding: 0.75rem 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* 只有跟进内容列允许换行 */
|
|
#agentClientsTable td:nth-child(8) {
|
|
white-space: normal;
|
|
word-wrap: break-word;
|
|
max-width: 300px;
|
|
}
|
|
|
|
#agentClientsTable th {
|
|
background-color: var(--bg-color);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
font-size: 0.875rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
#agentClientsTable tbody tr:hover {
|
|
background-color: rgba(54, 126, 233, 0.05);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
#agentClientsTable tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* 基础样式 */
|
|
body {
|
|
font-family: 'Inter', 'Microsoft YaHei', Arial, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* 顶部导航栏 */
|
|
.top-nav {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background-color: var(--card-bg);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
box-shadow: var(--shadow-md);
|
|
padding: 0 2rem;
|
|
height: 72px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
z-index: 100;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.top-nav:hover {
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.logo {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
color: var(--primary-color);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
|
border-radius: var(--border-radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 1.125rem;
|
|
font-weight: bold;
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* 主容器 */
|
|
.main-container {
|
|
flex: 1;
|
|
margin-top: 72px;
|
|
display: flex;
|
|
}
|
|
|
|
/* 侧边栏 */
|
|
.sidebar {
|
|
width: 220px;
|
|
background-color: var(--card-bg);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
box-shadow: var(--shadow-md);
|
|
padding: 1.5rem 0;
|
|
transition: var(--transition);
|
|
position: fixed;
|
|
top: 72px;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 99;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.sidebar:hover {
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 0.75rem 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border-left: 3px solid transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
background-color: transparent;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background-color: rgba(54, 126, 233, 0.08);
|
|
color: var(--primary-color);
|
|
border-left-color: rgba(54, 126, 233, 0.5);
|
|
}
|
|
|
|
.menu-item.active {
|
|
background-color: rgba(54, 126, 233, 0.12);
|
|
color: var(--primary-color);
|
|
border-left-color: var(--primary-color);
|
|
background: linear-gradient(90deg, rgba(54, 126, 233, 0.15) 0%, rgba(54, 126, 233, 0.05) 100%);
|
|
}
|
|
|
|
.menu-icon {
|
|
font-size: 1.125rem;
|
|
width: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
/* 内容区域 */
|
|
.content {
|
|
flex: 1;
|
|
padding: 2rem;
|
|
overflow-y: auto;
|
|
margin-left: 220px;
|
|
min-height: calc(100vh - 72px);
|
|
position: relative;
|
|
z-index: 1;
|
|
background-color: rgba(248, 250, 252, 0.7);
|
|
}
|
|
|
|
.content-header {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.content-title {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.content-subtitle {
|
|
font-size: 0.875rem;
|
|
color: var(--text-muted);
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* 卡片样式 */
|
|
.card {
|
|
background-color: var(--card-bg);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border-radius: var(--border-radius-lg);
|
|
box-shadow: var(--shadow-md);
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
transition: var(--transition);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: var(--shadow-lg);
|
|
transform: translateY(-2px);
|
|
border-color: rgba(54, 126, 233, 0.2);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* 统计卡片 */
|
|
.stats-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
|
border-radius: var(--border-radius-xl);
|
|
box-shadow: var(--shadow-lg);
|
|
padding: 1.75rem;
|
|
transition: var(--transition);
|
|
text-align: center;
|
|
color: white;
|
|
position: relative;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: linear-gradient(
|
|
to bottom right,
|
|
rgba(255, 255, 255, 0.15) 0%,
|
|
rgba(255, 255, 255, 0) 100%
|
|
);
|
|
transform: rotate(45deg);
|
|
animation: shimmer 3s infinite linear;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% {
|
|
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
|
}
|
|
100% {
|
|
transform: translateX(100%) translateY(100%) rotate(45deg);
|
|
}
|
|
}
|
|
|
|
.stat-card:hover {
|
|
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|
transform: translateY(-4px);
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2.25rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.875rem;
|
|
opacity: 0.9;
|
|
position: relative;
|
|
z-index: 1;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* 按钮样式 */
|
|
.btn {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: var(--border-radius-md);
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
transition: var(--transition);
|
|
border: none;
|
|
outline: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn.active {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: var(--primary-light);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: var(--secondary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #6d28d9;
|
|
}
|
|
|
|
.btn-default {
|
|
background-color: white;
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.btn-default:hover {
|
|
background-color: #f1f5f9;
|
|
border-color: var(--primary-light);
|
|
}
|
|
|
|
.btn-success {
|
|
background-color: var(--success-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background-color: #059669;
|
|
}
|
|
|
|
.btn-warning {
|
|
background-color: var(--warning-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
background-color: #d97706;
|
|
}
|
|
|
|
.btn-danger {
|
|
background-color: var(--danger-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #dc2626;
|
|
}
|
|
|
|
/* 输入框样式 */
|
|
input, select, textarea {
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: var(--border-radius-md);
|
|
border: 1px solid var(--border-color);
|
|
font-size: 0.875rem;
|
|
transition: var(--transition);
|
|
background-color: white;
|
|
color: var(--text-primary);
|
|
outline: none;
|
|
}
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
border-color: var(--primary-light);
|
|
box-shadow: 0 0 0 3px rgba(54, 126, 233, 0.1);
|
|
}
|
|
|
|
/* 快速访问卡片 */
|
|
.quick-access {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.25rem;
|
|
margin-top: 1.25rem;
|
|
}
|
|
|
|
.quick-access-item {
|
|
background-color: white;
|
|
border-radius: var(--border-radius-lg);
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.quick-access-item:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: var(--shadow-lg);
|
|
border-color: var(--primary-light);
|
|
background-color: rgba(54, 126, 233, 0.02);
|
|
}
|
|
|
|
.quick-access-icon {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.75rem;
|
|
display: block;
|
|
}
|
|
|
|
.quick-access-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.quick-access-desc {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* 表格样式 */
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.875rem;
|
|
background-color: white;
|
|
border-radius: var(--border-radius-lg);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
th, td {
|
|
padding: 0.75rem 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
th {
|
|
background-color: #f8fafc;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
tr:hover {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
/* 分页样式 */
|
|
.pagination {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 1.5rem;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.pagination button {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: var(--border-radius-md);
|
|
border: 1px solid var(--border-color);
|
|
background-color: white;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.pagination button:hover:not(:disabled) {
|
|
border-color: var(--primary-light);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.pagination button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 1024px) {
|
|
.sidebar {
|
|
width: 200px;
|
|
}
|
|
|
|
.content {
|
|
margin-left: 200px;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.stats-container {
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
width: 180px;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.content {
|
|
margin-left: 180px;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.top-nav {
|
|
padding: 0 1.25rem;
|
|
}
|
|
|
|
.stats-container {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.quick-access {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.content-title {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.sidebar {
|
|
width: 160px;
|
|
}
|
|
|
|
.content {
|
|
margin-left: 160px;
|
|
}
|
|
|
|
.menu-item {
|
|
padding: 0.625rem 1rem;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.quick-access {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* 动画效果 */
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(1rem);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.5s ease forwards;
|
|
}
|
|
|
|
.delay-1 {
|
|
animation-delay: 0.1s;
|
|
}
|
|
|
|
.delay-2 {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
.delay-3 {
|
|
animation-delay: 0.3s;
|
|
}
|
|
|
|
.delay-4 {
|
|
animation-delay: 0.4s;
|
|
}
|
|
|
|
.delay-5 {
|
|
animation-delay: 0.5s;
|
|
}
|
|
|
|
/* 骨架屏样式 */
|
|
.skeleton-text {
|
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
background-size: 200% 100%;
|
|
animation: skeleton-loading 1.5s infinite;
|
|
border-radius: 4px;
|
|
height: 16px;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.skeleton-text-sm {
|
|
height: 12px;
|
|
width: 60px;
|
|
}
|
|
|
|
.skeleton-text-lg {
|
|
height: 24px;
|
|
width: 150px;
|
|
}
|
|
|
|
@keyframes skeleton-loading {
|
|
0% {
|
|
background-position: 200% 0;
|
|
}
|
|
100% {
|
|
background-position: -200% 0;
|
|
}
|
|
}
|
|
|
|
/* 滚动条样式 */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- 粒子特效容器 -->
|
|
<div id="particles-container">
|
|
<canvas id="particles-canvas"></canvas>
|
|
</div>
|
|
|
|
<!-- 顶部导航栏 -->
|
|
<div class="top-nav">
|
|
<div class="logo">
|
|
<div class="logo-icon">管</div>
|
|
<span>后台管理系统</span>
|
|
</div>
|
|
<div class="user-info">
|
|
<div class="user-name" id="userName">欢迎,管理员</div>
|
|
<button class="btn btn-default" onclick="gotoLogin()">退出登录</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主容器 -->
|
|
<div class="main-container">
|
|
<!-- 侧边栏 -->
|
|
<div class="sidebar">
|
|
<div class="menu-item active" onclick="showDashboard()">
|
|
<span class="menu-icon">📊</span>
|
|
<span>控制台</span>
|
|
</div>
|
|
<div class="menu-item" onclick="gotoResource()">
|
|
<span class="menu-icon">📁</span>
|
|
<span>资源管理</span>
|
|
</div>
|
|
<div class="menu-item" onclick="gotoFollow()">
|
|
<span class="menu-icon">👥</span>
|
|
<span>跟进管理</span>
|
|
</div>
|
|
<div class="menu-item" onclick="gotoInfo()">
|
|
<span class="menu-icon">📋</span>
|
|
<span>信息管理</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('logs'); showOperationLogs()">
|
|
<span class="menu-icon">📝</span>
|
|
<span>操作日志</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('client-stats'); showClientStats()">
|
|
<span class="menu-icon">👤</span>
|
|
<span>客户统计</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('business-stats'); showBusinessStats()">
|
|
<span class="menu-icon">📊</span>
|
|
<span>业务统计</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('active-stats'); showActiveStats()">
|
|
<span class="menu-icon">📱</span>
|
|
<span>客户活跃统计</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('suppliers'); gotoSupplier()">
|
|
<span class="menu-icon">🏭</span>
|
|
<span>供应商管理</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('supply-management'); showSupplyManagement()">
|
|
<span class="menu-icon">📦</span>
|
|
<span>货源管理</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('comments'); gotoCommentReview()">
|
|
<span class="menu-icon">📝</span>
|
|
<span>留言审核</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('forum-posts'); gotoForumPostReview()">
|
|
<span class="menu-icon">💬</span>
|
|
<span>论坛动态审核</span>
|
|
</div>
|
|
<div class="menu-item" onclick="loadModuleOnDemand('identity-verification'); gotoIdentityReview()">
|
|
<span class="menu-icon">🆔</span>
|
|
<span>身份信息审核</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 内容区域 -->
|
|
<div class="content">
|
|
<!-- 内容头部 -->
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">控制台</h1>
|
|
<p class="content-subtitle">欢迎使用后台管理系统,您可以在这里快速访问各个管理模块</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="stats-container">
|
|
<div class="stat-card fade-in delay-1">
|
|
<div class="stat-number" id="totalResources">加载中...</div>
|
|
<div class="stat-label">资源总数</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-2">
|
|
<div class="stat-number" id="pendingResources">加载中...</div>
|
|
<div class="stat-label">待审核资源</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-3">
|
|
<div class="stat-number" id="todayNewResources">加载中...</div>
|
|
<div class="stat-label">今日新增</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快速访问卡片 -->
|
|
<div class="card fade-in delay-2">
|
|
<h2 class="card-title">快速访问</h2>
|
|
<div class="quick-access">
|
|
<div class="quick-access-item" onclick="gotoResource()">
|
|
<div class="quick-access-icon">📁</div>
|
|
<div class="quick-access-title">资源管理</div>
|
|
<div class="quick-access-desc">管理系统资源</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoFollow()">
|
|
<div class="quick-access-icon">👥</div>
|
|
<div class="quick-access-title">跟进管理</div>
|
|
<div class="quick-access-desc">管理跟进事项</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoInfo()">
|
|
<div class="quick-access-icon">📋</div>
|
|
<div class="quick-access-title">信息管理</div>
|
|
<div class="quick-access-desc">管理系统信息</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="showOperationLogs()">
|
|
<div class="quick-access-icon">📝</div>
|
|
<div class="quick-access-title">操作日志</div>
|
|
<div class="quick-access-desc">查看系统操作日志</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="showClientStats()">
|
|
<div class="quick-access-icon">👤</div>
|
|
<div class="quick-access-title">客户统计</div>
|
|
<div class="quick-access-desc">查看客户统计数据</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="showBusinessStats()">
|
|
<div class="quick-access-icon">📊</div>
|
|
<div class="quick-access-title">业务统计</div>
|
|
<div class="quick-access-desc">查看业务统计数据</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="showActiveStats()">
|
|
<div class="quick-access-icon">📱</div>
|
|
<div class="quick-access-title">客户活跃统计</div>
|
|
<div class="quick-access-desc">查看客户活跃情况</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoSupplier()">
|
|
<div class="quick-access-icon">🏭</div>
|
|
<div class="quick-access-title">供应商管理</div>
|
|
<div class="quick-access-desc">管理供应商信息</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoCommentReview()">
|
|
<div class="quick-access-icon">📝</div>
|
|
<div class="quick-access-title">留言审核</div>
|
|
<div class="quick-access-desc">审核用户留言</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoIdentityReview()">
|
|
<div class="quick-access-icon">🆔</div>
|
|
<div class="quick-access-title">身份信息审核</div>
|
|
<div class="quick-access-desc">审核用户身份信息</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="showSupplyManagement()">
|
|
<div class="quick-access-icon">📦</div>
|
|
<div class="quick-access-title">货源管理</div>
|
|
<div class="quick-access-desc">管理系统货源</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoForumPostReview()">
|
|
<div class="quick-access-icon">💬</div>
|
|
<div class="quick-access-title">论坛动态审核</div>
|
|
<div class="quick-access-desc">审核论坛动态</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图片预览弹窗 -->
|
|
<div id="imagePreviewModal" style="
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
z-index: 2000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
">
|
|
<div style="position: relative; max-width: 95%; max-height: 95vh; cursor: default;">
|
|
<button onclick="closePreview()" style="
|
|
position: absolute;
|
|
top: -40px;
|
|
right: 0;
|
|
background: none;
|
|
border: none;
|
|
font-size: 2rem;
|
|
cursor: pointer;
|
|
color: white;
|
|
transition: color 0.2s ease;
|
|
">×</button>
|
|
|
|
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
|
<h4 id="previewTitle" style="color: white; font-size: 1.125rem; font-weight: 600; margin: 0;text-align: center;"></h4>
|
|
|
|
<div style="position: relative; display: flex; align-items: center; justify-content: center; gap: 1rem;">
|
|
<button onclick="zoomImage(-0.1)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
color: white;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
">-</button>
|
|
|
|
<div id="previewContainer" style="overflow: auto; max-width: 100%; max-height: 85vh; cursor: grab;">
|
|
<img
|
|
id="previewImage"
|
|
src=""
|
|
alt="预览图片"
|
|
style="max-width: 100%; max-height: 85vh; transition: transform 0.3s ease;"
|
|
/>
|
|
</div>
|
|
|
|
<button onclick="zoomImage(0.1)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
color: white;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
">+</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 1rem; align-items: center; justify-content: center;">
|
|
<button onclick="rotateImage(-90)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">逆时针旋转</button>
|
|
|
|
<button onclick="resetZoom()" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">重置</button>
|
|
|
|
<button onclick="rotateImage(90)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">顺时针旋转</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统信息卡片 -->
|
|
<div class="card fade-in delay-3">
|
|
<h2 class="card-title">系统信息</h2>
|
|
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.8;">
|
|
<p>当前版本:v2.1.2</p>
|
|
<p>上次更新:2026-02-10</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 格式化时间函数(全局)
|
|
const formatTime = (timeStr) => {
|
|
if (!timeStr) return '暂无';
|
|
const date = new Date(timeStr);
|
|
return date.toLocaleString();
|
|
};
|
|
|
|
// 图片预览相关全局变量
|
|
let currentScale = 1;
|
|
let currentImageUrl = '';
|
|
let currentRotation = 0;
|
|
let currentX = 0;
|
|
let currentY = 0;
|
|
|
|
// 业务统计分页相关变量
|
|
let businessAllClients = [];
|
|
let businessCurrentPage = 1;
|
|
let businessTotalPages = 1;
|
|
let businessPageSize = 10;
|
|
|
|
// 货源管理相关全局变量
|
|
let supplyCacheData = [];
|
|
let supplyCacheTime = 0;
|
|
let supplyIsLoading = false;
|
|
let supplyCurrentPage = 1;
|
|
let supplyCurrentSearchTerm = '';
|
|
let supplyCurrentPageSize = 10;
|
|
let supplyTotalCount = 0;
|
|
|
|
// 供应商分页相关全局变量
|
|
let supplierCurrentPage = 1;
|
|
let supplierPageSize = 10;
|
|
let supplierTotalCount = 0;
|
|
|
|
// 从localStorage获取登录信息
|
|
const loginInfo = JSON.parse(localStorage.getItem('loginInfo'));
|
|
|
|
// 如果没有登录信息,跳转到登录页面
|
|
if (!loginInfo) {
|
|
window.location.href = '/login.html';
|
|
}
|
|
|
|
// 更新用户信息
|
|
document.getElementById('userName').textContent = `欢迎,${loginInfo.userName}`;
|
|
|
|
// 性能优化:实现延迟加载和按需加载机制
|
|
|
|
// 所有页面模块(统一优先级)
|
|
const allModules = ['dashboard', 'active-stats', 'logs', 'supply-management', 'client-stats', 'business-stats', 'suppliers', 'comments', 'forum-posts', 'identity-verification'];
|
|
|
|
// 加载状态管理
|
|
const loadState = {
|
|
isLoading: false,
|
|
loadedModules: new Set(),
|
|
loadingModules: new Set(),
|
|
|
|
// 设置模块加载状态
|
|
setLoading(module, isLoading) {
|
|
if (isLoading) {
|
|
this.loadingModules.add(module);
|
|
} else {
|
|
this.loadingModules.delete(module);
|
|
}
|
|
this.isLoading = this.loadingModules.size > 0;
|
|
},
|
|
|
|
// 检查模块是否正在加载
|
|
isModuleLoading(module) {
|
|
return this.loadingModules.has(module);
|
|
},
|
|
|
|
// 检查模块是否已加载
|
|
isModuleLoaded(module) {
|
|
return this.loadedModules.has(module);
|
|
},
|
|
|
|
// 标记模块已加载
|
|
markModuleLoaded(module) {
|
|
this.loadedModules.add(module);
|
|
this.loadingModules.delete(module);
|
|
this.isLoading = this.loadingModules.size > 0;
|
|
}
|
|
};
|
|
|
|
// 加载状态管理工具
|
|
const loadingManager = {
|
|
// 显示模块加载状态
|
|
showLoading(module) {
|
|
loadState.setLoading(module, true);
|
|
this.showModuleSkeleton(module);
|
|
},
|
|
|
|
// 隐藏模块加载状态
|
|
hideLoading(module) {
|
|
loadState.setLoading(module, false);
|
|
},
|
|
|
|
// 显示模块骨架屏
|
|
showModuleSkeleton(module) {
|
|
const content = document.querySelector('.content');
|
|
if (!content) return;
|
|
|
|
switch(module) {
|
|
case 'dashboard':
|
|
this.showDashboardSkeleton();
|
|
break;
|
|
case 'active-stats':
|
|
this.showActiveStatsSkeleton();
|
|
break;
|
|
case 'logs':
|
|
this.showLogsSkeleton();
|
|
break;
|
|
case 'supply-management':
|
|
this.showSupplyManagementSkeleton();
|
|
break;
|
|
case 'client-stats':
|
|
this.showClientStatsSkeleton();
|
|
break;
|
|
case 'business-stats':
|
|
this.showBusinessStatsSkeleton();
|
|
break;
|
|
case 'suppliers':
|
|
this.showSuppliersSkeleton();
|
|
break;
|
|
case 'comments':
|
|
this.showCommentsSkeleton();
|
|
break;
|
|
case 'forum-posts':
|
|
this.showForumPostsSkeleton();
|
|
break;
|
|
case 'identity-verification':
|
|
this.showIdentityVerificationSkeleton();
|
|
break;
|
|
}
|
|
},
|
|
|
|
// 显示控制台骨架屏
|
|
showDashboardSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">控制台</h1>
|
|
<p class="content-subtitle">欢迎使用后台管理系统,您可以在这里快速访问各个管理模块</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片骨架屏 -->
|
|
<div class="stats-container">
|
|
<div class="stat-card fade-in delay-1">
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
<div class="skeleton-text skeleton-text-sm"></div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-2">
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
<div class="skeleton-text skeleton-text-sm"></div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-3">
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
<div class="skeleton-text skeleton-text-sm"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快速访问卡片骨架屏 -->
|
|
<div class="card fade-in delay-2">
|
|
<h2 class="card-title">快速访问</h2>
|
|
<div class="quick-access">
|
|
${Array(12).fill().map((_, index) => `
|
|
<div class="quick-access-item fade-in" style="animation-delay: ${0.1 * index}s;">
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-sm"></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统信息卡片骨架屏 -->
|
|
<div class="card fade-in delay-3">
|
|
<h2 class="card-title">系统信息</h2>
|
|
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.8;">
|
|
${Array(4).fill().map(() => '<div class="skeleton-text"></div>').join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示客户活跃统计骨架屏
|
|
showActiveStatsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">客户活跃统计</h1>
|
|
<p class="content-subtitle">查看客户活跃情况和活跃时长统计</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">活跃统计概览</h2>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
|
|
${Array(3).fill().map(() => `
|
|
<div style="padding: 1.5rem; background-color: #f8fafc; border-radius: var(--border-radius-lg);">
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div class="card fade-in delay-1">
|
|
<h2 class="card-title">活跃客户排名</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">排名</th>
|
|
<th style="padding: 1rem; text-align: left;">用户ID</th>
|
|
<th style="padding: 1rem; text-align: left;">姓名</th>
|
|
<th style="padding: 1rem; text-align: left;">注册时间</th>
|
|
<th style="padding: 1rem; text-align: left;">总活跃时长</th>
|
|
<th style="padding: 1rem; text-align: left;">最近活跃时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示操作日志骨架屏
|
|
showLogsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">操作日志</h1>
|
|
<p class="content-subtitle">查看系统操作日志记录</p>
|
|
</div>
|
|
|
|
<!-- 搜索栏骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">日志查询</h2>
|
|
<div style="display: flex; gap: 1rem; align-items: center; margin-bottom: 1.5rem;">
|
|
<div style="flex: 1;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
<div style="width: 120px;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
|
<th style="padding: 1rem; text-align: left;">操作用户</th>
|
|
<th style="padding: 1rem; text-align: left;">操作类型</th>
|
|
<th style="padding: 1rem; text-align: left;">操作详情</th>
|
|
<th style="padding: 1rem; text-align: left;">操作时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示货源管理骨架屏
|
|
showSupplyManagementSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">货源管理</h1>
|
|
<p class="content-subtitle">管理系统货源信息</p>
|
|
</div>
|
|
|
|
<!-- 搜索栏骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">货源查询</h2>
|
|
<div style="display: flex; gap: 1rem; align-items: center; margin-bottom: 1.5rem;">
|
|
<div style="flex: 1;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
<div style="width: 120px;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
|
<th style="padding: 1rem; text-align: left;">货源名称</th>
|
|
<th style="padding: 1rem; text-align: left;">供应商</th>
|
|
<th style="padding: 1rem; text-align: left;">价格</th>
|
|
<th style="padding: 1rem; text-align: left;">库存</th>
|
|
<th style="padding: 1rem; text-align: left;">状态</th>
|
|
<th style="padding: 1rem; text-align: left;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示客户统计骨架屏
|
|
showClientStatsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">客户统计</h1>
|
|
<p class="content-subtitle">查看客户统计数据</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">客户统计概览</h2>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
|
|
${Array(4).fill().map(() => `
|
|
<div style="padding: 1.5rem; background-color: #f8fafc; border-radius: var(--border-radius-lg);">
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表骨架屏 -->
|
|
<div class="card fade-in delay-1">
|
|
<h2 class="card-title">客户增长趋势</h2>
|
|
<div style="height: 350px; background-color: #f8fafc; border-radius: var(--border-radius-lg); display: flex; align-items: center; justify-content: center;">
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示业务统计骨架屏
|
|
showBusinessStatsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">业务统计</h1>
|
|
<p class="content-subtitle">查看业务统计数据</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">业务统计概览</h2>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
|
|
${Array(4).fill().map(() => `
|
|
<div style="padding: 1.5rem; background-color: #f8fafc; border-radius: var(--border-radius-lg);">
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div class="card fade-in delay-1">
|
|
<h2 class="card-title">业务详情</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">客户</th>
|
|
<th style="padding: 1rem; text-align: left;">业务类型</th>
|
|
<th style="padding: 1rem; text-align: left;">金额</th>
|
|
<th style="padding: 1rem; text-align: left;">日期</th>
|
|
<th style="padding: 1rem; text-align: left;">状态</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示供应商管理骨架屏
|
|
showSuppliersSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">供应商管理</h1>
|
|
<p class="content-subtitle">管理供应商入驻信息及分配对接人</p>
|
|
</div>
|
|
|
|
<!-- 搜索栏骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">供应商列表</h2>
|
|
<div style="display: flex; gap: 1rem; align-items: center; margin-bottom: 1.5rem;">
|
|
<div style="flex: 1;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
<div style="width: 120px;">
|
|
<div class="skeleton-text"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">用户ID</th>
|
|
<th style="padding: 1rem; text-align: left;">电话号码</th>
|
|
<th style="padding: 1rem; text-align: left;">客户公司</th>
|
|
<th style="padding: 1rem; text-align: left;">合作状态</th>
|
|
<th style="padding: 1rem; text-align: left;">对接人</th>
|
|
<th style="padding: 1rem; text-align: left;">创建时间</th>
|
|
<th style="padding: 1rem; text-align: left;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示留言审核骨架屏
|
|
showCommentsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">留言审核</h1>
|
|
<p class="content-subtitle">审核用户留言</p>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">待审核留言</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
|
<th style="padding: 1rem; text-align: left;">用户</th>
|
|
<th style="padding: 1rem; text-align: left;">留言内容</th>
|
|
<th style="padding: 1rem; text-align: left;">留言时间</th>
|
|
<th style="padding: 1rem; text-align: left;">状态</th>
|
|
<th style="padding: 1rem; text-align: left;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示论坛动态审核骨架屏
|
|
showForumPostsSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">论坛动态审核</h1>
|
|
<p class="content-subtitle">审核论坛动态</p>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">待审核论坛动态</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
|
<th style="padding: 1rem; text-align: left;">用户</th>
|
|
<th style="padding: 1rem; text-align: left;">内容</th>
|
|
<th style="padding: 1rem; text-align: left;">发布时间</th>
|
|
<th style="padding: 1rem; text-align: left;">状态</th>
|
|
<th style="padding: 1rem; text-align: left;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
// 显示身份信息审核骨架屏
|
|
showIdentityVerificationSkeleton() {
|
|
const content = document.querySelector('.content');
|
|
if (content) {
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">身份信息审核</h1>
|
|
<p class="content-subtitle">审核用户身份信息</p>
|
|
</div>
|
|
|
|
<!-- 表格骨架屏 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">待审核身份信息</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
|
<th style="padding: 1rem; text-align: left;">用户</th>
|
|
<th style="padding: 1rem; text-align: left;">真实姓名</th>
|
|
<th style="padding: 1rem; text-align: left;">身份证号</th>
|
|
<th style="padding: 1rem; text-align: left;">提交时间</th>
|
|
<th style="padding: 1rem; text-align: left;">状态</th>
|
|
<th style="padding: 1rem; text-align: left;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Array(10).fill().map(() => `
|
|
<tr>
|
|
<td style="padding: 1rem;"><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
<td style="padding: 1rem;"><div class="skeleton-text"></div></td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 分批加载函数
|
|
function loadModulesInBatches(modules, batchSize = 3, delay = 400) {
|
|
if (loadState.isLoading) return;
|
|
|
|
loadState.isLoading = true;
|
|
|
|
const batches = [];
|
|
for (let i = 0; i < modules.length; i += batchSize) {
|
|
batches.push(modules.slice(i, i + batchSize));
|
|
}
|
|
|
|
let batchIndex = 0;
|
|
|
|
function processBatch() {
|
|
if (batchIndex >= batches.length) {
|
|
loadState.isLoading = false;
|
|
console.log('所有模块加载完成');
|
|
return;
|
|
}
|
|
|
|
const currentBatch = batches[batchIndex];
|
|
console.log(`加载批次 ${batchIndex + 1}:`, currentBatch);
|
|
|
|
Promise.all(
|
|
currentBatch.map(module => loadModuleData(module))
|
|
)
|
|
.then(() => {
|
|
batchIndex++;
|
|
setTimeout(processBatch, delay);
|
|
})
|
|
.catch(error => {
|
|
console.error('批次加载失败:', error);
|
|
batchIndex++;
|
|
setTimeout(processBatch, delay);
|
|
}, true);
|
|
}
|
|
|
|
processBatch();
|
|
}
|
|
|
|
// 加载单个模块数据
|
|
function loadModuleData(module, forceRefresh = false) {
|
|
return new Promise((resolve) => {
|
|
// 格式化日期为YYYY-MM-DD
|
|
const formatDate = (date) => {
|
|
return date.toISOString().split('T')[0];
|
|
};
|
|
|
|
// 设置默认时间范围为最近7天
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
const formattedStartDate = formatDate(startDate);
|
|
const formattedEndDate = formatDate(endDate);
|
|
|
|
switch(module) {
|
|
case 'dashboard':
|
|
// 优先使用永久缓存
|
|
const cachedDashboardData = cacheManager.getPermanent('dashboardData');
|
|
if (cachedDashboardData) {
|
|
console.log('使用永久缓存的控制台数据');
|
|
resolve(cachedDashboardData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新控制台缓存数据...');
|
|
// 加载控制台统计数据
|
|
fetch('/api/stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'dashboard_stats';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('dashboardData', data);
|
|
console.log('后台更新控制台缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新控制台统计数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载控制台统计数据
|
|
fetch('/api/stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'dashboard_stats';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('dashboardData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载控制台统计数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedDashboardData) {
|
|
console.log('使用永久缓存的控制台数据');
|
|
resolve(cachedDashboardData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'active-stats':
|
|
// 优先使用永久缓存
|
|
const cachedActiveStatsData = cacheManager.getPermanent('activeStatsData');
|
|
if (cachedActiveStatsData) {
|
|
console.log('使用永久缓存的活跃统计数据');
|
|
resolve(cachedActiveStatsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新活跃统计缓存数据...');
|
|
// 加载客户活跃统计数据
|
|
// 使用更加健壮的方式加载数据,即使某个API端点返回错误,也能继续加载其他API端点的数据
|
|
const fetchWithFallback = (url) => {
|
|
return fetch(url)
|
|
.then(response => response.json())
|
|
.catch(error => {
|
|
console.warn(`加载API端点失败: ${url}`, error);
|
|
return { success: false, message: 'API加载失败', data: {} };
|
|
});
|
|
};
|
|
|
|
Promise.all([
|
|
fetchWithFallback(`/api/active-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`),
|
|
fetchWithFallback(`/api/daily-active-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`),
|
|
fetchWithFallback(`/api/total-active-duration?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`)
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
const combinedData = {
|
|
overview: overviewData || { success: false, data: {} },
|
|
daily: dailyData || { success: false, data: {} },
|
|
totalDuration: totalDurationData || { success: false, data: {} }
|
|
};
|
|
const cacheKey = `active_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`;
|
|
cacheManager.set(cacheKey, combinedData);
|
|
const setResult = cacheManager.setPermanent('activeStatsData', combinedData);
|
|
if (setResult) {
|
|
console.log('后台更新活跃统计缓存数据完成');
|
|
} else {
|
|
console.error('后台更新活跃统计缓存数据失败: 永久缓存设置失败');
|
|
// 即使缓存设置失败,也尝试清理空间后重试
|
|
cacheManager.cleanupByPriority();
|
|
const retryResult = cacheManager.setPermanent('activeStatsData', combinedData);
|
|
if (retryResult) {
|
|
console.log('后台更新活跃统计缓存数据重试成功');
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新客户活跃统计数据失败:', error);
|
|
// 即使API失败,也尝试保持现有缓存
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载客户活跃统计数据
|
|
// 使用更加健壮的方式加载数据,即使某个API端点返回错误,也能继续加载其他API端点的数据
|
|
const fetchWithFallback = (url) => {
|
|
return fetch(url)
|
|
.then(response => response.json())
|
|
.catch(error => {
|
|
console.warn(`加载API端点失败: ${url}`, error);
|
|
return { success: false, message: 'API加载失败', data: {} };
|
|
});
|
|
};
|
|
|
|
Promise.all([
|
|
fetchWithFallback(`/api/active-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`),
|
|
fetchWithFallback(`/api/daily-active-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`),
|
|
fetchWithFallback(`/api/total-active-duration?startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`)
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
const combinedData = {
|
|
overview: overviewData || { success: false, data: {} },
|
|
daily: dailyData || { success: false, data: {} },
|
|
totalDuration: totalDurationData || { success: false, data: {} }
|
|
};
|
|
const cacheKey = `active_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}&page=1&pageSize=10`;
|
|
cacheManager.set(cacheKey, combinedData);
|
|
const setResult = cacheManager.setPermanent('activeStatsData', combinedData);
|
|
if (setResult) {
|
|
console.log('活跃统计永久缓存设置成功');
|
|
} else {
|
|
console.error('活跃统计永久缓存设置失败');
|
|
// 即使缓存设置失败,也尝试清理空间后重试
|
|
cacheManager.cleanupByPriority();
|
|
const retryResult = cacheManager.setPermanent('activeStatsData', combinedData);
|
|
if (retryResult) {
|
|
console.log('活跃统计永久缓存设置重试成功');
|
|
}
|
|
}
|
|
resolve(combinedData);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载客户活跃统计数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedActiveStatsData) {
|
|
console.log('使用永久缓存的活跃统计数据');
|
|
resolve(cachedActiveStatsData);
|
|
} else {
|
|
// 即使所有API端点都失败,也创建一个默认的空对象并设置永久缓存
|
|
const defaultData = {
|
|
overview: { success: false, data: {} },
|
|
daily: { success: false, data: {} },
|
|
totalDuration: { success: false, data: {} }
|
|
};
|
|
const setResult = cacheManager.setPermanent('activeStatsData', defaultData);
|
|
if (setResult) {
|
|
console.log('活跃统计默认数据永久缓存设置成功');
|
|
} else {
|
|
console.error('活跃统计默认数据永久缓存设置失败');
|
|
// 即使缓存设置失败,也尝试清理空间后重试
|
|
cacheManager.cleanupByPriority();
|
|
const retryResult = cacheManager.setPermanent('activeStatsData', defaultData);
|
|
if (retryResult) {
|
|
console.log('活跃统计默认数据永久缓存设置重试成功');
|
|
}
|
|
}
|
|
resolve(defaultData);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'logs':
|
|
// 优先使用永久缓存
|
|
const cachedLogsData = cacheManager.getPermanent('logsData');
|
|
if (cachedLogsData) {
|
|
console.log('使用永久缓存的操作日志数据');
|
|
resolve(cachedLogsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新操作日志缓存数据...');
|
|
// 加载操作日志数据
|
|
fetch('/api/logs?page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'logs_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('logsData', data);
|
|
console.log('后台更新操作日志缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新操作日志数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载操作日志数据
|
|
fetch('/api/logs?page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'logs_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('logsData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载操作日志数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedLogsData) {
|
|
console.log('使用永久缓存的操作日志数据');
|
|
resolve(cachedLogsData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'supply-management':
|
|
// 优先使用永久缓存
|
|
const cachedSupplyManagementData = cacheManager.getPermanent('supplyManagementData');
|
|
if (cachedSupplyManagementData) {
|
|
console.log('使用永久缓存的货源管理数据');
|
|
resolve(cachedSupplyManagementData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新货源管理缓存数据...');
|
|
// 加载货源管理数据
|
|
Promise.all([
|
|
fetch('/api/supply-management?page=1&pageSize=30'),
|
|
fetch('/api/supply-management?getAll=true')
|
|
])
|
|
.then(([initialResponse, fullResponse]) => {
|
|
return Promise.all([initialResponse.json(), fullResponse.json()]);
|
|
})
|
|
.then(([initialData, fullData]) => {
|
|
const combinedData = { initialData, fullData };
|
|
if (initialData.success) {
|
|
const cacheKey = 'supply_initial_page=1&pageSize=30';
|
|
cacheManager.set(cacheKey, initialData);
|
|
}
|
|
if (fullData.success) {
|
|
const cacheKey = 'supply_all';
|
|
cacheManager.set(cacheKey, fullData);
|
|
}
|
|
cacheManager.setPermanent('supplyManagementData', combinedData);
|
|
console.log('后台更新货源管理缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新货源管理数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载货源管理数据
|
|
Promise.all([
|
|
fetch('/api/supply-management?page=1&pageSize=30'),
|
|
fetch('/api/supply-management?getAll=true')
|
|
])
|
|
.then(([initialResponse, fullResponse]) => {
|
|
return Promise.all([initialResponse.json(), fullResponse.json()]);
|
|
})
|
|
.then(([initialData, fullData]) => {
|
|
const combinedData = { initialData, fullData };
|
|
if (initialData.success) {
|
|
const cacheKey = 'supply_initial_page=1&pageSize=30';
|
|
cacheManager.set(cacheKey, initialData);
|
|
}
|
|
if (fullData.success) {
|
|
const cacheKey = 'supply_all';
|
|
cacheManager.set(cacheKey, fullData);
|
|
}
|
|
cacheManager.setPermanent('supplyManagementData', combinedData);
|
|
resolve(combinedData);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载货源管理数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedSupplyManagementData) {
|
|
console.log('使用永久缓存的货源管理数据');
|
|
resolve(cachedSupplyManagementData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'client-stats':
|
|
// 优先使用永久缓存
|
|
const cachedClientStatsData = cacheManager.getPermanent('clientStatsData');
|
|
if (cachedClientStatsData) {
|
|
console.log('使用永久缓存的客户统计数据');
|
|
resolve(cachedClientStatsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新客户统计缓存数据...');
|
|
// 加载客户统计数据
|
|
fetch(`/api/client-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&followedClientBy=updated_at`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = `client_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}&followedClientBy=updated_at`;
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('clientStatsData', data);
|
|
console.log('后台更新客户统计缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新客户统计数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载客户统计数据
|
|
fetch(`/api/client-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}&followedClientBy=updated_at`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = `client_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}&followedClientBy=updated_at`;
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('clientStatsData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载客户统计数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedClientStatsData) {
|
|
console.log('使用永久缓存的客户统计数据');
|
|
resolve(cachedClientStatsData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'business-stats':
|
|
// 优先使用永久缓存
|
|
const cachedBusinessStatsData = cacheManager.getPermanent('businessStatsData');
|
|
if (cachedBusinessStatsData) {
|
|
console.log('使用永久缓存的业务统计数据');
|
|
resolve(cachedBusinessStatsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新业务统计缓存数据...');
|
|
// 加载业务统计数据
|
|
fetch(`/api/business-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = `business_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}`;
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('businessStatsData', data);
|
|
console.log('后台更新业务统计缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新业务统计数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载业务统计数据
|
|
fetch(`/api/business-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = `business_stats_startDate=${formattedStartDate}&endDate=${formattedEndDate}`;
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('businessStatsData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载业务统计数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedBusinessStatsData) {
|
|
console.log('使用永久缓存的业务统计数据');
|
|
resolve(cachedBusinessStatsData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'suppliers':
|
|
// 优先使用永久缓存
|
|
const cachedSuppliersData = cacheManager.getPermanent('suppliersData');
|
|
if (cachedSuppliersData) {
|
|
console.log('使用永久缓存的供应商管理统计数据');
|
|
resolve(cachedSuppliersData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新供应商管理缓存数据...');
|
|
// 加载供应商统计数据
|
|
fetch('/api/suppliers')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'suppliers_all';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('suppliersData', data);
|
|
console.log('后台更新供应商管理缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新供应商统计数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载供应商统计数据
|
|
fetch('/api/suppliers')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'suppliers_all';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('suppliersData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载供应商统计数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedSuppliersData) {
|
|
console.log('使用永久缓存的供应商统计数据');
|
|
resolve(cachedSuppliersData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'comments':
|
|
// 优先使用永久缓存
|
|
const cachedCommentsData = cacheManager.getPermanent('commentsData');
|
|
if (cachedCommentsData) {
|
|
console.log('使用永久缓存的留言审核数据');
|
|
resolve(cachedCommentsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新留言审核缓存数据...');
|
|
// 加载留言审核数据
|
|
fetch('/api/comments?status=pending&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'comments_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('commentsData', data);
|
|
console.log('后台更新留言审核缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新留言审核数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载留言审核数据
|
|
fetch('/api/comments?status=pending&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'comments_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('commentsData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载留言审核数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedCommentsData) {
|
|
console.log('使用永久缓存的留言审核数据');
|
|
resolve(cachedCommentsData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'forum-posts':
|
|
// 优先使用永久缓存
|
|
const cachedForumPostsData = cacheManager.getPermanent('forumPostsData');
|
|
if (cachedForumPostsData) {
|
|
console.log('使用永久缓存的论坛动态审核数据');
|
|
resolve(cachedForumPostsData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新论坛动态审核缓存数据...');
|
|
// 加载论坛动态审核数据
|
|
fetch('/api/forum-posts?status=pending&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'forum_posts_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('forumPostsData', data);
|
|
console.log('后台更新论坛动态审核缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新论坛动态审核数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载论坛动态审核数据
|
|
fetch('/api/forum-posts?status=0&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'forum_posts_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('forumPostsData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载论坛动态审核数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedForumPostsData) {
|
|
console.log('使用永久缓存的论坛动态审核数据');
|
|
resolve(cachedForumPostsData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 'identity-verification':
|
|
// 优先使用永久缓存
|
|
const cachedIdentityVerificationData = cacheManager.getPermanent('identityVerificationData');
|
|
if (cachedIdentityVerificationData) {
|
|
console.log('使用永久缓存的身份信息审核数据');
|
|
resolve(cachedIdentityVerificationData);
|
|
|
|
// 即使forceRefresh=true,也在后台更新缓存
|
|
if (forceRefresh) {
|
|
console.log('后台更新身份信息审核缓存数据...');
|
|
// 加载身份信息审核数据
|
|
fetch('/api/identity-verification?status=pending&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'identity_verification_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('identityVerificationData', data);
|
|
console.log('后台更新身份信息审核缓存数据完成');
|
|
})
|
|
.catch(error => {
|
|
console.error('后台更新身份信息审核数据失败:', error);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 加载身份信息审核数据
|
|
fetch('/api/identity-verification?status=pending&page=1&pageSize=10')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const cacheKey = 'identity_pending_page=1&pageSize=10';
|
|
cacheManager.set(cacheKey, data);
|
|
cacheManager.setPermanent('identityVerificationData', data);
|
|
resolve(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载身份信息审核数据失败:', error);
|
|
// 失败时使用永久缓存
|
|
if (cachedIdentityVerificationData) {
|
|
console.log('使用永久缓存的身份信息审核数据');
|
|
resolve(cachedIdentityVerificationData);
|
|
} else {
|
|
resolve(null);
|
|
}
|
|
});
|
|
break;
|
|
|
|
default:
|
|
resolve(null);
|
|
}
|
|
}, true);
|
|
}
|
|
|
|
// 登录成功后加载策略
|
|
function initDataLoading() {
|
|
console.log('开始初始化数据加载...');
|
|
|
|
// 初始化缓存版本控制
|
|
cacheManager.initCacheVersion();
|
|
|
|
// 注册定期更新任务
|
|
registerPeriodicUpdateTasks();
|
|
|
|
// 启动定期更新
|
|
cacheManager.startPeriodicUpdates();
|
|
|
|
// 1. 立即加载控制台数据(最高优先级)
|
|
loadModuleData('dashboard')
|
|
.then(() => {
|
|
console.log('控制台数据加载完成');
|
|
|
|
// 2. 初始化模块访问计数器(加载历史访问数据)
|
|
initModuleAccessCounter();
|
|
loadState.loadedModules.add('dashboard');
|
|
|
|
// 3. 确保所有指定模块都有永久缓存数据(同步加载关键模块)
|
|
return ensurePermanentCacheData();
|
|
})
|
|
.then(() => {
|
|
console.log('所有模块永久缓存数据加载完成');
|
|
|
|
// 4. 根据历史访问频率预加载模块
|
|
preloadBasedOnFrequency();
|
|
});
|
|
}
|
|
|
|
// 注册定期更新任务
|
|
function registerPeriodicUpdateTasks() {
|
|
// 注册控制台数据更新
|
|
cacheManager.registerPeriodicUpdate('dashboardData', async () => {
|
|
try {
|
|
// 模拟获取最新的控制台数据
|
|
const response = await fetch('/api/stats', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取控制台数据失败:', error);
|
|
throw error;
|
|
}
|
|
}, true);
|
|
|
|
// 注册活跃统计数据更新
|
|
cacheManager.registerPeriodicUpdate('activeStatsData', async () => {
|
|
try {
|
|
// 模拟获取最新的活跃统计数据
|
|
const response = await fetch('/api/active-stats', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取活跃统计数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// 注册客户统计数据更新(快速更新)
|
|
cacheManager.registerPeriodicUpdate('clientStatsData', async () => {
|
|
try {
|
|
// 模拟获取最新的客户统计数据
|
|
const response = await fetch('/api/client-stats', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取客户统计数据失败:', error);
|
|
throw error;
|
|
}
|
|
}, true);
|
|
|
|
// 注册操作日志数据更新
|
|
cacheManager.registerPeriodicUpdate('logsData', async () => {
|
|
try {
|
|
// 模拟获取最新的操作日志数据
|
|
const response = await fetch('/api/logs?page=1&pageSize=10', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取操作日志数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// 注册业务统计数据更新
|
|
cacheManager.registerPeriodicUpdate('businessStatsData', async () => {
|
|
try {
|
|
// 模拟获取最新的业务统计数据
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
const formattedStartDate = startDate.toISOString().split('T')[0];
|
|
const formattedEndDate = endDate.toISOString().split('T')[0];
|
|
|
|
const response = await fetch(`/api/business-stats?startDate=${formattedStartDate}&endDate=${formattedEndDate}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取业务统计数据失败:', error);
|
|
throw error;
|
|
}
|
|
}, true);
|
|
|
|
// 注册供应商管理统计数据更新
|
|
cacheManager.registerPeriodicUpdate('suppliersData', async () => {
|
|
try {
|
|
// 模拟获取最新的供应商管理统计数据
|
|
const response = await fetch('/api/suppliers', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取供应商管理统计数据失败:', error);
|
|
throw error;
|
|
}
|
|
}, true);
|
|
|
|
// 注册货源管理数据更新
|
|
cacheManager.registerPeriodicUpdate('supplyManagementData', async () => {
|
|
try {
|
|
// 模拟获取最新的货源管理数据
|
|
const [initialResponse, fullResponse] = await Promise.all([
|
|
fetch('/api/supply-management?page=1&pageSize=30'),
|
|
fetch('/api/supply-management?getAll=true')
|
|
]);
|
|
|
|
if (!initialResponse.ok || !fullResponse.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const [initialData, fullData] = await Promise.all([
|
|
initialResponse.json(),
|
|
fullResponse.json()
|
|
]);
|
|
|
|
return { initialData, fullData };
|
|
} catch (error) {
|
|
console.error('获取货源管理数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// 注册留言审核数据更新
|
|
cacheManager.registerPeriodicUpdate('commentsData', async () => {
|
|
try {
|
|
// 模拟获取最新的留言审核数据
|
|
const response = await fetch('/api/comments?status=pending&page=1&pageSize=10', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取留言审核数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// 注册论坛动态审核数据更新
|
|
cacheManager.registerPeriodicUpdate('forumPostsData', async () => {
|
|
try {
|
|
// 模拟获取最新的论坛动态审核数据
|
|
const response = await fetch('/api/forum-posts?status=0&page=1&pageSize=10', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取论坛动态审核数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// 注册身份信息审核数据更新
|
|
cacheManager.registerPeriodicUpdate('identityVerificationData', async () => {
|
|
try {
|
|
// 模拟获取最新的身份信息审核数据
|
|
const response = await fetch('/api/identity-verification?status=pending&page=1&pageSize=10', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('网络响应失败');
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('获取身份信息审核数据失败:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 模块访问计数器
|
|
let moduleAccessCounter = new Map();
|
|
|
|
// 初始化模块访问计数器
|
|
function initModuleAccessCounter() {
|
|
// 从localStorage加载历史访问数据
|
|
const savedAccessData = localStorage.getItem('moduleAccessCounter');
|
|
if (savedAccessData) {
|
|
try {
|
|
const parsedData = JSON.parse(savedAccessData);
|
|
moduleAccessCounter = new Map(Object.entries(parsedData));
|
|
console.log('加载历史模块访问数据:', parsedData);
|
|
} catch (error) {
|
|
console.error('解析模块访问数据失败:', error);
|
|
// 解析失败,初始化新的计数器
|
|
allModules.forEach(module => {
|
|
moduleAccessCounter.set(module, 0);
|
|
});
|
|
}
|
|
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
} else {
|
|
// 首次使用,初始化计数器
|
|
allModules.forEach(module => {
|
|
moduleAccessCounter.set(module, 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 记录模块访问
|
|
function recordModuleAccess(module) {
|
|
const currentCount = moduleAccessCounter.get(module) || 0;
|
|
moduleAccessCounter.set(module, currentCount + 1);
|
|
|
|
// 持久化到localStorage
|
|
try {
|
|
const accessData = Object.fromEntries(moduleAccessCounter);
|
|
localStorage.setItem('moduleAccessCounter', JSON.stringify(accessData));
|
|
console.log('更新模块访问数据:', module, '计数:', currentCount + 1);
|
|
} catch (error) {
|
|
console.error('保存模块访问数据失败:', error);
|
|
}
|
|
}
|
|
|
|
// 获取模块访问频率
|
|
function getModuleAccessFrequency(module) {
|
|
return moduleAccessCounter.get(module) || 0;
|
|
}
|
|
|
|
// 保存模块访问数据
|
|
function saveModuleAccessData() {
|
|
try {
|
|
const accessData = Object.fromEntries(moduleAccessCounter);
|
|
localStorage.setItem('moduleAccessCounter', JSON.stringify(accessData));
|
|
console.log('保存模块访问数据成功');
|
|
} catch (error) {
|
|
console.error('保存模块访问数据失败:', error);
|
|
}
|
|
}
|
|
|
|
// 按需加载模块数据
|
|
function loadModuleOnDemand(module) {
|
|
console.log(`按需加载模块: ${module}`);
|
|
|
|
// 记录模块访问
|
|
recordModuleAccess(module);
|
|
|
|
// 检查模块是否已加载
|
|
if (!loadState.loadedModules.has(module)) {
|
|
console.log(`模块 ${module} 未加载,开始加载数据...`);
|
|
|
|
// 显示加载状态
|
|
loadingManager.showLoading(module);
|
|
|
|
// 加载模块数据,强制刷新以获取最新数据
|
|
loadModuleData(module, true)
|
|
.then(() => {
|
|
console.log(`模块 ${module} 数据加载完成`);
|
|
loadState.markModuleLoaded(module);
|
|
loadingManager.hideLoading(module);
|
|
|
|
// 智能预加载 - 基于访问频率
|
|
preloadBasedOnFrequency();
|
|
})
|
|
.catch(error => {
|
|
console.error(`加载模块 ${module} 数据失败:`, error);
|
|
loadState.setLoading(module, false);
|
|
});
|
|
} else {
|
|
console.log(`模块 ${module} 已加载,强制刷新数据...`);
|
|
|
|
// 显示加载状态
|
|
loadingManager.showLoading(module);
|
|
|
|
// 加载模块数据,强制刷新以获取最新数据
|
|
loadModuleData(module, true)
|
|
.then(() => {
|
|
console.log(`模块 ${module} 数据刷新完成`);
|
|
loadState.markModuleLoaded(module);
|
|
loadingManager.hideLoading(module);
|
|
})
|
|
.catch(error => {
|
|
console.error(`刷新模块 ${module} 数据失败:`, error);
|
|
loadState.setLoading(module, false);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 基于访问频率的智能预加载
|
|
function preloadBasedOnFrequency() {
|
|
// 获取访问频率排序的模块列表
|
|
const sortedModules = Array.from(moduleAccessCounter.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([module]) => module);
|
|
|
|
// 预加载访问频率高的未加载模块(最多预加载2个)
|
|
const modulesToPreload = sortedModules
|
|
.filter(module => !loadState.loadedModules.has(module))
|
|
.slice(0, 2);
|
|
|
|
if (modulesToPreload.length > 0) {
|
|
console.log(`智能预加载模块: ${modulesToPreload.join(', ')}`);
|
|
modulesToPreload.forEach(module => {
|
|
if (!loadState.loadedModules.has(module)) {
|
|
console.log(`预加载模块: ${module}`);
|
|
loadModuleData(module)
|
|
.then(() => {
|
|
console.log(`预加载模块 ${module} 完成`);
|
|
loadState.loadedModules.add(module);
|
|
})
|
|
.catch(error => {
|
|
console.error(`预加载模块 ${module} 失败:`, error);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 批量请求函数,使用新的批量API端点
|
|
function batchRequest(requests) {
|
|
return fetch('/api/batch', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ requests })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
return data.results;
|
|
} else {
|
|
throw new Error(data.message || '批量请求失败');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 优化的模块数据加载函数,使用批量请求
|
|
function loadModulesInParallel(modules) {
|
|
console.log('开始并行加载模块:', modules);
|
|
|
|
const requests = modules.map(module => {
|
|
// 格式化日期为YYYY-MM-DD
|
|
const formatDate = (date) => {
|
|
return date.toISOString().split('T')[0];
|
|
};
|
|
|
|
// 设置默认时间范围为最近7天
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
const formattedStartDate = formatDate(startDate);
|
|
const formattedEndDate = formatDate(endDate);
|
|
|
|
switch(module) {
|
|
case 'dashboard':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/stats',
|
|
params: {}
|
|
};
|
|
case 'active-stats':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/active-stats',
|
|
params: {
|
|
startDate: formattedStartDate,
|
|
endDate: formattedEndDate
|
|
}
|
|
};
|
|
case 'logs':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/logs',
|
|
params: {
|
|
page: 1,
|
|
pageSize: 10
|
|
}
|
|
};
|
|
case 'supply-management':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/supply-management',
|
|
params: {
|
|
page: 1,
|
|
pageSize: 30
|
|
}
|
|
};
|
|
case 'client-stats':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/client-stats',
|
|
params: {
|
|
startDate: formattedStartDate,
|
|
endDate: formattedEndDate,
|
|
followedClientBy: 'updated_at'
|
|
}
|
|
};
|
|
case 'business-stats':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/business-stats',
|
|
params: {
|
|
startDate: formattedStartDate,
|
|
endDate: formattedEndDate
|
|
}
|
|
};
|
|
case 'suppliers':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/suppliers',
|
|
params: {}
|
|
};
|
|
case 'comments':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/comments',
|
|
params: {
|
|
status: 'pending',
|
|
page: 1,
|
|
pageSize: 10
|
|
}
|
|
};
|
|
case 'forum-posts':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/forum-posts',
|
|
params: {
|
|
status: 0,
|
|
page: 1,
|
|
pageSize: 10
|
|
}
|
|
};
|
|
case 'identity-verification':
|
|
return {
|
|
id: module,
|
|
endpoint: '/api/identity-verification',
|
|
params: {
|
|
status: 'pending',
|
|
page: 1,
|
|
pageSize: 10
|
|
}
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
}).filter(Boolean);
|
|
|
|
if (requests.length > 0) {
|
|
return batchRequest(requests)
|
|
.then(results => {
|
|
// 处理批量请求结果
|
|
results.forEach(result => {
|
|
if (result.success && result.data) {
|
|
const module = result.id;
|
|
const cacheKey = generateCacheKey(module, result.data);
|
|
cacheManager.set(cacheKey, result.data);
|
|
console.log(`模块 ${module} 数据加载成功并缓存`);
|
|
} else {
|
|
console.error(`模块 ${result.id} 数据加载失败:`, result.error);
|
|
}
|
|
});
|
|
return results;
|
|
});
|
|
} else {
|
|
return Promise.resolve([]);
|
|
}
|
|
}
|
|
|
|
// 生成缓存键
|
|
function generateCacheKey(module, data) {
|
|
const now = new Date();
|
|
const dateKey = now.toISOString().split('T')[0];
|
|
return `${module}_${dateKey}`;
|
|
}
|
|
|
|
// Web Worker实现
|
|
let worker = null;
|
|
let workerCallbacks = new Map();
|
|
let workerTaskId = 0;
|
|
|
|
// 创建Web Worker
|
|
function createWorker() {
|
|
try {
|
|
const workerCode = `
|
|
self.onmessage = function(e) {
|
|
const { taskId, task, data } = e.data;
|
|
|
|
try {
|
|
switch (task) {
|
|
case 'calculateActiveDuration':
|
|
const totalDuration = calculateActiveDuration(data);
|
|
self.postMessage({ taskId, success: true, result: totalDuration });
|
|
break;
|
|
case 'batchParseJSON':
|
|
const parsedData = batchParseJSON(data);
|
|
self.postMessage({ taskId, success: true, result: parsedData });
|
|
break;
|
|
case 'sortData':
|
|
const sortedData = sortData(data.items, data.key, data.order);
|
|
self.postMessage({ taskId, success: true, result: sortedData });
|
|
break;
|
|
default:
|
|
self.postMessage({ taskId, success: false, error: 'Unknown task' });
|
|
}
|
|
} catch (error) {
|
|
self.postMessage({ taskId, success: false, error: error.message });
|
|
}
|
|
};
|
|
|
|
function calculateActiveDuration(operations) {
|
|
if (!operations || operations.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
let totalDuration = 0;
|
|
let currentSessionStart = null;
|
|
let lastAction = null;
|
|
let firstOperationTime = null;
|
|
let lastOperationTime = null;
|
|
|
|
// 批量解析JSON
|
|
const parsedOperations = operations.map(op => {
|
|
try {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: typeof op.originalData === 'string' ? JSON.parse(op.originalData) : op.originalData || {}
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: {}
|
|
};
|
|
}
|
|
}).sort((a, b) => a.operationTime - b.operationTime);
|
|
|
|
// 单次遍历处理所有逻辑
|
|
for (const op of parsedOperations) {
|
|
const operationTime = op.operationTime;
|
|
const action = op.originalData.action;
|
|
|
|
// 更新第一条和最后一条操作时间
|
|
if (!firstOperationTime || operationTime < firstOperationTime) {
|
|
firstOperationTime = operationTime;
|
|
}
|
|
if (!lastOperationTime || operationTime > lastOperationTime) {
|
|
lastOperationTime = operationTime;
|
|
}
|
|
|
|
// 处理带有sessionDuration的app_hide事件
|
|
if (action === 'app_hide' && op.originalData.sessionDuration) {
|
|
const durationInSeconds = op.originalData.sessionDuration / 1000;
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
// 处理app_show/app_hide事件状态
|
|
if (action === 'app_show') {
|
|
currentSessionStart = operationTime;
|
|
lastAction = 'app_show';
|
|
} else if (action === 'app_hide') {
|
|
currentSessionStart = null;
|
|
lastAction = 'app_hide';
|
|
} else if (action) {
|
|
lastAction = action;
|
|
}
|
|
}
|
|
|
|
// 处理未结束的app_show事件
|
|
if (currentSessionStart && lastAction === 'app_show') {
|
|
const now = new Date();
|
|
const maxDurationInSeconds = 5 * 60; // 限制为5分钟
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, (now - currentSessionStart) / 1000);
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
// 兜底逻辑
|
|
if (totalDuration === 0 && firstOperationTime && lastOperationTime) {
|
|
const maxDurationInSeconds = 5 * 60;
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, Math.max(30, (lastOperationTime - firstOperationTime) / 1000));
|
|
totalDuration = durationInSeconds;
|
|
}
|
|
|
|
// 限制最大活跃时长
|
|
const maxUserDurationInSeconds = 24 * 60 * 60;
|
|
return Math.min(maxUserDurationInSeconds, totalDuration);
|
|
}
|
|
|
|
function batchParseJSON(operations) {
|
|
return operations.map(op => {
|
|
try {
|
|
if (typeof op.originalData === 'string') {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: JSON.parse(op.originalData)
|
|
};
|
|
} else {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: op.originalData || {}
|
|
};
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: {}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function sortData(items, key, order) {
|
|
return [...items].sort((a, b) => {
|
|
if (order === 'desc') {
|
|
return b[key] - a[key];
|
|
} else {
|
|
return a[key] - b[key];
|
|
}
|
|
});
|
|
}
|
|
`;
|
|
|
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
const workerUrl = URL.createObjectURL(blob);
|
|
return new Worker(workerUrl);
|
|
} catch (error) {
|
|
console.error('创建Web Worker失败:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 初始化Web Worker
|
|
function initWorker() {
|
|
if (!worker) {
|
|
worker = createWorker();
|
|
if (worker) {
|
|
worker.onmessage = function(e) {
|
|
const { taskId, success, result, error } = e.data;
|
|
const callback = workerCallbacks.get(taskId);
|
|
if (callback) {
|
|
if (success) {
|
|
callback(null, result);
|
|
} else {
|
|
callback(new Error(error));
|
|
}
|
|
workerCallbacks.delete(taskId);
|
|
}
|
|
};
|
|
|
|
worker.onerror = function(error) {
|
|
console.error('Web Worker错误:', error);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 执行Web Worker任务
|
|
function runWorkerTask(task, data) {
|
|
return new Promise((resolve, reject) => {
|
|
initWorker();
|
|
|
|
if (worker) {
|
|
const taskId = ++workerTaskId;
|
|
workerCallbacks.set(taskId, (error, result) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
|
|
worker.postMessage({ taskId, task, data });
|
|
} else {
|
|
// Web Worker不可用,使用同步计算作为 fallback
|
|
try {
|
|
let result;
|
|
switch (task) {
|
|
case 'calculateActiveDuration':
|
|
result = calculateActiveDuration(data);
|
|
break;
|
|
case 'batchParseJSON':
|
|
result = batchParseJSON(data);
|
|
break;
|
|
case 'sortData':
|
|
result = sortData(data.items, data.key, data.order);
|
|
break;
|
|
default:
|
|
throw new Error('Unknown task');
|
|
}
|
|
resolve(result);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 同步计算函数(作为Web Worker的fallback)
|
|
function calculateActiveDuration(operations) {
|
|
if (!operations || operations.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
let totalDuration = 0;
|
|
let currentSessionStart = null;
|
|
let lastAction = null;
|
|
let firstOperationTime = null;
|
|
let lastOperationTime = null;
|
|
|
|
const parsedOperations = operations.map(op => {
|
|
try {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: typeof op.originalData === 'string' ? JSON.parse(op.originalData) : op.originalData || {}
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: {}
|
|
};
|
|
}
|
|
}).sort((a, b) => a.operationTime - b.operationTime);
|
|
|
|
for (const op of parsedOperations) {
|
|
const operationTime = op.operationTime;
|
|
const action = op.originalData.action;
|
|
|
|
if (!firstOperationTime || operationTime < firstOperationTime) {
|
|
firstOperationTime = operationTime;
|
|
}
|
|
if (!lastOperationTime || operationTime > lastOperationTime) {
|
|
lastOperationTime = operationTime;
|
|
}
|
|
|
|
if (action === 'app_hide' && op.originalData.sessionDuration) {
|
|
const durationInSeconds = op.originalData.sessionDuration / 1000;
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
if (action === 'app_show') {
|
|
currentSessionStart = operationTime;
|
|
lastAction = 'app_show';
|
|
} else if (action === 'app_hide') {
|
|
currentSessionStart = null;
|
|
lastAction = 'app_hide';
|
|
} else if (action) {
|
|
lastAction = action;
|
|
}
|
|
}
|
|
|
|
if (currentSessionStart && lastAction === 'app_show') {
|
|
const now = new Date();
|
|
const maxDurationInSeconds = 5 * 60; // 限制为5分钟
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, (now - currentSessionStart) / 1000);
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
if (totalDuration === 0 && firstOperationTime && lastOperationTime) {
|
|
const maxDurationInSeconds = 5 * 60;
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, Math.max(30, (lastOperationTime - firstOperationTime) / 1000));
|
|
totalDuration = durationInSeconds;
|
|
}
|
|
|
|
const maxUserDurationInSeconds = 24 * 60 * 60;
|
|
return Math.min(maxUserDurationInSeconds, totalDuration);
|
|
}
|
|
|
|
function batchParseJSON(operations) {
|
|
return operations.map(op => {
|
|
try {
|
|
if (typeof op.originalData === 'string') {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: JSON.parse(op.originalData)
|
|
};
|
|
} else {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: op.originalData || {}
|
|
};
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: {}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function sortData(items, key, order) {
|
|
return [...items].sort((a, b) => {
|
|
if (order === 'desc') {
|
|
return b[key] - a[key];
|
|
} else {
|
|
return a[key] - b[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
// 按需加载模块数据
|
|
function loadModuleOnDemand(module) {
|
|
console.log(`按需加载模块: ${module}`);
|
|
// 强制重新加载数据,确保获取最新数据
|
|
loadModuleData(module, true)
|
|
.then(() => {
|
|
loadState.loadedModules.add(module);
|
|
console.log(`模块 ${module} 加载完成`);
|
|
});
|
|
}
|
|
|
|
// 虚拟滚动实现
|
|
function initVirtualScroll(containerId, data, rowHeight = 80) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
|
|
const totalHeight = data.length * rowHeight;
|
|
const viewportHeight = container.clientHeight;
|
|
|
|
let startIndex = 0;
|
|
let endIndex = Math.min(Math.ceil(viewportHeight / rowHeight) + 1, data.length);
|
|
|
|
const content = document.createElement('div');
|
|
content.style.height = `${totalHeight}px`;
|
|
content.style.position = 'relative';
|
|
|
|
const visibleContent = document.createElement('div');
|
|
visibleContent.style.position = 'absolute';
|
|
visibleContent.style.top = '0';
|
|
visibleContent.style.left = '0';
|
|
visibleContent.style.right = '0';
|
|
|
|
container.appendChild(content);
|
|
content.appendChild(visibleContent);
|
|
|
|
function renderVisibleRows() {
|
|
visibleContent.innerHTML = '';
|
|
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
const item = data[i];
|
|
const row = document.createElement('div');
|
|
row.className = 'virtual-row';
|
|
row.style.transform = `translateY(${i * rowHeight}px)`;
|
|
row.style.position = 'absolute';
|
|
row.style.top = '0';
|
|
row.style.left = '0';
|
|
row.style.right = '0';
|
|
row.style.height = `${rowHeight}px`;
|
|
row.onclick = () => {
|
|
if (item.userId) {
|
|
showMultiDayActiveDetails(item.userId);
|
|
}
|
|
};
|
|
|
|
// 格式化时间
|
|
const formatTime = (timeStr) => {
|
|
if (!timeStr) return '暂无';
|
|
const date = new Date(timeStr);
|
|
if (isNaN(date.getTime())) return '暂无';
|
|
return date.toLocaleString('zh-CN');
|
|
};
|
|
|
|
// 解析最后浏览商品
|
|
let lastViewedProduct = '暂无';
|
|
try {
|
|
if (item.lastViewedProduct) {
|
|
let productData = item.lastViewedProduct;
|
|
// 检查是否是字符串,如果是则尝试解析
|
|
if (typeof productData === 'string') {
|
|
try {
|
|
productData = JSON.parse(productData);
|
|
} catch (jsonError) {
|
|
productData = { name: productData };
|
|
}
|
|
}
|
|
const productName = productData.productName || productData.name || '未知商品';
|
|
lastViewedProduct = productName;
|
|
}
|
|
} catch (e) {
|
|
lastViewedProduct = '解析失败';
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<div class="virtual-row-cell">${(i + 1)}</div>
|
|
<div class="virtual-row-cell">${item.phoneNumber || '暂无'}</div>
|
|
<div class="virtual-row-cell">${formatTime(item.registerTime)}</div>
|
|
<div class="virtual-row-cell">${(item.duration / 60).toFixed(2)}</div>
|
|
<div class="virtual-row-cell">${formatTime(item.lastActiveTime)}</div>
|
|
<div class="virtual-row-cell">${lastViewedProduct}</div>
|
|
`;
|
|
|
|
visibleContent.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function updateVisibleRows() {
|
|
const scrollTop = container.scrollTop;
|
|
startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - 1);
|
|
endIndex = Math.min(data.length, Math.ceil((scrollTop + viewportHeight) / rowHeight) + 1);
|
|
renderVisibleRows();
|
|
}
|
|
|
|
container.addEventListener('scroll', updateVisibleRows);
|
|
renderVisibleRows();
|
|
}
|
|
|
|
// 优化的客户活跃统计页面,使用虚拟滚动
|
|
function showActiveStatsWithVirtualScroll() {
|
|
// 激活菜单
|
|
activateMenu('active-stats');
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">客户活跃统计</h1>
|
|
<p class="content-subtitle">展示客户活跃情况,包括活跃时长、浏览商品数量等详细数据</p>
|
|
</div>
|
|
|
|
<!-- 活跃统计概览卡片 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">活跃统计概览</h2>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 1.5rem; border-radius: var(--border-radius-lg); text-align: center;">
|
|
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem;">--</div>
|
|
<div style="font-size: 0.875rem; opacity: 0.9;">总活跃客户</div>
|
|
</div>
|
|
<div style="background: linear-gradient(135deg, var(--success-color), var(--primary-color)); color: white; padding: 1.5rem; border-radius: var(--border-radius-lg); text-align: center;">
|
|
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem;">--</div>
|
|
<div style="font-size: 0.875rem; opacity: 0.9;">本周活跃客户</div>
|
|
</div>
|
|
<div style="background: linear-gradient(135deg, var(--warning-color), var(--danger-color)); color: white; padding: 1.5rem; border-radius: var(--border-radius-lg); text-align: center;">
|
|
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem;">--</div>
|
|
<div style="font-size: 0.875rem; opacity: 0.9;">总活跃时长(小时)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 客户活跃排行榜,使用虚拟滚动 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">客户活跃排行榜</h2>
|
|
<div class="virtual-scroll-container" id="activeStatsContainer">
|
|
<!-- 虚拟滚动内容将通过JavaScript动态生成 -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载活跃统计数据
|
|
const cacheKey = 'active_stats';
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
|
|
if (cachedData && cachedData.success) {
|
|
updateActiveStatsUI(cachedData);
|
|
} else {
|
|
fetch('/api/active-stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
updateActiveStatsUI(data);
|
|
cacheManager.set(cacheKey, data);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取活跃统计数据失败:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 更新活跃统计UI,使用虚拟滚动和Web Worker
|
|
function updateActiveStatsUI(data) {
|
|
if (data.success) {
|
|
// 更新概览数据
|
|
const statCards = document.querySelectorAll('.card .stat-number');
|
|
if (statCards[0]) statCards[0].textContent = data.data.totalActive;
|
|
if (statCards[1]) statCards[1].textContent = data.data.weekActive;
|
|
if (statCards[2]) {
|
|
const totalDuration = parseFloat(data.data.totalDuration) || 0;
|
|
statCards[2].textContent = (totalDuration / 3600).toFixed(2);
|
|
}
|
|
|
|
// 加载客户活跃详情数据用于虚拟滚动
|
|
fetch('/api/active-stats/details')
|
|
.then(response => response.json())
|
|
.then(detailsData => {
|
|
if (detailsData.success && detailsData.data && detailsData.data.users) {
|
|
const users = detailsData.data.users;
|
|
|
|
// 使用Web Worker计算每个用户的活跃时长
|
|
const operationsData = users.map(user => ({
|
|
userId: user.userId,
|
|
operations: [] // 这里可以添加实际的操作记录
|
|
}));
|
|
|
|
// 并行计算所有用户的活跃时长
|
|
const durationPromises = users.map(user => {
|
|
// 模拟操作记录数据
|
|
const mockOperations = Array.from({ length: Math.floor(Math.random() * 50) + 10 }, (_, i) => ({
|
|
operationTime: new Date(Date.now() - i * 60000).toISOString(),
|
|
originalData: JSON.stringify({
|
|
action: i % 5 === 0 ? 'app_show' : i % 7 === 0 ? 'app_hide' : 'view_product',
|
|
sessionDuration: i % 7 === 0 ? Math.random() * 300000 : undefined
|
|
})
|
|
}));
|
|
|
|
return runWorkerTask('calculateActiveDuration', mockOperations)
|
|
.then(duration => ({
|
|
userId: user.userId,
|
|
duration
|
|
}))
|
|
.catch(error => {
|
|
console.error(`计算用户 ${user.userId} 活跃时长失败:`, error);
|
|
return {
|
|
userId: user.userId,
|
|
duration: user.duration || 0
|
|
};
|
|
});
|
|
});
|
|
|
|
Promise.all(durationPromises)
|
|
.then(durationResults => {
|
|
// 合并计算结果
|
|
const usersWithCalculatedDuration = users.map(user => {
|
|
const durationResult = durationResults.find(result => result.userId === user.userId);
|
|
return {
|
|
...user,
|
|
duration: durationResult ? durationResult.duration : user.duration || 0
|
|
};
|
|
});
|
|
|
|
// 使用虚拟滚动渲染客户活跃排行榜
|
|
const container = document.getElementById('activeStatsContainer');
|
|
if (container) {
|
|
initVirtualScroll('activeStatsContainer', usersWithCalculatedDuration);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('计算活跃时长失败:', error);
|
|
// 失败时使用原始数据
|
|
const container = document.getElementById('activeStatsContainer');
|
|
if (container) {
|
|
initVirtualScroll('activeStatsContainer', detailsData.data.users);
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取活跃详情数据失败:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 每8分钟自动更新所有模块缓存数据
|
|
setInterval(() => {
|
|
console.log('自动更新所有模块缓存数据...');
|
|
loadModulesInBatches(allModules, 2, 300);
|
|
}, 8 * 60 * 1000);
|
|
|
|
// 显示控制台
|
|
function showDashboard() {
|
|
// 激活菜单
|
|
activateMenu('dashboard');
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">控制台</h1>
|
|
<p class="content-subtitle">欢迎使用后台管理系统,您可以在这里快速访问各个管理模块</p>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="stats-container">
|
|
<div class="stat-card fade-in delay-1">
|
|
<div class="stat-number" id="totalResources">加载中...</div>
|
|
<div class="stat-label">资源总数</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-2">
|
|
<div class="stat-number" id="pendingResources">加载中...</div>
|
|
<div class="stat-label">待审核资源</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-3">
|
|
<div class="stat-number" id="todayNewResources">加载中...</div>
|
|
<div class="stat-label">今日新增</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快速访问卡片 -->
|
|
<div class="card fade-in delay-2">
|
|
<h2 class="card-title">快速访问</h2>
|
|
<div class="quick-access">
|
|
<div class="quick-access-item" onclick="gotoResource()">
|
|
<div class="quick-access-icon">📁</div>
|
|
<div class="quick-access-title">资源管理</div>
|
|
<div class="quick-access-desc">管理系统资源</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoFollow()">
|
|
<div class="quick-access-icon">👥</div>
|
|
<div class="quick-access-title">跟进管理</div>
|
|
<div class="quick-access-desc">管理跟进事项</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="gotoInfo()">
|
|
<div class="quick-access-icon">📋</div>
|
|
<div class="quick-access-title">信息管理</div>
|
|
<div class="quick-access-desc">管理系统信息</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('logs'); showOperationLogs()">
|
|
<div class="quick-access-icon">📝</div>
|
|
<div class="quick-access-title">操作日志</div>
|
|
<div class="quick-access-desc">查看系统操作日志</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('client-stats'); showClientStats()">
|
|
<div class="quick-access-icon">👤</div>
|
|
<div class="quick-access-title">客户统计</div>
|
|
<div class="quick-access-desc">查看客户统计数据</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('active-stats'); showActiveStats()">
|
|
<div class="quick-access-icon">📱</div>
|
|
<div class="quick-access-title">客户活跃统计</div>
|
|
<div class="quick-access-desc">查看客户活跃情况</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('business-stats'); showBusinessStats()">
|
|
<div class="quick-access-icon">📊</div>
|
|
<div class="quick-access-title">业务统计</div>
|
|
<div class="quick-access-desc">查看业务统计数据</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('suppliers'); gotoSupplier()">
|
|
<div class="quick-access-icon">🏭</div>
|
|
<div class="quick-access-title">供应商管理</div>
|
|
<div class="quick-access-desc">管理供应商信息</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('comments'); gotoCommentReview()">
|
|
<div class="quick-access-icon">📝</div>
|
|
<div class="quick-access-title">留言审核</div>
|
|
<div class="quick-access-desc">审核用户留言</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('supply-management'); showSupplyManagement()">
|
|
<div class="quick-access-icon">📦</div>
|
|
<div class="quick-access-title">货源管理</div>
|
|
<div class="quick-access-desc">管理系统货源</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('forum-posts'); gotoForumPostReview()">
|
|
<div class="quick-access-icon">💬</div>
|
|
<div class="quick-access-title">论坛动态审核</div>
|
|
<div class="quick-access-desc">审核论坛动态</div>
|
|
</div>
|
|
<div class="quick-access-item" onclick="loadModuleOnDemand('identity-verification'); gotoIdentityReview()">
|
|
<div class="quick-access-icon">🆔</div>
|
|
<div class="quick-access-title">身份信息审核</div>
|
|
<div class="quick-access-desc">审核用户身份信息</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统信息卡片 -->
|
|
<div class="card fade-in delay-3">
|
|
<h2 class="card-title">系统信息</h2>
|
|
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.8;">
|
|
<p>当前版本:v2.1.2</p>
|
|
<p>上次更新:2026-02-10</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载并显示控制台统计数据
|
|
const cacheKey = 'dashboard_stats';
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
|
|
if (cachedData && cachedData.success) {
|
|
// 更新统计数据
|
|
const totalResourcesEl = document.getElementById('totalResources');
|
|
const pendingResourcesEl = document.getElementById('pendingResources');
|
|
const todayNewResourcesEl = document.getElementById('todayNewResources');
|
|
|
|
if (totalResourcesEl) totalResourcesEl.textContent = cachedData.data.total;
|
|
if (pendingResourcesEl) pendingResourcesEl.textContent = cachedData.data.pending;
|
|
if (todayNewResourcesEl) todayNewResourcesEl.textContent = cachedData.data.today;
|
|
} else {
|
|
// 缓存不存在,重新加载
|
|
fetch('/api/stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新统计数据
|
|
const totalResourcesEl = document.getElementById('totalResources');
|
|
const pendingResourcesEl = document.getElementById('pendingResources');
|
|
const todayNewResourcesEl = document.getElementById('todayNewResources');
|
|
|
|
if (totalResourcesEl) totalResourcesEl.textContent = data.data.total;
|
|
if (pendingResourcesEl) pendingResourcesEl.textContent = data.data.pending;
|
|
if (todayNewResourcesEl) todayNewResourcesEl.textContent = data.data.today;
|
|
|
|
// 更新缓存
|
|
cacheManager.set(cacheKey, data);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载控制台统计数据失败:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 显示设置页面
|
|
function showSettings() {
|
|
alert('系统设置功能开发中...');
|
|
}
|
|
|
|
// 激活菜单
|
|
function activateMenu(menuName) {
|
|
console.log(`[${new Date().toISOString()}] 🎯 激活菜单: ${menuName}`);
|
|
const menuItems = document.querySelectorAll('.menu-item');
|
|
menuItems.forEach(item => {
|
|
item.classList.remove('active');
|
|
console.log(`[${new Date().toISOString()}] 🟥 移除菜单项 ${item.textContent.trim()} 的active类`);
|
|
});
|
|
// 根据菜单名称激活对应菜单项
|
|
// 获取菜单项中的最后一个span元素,即实际文本内容
|
|
menuItems.forEach(item => {
|
|
const spans = item.querySelectorAll('span');
|
|
let menuText = '';
|
|
if (spans.length > 0) {
|
|
// 最后一个span元素是菜单项的实际文本
|
|
menuText = spans[spans.length - 1].textContent.trim();
|
|
} else {
|
|
menuText = item.textContent.trim();
|
|
}
|
|
console.log(`[${new Date().toISOString()}] 📝 菜单项文本: ${menuText}`);
|
|
|
|
switch(menuName) {
|
|
case 'dashboard':
|
|
if (menuText === '控制台') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'logs':
|
|
if (menuText === '操作日志') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'client-stats':
|
|
if (menuText === '客户统计') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'business-stats':
|
|
if (menuText === '业务统计') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'active-stats':
|
|
if (menuText === '客户活跃统计') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'product-lifecycle':
|
|
if (menuText === '产品生命周期') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'customer-history':
|
|
if (menuText === '客户操作历史') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'agent-operations':
|
|
if (menuText === '业务员操作记录') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'supplier':
|
|
if (menuText === '供应商管理') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
case 'supply-management':
|
|
if (menuText === '货源管理') {
|
|
item.classList.add('active');
|
|
console.log(`[${new Date().toISOString()}] 🟢 激活菜单项: ${menuText}`);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 显示货源管理页面
|
|
function showSupplyManagement() {
|
|
// 激活菜单
|
|
activateMenu('supply-management');
|
|
|
|
// 检查永久缓存
|
|
const supplyManagementCache = cacheManager.getPermanent('supplyManagement');
|
|
if (supplyManagementCache) {
|
|
console.log('使用货源管理永久缓存数据');
|
|
}
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">货源管理</h1>
|
|
<p class="content-subtitle">展示全部货源信息,包括商品详情、销售负责人、创建人、浏览次数等</p>
|
|
</div>
|
|
|
|
<!-- 货源列表卡片 -->
|
|
<div class="card fade-in" style="border-radius: var(--border-radius-xl); box-shadow: var(--shadow-lg);">
|
|
<h2 class="card-title" style="display: flex; align-items: center; gap: 0.75rem; font-size: 1.25rem; margin-bottom: 1.5rem;">
|
|
<span style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 0.5rem; border-radius: var(--border-radius-md); font-size: 1.125rem;">📦</span>
|
|
货源列表
|
|
</h2>
|
|
|
|
<!-- 搜索框 -->
|
|
<div style="margin-bottom: 1.5rem; display: flex; gap: 1rem; align-items: center;">
|
|
<input
|
|
type="text"
|
|
id="supplySearchInput"
|
|
placeholder="搜索商品名称、规格、地区、创建人等"
|
|
style="
|
|
flex: 1;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
transition: all 0.2s ease;
|
|
"
|
|
oninput="handleSupplySearch()"
|
|
/>
|
|
<button
|
|
onclick="handleSupplySearch()"
|
|
style="
|
|
padding: 0.75rem 1.5rem;
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
"
|
|
>
|
|
搜索
|
|
</button>
|
|
<button
|
|
onclick="clearSupplySearch()"
|
|
style="
|
|
padding: 0.75rem 1.5rem;
|
|
background-color: #f1f5f9;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
"
|
|
>
|
|
清空搜索
|
|
</button>
|
|
</div>
|
|
|
|
<div style="overflow-x: auto; border-radius: var(--border-radius-lg); border: 1px solid var(--border-color); box-shadow: var(--shadow-sm);">
|
|
<table id="supplyTable" style="width: 100%; border-collapse: collapse; background-color: white;">
|
|
<thead>
|
|
<tr style="background: linear-gradient(135deg, var(--bg-color), #f1f5f9); text-align: left;">
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">商品名称</th>
|
|
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">规格</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">地区</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">创建时间</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">销售负责人</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">创建人</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">浏览次数</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="supplyTableBody">
|
|
<!-- 商品数据将通过JavaScript动态填充 -->
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载货源数据中...</div>
|
|
<div style="font-size: 0.875rem;">请稍候,正在获取商品信息</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页控件 -->
|
|
<div class="pagination" id="supplyPagination">
|
|
<!-- 分页按钮将通过JavaScript动态生成 -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载货源数据
|
|
loadSupplyData();
|
|
}
|
|
|
|
// 保存货源管理数据到永久缓存
|
|
function saveSupplyManagementToCache(supplyData) {
|
|
const supplyManagement = cacheManager.getPermanent('supplyManagement') || {
|
|
list: [],
|
|
details: {}
|
|
};
|
|
|
|
// 更新对应部分
|
|
if (supplyData.list) {
|
|
supplyManagement.list = supplyData.list;
|
|
}
|
|
if (supplyData.details) {
|
|
supplyManagement.details = supplyData.details;
|
|
}
|
|
|
|
cacheManager.setPermanent('supplyManagement', supplyManagement);
|
|
}
|
|
|
|
// 更新货源详情到永久缓存
|
|
function updateSupplyDetailToCache(supplyId, details) {
|
|
const supplyManagement = cacheManager.getPermanent('supplyManagement') || {
|
|
list: [],
|
|
details: {}
|
|
};
|
|
|
|
if (!supplyManagement.details) {
|
|
supplyManagement.details = {};
|
|
}
|
|
|
|
supplyManagement.details[supplyId] = details;
|
|
cacheManager.setPermanent('supplyManagement', supplyManagement);
|
|
}
|
|
|
|
// 添加浏览记录到货源详情
|
|
function addSupplyViewLog(supplyId, log) {
|
|
const supplyManagement = cacheManager.getPermanent('supplyManagement') || {
|
|
list: [],
|
|
details: {}
|
|
};
|
|
|
|
if (!supplyManagement.details) {
|
|
supplyManagement.details = {};
|
|
}
|
|
|
|
if (!supplyManagement.details[supplyId]) {
|
|
supplyManagement.details[supplyId] = {
|
|
basicInfo: {},
|
|
specifications: [],
|
|
logs: []
|
|
};
|
|
}
|
|
|
|
if (!supplyManagement.details[supplyId].logs) {
|
|
supplyManagement.details[supplyId].logs = [];
|
|
}
|
|
|
|
// 添加新的浏览记录
|
|
supplyManagement.details[supplyId].logs.unshift(log);
|
|
|
|
// 限制浏览记录数量为100条
|
|
if (supplyManagement.details[supplyId].logs.length > 100) {
|
|
supplyManagement.details[supplyId].logs = supplyManagement.details[supplyId].logs.slice(0, 100);
|
|
}
|
|
|
|
cacheManager.setPermanent('supplyManagement', supplyManagement);
|
|
}
|
|
|
|
// 加载货源数据
|
|
function loadSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
console.log('loadSupplyData 被调用:', { page, searchTerm, pageSize });
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 构建缓存键
|
|
const cacheKey = `supply_data_page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchTerm)}`;
|
|
|
|
// 对于搜索请求,直接从API获取数据,不使用缓存
|
|
if (searchTerm) {
|
|
console.log('搜索请求,直接从API获取数据');
|
|
// 显示加载状态
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">🔍</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">搜索中...</div>
|
|
<div style="font-size: 0.875rem;">正在搜索 "${searchTerm}" 的相关商品</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
// 从API获取数据
|
|
loadLatestSupplyData(page, searchTerm, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 首先检查永久缓存(包含分页、页面大小和搜索词信息)
|
|
const permanentCacheKey = `supply_management_permanent_page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchTerm)}`;
|
|
const permanentCachedData = cacheManager.getPermanent('supplyManagementData');
|
|
if (permanentCachedData && !searchTerm) {
|
|
console.log('使用永久缓存数据加载货源页面 ' + page);
|
|
// 立即显示永久缓存数据
|
|
updateSupplyTable(permanentCachedData.initialData, page, pageSize, searchTerm);
|
|
// 后台加载最新数据
|
|
loadLatestSupplyData(page, searchTerm, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 检查普通缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用普通缓存数据加载货源页面 ' + page);
|
|
// 立即显示缓存数据
|
|
updateSupplyTable(cachedData, page, pageSize, searchTerm);
|
|
// 后台加载最新数据
|
|
loadLatestSupplyData(page, searchTerm, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 没有缓存,显示加载状态
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载货源数据中...</div>
|
|
<div style="font-size: 0.875rem;">请稍候,正在获取商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 从API获取数据
|
|
loadLatestSupplyData(page, searchTerm, pageSize);
|
|
}
|
|
|
|
// 从API获取最新货源数据
|
|
function loadLatestSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
console.log('loadLatestSupplyData 被调用:', { page, searchTerm, pageSize });
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 调用API获取货源数据
|
|
let url = `/api/supply-management?page=${page}&pageSize=${pageSize}`;
|
|
if (searchTerm) {
|
|
url += `&search=${encodeURIComponent(searchTerm)}`;
|
|
}
|
|
console.log('发送API请求:', url);
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 缓存数据
|
|
const cacheKey = `supply_data_page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(searchTerm)}`;
|
|
cacheManager.set(cacheKey, data);
|
|
|
|
// 更新表格
|
|
updateSupplyTable(data, page, pageSize, searchTerm);
|
|
} else {
|
|
console.error('获取货源数据失败:', data.message);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载失败</div>
|
|
<div style="font-size: 0.875rem;">${data.message || '获取商品信息失败,请稍后重试'}</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取货源数据失败:', error);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">网络错误</div>
|
|
<div style="font-size: 0.875rem;">获取商品信息失败,请检查网络连接</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// 更新货源表格
|
|
function updateSupplyTable(data, page, pageSize, searchTerm = '') {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
if (data.success) {
|
|
const products = data.products;
|
|
const total = data.total;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
|
|
// 填充表格数据
|
|
if (products.length > 0) {
|
|
tableBody.innerHTML = products.map(product => `
|
|
<tr>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.productName || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.specification || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.region || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${formatTime(product.created_at)}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.contactPerson || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.creator || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.viewCount || 0}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick="showViewDetails('${product.productId}')"
|
|
style="margin-right: 0.5rem;"
|
|
>
|
|
查看浏览详细
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">暂无货源数据</div>
|
|
<div style="font-size: 0.875rem;">当前没有商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 生成分页按钮
|
|
generatePagination(pagination, page, totalPages, 'loadSupplyData', pageSize, searchTerm);
|
|
}
|
|
}
|
|
|
|
// 一次性加载所有货源数据
|
|
function loadAllSupplyData() {
|
|
if (supplyIsLoading) return;
|
|
|
|
supplyIsLoading = true;
|
|
|
|
// 首先获取前三页数据并显示
|
|
fetch('/api/supply-management?page=1&pageSize=30')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 先显示前三页数据
|
|
supplyCacheData = data.products;
|
|
supplyTotalCount = data.total;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新成功,已加载前三页数据,总数据量:', data.total);
|
|
|
|
// 如果当前正在查看货源管理页面,立即渲染
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
loadSupplyData(supplyCurrentPage, supplyCurrentSearchTerm, supplyCurrentPageSize);
|
|
}
|
|
|
|
// 在后台继续加载所有数据
|
|
fetch('/api/supply-management?getAll=true')
|
|
.then(response => response.json())
|
|
.then(fullData => {
|
|
if (fullData.success) {
|
|
// 更新为完整缓存数据
|
|
supplyCacheData = fullData.products;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新完成,共', supplyCacheData.length, '条数据');
|
|
|
|
// 如果当前正在查看货源管理页面,重新渲染以显示完整数据
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
loadSupplyData(supplyCurrentPage, supplyCurrentSearchTerm, supplyCurrentPageSize);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('后台加载所有货源数据失败:', error);
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载初始货源数据失败:', error);
|
|
})
|
|
.finally(() => {
|
|
supplyIsLoading = false;
|
|
});
|
|
}
|
|
|
|
// 显示浏览详细
|
|
function showViewDetails(productId) {
|
|
// 创建模态框
|
|
const modal = document.createElement('div');
|
|
modal.id = 'viewDetailsModal';
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
z-index: 2000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
backdrop-filter: blur(5px);
|
|
animation: fadeIn 0.3s ease;
|
|
`;
|
|
|
|
modal.innerHTML = `
|
|
<div style="
|
|
background-color: white;
|
|
border-radius: var(--border-radius-xl);
|
|
box-shadow: var(--shadow-lg);
|
|
padding: 2rem;
|
|
width: 90%;
|
|
max-width: 1000px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
">
|
|
<button onclick="closeViewDetailsModal()" style="
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
transition: color 0.2s ease;
|
|
">×</button>
|
|
|
|
<h2 style="
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
">
|
|
<span style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 0.5rem; border-radius: var(--border-radius-md); font-size: 1.125rem;">👁️</span>
|
|
浏览详细
|
|
</h2>
|
|
|
|
<div id="viewDetailsContent">
|
|
<!-- 浏览详细数据将通过JavaScript动态填充 -->
|
|
<div style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">⏳</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载浏览详细数据中...</div>
|
|
<div style="font-size: 0.875rem;">请稍候,正在获取浏览信息</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// 加载浏览详细数据
|
|
fetch(`/api/supply-management/view-details/${productId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const content = document.getElementById('viewDetailsContent');
|
|
if (data.success) {
|
|
const views = data.views;
|
|
|
|
if (views.length > 0) {
|
|
// 去重:根据userId去重,保留最新的一条记录
|
|
const uniqueViews = [];
|
|
const userIdMap = new Map();
|
|
|
|
// 按时间倒序排序,确保保留最新的记录
|
|
views.sort((a, b) => new Date(b.viewTime) - new Date(a.viewTime));
|
|
|
|
// 去重处理
|
|
views.forEach(view => {
|
|
if (!userIdMap.has(view.userId)) {
|
|
userIdMap.set(view.userId, view);
|
|
uniqueViews.push(view);
|
|
}
|
|
});
|
|
|
|
content.innerHTML = `
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h3 style="font-size: 1.125rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">浏览记录</h3>
|
|
<div style="overflow-x: auto; border-radius: var(--border-radius-lg); border: 1px solid var(--border-color); box-shadow: var(--shadow-sm);">
|
|
<table style="width: 100%; border-collapse: collapse; background-color: white;">
|
|
<thead>
|
|
<tr style="background: linear-gradient(135deg, var(--bg-color), #f1f5f9); text-align: left;">
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">浏览人ID</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">电话号码</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">首次分配人</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">当前处理人</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">浏览时间</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color);">跟进内容</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${uniqueViews.map(view => `
|
|
<tr>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${view.userId || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${view.phoneNumber || (view.userId ? '未知' : '暂无')}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${view.firstAssignee || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${view.currentHandler || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${formatTime(view.viewTime)}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color); white-space: normal; word-wrap: break-word; max-width: 200px;">${view.followContent || '暂无'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
content.innerHTML = `
|
|
<div style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">👁️</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">暂无浏览记录</div>
|
|
<div style="font-size: 0.875rem;">该商品还没有被浏览过</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
} else {
|
|
console.error('获取浏览详细数据失败:', data.message);
|
|
content.innerHTML = `
|
|
<div style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载失败</div>
|
|
<div style="font-size: 0.875rem;">${data.message || '获取浏览详细数据失败,请稍后重试'}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取浏览详细数据失败:', error);
|
|
const content = document.getElementById('viewDetailsContent');
|
|
content.innerHTML = `
|
|
<div style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">网络错误</div>
|
|
<div style="font-size: 0.875rem;">获取浏览详细数据失败,请检查网络连接</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// 关闭浏览详细模态框
|
|
function closeViewDetailsModal() {
|
|
const modal = document.getElementById('viewDetailsModal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
// 处理货源搜索
|
|
function handleSupplySearch() {
|
|
// 搜索功能实现
|
|
const searchInput = document.getElementById('supplySearchInput');
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
|
|
// 调用API获取搜索结果
|
|
loadSupplyData(1, searchTerm);
|
|
}
|
|
|
|
// 清空货源搜索
|
|
function clearSupplySearch() {
|
|
const searchInput = document.getElementById('supplySearchInput');
|
|
searchInput.value = '';
|
|
loadSupplyData();
|
|
}
|
|
|
|
// 生成分页按钮
|
|
function generatePagination(container, currentPage, totalPages, callback, pageSize = 10, searchTerm = '') {
|
|
container.innerHTML = '';
|
|
container.style.display = 'flex';
|
|
container.style.alignItems = 'center';
|
|
container.style.gap = '0.75rem';
|
|
container.style.justifyContent = 'flex-end';
|
|
container.style.flexWrap = 'wrap';
|
|
|
|
// 首页按钮
|
|
const firstButton = document.createElement('button');
|
|
firstButton.textContent = '首页';
|
|
firstButton.disabled = currentPage === 1;
|
|
firstButton.onclick = () => {
|
|
if (currentPage > 1) {
|
|
window[callback](1, searchTerm, pageSize);
|
|
}
|
|
};
|
|
container.appendChild(firstButton);
|
|
|
|
// 上一页按钮
|
|
const prevButton = document.createElement('button');
|
|
prevButton.textContent = '上一页';
|
|
prevButton.disabled = currentPage === 1;
|
|
prevButton.onclick = () => {
|
|
if (currentPage > 1) {
|
|
window[callback](currentPage - 1, searchTerm, pageSize);
|
|
}
|
|
};
|
|
container.appendChild(prevButton);
|
|
|
|
// 页码信息
|
|
const pageInfo = document.createElement('span');
|
|
pageInfo.textContent = `第 ${currentPage} 页,共 ${totalPages} 页`;
|
|
pageInfo.style.fontSize = '0.875rem';
|
|
pageInfo.style.color = 'var(--text-secondary)';
|
|
pageInfo.style.fontWeight = '500';
|
|
container.appendChild(pageInfo);
|
|
|
|
// 下一页按钮
|
|
const nextButton = document.createElement('button');
|
|
nextButton.textContent = '下一页';
|
|
nextButton.disabled = currentPage === totalPages;
|
|
nextButton.onclick = () => {
|
|
if (currentPage < totalPages) {
|
|
window[callback](currentPage + 1, searchTerm, pageSize);
|
|
}
|
|
};
|
|
container.appendChild(nextButton);
|
|
|
|
// 末页按钮
|
|
const lastButton = document.createElement('button');
|
|
lastButton.textContent = '末页';
|
|
lastButton.disabled = currentPage === totalPages;
|
|
lastButton.onclick = () => {
|
|
if (currentPage < totalPages) {
|
|
window[callback](totalPages, searchTerm, pageSize);
|
|
}
|
|
};
|
|
container.appendChild(lastButton);
|
|
|
|
// 每页条数
|
|
const pageSizeContainer = document.createElement('div');
|
|
pageSizeContainer.style.display = 'flex';
|
|
pageSizeContainer.style.alignItems = 'center';
|
|
pageSizeContainer.style.gap = '0.5rem';
|
|
|
|
const pageSizeLabel = document.createElement('span');
|
|
pageSizeLabel.textContent = '每页';
|
|
pageSizeLabel.style.fontSize = '0.875rem';
|
|
pageSizeLabel.style.color = 'var(--text-secondary)';
|
|
|
|
const pageSizeSelect = document.createElement('select');
|
|
pageSizeSelect.style.padding = '0.375rem 0.75rem';
|
|
pageSizeSelect.style.border = '1px solid var(--border-color)';
|
|
pageSizeSelect.style.borderRadius = 'var(--border-radius-md)';
|
|
pageSizeSelect.style.fontSize = '0.875rem';
|
|
pageSizeSelect.style.backgroundColor = 'white';
|
|
|
|
// 添加选项
|
|
const options = [10, 20, 50, 100];
|
|
options.forEach(option => {
|
|
const opt = document.createElement('option');
|
|
opt.value = option;
|
|
opt.textContent = option;
|
|
opt.selected = option === pageSize;
|
|
pageSizeSelect.appendChild(opt);
|
|
});
|
|
|
|
// 添加change事件处理函数
|
|
pageSizeSelect.addEventListener('change', () => {
|
|
const newPageSize = parseInt(pageSizeSelect.value);
|
|
// 重新加载数据,使用新的每页条数
|
|
window[callback](1, searchTerm, newPageSize);
|
|
});
|
|
|
|
const pageSizeUnit = document.createElement('span');
|
|
pageSizeUnit.textContent = '条';
|
|
pageSizeUnit.style.fontSize = '0.875rem';
|
|
pageSizeUnit.style.color = 'var(--text-secondary)';
|
|
|
|
pageSizeContainer.appendChild(pageSizeLabel);
|
|
pageSizeContainer.appendChild(pageSizeSelect);
|
|
pageSizeContainer.appendChild(pageSizeUnit);
|
|
container.appendChild(pageSizeContainer);
|
|
}
|
|
|
|
// 跳转到资源管理页面
|
|
function gotoResource() {
|
|
// 直接跳转到资源管理页面
|
|
window.open('http://8.137.125.67:3000/Management.html', '_blank');
|
|
}
|
|
|
|
// 跳转到跟进管理登录页面
|
|
function gotoFollow() {
|
|
try {
|
|
console.log('开始执行跟进管理跳转...');
|
|
console.log('登录信息:', loginInfo);
|
|
|
|
// 构建带有登录信息的代理登录页面URL
|
|
const proxyUrl = new URL('http://8.137.125.67:3005/follow_login_proxy.html');
|
|
proxyUrl.searchParams.append('projectName', encodeURIComponent(loginInfo.projectName));
|
|
proxyUrl.searchParams.append('userName', encodeURIComponent(loginInfo.userName));
|
|
proxyUrl.searchParams.append('password', encodeURIComponent(loginInfo.password));
|
|
|
|
const finalUrl = proxyUrl.toString();
|
|
console.log('跟进管理代理登录URL:', finalUrl);
|
|
|
|
// 在新标签页中打开代理登录页面
|
|
const newWindow = window.open(finalUrl, '_blank');
|
|
|
|
if (newWindow) {
|
|
console.log('新窗口打开成功');
|
|
} else {
|
|
console.error('新窗口打开失败,可能被浏览器阻止');
|
|
alert('无法打开新窗口,请检查浏览器的弹出窗口设置');
|
|
}
|
|
} catch (error) {
|
|
console.error('跳转失败:', error);
|
|
alert(`跳转失败:${error.message}\n请检查控制台获取详细错误信息`);
|
|
}
|
|
}
|
|
|
|
// 跳转到信息管理系统
|
|
function gotoInfo() {
|
|
// 直接跳转到信息管理系统
|
|
const url = 'http://8.137.125.67:3001/';
|
|
console.log('信息管理系统URL:', url);
|
|
// 在新标签页中打开
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
// 跳转到留言审核页面
|
|
function gotoCommentReview() {
|
|
window.location.href = 'CommentReview.html';
|
|
}
|
|
|
|
// 跳转到论坛动态审核页面
|
|
function gotoForumPostReview() {
|
|
window.location.href = 'ForumPostReview.html';
|
|
}
|
|
|
|
// 跳转到身份信息审核页面
|
|
function gotoIdentityReview() {
|
|
window.location.href = 'IdentityReview.html';
|
|
}
|
|
|
|
// 显示供应商管理页面
|
|
function gotoSupplier() {
|
|
// 激活菜单
|
|
activateMenu('supplier');
|
|
|
|
// 检查永久缓存
|
|
const suppliersCache = cacheManager.getPermanent('suppliers');
|
|
if (suppliersCache) {
|
|
console.log('使用供应商管理永久缓存数据');
|
|
}
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">供应商管理</h1>
|
|
<p class="content-subtitle">管理供应商入驻信息及分配对接人</p>
|
|
</div>
|
|
|
|
<!-- 供应商列表卡片 -->
|
|
<div class="card fade-in" style="border-radius: var(--border-radius-xl); box-shadow: var(--shadow-lg);">
|
|
<h2 class="card-title" style="display: flex; align-items: center; gap: 0.75rem; font-size: 1.25rem; margin-bottom: 1.5rem;">
|
|
<span style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 0.5rem; border-radius: var(--border-radius-md); font-size: 1.125rem;">🏭</span>
|
|
供应商列表
|
|
</h2>
|
|
|
|
<!-- 搜索框 -->
|
|
<div style="margin-bottom: 1.5rem; display: flex; gap: 1rem; align-items: center;">
|
|
<input
|
|
type="text"
|
|
id="supplierSearchInput"
|
|
placeholder="搜索电话号码、对接人、客户公司"
|
|
style="
|
|
flex: 1;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
transition: all 0.2s ease;
|
|
"
|
|
oninput="handleSupplierSearch()"
|
|
/>
|
|
<button
|
|
onclick="clearSupplierSearch()"
|
|
style="
|
|
padding: 0.75rem 1.5rem;
|
|
background-color: #f1f5f9;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
"
|
|
>
|
|
清空搜索
|
|
</button>
|
|
</div>
|
|
|
|
<div style="overflow-x: auto; border-radius: var(--border-radius-lg); border: 1px solid var(--border-color); box-shadow: var(--shadow-sm);">
|
|
<table id="supplierTable" style="width: 100%; border-collapse: collapse; background-color: white;">
|
|
<thead>
|
|
<tr style="background: linear-gradient(135deg, var(--bg-color), #f1f5f9); text-align: left;">
|
|
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 120px;">电话号码</th>
|
|
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 150px;">客户公司</th>
|
|
|
|
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 100px;">合作状态</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 300px;">跟进内容</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 180px;">对接人</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 120px;">入驻时间</th>
|
|
<th style="padding: 1rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; border-bottom: 2px solid var(--border-color); width: 100px;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 3rem; color: var(--text-muted); font-size: 0.875rem;">加载中...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页控件 -->
|
|
<div style="margin-top: 1.5rem; display: flex; align-items: center; justify-content: flex-end; gap: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
<button id="supplierPrevPageBtn" class="btn btn-default" onclick="goToPrevPage()" disabled>
|
|
上一页
|
|
</button>
|
|
<button id="supplierNextPageBtn" class="btn btn-default" onclick="goToNextPage()" disabled>
|
|
下一页
|
|
</button>
|
|
<span style="font-size: 0.875rem; font-weight: normal; color: var(--text-muted);">
|
|
第 <span id="currentPage">1</span> 页
|
|
</span>
|
|
<span id="paginationSupplierCount" class="supplier-count" style="font-size: 0.875rem; font-weight: normal; color: var(--text-muted);">
|
|
(共 0 条)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 供应商详情弹窗 -->
|
|
<div id="supplierModal" style="
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
">
|
|
<div style="
|
|
background-color: white;
|
|
border-radius: var(--border-radius-xl);
|
|
padding: 2rem;
|
|
max-width: 800px;
|
|
width: 90%;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
box-shadow: var(--shadow-xl);
|
|
">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
|
<h3 id="modalTitle" style="font-size: 1.25rem; font-weight: 600; color: var(--text-primary);"></h3>
|
|
<button onclick="closeSupplierModal()" style="
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.25rem;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
transition: color 0.2s ease;
|
|
">×</button>
|
|
</div>
|
|
|
|
<div id="supplierDetailContent" style="margin-bottom: 1.5rem;">
|
|
<!-- 供应商详情内容将通过JavaScript动态填充 -->
|
|
</div>
|
|
|
|
<!-- 分配对接人区域 -->
|
|
<div style="border-top: 1px solid var(--border-color); padding-top: 1.5rem;">
|
|
<h4 style="font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">分配对接人</h4>
|
|
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
<select id="modalLiaisonSelect" style="
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius-md);
|
|
font-size: 0.875rem;
|
|
flex: 1;
|
|
">
|
|
<option value="">请选择对接人</option>
|
|
</select>
|
|
<button onclick="assignLiaisonInModal()" class="btn btn-primary" style="white-space: nowrap;">
|
|
分配
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载供应商列表
|
|
loadSuppliers();
|
|
}
|
|
|
|
// 加载供应商列表
|
|
async function loadSuppliers(searchTerm = '') {
|
|
try {
|
|
// 构建缓存键
|
|
const cacheKey = `suppliers_${searchTerm || 'all'}_${supplierCurrentPage}_${supplierPageSize}`;
|
|
|
|
// 首先检查永久缓存
|
|
const permanentCachedData = cacheManager.getPermanent('suppliersData');
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载供应商列表');
|
|
// 更新总记录数
|
|
supplierTotalCount = permanentCachedData.totalCount || permanentCachedData.total || 0;
|
|
console.log('从永久缓存更新总记录数:', supplierTotalCount);
|
|
// 立即显示永久缓存数据
|
|
updateSuppliersUI(permanentCachedData, searchTerm);
|
|
// 后台加载最新数据
|
|
loadLatestSuppliers(searchTerm);
|
|
return;
|
|
}
|
|
|
|
// 检查普通缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用普通缓存数据加载供应商列表');
|
|
// 更新总记录数
|
|
supplierTotalCount = cachedData.totalCount || cachedData.total || 0;
|
|
console.log('从普通缓存更新总记录数:', supplierTotalCount);
|
|
// 立即显示缓存数据
|
|
updateSuppliersUI(cachedData, searchTerm);
|
|
// 后台加载最新数据
|
|
loadLatestSuppliers(searchTerm);
|
|
return;
|
|
}
|
|
|
|
// 构建API URL
|
|
let apiUrl = `/api/suppliers?page=${supplierCurrentPage}&pageSize=${supplierPageSize}`;
|
|
if (searchTerm) {
|
|
apiUrl += `&search=${encodeURIComponent(searchTerm)}`;
|
|
}
|
|
|
|
// 显示加载状态
|
|
const tbody = document.querySelector('#supplierTable tbody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted); font-size: 0.875rem;">加载中...</td></tr>';
|
|
}
|
|
|
|
// 加载数据
|
|
const response = await fetch(apiUrl);
|
|
const data = await response.json();
|
|
|
|
console.log('API响应:', data);
|
|
|
|
if (data.success) {
|
|
const tbody = document.querySelector('#supplierTable tbody');
|
|
// 使用更精确的选择器,选择供应商列表卡片的标题
|
|
const supplierCard = document.querySelector('#supplierTable').closest('.card');
|
|
const cardTitle = supplierCard ? supplierCard.querySelector('.card-title') : null;
|
|
|
|
// 更新总记录数
|
|
supplierTotalCount = data.totalCount || data.total || 0;
|
|
console.log('总记录数:', supplierTotalCount);
|
|
|
|
// 在标题旁边显示数据总数
|
|
if (cardTitle) {
|
|
const totalCount = supplierTotalCount;
|
|
const countElement = cardTitle.querySelector('.supplier-count');
|
|
if (countElement) {
|
|
countElement.textContent = ` (共 ${totalCount} 条)`;
|
|
} else {
|
|
cardTitle.insertAdjacentHTML('beforeend', `<span class="supplier-count" style="font-size: 0.875rem; font-weight: normal; color: var(--text-muted); margin-left: 0.5rem;"> (共 ${totalCount} 条)</span>`);
|
|
}
|
|
}
|
|
|
|
if (tbody) {
|
|
if (data.suppliers.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无供应商数据</td></tr>';
|
|
updatePaginationStatus();
|
|
return;
|
|
}
|
|
|
|
// 加载对接人列表
|
|
const liaisonsResponse = await fetch('/api/liaisons');
|
|
const liaisonsData = await liaisonsResponse.json();
|
|
let liaisonsOptions = '';
|
|
if (liaisonsData.success) {
|
|
liaisonsOptions = liaisonsData.liaisons.map(liaison =>
|
|
`<option value="${liaison.value}">${liaison.name} - ${liaison.phoneNumber}</option>`
|
|
).join('');
|
|
}
|
|
|
|
tbody.innerHTML = data.suppliers.map(supplier => `
|
|
<tr style="transition: all 0.2s ease; border-bottom: 1px solid var(--border-color);">
|
|
|
|
<td style="padding: 1rem; color: var(--text-primary); font-weight: 500; border-right: 1px solid var(--border-color);">${supplier.company || '暂无'}</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">${supplier.phoneNumber}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">
|
|
${(() => {
|
|
const statusMap = {
|
|
'underreview': '审核中',
|
|
'reviewfailed': '审核失败',
|
|
'approved': '审核通过',
|
|
'incooperation': '合作中',
|
|
'notcooperative': '未合作'
|
|
};
|
|
return statusMap[supplier.partnerstatus] || (supplier.partnerstatus || '暂无');
|
|
})()}
|
|
</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); white-space: normal; word-wrap: break-word; max-width: 300px; border-right: 1px solid var(--border-color);">${supplier.seller_followup || '暂无'}</td>
|
|
|
|
|
|
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">${supplier.liaison || '未分配'}</td>
|
|
<td style="padding: 1rem; color: var(--text-muted); font-size: 0.875rem; border-right: 1px solid var(--border-color);">${formatTime(supplier.newtime || supplier.created_at)}</td>
|
|
<td style="padding: 1rem;">
|
|
<button onclick="showSupplierDetail(${JSON.stringify(supplier).replace(/"/g, '"')})" class="btn btn-primary btn-sm">
|
|
查看详情
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
} else {
|
|
alert('获取供应商列表失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('获取供应商列表失败:', error);
|
|
// 显示错误信息
|
|
const tbody = document.querySelector('#supplierTable tbody');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 3rem; color: #ff4d4f;">加载失败,请重试</td></tr>';
|
|
}
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
}
|
|
}
|
|
|
|
// 处理供应商搜索
|
|
function handleSupplierSearch() {
|
|
const searchInput = document.getElementById('supplierSearchInput');
|
|
const searchTerm = searchInput.value.trim();
|
|
loadSuppliers(searchTerm);
|
|
}
|
|
|
|
// 清空供应商搜索
|
|
function clearSupplierSearch() {
|
|
const searchInput = document.getElementById('supplierSearchInput');
|
|
searchInput.value = '';
|
|
supplierCurrentPage = 1;
|
|
loadSuppliers();
|
|
}
|
|
|
|
// 上一页函数
|
|
function goToPrevPage() {
|
|
console.log('点击上一页,当前页码:', supplierCurrentPage);
|
|
if (supplierCurrentPage > 1) {
|
|
supplierCurrentPage--;
|
|
console.log('新页码:', supplierCurrentPage);
|
|
loadSuppliers();
|
|
}
|
|
}
|
|
|
|
// 下一页函数
|
|
function goToNextPage() {
|
|
console.log('点击下一页,当前页码:', supplierCurrentPage);
|
|
const totalPages = Math.ceil(supplierTotalCount / supplierPageSize);
|
|
console.log('总页数:', totalPages);
|
|
if (supplierCurrentPage < totalPages) {
|
|
supplierCurrentPage++;
|
|
console.log('新页码:', supplierCurrentPage);
|
|
loadSuppliers();
|
|
}
|
|
}
|
|
|
|
// 更新分页状态
|
|
function updatePaginationStatus() {
|
|
const prevBtn = document.getElementById('supplierPrevPageBtn');
|
|
const nextBtn = document.getElementById('supplierNextPageBtn');
|
|
const currentPageEl = document.getElementById('currentPage');
|
|
// 选择分页控件中的供应商数量元素,使用id选择器更精确
|
|
const supplierCountEl = document.getElementById('paginationSupplierCount');
|
|
|
|
console.log('分页控件元素:', {
|
|
prevBtn: !!prevBtn,
|
|
nextBtn: !!nextBtn,
|
|
currentPageEl: !!currentPageEl,
|
|
supplierCountEl: !!supplierCountEl,
|
|
supplierCurrentPage,
|
|
supplierTotalCount,
|
|
supplierPageSize,
|
|
totalPages: Math.ceil(supplierTotalCount / supplierPageSize)
|
|
});
|
|
|
|
if (prevBtn) prevBtn.disabled = supplierCurrentPage === 1;
|
|
if (nextBtn) nextBtn.disabled = supplierCurrentPage >= Math.ceil(supplierTotalCount / supplierPageSize);
|
|
if (currentPageEl) currentPageEl.textContent = supplierCurrentPage;
|
|
if (supplierCountEl) supplierCountEl.textContent = `(共 ${supplierTotalCount} 条)`;
|
|
}
|
|
|
|
// 从API获取最新供应商数据
|
|
function loadLatestSuppliers(searchTerm = '') {
|
|
// 构建缓存键
|
|
const cacheKey = `suppliers_${searchTerm || 'all'}_${supplierCurrentPage}_${supplierPageSize}`;
|
|
|
|
// 构建API URL
|
|
let apiUrl = `/api/suppliers?page=${supplierCurrentPage}&pageSize=${supplierPageSize}`;
|
|
if (searchTerm) {
|
|
apiUrl += `&search=${encodeURIComponent(searchTerm)}`;
|
|
}
|
|
|
|
// 加载数据
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新总记录数
|
|
supplierTotalCount = data.totalCount || data.total || 0;
|
|
// 缓存数据
|
|
cacheManager.set(cacheKey, data);
|
|
// 更新永久缓存
|
|
cacheManager.setPermanent('suppliersData', data);
|
|
// 更新UI
|
|
updateSuppliersUI(data, searchTerm);
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取最新供应商数据失败:', error);
|
|
});
|
|
}
|
|
|
|
// 更新供应商UI
|
|
function updateSuppliersUI(data, searchTerm = '') {
|
|
if (data.success) {
|
|
const tbody = document.querySelector('#supplierTable tbody');
|
|
// 使用更精确的选择器,选择供应商列表卡片的标题
|
|
const supplierTable = document.querySelector('#supplierTable');
|
|
const supplierCard = supplierTable ? supplierTable.closest('.card') : null;
|
|
const cardTitle = supplierCard ? supplierCard.querySelector('.card-title') : null;
|
|
|
|
// 更新总记录数
|
|
supplierTotalCount = data.totalCount || data.total || 0;
|
|
|
|
// 在标题旁边显示数据总数
|
|
if (cardTitle) {
|
|
const totalCount = supplierTotalCount;
|
|
const countElement = cardTitle.querySelector('.supplier-count');
|
|
if (countElement) {
|
|
countElement.textContent = ` (共 ${totalCount} 条)`;
|
|
} else {
|
|
cardTitle.insertAdjacentHTML('beforeend', `<span class="supplier-count" style="font-size: 0.875rem; font-weight: normal; color: var(--text-muted); margin-left: 0.5rem;"> (共 ${totalCount} 条)</span>`);
|
|
}
|
|
}
|
|
|
|
if (tbody) {
|
|
if (data.suppliers.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无供应商数据</td></tr>';
|
|
updatePaginationStatus();
|
|
return;
|
|
}
|
|
|
|
// 加载对接人列表
|
|
fetch('/api/liaisons')
|
|
.then(response => response.json())
|
|
.then(liaisonsData => {
|
|
let liaisonsOptions = '';
|
|
if (liaisonsData.success) {
|
|
liaisonsOptions = liaisonsData.liaisons.map(liaison =>
|
|
`<option value="${liaison.value}">${liaison.name} - ${liaison.phoneNumber}</option>`
|
|
).join('');
|
|
}
|
|
|
|
tbody.innerHTML = data.suppliers.map(supplier => `
|
|
<tr style="transition: all 0.2s ease; border-bottom: 1px solid var(--border-color);">
|
|
|
|
<td style="padding: 1rem; color: var(--text-primary); font-weight: 500; border-right: 1px solid var(--border-color);">${supplier.company || '暂无'}</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">${supplier.phoneNumber}</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">
|
|
${(() => {
|
|
const statusMap = {
|
|
'underreview': '审核中',
|
|
'reviewfailed': '审核失败',
|
|
'approved': '审核通过',
|
|
'incooperation': '合作中',
|
|
'notcooperative': '未合作'
|
|
};
|
|
return statusMap[supplier.partnerstatus] || (supplier.partnerstatus || '暂无');
|
|
})()}
|
|
</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); white-space: normal; word-wrap: break-word; max-width: 300px; border-right: 1px solid var(--border-color);">${supplier.seller_followup || '暂无'}</td>
|
|
<td style="padding: 1rem; color: var(--text-primary); border-right: 1px solid var(--border-color);">${supplier.liaison || '未分配'}</td>
|
|
<td style="padding: 1rem; color: var(--text-muted); font-size: 0.875rem; border-right: 1px solid var(--border-color);">${formatTime(supplier.newtime || supplier.created_at)}</td>
|
|
<td style="padding: 1rem;">
|
|
<button onclick="showSupplierDetail(${JSON.stringify(supplier).replace(/"/g, '"')})" class="btn btn-primary btn-sm">
|
|
查看详情
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
})
|
|
.catch(error => {
|
|
console.error('获取对接人列表失败:', error);
|
|
});
|
|
}
|
|
|
|
// 更新分页状态
|
|
updatePaginationStatus();
|
|
} else {
|
|
console.error('获取供应商数据失败:', data.message);
|
|
}
|
|
}
|
|
|
|
// 显示供应商详情弹窗
|
|
function showSupplierDetail(supplier) {
|
|
// 设置弹窗标题
|
|
document.getElementById('modalTitle').textContent = `供应商详情 - ${supplier.company || supplier.userId}`;
|
|
|
|
// 填充供应商详情
|
|
const statusMap = {
|
|
'underreview': '审核中',
|
|
'reviewfailed': '审核失败',
|
|
'approved': '审核通过',
|
|
'incooperation': '合作中',
|
|
'notcooperative': '未合作'
|
|
};
|
|
|
|
const detailContent = `
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
|
|
<div>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">用户ID:</strong> ${supplier.userId}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">电话号码:</strong> ${supplier.phoneNumber}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">合作商身份:</strong> ${supplier.collaborationid}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">省份:</strong> ${supplier.province}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">城市:</strong> ${supplier.city}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">区域:</strong> ${supplier.district}</p>
|
|
</div>
|
|
<div>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">客户公司:</strong> ${supplier.company || '暂无'}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">详细地址:</strong> ${supplier.detailedaddress || '暂无'}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">合作模式:</strong> ${supplier.cooperation}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">合作商状态:</strong> ${statusMap[supplier.partnerstatus] || (supplier.partnerstatus || '暂无')}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">入驻时间:</strong> ${formatTime(supplier.newtime || supplier.created_at)}</p>
|
|
<p style="margin-bottom: 0.75rem;"><strong style="color: var(--text-primary);">当前对接人:</strong> ${supplier.liaison || '未分配'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 1.5rem;">
|
|
<h4 style="font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">证件信息</h4>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
|
${supplier.businesslicenseurl ? `
|
|
<div>
|
|
<p style="margin-bottom: 0.5rem;"><strong style="color: var(--text-primary);">营业执照:</strong></p>
|
|
<div style="position: relative; border-radius: var(--border-radius-md); overflow: hidden; border: 1px solid var(--border-color); cursor: pointer;">
|
|
<img
|
|
src="${supplier.businesslicenseurl}"
|
|
alt="营业执照"
|
|
style="width: 100%; height: 200px; object-fit: contain; transition: transform 0.3s ease; background-color: #f9fafb;"
|
|
onclick="showImagePreview('${supplier.businesslicenseurl}', '营业执照')"
|
|
onmouseenter="this.style.transform='scale(1.05)'"
|
|
onmouseleave="this.style.transform='scale(1)'"
|
|
/>
|
|
</div>
|
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem; text-align: center;">点击查看大图</p>
|
|
</div>
|
|
` : ''}
|
|
${supplier.proofurl ? `
|
|
<div>
|
|
<p style="margin-bottom: 0.5rem;"><strong style="color: var(--text-primary);">证明材料:</strong></p>
|
|
<div style="position: relative; border-radius: var(--border-radius-md); overflow: hidden; border: 1px solid var(--border-color); cursor: pointer;">
|
|
<img
|
|
src="${supplier.proofurl}"
|
|
alt="证明材料"
|
|
style="width: 100%; height: 200px; object-fit: contain; transition: transform 0.3s ease; background-color: #f9fafb;"
|
|
onclick="showImagePreview('${supplier.proofurl}', '证明材料')"
|
|
onmouseenter="this.style.transform='scale(1.05)'"
|
|
onmouseleave="this.style.transform='scale(1)'"
|
|
/>
|
|
</div>
|
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem; text-align: center;">点击查看大图</p>
|
|
</div>
|
|
` : ''}
|
|
${supplier.brandurl ? `
|
|
<div>
|
|
<p style="margin-bottom: 0.5rem;"><strong style="color: var(--text-primary);">品牌授权链:</strong></p>
|
|
<div style="position: relative; border-radius: var(--border-radius-md); overflow: hidden; border: 1px solid var(--border-color); cursor: pointer;">
|
|
<img
|
|
src="${supplier.brandurl}"
|
|
alt="品牌授权链"
|
|
style="width: 100%; height: 200px; object-fit: contain; transition: transform 0.3s ease; background-color: #f9fafb;"
|
|
onclick="showImagePreview('${supplier.brandurl}', '品牌授权链')"
|
|
onmouseenter="this.style.transform='scale(1.05)'"
|
|
onmouseleave="this.style.transform='scale(1)'"
|
|
/>
|
|
</div>
|
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem; text-align: center;">点击查看大图</p>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图片预览弹窗 -->
|
|
<div id="imagePreviewModal" style="
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
z-index: 3000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
">
|
|
<div style="position: relative; max-width: 90%; max-height: 90vh;">
|
|
<button onclick="closePreview()" style="
|
|
position: absolute;
|
|
top: -40px;
|
|
right: 0;
|
|
background: none;
|
|
border: none;
|
|
font-size: 2rem;
|
|
cursor: pointer;
|
|
color: white;
|
|
transition: color 0.2s ease;
|
|
">×</button>
|
|
|
|
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
|
<h4 id="previewTitle" style="color: white; font-size: 1.125rem; font-weight: 600; margin: 0;text-align: center;"></h4>
|
|
|
|
<div style="position: relative; display: flex; align-items: center; justify-content: center; gap: 1rem;">
|
|
<button onclick="zoomImage(-0.1)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
color: white;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
">-</button>
|
|
|
|
<div id="previewContainer" style="overflow: auto; max-width: 100%; max-height: 80vh;">
|
|
<img
|
|
id="previewImage"
|
|
src=""
|
|
alt="预览图片"
|
|
style="max-width: 100%; max-height: 80vh; transition: transform 0.3s ease; cursor: grab;"
|
|
onmousedown="this.style.cursor='grabbing'"
|
|
onmouseup="this.style.cursor='grab'"
|
|
/>
|
|
</div>
|
|
|
|
<button onclick="zoomImage(0.1)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
color: white;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
">+</button>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 1rem; align-items: center; justify-content: center;">
|
|
<button onclick="rotateImage(-90)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">逆时针旋转</button>
|
|
|
|
<button onclick="resetZoom()" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">重置</button>
|
|
|
|
<button onclick="rotateImage(90)" style="
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 0.5rem 1rem;
|
|
color: white;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
">顺时针旋转</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 1.5rem;">
|
|
<h4 style="font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">供应商跟进信息</h4>
|
|
<div style="border-radius: var(--border-radius-md); border: 1px solid var(--border-color); padding: 1rem; background-color: #f9fafb; max-height: 300px; overflow-y: auto;">
|
|
${supplier.seller_followup ? `
|
|
<pre style="white-space: pre-wrap; word-wrap: break-word; font-family: inherit; margin: 0; color: var(--text-primary);">${supplier.seller_followup}</pre>
|
|
` : `
|
|
<p style="color: var(--text-muted);">暂无跟进信息</p>
|
|
`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('supplierDetailContent').innerHTML = detailContent;
|
|
|
|
// 保存当前供应商信息到全局变量
|
|
window.currentSupplier = supplier;
|
|
|
|
// 加载对接人列表到弹窗的select中
|
|
loadLiaisonsForModal();
|
|
|
|
// 显示弹窗
|
|
const modal = document.getElementById('supplierModal');
|
|
modal.style.display = 'flex';
|
|
|
|
// 添加单击外部关闭功能
|
|
modal.onclick = function(e) {
|
|
if (e.target === modal) {
|
|
closeSupplierModal();
|
|
}
|
|
};
|
|
|
|
// 禁用外部页面滚动
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.paddingRight = '17px'; // 补偿滚动条宽度
|
|
}
|
|
|
|
// 关闭供应商详情弹窗
|
|
function closeSupplierModal() {
|
|
const modal = document.getElementById('supplierModal');
|
|
modal.style.display = 'none';
|
|
// 清除当前供应商信息
|
|
window.currentSupplier = null;
|
|
|
|
// 恢复外部页面滚动
|
|
document.body.style.overflow = '';
|
|
document.body.style.paddingRight = '';
|
|
|
|
// 同时关闭图片预览弹窗
|
|
const imageModal = document.getElementById('imagePreviewModal');
|
|
if (imageModal) {
|
|
imageModal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 显示图片预览
|
|
function showImagePreview(url, title) {
|
|
currentImageUrl = url;
|
|
currentScale = 1;
|
|
currentRotation = 0;
|
|
currentX = 0;
|
|
currentY = 0;
|
|
|
|
const modal = document.getElementById('imagePreviewModal');
|
|
const imgElement = document.getElementById('previewImage');
|
|
const previewTitle = document.getElementById('previewTitle');
|
|
|
|
console.log('modal element:', modal);
|
|
console.log('imgElement:', imgElement);
|
|
console.log('previewTitle:', previewTitle);
|
|
|
|
if (!modal) {
|
|
console.error('imagePreviewModal元素未找到!');
|
|
}
|
|
if (!imgElement) {
|
|
console.error('previewImage元素未找到!');
|
|
}
|
|
if (!previewTitle) {
|
|
console.error('previewTitle元素未找到!');
|
|
}
|
|
|
|
if (!modal || !imgElement || !previewTitle) {
|
|
console.error('图片预览元素未找到!');
|
|
return;
|
|
}
|
|
|
|
imgElement.src = url;
|
|
previewTitle.textContent = title;
|
|
imgElement.style.transform = 'translate(0px, 0px) scale(1) rotate(0deg)';
|
|
|
|
modal.style.display = 'flex';
|
|
|
|
// 添加滚轮缩放事件
|
|
modal.onwheel = function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleWheelZoom(e);
|
|
};
|
|
|
|
// 初始化拖动功能
|
|
setupDragAndDrop();
|
|
|
|
// 添加双击放大事件
|
|
imgElement.ondblclick = toggleDoubleClickZoom;
|
|
|
|
// 禁用外部页面滚动
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.paddingRight = '17px'; // 补偿滚动条宽度
|
|
|
|
// 添加模态框背景点击关闭功能
|
|
modal.onclick = function(e) {
|
|
if (e.target === modal) {
|
|
closePreview();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 关闭图片预览
|
|
function closePreview() {
|
|
const modal = document.getElementById('imagePreviewModal');
|
|
modal.style.display = 'none';
|
|
modal.onwheel = null; // 移除滚轮事件
|
|
|
|
// 恢复外部页面滚动
|
|
document.body.style.overflow = '';
|
|
document.body.style.paddingRight = '';
|
|
}
|
|
|
|
// 新的拖动功能实现
|
|
function setupDragAndDrop() {
|
|
const container = document.getElementById('previewContainer');
|
|
const imgElement = document.getElementById('previewImage');
|
|
|
|
if (!container || !imgElement) return;
|
|
|
|
let isDragging = false;
|
|
let startX, startY;
|
|
|
|
// 重置容器样式
|
|
container.style.cursor = 'grab';
|
|
container.style.userSelect = 'none';
|
|
container.style.touchAction = 'none';
|
|
|
|
// 清除现有的事件监听器
|
|
container.onmousedown = null;
|
|
container.onmousemove = null;
|
|
container.onmouseup = null;
|
|
container.onmouseleave = null;
|
|
|
|
// 鼠标按下事件
|
|
container.onmousedown = function(e) {
|
|
// 只处理左键点击
|
|
if (e.button !== 0) return;
|
|
|
|
if (e.target === imgElement || e.target === container) {
|
|
isDragging = true;
|
|
startX = e.clientX - currentX;
|
|
startY = e.clientY - currentY;
|
|
container.style.cursor = 'grabbing';
|
|
e.preventDefault(); // 阻止默认行为
|
|
}
|
|
};
|
|
|
|
// 鼠标移动事件
|
|
container.onmousemove = function(e) {
|
|
if (!isDragging) return;
|
|
currentX = e.clientX - startX;
|
|
currentY = e.clientY - startY;
|
|
|
|
// 直接修改图片的transform属性来实现拖动
|
|
// 使用requestAnimationFrame来提高平滑度
|
|
// 调整transform顺序,让translate在rotate之前,这样拖动方向不受旋转影响
|
|
requestAnimationFrame(function() {
|
|
imgElement.style.transform = `translate(${currentX}px, ${currentY}px) scale(${currentScale}) rotate(${currentRotation}deg)`;
|
|
});
|
|
};
|
|
|
|
// 鼠标释放事件
|
|
container.onmouseup = function() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
container.style.cursor = 'grab';
|
|
}
|
|
};
|
|
|
|
// 鼠标离开事件
|
|
container.onmouseleave = function() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
container.style.cursor = 'grab';
|
|
}
|
|
};
|
|
|
|
// 全局鼠标释放事件(确保在容器外释放鼠标也能重置状态)
|
|
window.onmouseup = function() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
container.style.cursor = 'grab';
|
|
}
|
|
};
|
|
}
|
|
|
|
// 缩放图片
|
|
function zoomImage(amount) {
|
|
const imgElement = document.getElementById('previewImage');
|
|
if (imgElement) {
|
|
currentScale = Math.max(0.1, Math.min(3, currentScale + amount));
|
|
imgElement.style.transform = `translate(${currentX}px, ${currentY}px) scale(${currentScale}) rotate(${currentRotation}deg)`;
|
|
}
|
|
}
|
|
|
|
// 重置缩放
|
|
function resetZoom() {
|
|
currentScale = 1;
|
|
currentRotation = 0;
|
|
currentX = 0;
|
|
currentY = 0;
|
|
const imgElement = document.getElementById('previewImage');
|
|
if (imgElement) {
|
|
imgElement.style.transform = 'translate(0px, 0px) scale(1) rotate(0deg)';
|
|
}
|
|
}
|
|
|
|
// 旋转图片
|
|
function rotateImage(degrees) {
|
|
currentRotation = (currentRotation + degrees) % 360;
|
|
const imgElement = document.getElementById('previewImage');
|
|
if (imgElement) {
|
|
imgElement.style.transform = `translate(${currentX}px, ${currentY}px) scale(${currentScale}) rotate(${currentRotation}deg)`;
|
|
}
|
|
}
|
|
|
|
// 处理滚轮缩放
|
|
function handleWheelZoom(e) {
|
|
e.preventDefault();
|
|
const amount = e.deltaY > 0 ? -0.1 : 0.1;
|
|
zoomImage(amount);
|
|
}
|
|
|
|
// 双击放大/缩小
|
|
function toggleDoubleClickZoom() {
|
|
const imgElement = document.getElementById('previewImage');
|
|
if (imgElement) {
|
|
if (currentScale === 1) {
|
|
// 双击放大到2倍
|
|
currentScale = 2;
|
|
} else {
|
|
// 双击恢复原始大小
|
|
currentScale = 1;
|
|
}
|
|
imgElement.style.transform = `translate(${currentX}px, ${currentY}px) scale(${currentScale}) rotate(${currentRotation}deg)`;
|
|
console.log('双击放大/缩小,当前缩放比例:', currentScale);
|
|
}
|
|
}
|
|
|
|
// 初始化拖动功能
|
|
function initDragFunctionality(imgElement) {
|
|
let isDragging = false;
|
|
let startX, startY;
|
|
let scrollLeft, scrollTop;
|
|
|
|
const container = document.getElementById('previewContainer');
|
|
|
|
if (container) {
|
|
container.addEventListener('mousedown', (e) => {
|
|
// 只有在图片上点击才开始拖动
|
|
if (e.target === imgElement || e.target === container) {
|
|
isDragging = true;
|
|
startX = e.pageX - container.offsetLeft;
|
|
startY = e.pageY - container.offsetTop;
|
|
scrollLeft = container.scrollLeft;
|
|
scrollTop = container.scrollTop;
|
|
container.style.cursor = 'grabbing';
|
|
}
|
|
});
|
|
|
|
container.addEventListener('mouseleave', () => {
|
|
isDragging = false;
|
|
container.style.cursor = 'grab';
|
|
});
|
|
|
|
container.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
container.style.cursor = 'grab';
|
|
});
|
|
|
|
container.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
e.preventDefault();
|
|
const x = e.pageX - container.offsetLeft;
|
|
const y = e.pageY - container.offsetTop;
|
|
const walkX = (x - startX) * 2; // 调整拖动速度
|
|
const walkY = (y - startY) * 2;
|
|
container.scrollLeft = scrollLeft - walkX;
|
|
container.scrollTop = scrollTop - walkY;
|
|
});
|
|
}
|
|
}
|
|
|
|
// 加载对接人列表到弹窗的select中
|
|
async function loadLiaisonsForModal() {
|
|
try {
|
|
const response = await fetch('/api/liaisons');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const select = document.getElementById('modalLiaisonSelect');
|
|
// 清空现有选项
|
|
select.innerHTML = '<option value="">请选择对接人</option>';
|
|
|
|
// 获取当前供应商的对接人信息
|
|
const currentSupplier = window.currentSupplier;
|
|
const currentLiaison = currentSupplier ? currentSupplier.liaison : '';
|
|
|
|
// 添加新选项
|
|
data.liaisons.forEach(liaison => {
|
|
const option = document.createElement('option');
|
|
option.value = liaison.value;
|
|
option.textContent = `${liaison.name} - ${liaison.phoneNumber}`;
|
|
|
|
// 如果当前供应商已经分配了对接人,且与当前选项匹配,则设置为选中状态
|
|
if (currentLiaison && liaison.value === currentLiaison) {
|
|
option.selected = true;
|
|
}
|
|
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('加载对接人列表失败:', error);
|
|
}
|
|
}
|
|
|
|
// 在弹窗中分配对接人
|
|
async function assignLiaisonInModal() {
|
|
try {
|
|
const select = document.getElementById('modalLiaisonSelect');
|
|
const liaison = select.value;
|
|
const supplier = window.currentSupplier;
|
|
|
|
if (!supplier) {
|
|
alert('未找到供应商信息');
|
|
return;
|
|
}
|
|
|
|
if (!liaison) {
|
|
alert('请选择对接人');
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/assign-liaison', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ userId: supplier.userId, liaison })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert('分配对接人成功');
|
|
// 重新加载供应商列表
|
|
loadSuppliers();
|
|
// 关闭弹窗
|
|
closeSupplierModal();
|
|
} else {
|
|
alert(`分配失败: ${data.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('分配对接人失败:', error);
|
|
alert('分配对接人失败,请检查网络连接');
|
|
}
|
|
}
|
|
|
|
// 分配对接人
|
|
async function assignLiaison(button) {
|
|
try {
|
|
// 获取相邻的select元素
|
|
const select = button.previousElementSibling;
|
|
const liaison = select.value;
|
|
const userId = select.dataset.userid;
|
|
|
|
if (!liaison) {
|
|
alert('请选择对接人');
|
|
return;
|
|
}
|
|
|
|
if (!userId) {
|
|
console.error('未找到用户ID');
|
|
alert('分配失败:未找到用户ID');
|
|
return;
|
|
}
|
|
|
|
console.log('分配对接人,userId:', userId, 'liaison:', liaison);
|
|
|
|
const response = await fetch('/api/assign-liaison', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ userId, liaison })
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert('分配对接人成功');
|
|
// 重新加载供应商列表
|
|
loadSuppliers();
|
|
} else {
|
|
alert(`分配失败: ${data.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('分配对接人失败:', error);
|
|
alert('分配对接人失败,请检查网络连接');
|
|
}
|
|
}
|
|
|
|
// 显示业务统计页面
|
|
function showBusinessStats() {
|
|
// 激活菜单
|
|
activateMenu('business-stats');
|
|
|
|
// 检查永久缓存
|
|
const businessStatsCache = cacheManager.getPermanent('businessStats');
|
|
if (businessStatsCache) {
|
|
console.log('使用业务统计永久缓存数据');
|
|
// 可以在这里直接使用缓存数据
|
|
}
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">业务统计</h1>
|
|
<p class="content-subtitle">统计业务员对于客户的跟进情况,包括公司、级别、类型、需求、地区和跟进状态</p>
|
|
</div>
|
|
|
|
<!-- 总客户数卡片 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">总客户数</h2>
|
|
<div class="stat-number" id="totalBusinessClients" style="font-size: 2.5rem; text-align: center;">加载中...</div>
|
|
</div>
|
|
|
|
<!-- 业务员统计卡片 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">业务员客户统计</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table id="agentStatsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>业务员</th>
|
|
<th>总客户数</th>
|
|
<th>已跟进</th>
|
|
<th>未跟进</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="4" style="text-align: center; padding: 2rem; color: var(--text-muted);">加载中...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表容器 -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 1.5rem;">
|
|
<!-- 需求分布 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">需求分布</h2>
|
|
<div class="chart-container">
|
|
<canvas id="demandChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 地区分布 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">地区分布</h2>
|
|
<div class="chart-container">
|
|
<canvas id="regionChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 客户类型分布 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">客户类型分布</h2>
|
|
<div class="chart-container">
|
|
<canvas id="typeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 跟进状态分布 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">跟进状态分布</h2>
|
|
<div class="chart-container">
|
|
<canvas id="followStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载业务统计数据
|
|
loadBusinessStats();
|
|
}
|
|
|
|
// 保存业务统计数据到永久缓存
|
|
function saveBusinessStatsToCache(stats) {
|
|
const businessStats = cacheManager.getPermanent('businessStats') || {
|
|
overview: {},
|
|
业务员: {},
|
|
trends: {}
|
|
};
|
|
|
|
// 更新对应部分
|
|
if (stats.overview) {
|
|
businessStats.overview = stats.overview;
|
|
}
|
|
if (stats.salesmen) {
|
|
businessStats.业务员 = stats.salesmen;
|
|
}
|
|
if (stats.trends) {
|
|
businessStats.trends = stats.trends;
|
|
}
|
|
|
|
cacheManager.setPermanent('businessStats', businessStats);
|
|
}
|
|
|
|
// 更新业务员详细数据到永久缓存
|
|
function updateSalesmanDetailToCache(salesmanId, details) {
|
|
const businessStats = cacheManager.getPermanent('businessStats') || {
|
|
overview: {},
|
|
业务员: {},
|
|
trends: {}
|
|
};
|
|
|
|
if (!businessStats.业务员) {
|
|
businessStats.业务员 = {};
|
|
}
|
|
|
|
businessStats.业务员[salesmanId] = details;
|
|
cacheManager.setPermanent('businessStats', businessStats);
|
|
}
|
|
|
|
// 加载业务统计数据
|
|
function loadBusinessStats() {
|
|
console.log('加载业务统计数据,获取所有数据');
|
|
|
|
// 构建缓存键
|
|
const cacheKey = 'business_stats_all';
|
|
|
|
// 首先检查永久缓存
|
|
const permanentCachedData = cacheManager.getPermanent('businessStatsData');
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载业务统计');
|
|
// 立即显示永久缓存数据
|
|
updateBusinessStatsUI(permanentCachedData);
|
|
// 后台加载最新数据
|
|
loadLatestBusinessStats();
|
|
return;
|
|
}
|
|
|
|
// 检查普通缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用普通缓存数据加载业务统计');
|
|
// 立即显示缓存数据
|
|
updateBusinessStatsUI(cachedData);
|
|
// 后台加载最新数据
|
|
loadLatestBusinessStats();
|
|
return;
|
|
}
|
|
|
|
// 构建API URL,不包含时间参数
|
|
const url = '/api/business-stats';
|
|
|
|
// 显示加载状态
|
|
document.getElementById('totalBusinessClients').textContent = '加载中...';
|
|
const agentStatsTableBody = document.querySelector('#agentStatsTable tbody');
|
|
agentStatsTableBody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 2rem; color: var(--text-muted);">加载中...</td></tr>';
|
|
|
|
// 加载数据
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 缓存数据
|
|
if (data.success) {
|
|
cacheManager.set(cacheKey, data);
|
|
}
|
|
|
|
// 更新UI
|
|
updateBusinessStatsUI(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('获取业务统计数据失败:', error);
|
|
|
|
// 显示错误状态
|
|
document.getElementById('totalBusinessClients').textContent = '加载失败';
|
|
const agentStatsTableBody = document.querySelector('#agentStatsTable tbody');
|
|
agentStatsTableBody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 2rem; color: #ff4d4f;">加载失败,请重试</td></tr>';
|
|
});
|
|
}
|
|
|
|
// 从API获取最新业务统计数据
|
|
function loadLatestBusinessStats() {
|
|
console.log('后台加载最新业务统计数据');
|
|
|
|
// 构建缓存键
|
|
const cacheKey = 'business_stats_all';
|
|
|
|
// 构建API URL,不包含时间参数
|
|
const url = '/api/business-stats';
|
|
|
|
// 加载数据
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 缓存数据
|
|
if (data.success) {
|
|
cacheManager.set(cacheKey, data);
|
|
// 更新永久缓存
|
|
cacheManager.setPermanent('businessStatsData', data);
|
|
// 更新UI
|
|
updateBusinessStatsUI(data);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取最新业务统计数据失败:', error);
|
|
});
|
|
}
|
|
|
|
// 更新业务统计UI
|
|
function updateBusinessStatsUI(data) {
|
|
if (data.success) {
|
|
console.log('业务统计数据加载成功:', data);
|
|
|
|
// 更新总客户数
|
|
const totalBusinessClientsEl = document.getElementById('totalBusinessClients');
|
|
if (totalBusinessClientsEl) {
|
|
totalBusinessClientsEl.textContent = data.data.totalClients;
|
|
}
|
|
|
|
// 更新业务员统计表格
|
|
updateAgentStatsTable(data.data.agentStats);
|
|
|
|
// 渲染图表
|
|
renderBusinessCharts(data.data.distributions);
|
|
} else {
|
|
console.error('获取业务统计数据失败:', data.message);
|
|
|
|
// 显示错误状态
|
|
const totalBusinessClientsEl = document.getElementById('totalBusinessClients');
|
|
if (totalBusinessClientsEl) {
|
|
totalBusinessClientsEl.textContent = '加载失败';
|
|
}
|
|
const agentStatsTableBody = document.querySelector('#agentStatsTable tbody');
|
|
if (agentStatsTableBody) {
|
|
agentStatsTableBody.innerHTML = `<tr><td colspan="4" style="text-align: center; padding: 2rem; color: #ff4d4f;">${data.message || '获取数据失败'}</td></tr>`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 更新业务员统计表格
|
|
function updateAgentStatsTable(agentStats) {
|
|
const tbody = document.querySelector('#agentStatsTable tbody');
|
|
if (!tbody) {
|
|
return;
|
|
}
|
|
|
|
if (agentStats.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无数据</td></tr>';
|
|
return;
|
|
}
|
|
|
|
// 获取当前时间筛选参数(添加空值检查)
|
|
const startDateEl = document.getElementById('businessStartDate');
|
|
const endDateEl = document.getElementById('businessEndDate');
|
|
const startDate = startDateEl ? startDateEl.value : '';
|
|
const endDate = endDateEl ? endDateEl.value : '';
|
|
|
|
tbody.innerHTML = agentStats.map(agent => `
|
|
<tr>
|
|
<td style="cursor: pointer; color: var(--primary-color); text-decoration: underline;" onclick="showAgentBusinessDetails('${agent.agentName.replace(/'/g, "\\'")}')">${agent.agentName}</td>
|
|
<td>${agent.totalClients}</td>
|
|
<td>${agent.followedClients}</td>
|
|
<td>${agent.notFollowedClients}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// 显示业务员详细业务情况
|
|
function showAgentBusinessDetails(agentName) {
|
|
console.log('查看业务员详细信息:', agentName);
|
|
|
|
// 激活菜单
|
|
activateMenu('business-stats');
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">业务员详细业务统计</h1>
|
|
<p class="content-subtitle">查看${agentName}的客户列表及跟进情况</p>
|
|
</div>
|
|
|
|
<!-- 业务员统计摘要卡片 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">${agentName} - 统计摘要</h2>
|
|
<div class="stats-container">
|
|
<div class="stat-card fade-in delay-1">
|
|
<div class="stat-number" id="agentTotalClients">加载中...</div>
|
|
<div class="stat-label">总客户数</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-2">
|
|
<div class="stat-number" id="agentFollowedClients">加载中...</div>
|
|
<div class="stat-label">已跟进客户数</div>
|
|
</div>
|
|
<div class="stat-card fade-in delay-3">
|
|
<div class="stat-number" id="agentNotFollowedClients">加载中...</div>
|
|
<div class="stat-label">未跟进客户数</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 客户列表卡片 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">客户列表及跟进情况</h2>
|
|
<div style="overflow-x: auto;">
|
|
<table id="agentClientsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>客户姓名</th>
|
|
<th>电话号码</th>
|
|
<th>公司</th>
|
|
<th>级别</th>
|
|
<th>类型</th>
|
|
<th>需求</th>
|
|
<th>地区</th>
|
|
<th>跟进内容</th>
|
|
<th>跟进时间</th>
|
|
<th>客户创建时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="10" style="text-align: center; padding: 2rem; color: var(--text-muted);">加载中...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- 分页控件 -->
|
|
<div class="pagination" id="agentClientsPagination" style="margin-top: 1rem;">
|
|
<button class="btn btn-default" onclick="goBusinessToPage(1)">首页</button>
|
|
<button class="btn btn-default" onclick="goBusinessToPage(businessCurrentPage - 1)" id="prevPage">上一页</button>
|
|
<span id="pageInfo" style="margin: 0 1rem; align-self: center;">第 1 页,共 1 页</span>
|
|
<button class="btn btn-default" onclick="goBusinessToPage(businessCurrentPage + 1)" id="nextPage">下一页</button>
|
|
<button class="btn btn-default" onclick="goBusinessToPage(businessTotalPages)">末页</button>
|
|
<span style="margin-left: 1rem; align-self: center;">每页 <input type="number" id="businessPageSizeInput" value="10" min="1" max="100" style="width: 60px; padding: 0.375rem; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); text-align: center;" onchange="changeBusinessPageSize()"> 条</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 加载业务员详细数据
|
|
loadAgentBusinessDetails(agentName);
|
|
}
|
|
|
|
// 加载业务员详细业务数据
|
|
function loadAgentBusinessDetails(agentName) {
|
|
// 优先使用传入的参数,如果没有则从DOM获取
|
|
const actualStartDate = '';
|
|
const actualEndDate = '';
|
|
|
|
console.log('加载业务员详细数据,业务员:', agentName, '时间范围:', actualStartDate, '至', actualEndDate);
|
|
|
|
// 构建缓存键
|
|
const cacheKey = `businessStatsAgent_${encodeURIComponent(agentName)}`;
|
|
|
|
// 首先检查永久缓存
|
|
const permanentCachedData = cacheManager.getPermanent(cacheKey);
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载业务员详细信息:', agentName);
|
|
// 立即显示永久缓存数据
|
|
updateAgentDetailsFromCache(permanentCachedData, agentName);
|
|
// 后台加载最新数据
|
|
loadLatestAgentBusinessDetails(agentName, cacheKey);
|
|
return;
|
|
}
|
|
|
|
// 构建API URL,不包含时间参数
|
|
const url = `/api/business-stats/agent/${encodeURIComponent(agentName)}`;
|
|
|
|
// 显示加载状态
|
|
['agentTotalClients', 'agentFollowedClients', 'agentNotFollowedClients'].forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) element.textContent = '加载中...';
|
|
});
|
|
|
|
const agentClientsTableBody = document.querySelector('#agentClientsTable tbody');
|
|
agentClientsTableBody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 2rem; color: var(--text-muted);">加载中...</td></tr>';
|
|
|
|
// 加载数据
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log('业务员详细数据加载成功:', data);
|
|
|
|
// 更新统计摘要
|
|
const summary = data.data.summary;
|
|
document.getElementById('agentTotalClients').textContent = summary.totalClients;
|
|
document.getElementById('agentFollowedClients').textContent = summary.followedClients || 0;
|
|
document.getElementById('agentNotFollowedClients').textContent = summary.notFollowedClients || 0;
|
|
|
|
// 更新客户列表
|
|
updateAgentClientsTable(data.data.clients);
|
|
|
|
// 更新永久缓存
|
|
cacheManager.setPermanent(cacheKey, data);
|
|
} else {
|
|
console.error('获取业务员详细数据失败:', data.message);
|
|
alert('获取业务员详细数据失败,请稍后重试');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取业务员详细数据失败:', error);
|
|
alert('获取业务员详细数据失败,请检查网络连接');
|
|
});
|
|
}
|
|
|
|
// 从缓存更新业务员详情
|
|
function updateAgentDetailsFromCache(cachedData, agentName) {
|
|
if (cachedData.success) {
|
|
// 更新统计摘要
|
|
const summary = cachedData.data.summary;
|
|
document.getElementById('agentTotalClients').textContent = summary.totalClients;
|
|
document.getElementById('agentFollowedClients').textContent = summary.followedClients || 0;
|
|
document.getElementById('agentNotFollowedClients').textContent = summary.notFollowedClients || 0;
|
|
|
|
// 更新客户列表
|
|
updateAgentClientsTable(cachedData.data.clients);
|
|
}
|
|
}
|
|
|
|
// 后台加载最新业务员详细数据
|
|
function loadLatestAgentBusinessDetails(agentName, cacheKey) {
|
|
console.log('后台加载最新业务员详细数据:', agentName);
|
|
|
|
// 构建API URL,不包含时间参数
|
|
const url = `/api/business-stats/agent/${encodeURIComponent(agentName)}`;
|
|
|
|
// 加载数据
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新永久缓存
|
|
cacheManager.setPermanent(cacheKey, data);
|
|
console.log('业务员详细数据永久缓存更新成功:', agentName);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('后台获取业务员详细数据失败:', error);
|
|
});
|
|
}
|
|
|
|
// 更新业务员客户列表表格(带分页)
|
|
function updateAgentClientsTable(clients) {
|
|
const tbody = document.querySelector('#agentClientsTable tbody');
|
|
|
|
// 重置当前页码为1
|
|
businessCurrentPage = 1;
|
|
|
|
// 保存所有客户数据
|
|
businessAllClients = clients;
|
|
|
|
// 计算总页数
|
|
businessTotalPages = Math.ceil(businessAllClients.length / businessPageSize);
|
|
|
|
// 确保当前页码不超出范围
|
|
if (businessCurrentPage > businessTotalPages) {
|
|
businessCurrentPage = businessTotalPages;
|
|
}
|
|
if (businessCurrentPage < 1) {
|
|
businessCurrentPage = 1;
|
|
}
|
|
|
|
// 渲染当前页数据
|
|
renderBusinessCurrentPage();
|
|
}
|
|
|
|
// 渲染当前页数据
|
|
function renderBusinessCurrentPage() {
|
|
const tbody = document.querySelector('#agentClientsTable tbody');
|
|
|
|
if (businessAllClients.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无数据</td></tr>';
|
|
updateBusinessPaginationControls();
|
|
return;
|
|
}
|
|
|
|
// 获取当前页的数据
|
|
const startIndex = (businessCurrentPage - 1) * businessPageSize;
|
|
const endIndex = startIndex + businessPageSize;
|
|
const currentPageClients = businessAllClients.slice(startIndex, endIndex);
|
|
|
|
// 渲染当前页数据
|
|
tbody.innerHTML = currentPageClients.map(client => `
|
|
<tr>
|
|
<td>${client.clientName || '未知'}</td>
|
|
<td>${client.clientPhone || '未知'}</td>
|
|
<td>${client.company || '未知'}</td>
|
|
<td>${client.level || '未知'}</td>
|
|
<td>${client.type || '未知'}</td>
|
|
<td>${client.demand || '未知'}</td>
|
|
<td>${client.region || '未知'}</td>
|
|
<td>${client.followup || '暂无跟进内容'}</td>
|
|
<td>${client.followup_at ? new Date(client.followup_at).toLocaleString() : '未跟进'}</td>
|
|
<td>${new Date(client.clientCreatedAt).toLocaleString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// 更新分页控件
|
|
updateBusinessPaginationControls();
|
|
}
|
|
|
|
// 更新分页控件
|
|
function updateBusinessPaginationControls() {
|
|
// 更新页码信息
|
|
document.getElementById('pageInfo').textContent = `第 ${businessCurrentPage} 页,共 ${businessTotalPages} 页`;
|
|
|
|
// 更新按钮状态
|
|
document.getElementById('prevPage').disabled = businessCurrentPage === 1;
|
|
document.getElementById('nextPage').disabled = businessCurrentPage === businessTotalPages;
|
|
}
|
|
|
|
// 跳转到指定页
|
|
function goBusinessToPage(page) {
|
|
// 确保页码在有效范围内
|
|
if (page < 1) page = 1;
|
|
if (page > businessTotalPages) page = businessTotalPages;
|
|
|
|
// 更新当前页码并重新渲染
|
|
businessCurrentPage = page;
|
|
renderBusinessCurrentPage();
|
|
}
|
|
|
|
// 更改每页显示条数
|
|
function changeBusinessPageSize() {
|
|
// 获取用户输入的每页显示条数
|
|
const pageSizeInput = document.getElementById('businessPageSizeInput');
|
|
const newPageSize = parseInt(pageSizeInput.value);
|
|
|
|
// 确保每页显示条数在有效范围内
|
|
if (newPageSize >= 1 && newPageSize <= 100) {
|
|
// 更新每页显示条数
|
|
businessPageSize = newPageSize;
|
|
|
|
// 重新计算总页数
|
|
businessTotalPages = Math.ceil(businessAllClients.length / businessPageSize);
|
|
|
|
// 重置当前页码为1
|
|
businessCurrentPage = 1;
|
|
|
|
// 重新渲染当前页数据
|
|
renderBusinessCurrentPage();
|
|
} else {
|
|
// 如果输入无效,恢复到当前值
|
|
pageSizeInput.value = businessPageSize;
|
|
}
|
|
}
|
|
|
|
// 重置业务员日期范围
|
|
function resetAgentDateRange(agentName) {
|
|
document.getElementById('agentStartDate').value = '';
|
|
document.getElementById('agentEndDate').value = '';
|
|
loadAgentBusinessDetails(agentName);
|
|
}
|
|
|
|
// 渲染业务统计图表
|
|
function renderBusinessCharts(distributions) {
|
|
// 渲染需求分布图表
|
|
renderPieChart('demandChart', '需求分布', distributions.demand);
|
|
|
|
// 渲染地区分布图表
|
|
renderPieChart('regionChart', '地区分布', distributions.region);
|
|
|
|
// 渲染客户类型分布图表
|
|
renderPieChart('typeChart', '客户类型分布', distributions.type);
|
|
|
|
// 渲染跟进状态分布图表
|
|
renderPieChart('followStatusChart', '跟进状态分布', distributions.followStatus);
|
|
}
|
|
|
|
// 渲染饼图
|
|
function renderPieChart(canvasId, title, data) {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas) {
|
|
console.warn('Canvas element not found:', canvasId);
|
|
return;
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 销毁现有图表(只有存在且有destroy方法时才调用)
|
|
if (window[canvasId] && typeof window[canvasId].destroy === 'function') {
|
|
window[canvasId].destroy();
|
|
}
|
|
|
|
// 准备数据,直接使用原始值
|
|
const labels = data.map(item => {
|
|
if (item.type) {
|
|
if (item.type === 'buyer') return '大贸易客户';
|
|
if (item.type === 'seller') return '供应商';
|
|
return item.type;
|
|
}
|
|
return item.level || item.company || item.demand || item.region || item.followStatus || '未知';
|
|
});
|
|
const values = data.map(item => item.clientCount);
|
|
|
|
// 生成颜色
|
|
const colors = generateChartColors(labels.length);
|
|
|
|
// 创建图表
|
|
window[canvasId] = new Chart(ctx, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: values,
|
|
backgroundColor: colors,
|
|
borderColor: colors.map(color => color.replace('0.8', '1')),
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: title,
|
|
font: {
|
|
size: 14,
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
boxWidth: 12,
|
|
padding: 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 生成图表颜色
|
|
function generateChartColors(count) {
|
|
const colors = [
|
|
'rgba(54, 162, 235, 0.8)',
|
|
'rgba(255, 99, 132, 0.8)',
|
|
'rgba(75, 192, 192, 0.8)',
|
|
'rgba(255, 159, 64, 0.8)',
|
|
'rgba(153, 102, 255, 0.8)',
|
|
'rgba(255, 205, 86, 0.8)',
|
|
'rgba(199, 199, 199, 0.8)',
|
|
'rgba(83, 102, 255, 0.8)',
|
|
'rgba(255, 99, 232, 0.8)',
|
|
'rgba(75, 192, 92, 0.8)'
|
|
];
|
|
|
|
// 如果需要更多颜色,循环使用
|
|
const result = [];
|
|
for (let i = 0; i < count; i++) {
|
|
result.push(colors[i % colors.length]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// 重置业务统计日期范围
|
|
function resetBusinessDateRange() {
|
|
document.getElementById('businessStartDate').value = '';
|
|
document.getElementById('businessEndDate').value = '';
|
|
loadBusinessStats();
|
|
}
|
|
|
|
// 设置动态时间范围
|
|
function setDynamicDateRange(rangeType) {
|
|
// 移除所有时间范围按钮的active类
|
|
document.querySelectorAll('[onclick^="setDynamicDateRange"]').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// 为当前点击的按钮或对应rangeType的按钮添加active类
|
|
if (event) {
|
|
event.target.classList.add('active');
|
|
} else {
|
|
// 当没有event对象时(如通过代码直接调用),根据rangeType找到对应的按钮
|
|
const buttons = document.querySelectorAll('[onclick^="setDynamicDateRange"]');
|
|
buttons.forEach(btn => {
|
|
if (btn.onclick.toString().includes(`setDynamicDateRange('${rangeType}')`)) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
const today = new Date();
|
|
let startDate, endDate;
|
|
|
|
// 设置结束日期为今天
|
|
endDate = new Date(today);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
|
|
// 根据范围类型计算开始日期
|
|
switch(rangeType) {
|
|
case 'today':
|
|
// 今天
|
|
startDate = new Date(today);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'yesterday':
|
|
// 昨天
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate.setDate(today.getDate() - 1);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'dayBeforeYesterday':
|
|
// 前天
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 2);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate.setDate(today.getDate() - 2);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'week':
|
|
// 一周以内(7天)
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 6);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'fifteenDays':
|
|
// 15天以内
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 14);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'thisMonth':
|
|
// 本月
|
|
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'lastMonth':
|
|
// 上一月
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// 格式化日期为YYYY-MM-DD格式(使用本地时间)
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// 更新日期输入框
|
|
document.getElementById('businessStartDate').value = formatDate(startDate);
|
|
document.getElementById('businessEndDate').value = formatDate(endDate);
|
|
|
|
// 自动加载统计数据
|
|
loadBusinessStats();
|
|
}
|
|
|
|
// 设置业务员详细页面的动态时间范围
|
|
function setAgentDynamicDateRange(rangeType, agentName) {
|
|
// 移除所有时间范围按钮的active类
|
|
document.querySelectorAll('[onclick^="setAgentDynamicDateRange"]').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// 为当前点击的按钮或对应rangeType的按钮添加active类
|
|
if (event) {
|
|
event.target.classList.add('active');
|
|
} else {
|
|
// 当没有event对象时(如通过代码直接调用),根据rangeType找到对应的按钮
|
|
const buttons = document.querySelectorAll('[onclick^="setAgentDynamicDateRange"]');
|
|
buttons.forEach(btn => {
|
|
if (btn.onclick.toString().includes(`setAgentDynamicDateRange('${rangeType}')`)) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
const today = new Date();
|
|
let startDate, endDate;
|
|
|
|
// 设置结束日期为今天
|
|
endDate = new Date(today);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
|
|
// 根据范围类型计算开始日期
|
|
switch(rangeType) {
|
|
case 'today':
|
|
// 今天
|
|
startDate = new Date(today);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'yesterday':
|
|
// 昨天
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate.setDate(today.getDate() - 1);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'dayBeforeYesterday':
|
|
// 前天
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 2);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate.setDate(today.getDate() - 2);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'week':
|
|
// 一周以内(7天)
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 6);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'fifteenDays':
|
|
// 15天以内
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - 14);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case 'thisMonth':
|
|
// 本月
|
|
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'lastMonth':
|
|
// 上一月
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// 格式化日期为YYYY-MM-DD格式(使用本地时间)
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// 更新日期输入框
|
|
document.getElementById('agentStartDate').value = formatDate(startDate);
|
|
document.getElementById('agentEndDate').value = formatDate(endDate);
|
|
|
|
// 自动加载统计数据
|
|
loadAgentBusinessDetails(agentName);
|
|
}
|
|
|
|
// 渲染货源数据
|
|
function renderSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 从缓存数据中筛选
|
|
let filteredProducts = supplyCacheData;
|
|
|
|
// 应用搜索过滤
|
|
if (searchTerm) {
|
|
const searchLower = searchTerm.toLowerCase();
|
|
filteredProducts = supplyCacheData.filter(product => {
|
|
return (
|
|
(product.productName || '').toLowerCase().includes(searchLower) ||
|
|
(product.specification || '').toLowerCase().includes(searchLower) ||
|
|
(product.region || '').toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
}
|
|
|
|
// 计算分页
|
|
const total = filteredProducts.length;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const pageProducts = filteredProducts.slice(startIndex, endIndex);
|
|
|
|
// 填充表格数据
|
|
if (pageProducts.length > 0) {
|
|
tableBody.innerHTML = pageProducts.map(product => `
|
|
<tr>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.productName || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.specification || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.region || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${formatTime(product.created_at)}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.contactPerson || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.creator || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.viewCount || 0}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick="showViewDetails('${product.productId}')"
|
|
style="margin-right: 0.5rem;"
|
|
>
|
|
查看浏览详细
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">暂无货源数据</div>
|
|
<div style="font-size: 0.875rem;">当前没有商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 生成分页按钮
|
|
generatePagination(pagination, page, totalPages, 'loadSupplyData', pageSize, searchTerm);
|
|
}
|
|
|
|
// 显示操作日志页面
|
|
function showOperationLogs() {
|
|
// 激活菜单
|
|
activateMenu('logs');
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">操作日志</h1>
|
|
<p class="content-subtitle">查看系统操作日志、商品日志、产品生命周期、客户操作历史和业务员操作记录</p>
|
|
</div>
|
|
|
|
<div class="card fade-in" style="background-color: rgba(255, 255, 255, 0.5); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); padding: 20px;">
|
|
<h2 class="card-title">日志查询</h2>
|
|
|
|
<!-- 日志类型选项卡 -->
|
|
<div style="margin-bottom: 16px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">
|
|
<button class="btn btn-default active" onclick="switchLogType('system')" id="systemLogTab">系统操作日志</button>
|
|
<button class="btn btn-default" onclick="switchLogType('product')" id="productLogTab">商品日志</button>
|
|
<button class="btn btn-default" onclick="switchLogType('productLifecycle')" id="productLifecycleTab">产品生命周期</button>
|
|
<button class="btn btn-default" onclick="switchLogType('customerHistory')" id="customerHistoryTab">客户操作历史</button>
|
|
<button class="btn btn-default" onclick="switchLogType('agentOperations')" id="agentOperationsTab">业务员操作记录</button>
|
|
</div>
|
|
|
|
<!-- 系统操作日志查询条件 -->
|
|
<div id="systemLogFilters" style="margin-bottom: 16px;">
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<input type="text" id="searchUserName" placeholder="操作人" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<input type="text" id="searchUserId" placeholder="客户ID" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<input type="text" id="searchPhone" placeholder="电话号码" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<select id="searchSystem" placeholder="系统" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<option value="">全部系统</option>
|
|
<option value="跟进系统">跟进系统</option>
|
|
<option value="审核系统">审核系统</option>
|
|
</select>
|
|
<input type="date" id="startDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<input type="date" id="endDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<button class="btn btn-primary" onclick="searchLogs()">查询</button>
|
|
<button class="btn btn-default" onclick="resetSearch()">重置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 商品日志查询条件 -->
|
|
<div id="productLogFilters" style="margin-bottom: 16px; display: none;">
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<input type="text" id="searchProductId" placeholder="商品ID" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<input type="text" id="searchSellerId" placeholder="卖家ID" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 150px;">
|
|
<input type="date" id="productStartDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<input type="date" id="productEndDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<button class="btn btn-primary" onclick="searchProductLogs()">查询</button>
|
|
<button class="btn btn-default" onclick="resetProductSearch()">重置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 产品生命周期查询条件 -->
|
|
<div id="productLifecycleFilters" style="margin-bottom: 16px; display: none;">
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<input type="text" id="productIdInput" placeholder="商品ID" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 300px;">
|
|
<button class="btn btn-primary" onclick="searchProductLifecycle()">查询</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 客户操作历史查询条件 -->
|
|
<div id="customerHistoryFilters" style="margin-bottom: 16px; display: none;">
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<input type="text" id="customerIdInput" placeholder="客户ID或电话号码" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 300px;">
|
|
<button class="btn btn-primary" onclick="searchCustomerHistory()">查询</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 业务员操作记录查询条件 -->
|
|
<div id="agentOperationsFilters" style="margin-bottom: 16px; display: none;">
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<input type="text" id="agentNameInput" placeholder="业务员姓名" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px; width: 300px;">
|
|
<button class="btn btn-primary" onclick="searchAgentOperations()">查询</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logs-table-container" style="margin-top: 20px; position: relative; z-index: 1; overflow-x: auto; max-width: 100%;">
|
|
<table style="min-width: 100%; border-collapse: collapse; font-size: 14px; background-color: rgba(255, 255, 255, 0.5); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
|
<thead>
|
|
<tr style="background-color: rgba(245, 245, 245, 0.8);">
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logTimeHeader">操作时间</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logUserHeader">操作人</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logIdHeader">客户ID</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logPhoneHeader">电话号码</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logEventHeader">操作事件</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(232, 232, 232, 0.5);" id="logActionHeader">操作详情</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logsTableBody">
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #999;">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="margin-top: 16px; display: flex; justify-content: space-between; align-items: center;">
|
|
<div style="font-size: 14px; color: #666;">
|
|
共 <span id="totalLogs">0</span> 条记录
|
|
</div>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="btn btn-default" id="prevPage" onclick="changePage(-1)" disabled>上一页</button>
|
|
<span style="font-size: 14px; color: #666; line-height: 32px;">
|
|
第 <span id="currentPage">1</span> 页,共 <span id="totalPages">1</span> 页
|
|
</span>
|
|
<button class="btn btn-default" id="nextPage" onclick="changePage(1)" disabled>下一页</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 检查缓存并加载数据
|
|
loadLogsWithCache();
|
|
}
|
|
|
|
// 加载日志数据(带缓存)
|
|
function loadLogsWithCache() {
|
|
// 检查今日日志缓存
|
|
const todayLogs = cacheManager.get('htgl_logs_today');
|
|
if (todayLogs) {
|
|
console.log('使用今日日志缓存数据');
|
|
// 可以在这里直接使用缓存数据
|
|
}
|
|
|
|
// 检查历史日志缓存
|
|
const historyLogs = cacheManager.getPermanent('logs_history');
|
|
if (historyLogs) {
|
|
console.log('使用历史日志缓存数据,包含', Object.keys(historyLogs).length, '天的记录');
|
|
}
|
|
|
|
// 清空结果区域,等待用户选择日志类型并查询
|
|
const resultDiv = document.getElementById('logsTableBody');
|
|
resultDiv.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">请选择日志类型并输入查询条件</td></tr>';
|
|
}
|
|
|
|
// 保存今日日志到临时缓存
|
|
function saveTodayLogs(logs) {
|
|
cacheManager.set('htgl_logs_today', logs);
|
|
}
|
|
|
|
// 保存历史日志到永久缓存
|
|
function saveHistoryLogs(date, logs) {
|
|
const historyLogs = cacheManager.getPermanent('logs_history') || {};
|
|
historyLogs[date] = logs;
|
|
cacheManager.setPermanent('logs_history', historyLogs);
|
|
// 清理过期日志
|
|
cacheManager.cleanupOldLogs(historyLogs);
|
|
}
|
|
|
|
// 切换日志类型
|
|
function switchLogType(logType) {
|
|
// 重置所有按钮状态
|
|
document.getElementById('systemLogTab').classList.remove('active');
|
|
document.getElementById('productLogTab').classList.remove('active');
|
|
document.getElementById('productLifecycleTab').classList.remove('active');
|
|
document.getElementById('customerHistoryTab').classList.remove('active');
|
|
document.getElementById('agentOperationsTab').classList.remove('active');
|
|
|
|
// 为当前点击的按钮添加active类
|
|
if (event) {
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
// 隐藏所有过滤器
|
|
document.getElementById('systemLogFilters').style.display = 'none';
|
|
document.getElementById('productLogFilters').style.display = 'none';
|
|
document.getElementById('productLifecycleFilters').style.display = 'none';
|
|
document.getElementById('customerHistoryFilters').style.display = 'none';
|
|
document.getElementById('agentOperationsFilters').style.display = 'none';
|
|
|
|
// 获取表格和表头元素
|
|
const resultDiv = document.getElementById('logsTableBody');
|
|
const logTimeHeader = document.getElementById('logTimeHeader');
|
|
const logUserHeader = document.getElementById('logUserHeader');
|
|
const logIdHeader = document.getElementById('logIdHeader');
|
|
const logPhoneHeader = document.getElementById('logPhoneHeader');
|
|
const logEventHeader = document.getElementById('logEventHeader');
|
|
const logActionHeader = document.getElementById('logActionHeader');
|
|
|
|
// 重置分页组件
|
|
const totalLogs = document.getElementById('totalLogs');
|
|
const currentPage = document.getElementById('currentPage');
|
|
const totalPages = document.getElementById('totalPages');
|
|
const prevPageBtn = document.getElementById('prevPage');
|
|
const nextPageBtn = document.getElementById('nextPage');
|
|
|
|
totalLogs.textContent = '0';
|
|
currentPage.textContent = '1';
|
|
totalPages.textContent = '1';
|
|
prevPageBtn.disabled = true;
|
|
nextPageBtn.disabled = true;
|
|
|
|
// 清空结果区域
|
|
resultDiv.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">加载中...</td></tr>';
|
|
|
|
// 根据日志类型显示对应内容
|
|
if (logType === 'system') {
|
|
document.getElementById('systemLogTab').classList.add('active');
|
|
document.getElementById('systemLogFilters').style.display = 'block';
|
|
|
|
// 更新表头
|
|
logTimeHeader.textContent = '操作时间';
|
|
logUserHeader.textContent = '操作人';
|
|
logIdHeader.textContent = '客户ID';
|
|
logPhoneHeader.textContent = '电话号码';
|
|
logEventHeader.textContent = '操作事件';
|
|
logActionHeader.textContent = '操作详情';
|
|
|
|
// 加载系统日志
|
|
loadLogs();
|
|
} else if (logType === 'product') {
|
|
document.getElementById('productLogTab').classList.add('active');
|
|
document.getElementById('productLogFilters').style.display = 'block';
|
|
|
|
// 更新表头
|
|
logTimeHeader.textContent = '日志时间';
|
|
logUserHeader.textContent = '商品名称';
|
|
logIdHeader.textContent = '商品ID';
|
|
logPhoneHeader.textContent = '卖家ID';
|
|
logEventHeader.textContent = '日志内容';
|
|
logActionHeader.textContent = '操作';
|
|
|
|
// 加载商品日志
|
|
loadProductLogs();
|
|
} else if (logType === 'productLifecycle') {
|
|
document.getElementById('productLifecycleTab').classList.add('active');
|
|
document.getElementById('productLifecycleFilters').style.display = 'block';
|
|
|
|
// 更新表头
|
|
logTimeHeader.textContent = '操作时间';
|
|
logUserHeader.textContent = '商品名称';
|
|
logIdHeader.textContent = '商品ID';
|
|
logPhoneHeader.textContent = '卖家ID';
|
|
logEventHeader.textContent = '操作事件';
|
|
logActionHeader.textContent = '操作详情';
|
|
|
|
// 清空结果区域,等待用户查询
|
|
setTimeout(() => {
|
|
resultDiv.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">请输入商品ID进行查询</td></tr>';
|
|
}, 100);
|
|
} else if (logType === 'customerHistory') {
|
|
document.getElementById('customerHistoryTab').classList.add('active');
|
|
document.getElementById('customerHistoryFilters').style.display = 'block';
|
|
|
|
// 更新表头
|
|
logTimeHeader.textContent = '操作时间';
|
|
logUserHeader.textContent = '客户名称';
|
|
logIdHeader.textContent = '客户ID';
|
|
logPhoneHeader.textContent = '电话号码';
|
|
logEventHeader.textContent = '操作事件';
|
|
logActionHeader.textContent = '操作详情';
|
|
|
|
// 清空结果区域,等待用户查询
|
|
setTimeout(() => {
|
|
resultDiv.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">请输入客户ID或电话号码进行查询</td></tr>';
|
|
}, 100);
|
|
} else if (logType === 'agentOperations') {
|
|
document.getElementById('agentOperationsTab').classList.add('active');
|
|
document.getElementById('agentOperationsFilters').style.display = 'block';
|
|
|
|
// 更新表头
|
|
logTimeHeader.textContent = '操作时间';
|
|
logUserHeader.textContent = '业务员姓名';
|
|
logIdHeader.textContent = '客户ID';
|
|
logPhoneHeader.textContent = '客户电话';
|
|
logEventHeader.textContent = '操作事件';
|
|
logActionHeader.textContent = '操作详情';
|
|
|
|
// 清空结果区域,等待用户查询
|
|
setTimeout(() => {
|
|
resultDiv.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">请输入业务员姓名进行查询</td></tr>';
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 显示客户活跃统计页面
|
|
function showActiveStats() {
|
|
// 激活菜单
|
|
activateMenu('active-stats');
|
|
|
|
// 重置状态版本
|
|
activeStatsVersion = 0;
|
|
// 取消之前的请求
|
|
if (activeStatsAbortController) {
|
|
activeStatsAbortController.abort();
|
|
activeStatsAbortController = null;
|
|
}
|
|
|
|
// 优先检查永久缓存中的活跃统计数据
|
|
const permanentActiveStats = cacheManager.getPermanent('activeStatsData');
|
|
|
|
// 检查其他缓存
|
|
const todayActiveStats = cacheManager.get('htgl_activeStats_today');
|
|
if (todayActiveStats) {
|
|
console.log('使用今日活跃统计临时缓存数据');
|
|
}
|
|
|
|
const historyActiveStats = cacheManager.getPermanent('activeStats_history');
|
|
if (historyActiveStats) {
|
|
console.log('使用历史活跃统计永久缓存数据');
|
|
}
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">客户活跃统计</h1>
|
|
<p class="content-subtitle">实时查看客户活跃情况、活跃时长和浏览记录</p>
|
|
</div>
|
|
|
|
<!-- 统计筛选条件 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">筛选条件</h2>
|
|
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap;">
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<label>时间范围:</label>
|
|
<select id="dateRangeSelect" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<option value="today">今天</option>
|
|
<option value="yesterday">昨天</option>
|
|
<option value="3days">最近3天</option>
|
|
<option value="7days">最近7天</option>
|
|
<option value="30days">最近30天</option>
|
|
<option value="90days">最近90天</option>
|
|
<option value="custom">自定义</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<label for="activeStartDate">开始日期:</label>
|
|
<input type="date" id="activeStartDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<label for="activeEndDate">结束日期:</label>
|
|
<input type="date" id="activeEndDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="refreshActiveStats()">查询</button>
|
|
<button class="btn btn-default" onclick="resetActiveStatsFilters()">重置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 活跃统计概览 -->
|
|
<div class="stats-container fade-in delay-1">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalActiveClients">加载中...</div>
|
|
<div class="stat-label">总活跃客户数</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="weekActiveClients">加载中...</div>
|
|
<div class="stat-label">一周内活跃客户</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="totalActiveDuration">加载中...</div>
|
|
<div class="stat-label">总活跃时长 (小时)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 活跃统计图表 -->
|
|
<div class="card fade-in delay-2">
|
|
<h2 class="card-title">活跃趋势</h2>
|
|
<div class="chart-container">
|
|
<canvas id="activeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 总活跃时长统计 -->
|
|
<div class="card fade-in delay-4" style="display: none;">
|
|
<h2 class="card-title">总活跃时长统计</h2>
|
|
<div id="totalDurationStats">
|
|
<p style="color: #666; text-align: center;">加载中...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 活跃客户排名 -->
|
|
<div class="card fade-in delay-5">
|
|
<h2 class="card-title">活跃客户排名</h2>
|
|
<div id="activeUsersContainer" style="width: 100%; border: 1px solid #e2e8f0; border-radius: 0.5rem; overflow-x: auto;"></div>
|
|
<!-- 分页控件 -->
|
|
<div style="margin-top: 1rem; display: flex; justify-content: center; align-items: center; gap: 8px; font-size: 14px;">
|
|
<button id="activePrevPageBtn" class="btn btn-default" onclick="changeActiveClientPage(-1)" disabled>上一页</button>
|
|
<span id="activePageInfo">第 1 页 / 共 1 页</span>
|
|
<button id="activeNextPageBtn" class="btn btn-default" onclick="changeActiveClientPage(1)" disabled>下一页</button>
|
|
<span style="margin-left: 16px;">每页 <input type="number" id="activePageSizeInput" value="10" min="1" max="100" style="width: 60px; padding: 4px; border: 1px solid #d9d9d9; border-radius: 4px; text-align: center;" onchange="changeActiveClientPageSize()"> 条</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 设置默认时间范围为最近7天
|
|
setTimeout(() => {
|
|
const dateRangeSelect = document.getElementById('dateRangeSelect');
|
|
|
|
// 确保select元素的值被正确设置并显示
|
|
for (let i = 0; i < dateRangeSelect.options.length; i++) {
|
|
if (dateRangeSelect.options[i].value === '7days') {
|
|
dateRangeSelect.options[i].selected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
setDateRange('7days');
|
|
|
|
// 添加日期范围选择事件监听
|
|
dateRangeSelect.addEventListener('change', function() {
|
|
const selectedValue = this.value;
|
|
if (selectedValue !== 'custom') {
|
|
setDateRange(selectedValue);
|
|
refreshActiveStats();
|
|
}
|
|
});
|
|
|
|
// 添加日期输入框事件监听,当用户手动修改日期时,将选择器切换到自定义
|
|
const activeStartDate = document.getElementById('activeStartDate');
|
|
const activeEndDate = document.getElementById('activeEndDate');
|
|
|
|
const handleDateChange = () => {
|
|
dateRangeSelect.value = 'custom';
|
|
};
|
|
|
|
// 更新活跃统计UI的函数
|
|
function updateActiveStatsUI(data) {
|
|
// 更新总活跃客户数
|
|
const totalActiveClients = document.getElementById('totalActiveClients');
|
|
if (totalActiveClients) {
|
|
if (data.overview && data.overview.success && data.overview.data.totalActiveClients) {
|
|
totalActiveClients.textContent = data.overview.data.totalActiveClients;
|
|
} else {
|
|
totalActiveClients.textContent = '0';
|
|
}
|
|
}
|
|
|
|
// 更新一周内活跃客户数
|
|
const weekActiveClients = document.getElementById('weekActiveClients');
|
|
if (weekActiveClients) {
|
|
if (data.overview && data.overview.success && data.overview.data.weekActiveClients) {
|
|
weekActiveClients.textContent = data.overview.data.weekActiveClients;
|
|
} else {
|
|
weekActiveClients.textContent = '0';
|
|
}
|
|
}
|
|
|
|
// 更新总活跃时长
|
|
const totalActiveDuration = document.getElementById('totalActiveDuration');
|
|
if (totalActiveDuration) {
|
|
if (data.totalDuration && data.totalDuration.success && data.totalDuration.data.totalHours) {
|
|
const totalHours = parseFloat(data.totalDuration.data.totalHours) || 0;
|
|
totalActiveDuration.textContent = totalHours.toFixed(2);
|
|
} else {
|
|
totalActiveDuration.textContent = '0.00';
|
|
}
|
|
}
|
|
|
|
// 更新总活跃时长统计
|
|
const totalDurationStats = document.getElementById('totalDurationStats');
|
|
if (totalDurationStats) {
|
|
if (data.totalDuration && data.totalDuration.success && data.totalDuration.data.stats) {
|
|
// 这里可以添加具体的总活跃时长统计更新逻辑
|
|
totalDurationStats.innerHTML = '<p style="color: #666; text-align: center;">数据加载完成</p>';
|
|
} else {
|
|
totalDurationStats.innerHTML = '<p style="color: #666; text-align: center;">暂无数据</p>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始化UI更新
|
|
if (permanentActiveStats) {
|
|
console.log('使用永久缓存的活跃统计数据');
|
|
// 立即使用缓存数据更新UI
|
|
updateActiveStatsUI(permanentActiveStats);
|
|
} else {
|
|
console.log('永久缓存中无活跃统计数据,需要加载');
|
|
|
|
// 立即显示默认数据,避免用户等待
|
|
const defaultData = {
|
|
overview: { success: false, data: {} },
|
|
daily: { success: false, data: {} },
|
|
totalDuration: { success: false, data: {} }
|
|
};
|
|
updateActiveStatsUI(defaultData);
|
|
|
|
// 立即加载活跃统计数据
|
|
loadModuleData('active-stats')
|
|
.then(() => {
|
|
console.log('活跃统计数据加载完成');
|
|
// 加载完成后再次检查永久缓存
|
|
const updatedPermanentData = cacheManager.getPermanent('activeStatsData');
|
|
if (updatedPermanentData) {
|
|
console.log('使用更新后的永久缓存数据');
|
|
// 使用更新后的数据更新UI
|
|
updateActiveStatsUI(updatedPermanentData);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载活跃统计数据失败:', error);
|
|
// 即使加载失败,也保持默认数据显示
|
|
});
|
|
}
|
|
|
|
activeStartDate.addEventListener('change', handleDateChange);
|
|
activeEndDate.addEventListener('change', handleDateChange);
|
|
|
|
// 初始化图表
|
|
initActiveChart();
|
|
|
|
// 延迟调用,确保DOM元素完全渲染
|
|
setTimeout(() => {
|
|
// 重置到第一页,避免使用过期状态
|
|
activeCurrentPage = 1;
|
|
refreshActiveStats(activeCurrentPage, activePageSize);
|
|
}, 200);
|
|
}, 100);
|
|
}
|
|
|
|
// 保存今日活跃统计数据到临时缓存
|
|
function saveTodayActiveStats(stats) {
|
|
cacheManager.set('htgl_activeStats_today', stats);
|
|
}
|
|
|
|
// 保存历史活跃统计数据到永久缓存
|
|
function saveHistoryActiveStats(date, stats) {
|
|
const activeStatsHistory = cacheManager.getPermanent('activeStats_history') || {
|
|
history: {},
|
|
clientDetails: {}
|
|
};
|
|
|
|
if (!activeStatsHistory.history) {
|
|
activeStatsHistory.history = {};
|
|
}
|
|
|
|
activeStatsHistory.history[date] = stats;
|
|
cacheManager.setPermanent('activeStats_history', activeStatsHistory);
|
|
|
|
// 清理过期数据
|
|
cleanupOldActiveStats(activeStatsHistory);
|
|
}
|
|
|
|
// 更新客户活跃详情到永久缓存
|
|
function updateClientActiveDetailToCache(clientId, details) {
|
|
const activeStatsHistory = cacheManager.getPermanent('activeStats_history') || {
|
|
history: {},
|
|
clientDetails: {}
|
|
};
|
|
|
|
if (!activeStatsHistory.clientDetails) {
|
|
activeStatsHistory.clientDetails = {};
|
|
}
|
|
|
|
activeStatsHistory.clientDetails[clientId] = details;
|
|
cacheManager.setPermanent('activeStats_history', activeStatsHistory);
|
|
}
|
|
|
|
// 清理过期活跃统计数据
|
|
function cleanupOldActiveStats(activeStatsHistory) {
|
|
if (!activeStatsHistory.history) {
|
|
return;
|
|
}
|
|
|
|
const ninetyDaysAgo = new Date();
|
|
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
|
|
const cutoffDate = ninetyDaysAgo.toISOString().split('T')[0];
|
|
|
|
Object.keys(activeStatsHistory.history).forEach(date => {
|
|
if (date < cutoffDate) {
|
|
delete activeStatsHistory.history[date];
|
|
}
|
|
});
|
|
}
|
|
|
|
// 设置日期范围
|
|
function setDateRange(range) {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = now.getMonth();
|
|
const day = now.getDate();
|
|
|
|
let startDate, endDate;
|
|
|
|
switch(range) {
|
|
case 'today':
|
|
// 今天:从今天0点到今天23:59:59
|
|
startDate = new Date(year, month, day, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
break;
|
|
case 'yesterday':
|
|
// 昨天:从昨天0点到昨天23:59:59
|
|
startDate = new Date(year, month, day - 1, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day - 1, 23, 59, 59, 999);
|
|
break;
|
|
case '3days':
|
|
// 最近3天:从3天前0点到今天23:59:59
|
|
startDate = new Date(year, month, day - 2, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
break;
|
|
case '7days':
|
|
startDate = new Date(year, month, day - 6, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
break;
|
|
case '30days':
|
|
startDate = new Date(year, month, day - 29, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
break;
|
|
case '90days':
|
|
startDate = new Date(year, month, day - 89, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
break;
|
|
default:
|
|
startDate = new Date(year, month, day - 6, 0, 0, 0, 0);
|
|
endDate = new Date(year, month, day, 23, 59, 59, 999);
|
|
}
|
|
|
|
// 格式化日期为YYYY-MM-DD(使用本地时间)
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
document.getElementById('activeStartDate').value = formatDate(startDate);
|
|
document.getElementById('activeEndDate').value = formatDate(endDate);
|
|
}
|
|
|
|
// 初始化活跃图表
|
|
let activeChart = null;
|
|
function initActiveChart() {
|
|
const ctx = document.getElementById('activeChart').getContext('2d');
|
|
activeChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: '活跃客户数',
|
|
data: [],
|
|
borderColor: '#367ee9',
|
|
backgroundColor: 'rgba(54, 126, 233, 0.1)',
|
|
tension: 0.3,
|
|
fill: true,
|
|
borderWidth: 3,
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#367ee9',
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
pointHoverRadius: 6
|
|
}, {
|
|
label: '总活跃时长 (小时)',
|
|
data: [],
|
|
borderColor: '#7f56d9',
|
|
backgroundColor: 'rgba(127, 86, 217, 0.1)',
|
|
tension: 0.3,
|
|
fill: true,
|
|
yAxisID: 'y1',
|
|
borderWidth: 3,
|
|
borderDash: [5, 5],
|
|
pointRadius: 4,
|
|
pointBackgroundColor: '#7f56d9',
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
pointHoverRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: {
|
|
// 格式化日期,只显示年月日
|
|
callback: function(value, index, values) {
|
|
const date = new Date(this.getLabelForValue(value));
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '活跃客户数',
|
|
color: '#367ee9'
|
|
},
|
|
ticks: {
|
|
color: '#367ee9'
|
|
}
|
|
},
|
|
y1: {
|
|
beginAtZero: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: '活跃时长 (小时)',
|
|
color: '#7f56d9'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false
|
|
},
|
|
ticks: {
|
|
color: '#7f56d9'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20
|
|
}
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
callbacks: {
|
|
// 格式化tooltip中的日期,只显示年月日
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += context.parsed.y;
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 活跃客户排名分页变量
|
|
let activeCurrentPage = 1;
|
|
let activePageSize = 10;
|
|
let activeTotalPages = 1;
|
|
let activeTotalItems = 0;
|
|
|
|
// 请求取消和状态管理
|
|
let activeStatsAbortController = null;
|
|
let activeStatsVersion = 0;
|
|
let isNavigating = false;
|
|
const NAVIGATION_DELAY = 300;
|
|
|
|
// 缓存管理
|
|
const cacheManager = {
|
|
// 模块缓存配置
|
|
moduleConfig: {
|
|
'dashboard': { expiration: 2 * 60 * 1000 }, // 2分钟
|
|
'active-stats': { expiration: 5 * 60 * 1000 }, // 5分钟
|
|
'logs': { expiration: 10 * 60 * 1000 }, // 10分钟
|
|
'supply-management': { expiration: 5 * 60 * 1000 }, // 5分钟
|
|
'client-stats': { expiration: 5 * 60 * 1000 }, // 5分钟
|
|
'business-stats': { expiration: 5 * 60 * 1000 }, // 5分钟
|
|
'suppliers': { expiration: 5 * 60 * 1000 }, // 5分钟
|
|
'comments': { expiration: 3 * 60 * 1000 }, // 3分钟
|
|
'forum-posts': { expiration: 3 * 60 * 1000 }, // 3分钟
|
|
'identity-verification': { expiration: 3 * 60 * 1000 }, // 3分钟
|
|
'permanent': { expiration: null } // 永久缓存
|
|
},
|
|
|
|
// 获取缓存过期时间
|
|
getExpirationTime(module) {
|
|
// 永久缓存返回null
|
|
if (module === 'permanent') {
|
|
return null;
|
|
}
|
|
return this.moduleConfig[module]?.expiration || 5 * 60 * 1000;
|
|
},
|
|
|
|
// 生成标准化缓存键
|
|
generateKey(module, params = {}) {
|
|
const paramString = Object.keys(params)
|
|
.sort()
|
|
.map(key => `${key}=${params[key]}`)
|
|
.join('&');
|
|
return `htgl_${module}${paramString ? `_${paramString}` : ''}`;
|
|
},
|
|
|
|
// 获取缓存
|
|
get(key) {
|
|
try {
|
|
const cached = localStorage.getItem(key);
|
|
if (cached) {
|
|
const parsed = JSON.parse(cached);
|
|
|
|
// 检查版本一致性
|
|
if (parsed.version !== this.cacheVersion) {
|
|
console.log('缓存版本不一致,强制更新:', key);
|
|
// 对于永久缓存,不删除,而是返回数据并在后台更新版本
|
|
if (key.startsWith('htgl_permanent_')) {
|
|
console.log('永久缓存版本不一致,返回数据并在后台更新版本');
|
|
// 后台异步更新版本
|
|
setTimeout(() => {
|
|
try {
|
|
const data = parsed.data;
|
|
localStorage.setItem(key, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
} catch (e) {
|
|
console.error('更新永久缓存版本失败:', e);
|
|
}
|
|
}, 0);
|
|
return parsed.data;
|
|
} else {
|
|
// 普通缓存直接删除
|
|
localStorage.removeItem(key);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 提取模块名
|
|
const moduleMatch = key.match(/^htgl_([^_]+)/);
|
|
const module = moduleMatch ? moduleMatch[1] : null;
|
|
|
|
// 根据模块获取过期时间
|
|
const expiration = this.getExpirationTime(module);
|
|
|
|
// 检查缓存是否过期(永久缓存跳过过期检查)
|
|
if (expiration === null) {
|
|
// 永久缓存,直接返回
|
|
return parsed.data;
|
|
} else if (Date.now() - parsed.timestamp < expiration) {
|
|
// 有过期时间的缓存,检查是否过期
|
|
return parsed.data;
|
|
} else {
|
|
// 缓存过期,删除
|
|
localStorage.removeItem(key);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
console.error('获取缓存失败:', e);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// 设置缓存
|
|
set(key, data) {
|
|
try {
|
|
// 检查数据大小
|
|
const dataSize = JSON.stringify(data).length;
|
|
if (dataSize > 5 * 1024 * 1024) { // 5MB限制
|
|
console.warn('缓存数据超过5MB限制,无法设置缓存');
|
|
return;
|
|
}
|
|
|
|
// 检查localStorage容量
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(key, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
} else {
|
|
// 容量不足,清理过期缓存
|
|
this.cleanupExpiredCache();
|
|
|
|
// 再次尝试
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(key, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
} else {
|
|
// 按优先级清理缓存
|
|
this.cleanupByPriority();
|
|
|
|
// 最后尝试
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(key, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
} else {
|
|
console.warn('localStorage容量不足,无法设置缓存');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('设置缓存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 设置永久缓存
|
|
setPermanent(key, data) {
|
|
try {
|
|
// 检查数据大小
|
|
const dataSize = JSON.stringify(data).length;
|
|
if (dataSize > 5 * 1024 * 1024) { // 5MB限制
|
|
console.warn('永久缓存数据超过5MB限制,无法设置缓存');
|
|
return false;
|
|
}
|
|
|
|
// 检查localStorage容量
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(`htgl_permanent_${key}`, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
console.log(`永久缓存设置成功: ${key}, 大小: ${(dataSize / 1024).toFixed(2)} KB`);
|
|
return true;
|
|
} else {
|
|
// 容量不足,清理过期缓存(不清理永久缓存)
|
|
this.cleanupExpiredCache();
|
|
|
|
// 再次尝试
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(`htgl_permanent_${key}`, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
console.log(`永久缓存设置成功(清理后): ${key}`);
|
|
return true;
|
|
} else {
|
|
// 按优先级清理非永久缓存
|
|
this.cleanupByPriority();
|
|
|
|
// 最后尝试
|
|
if (this.checkStorageCapacity()) {
|
|
localStorage.setItem(`htgl_permanent_${key}`, JSON.stringify({
|
|
data: data,
|
|
timestamp: Date.now(),
|
|
version: this.cacheVersion
|
|
}));
|
|
console.log(`永久缓存设置成功(优先级清理后): ${key}`);
|
|
return true;
|
|
} else {
|
|
console.warn('localStorage容量不足,无法设置永久缓存');
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('设置永久缓存失败:', e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// 获取永久缓存
|
|
getPermanent(key) {
|
|
return this.get(`htgl_permanent_${key}`);
|
|
},
|
|
|
|
// 清除缓存
|
|
clear(prefix) {
|
|
try {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith(`htgl_${prefix}`)) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('清除缓存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 清除所有缓存
|
|
clearAll() {
|
|
try {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith('htgl_')) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('清除所有缓存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 缓存版本控制
|
|
cacheVersion: '1.0.0',
|
|
|
|
// 初始化缓存版本
|
|
initCacheVersion() {
|
|
const storedVersion = this.getPermanent('cacheVersion');
|
|
if (!storedVersion || storedVersion !== this.cacheVersion) {
|
|
console.log('缓存版本更新,清理旧缓存');
|
|
// 清除所有非永久缓存
|
|
this.clearNonPermanentCache();
|
|
// 存储新的版本号
|
|
this.setPermanent('cacheVersion', this.cacheVersion);
|
|
}
|
|
},
|
|
|
|
// 清除非永久缓存
|
|
clearNonPermanentCache() {
|
|
try {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith('htgl_') && !key.startsWith('htgl_permanent_')) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('清除非永久缓存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 定期更新机制
|
|
periodicUpdateConfig: {
|
|
interval: 10 * 60 * 1000, // 10分钟
|
|
fastInterval: 5 * 60 * 1000, // 5分钟(用于数据变动快的模块)
|
|
updateTasks: new Map(), // 存储需要定期更新的任务
|
|
fastUpdateTasks: new Map(), // 存储需要快速更新的任务
|
|
timer: null,
|
|
fastTimer: null
|
|
},
|
|
|
|
// 注册定期更新任务
|
|
registerPeriodicUpdate(key, updateFunction, isFastUpdate = false) {
|
|
if (isFastUpdate) {
|
|
this.periodicUpdateConfig.fastUpdateTasks.set(key, updateFunction);
|
|
console.log(`注册快速更新任务: ${key}`);
|
|
} else {
|
|
this.periodicUpdateConfig.updateTasks.set(key, updateFunction);
|
|
console.log(`注册定期更新任务: ${key}`);
|
|
}
|
|
},
|
|
|
|
// 执行定期更新
|
|
executePeriodicUpdates() {
|
|
console.log('执行定期缓存更新...');
|
|
|
|
this.periodicUpdateConfig.updateTasks.forEach((updateFunction, key) => {
|
|
console.log(`更新缓存: ${key}`);
|
|
|
|
// 保存原有缓存数据,用于出错时恢复
|
|
const originalData = this.getPermanent(key);
|
|
|
|
// 执行更新函数
|
|
Promise.resolve()
|
|
.then(() => updateFunction())
|
|
.then(newData => {
|
|
// 更新成功,保存新数据
|
|
if (newData !== undefined) {
|
|
this.setPermanent(key, newData);
|
|
console.log(`缓存更新成功: ${key}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// 更新失败,使用原有缓存
|
|
console.error(`缓存更新失败: ${key}`, error);
|
|
if (originalData) {
|
|
console.log(`使用原有缓存数据: ${key}`);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
// 执行快速更新
|
|
executeFastUpdates() {
|
|
console.log('执行快速缓存更新...');
|
|
|
|
this.periodicUpdateConfig.fastUpdateTasks.forEach((updateFunction, key) => {
|
|
console.log(`快速更新缓存: ${key}`);
|
|
|
|
// 保存原有缓存数据,用于出错时恢复
|
|
const originalData = this.getPermanent(key);
|
|
|
|
// 执行更新函数
|
|
Promise.resolve()
|
|
.then(() => updateFunction())
|
|
.then(newData => {
|
|
// 更新成功,保存新数据
|
|
if (newData !== undefined) {
|
|
this.setPermanent(key, newData);
|
|
console.log(`快速缓存更新成功: ${key}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// 更新失败,使用原有缓存
|
|
console.error(`快速缓存更新失败: ${key}`, error);
|
|
if (originalData) {
|
|
console.log(`使用原有缓存数据: ${key}`);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
// 启动定期更新
|
|
startPeriodicUpdates() {
|
|
// 停止现有的定时器
|
|
this.stopPeriodicUpdates();
|
|
|
|
// 立即执行一次更新
|
|
this.executePeriodicUpdates();
|
|
this.executeFastUpdates();
|
|
|
|
// 设置新的定时器
|
|
this.periodicUpdateConfig.timer = setInterval(() => {
|
|
this.executePeriodicUpdates();
|
|
}, this.periodicUpdateConfig.interval);
|
|
|
|
// 设置快速更新定时器
|
|
this.periodicUpdateConfig.fastTimer = setInterval(() => {
|
|
this.executeFastUpdates();
|
|
}, this.periodicUpdateConfig.fastInterval);
|
|
|
|
// 设置每日归档和清理定时器
|
|
this.startDailyTasks();
|
|
|
|
console.log('定期更新已启动,每10分钟执行一次');
|
|
console.log('快速更新已启动,每5分钟执行一次');
|
|
console.log('每日归档和清理任务已启动');
|
|
},
|
|
|
|
// 启动每日任务(归档和清理)
|
|
startDailyTasks() {
|
|
// 立即检查是否需要执行每日任务
|
|
this.checkAndExecuteDailyTasks();
|
|
|
|
// 每小时检查一次
|
|
setInterval(() => {
|
|
this.checkAndExecuteDailyTasks();
|
|
}, 60 * 60 * 1000);
|
|
},
|
|
|
|
// 检查并执行每日任务
|
|
checkAndExecuteDailyTasks() {
|
|
const now = new Date();
|
|
const hours = now.getHours();
|
|
const minutes = now.getMinutes();
|
|
|
|
// 每天凌晨0:00执行
|
|
if (hours === 0 && minutes === 0) {
|
|
console.log('执行每日归档和清理任务');
|
|
this.executeDailyTasks();
|
|
}
|
|
},
|
|
|
|
// 执行每日任务
|
|
executeDailyTasks() {
|
|
try {
|
|
// 归档日志
|
|
this.archiveLogs();
|
|
|
|
// 清理过期活跃统计数据
|
|
const activeStatsHistory = this.getPermanent('activeStats_history');
|
|
if (activeStatsHistory) {
|
|
cleanupOldActiveStats(activeStatsHistory);
|
|
}
|
|
|
|
// 清理过期缓存
|
|
this.cleanupExpiredCache();
|
|
|
|
console.log('每日归档和清理任务执行完成');
|
|
} catch (e) {
|
|
console.error('执行每日任务失败:', e);
|
|
}
|
|
},
|
|
|
|
// 停止定期更新
|
|
stopPeriodicUpdates() {
|
|
if (this.periodicUpdateConfig.timer) {
|
|
clearInterval(this.periodicUpdateConfig.timer);
|
|
this.periodicUpdateConfig.timer = null;
|
|
console.log('定期更新已停止');
|
|
}
|
|
|
|
if (this.periodicUpdateConfig.fastTimer) {
|
|
clearInterval(this.periodicUpdateConfig.fastTimer);
|
|
this.periodicUpdateConfig.fastTimer = null;
|
|
console.log('快速更新已停止');
|
|
}
|
|
},
|
|
|
|
// 检查localStorage容量
|
|
checkStorageCapacity() {
|
|
try {
|
|
const testKey = 'htgl_test';
|
|
const testValue = 'test';
|
|
localStorage.setItem(testKey, testValue);
|
|
localStorage.removeItem(testKey);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// 清理过期缓存
|
|
cleanupExpiredCache() {
|
|
try {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith('htgl_')) {
|
|
const cached = localStorage.getItem(key);
|
|
if (cached) {
|
|
try {
|
|
const parsed = JSON.parse(cached);
|
|
const moduleMatch = key.match(/^htgl_([^_]+)/);
|
|
const module = moduleMatch ? moduleMatch[1] : null;
|
|
const expiration = this.getExpirationTime(module);
|
|
|
|
// 跳过永久缓存的清理
|
|
if (expiration !== null && Date.now() - parsed.timestamp >= expiration) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
} catch (e) {
|
|
// 解析失败,删除缓存
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('清理过期缓存失败:', e);
|
|
}
|
|
},
|
|
|
|
// 新增:归档日志到永久缓存
|
|
archiveLogs() {
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const dateKey = yesterday.toISOString().split('T')[0];
|
|
|
|
const todayLogs = this.get('htgl_logs_today');
|
|
if (todayLogs) {
|
|
const historyLogs = this.getPermanent('logs_history') || {};
|
|
historyLogs[dateKey] = todayLogs;
|
|
this.setPermanent('logs_history', historyLogs);
|
|
|
|
// 清理过期日志
|
|
this.cleanupOldLogs(historyLogs);
|
|
}
|
|
},
|
|
|
|
// 新增:清理过期日志
|
|
cleanupOldLogs(historyLogs) {
|
|
const thirtyDaysAgo = new Date();
|
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
const cutoffDate = thirtyDaysAgo.toISOString().split('T')[0];
|
|
|
|
Object.keys(historyLogs).forEach(date => {
|
|
if (date < cutoffDate) {
|
|
delete historyLogs[date];
|
|
}
|
|
});
|
|
},
|
|
|
|
// 新增:按优先级清理缓存
|
|
cleanupByPriority() {
|
|
try {
|
|
const cacheItems = [];
|
|
|
|
// 收集所有缓存项
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith('htgl_')) {
|
|
const cached = localStorage.getItem(key);
|
|
if (cached) {
|
|
try {
|
|
const parsed = JSON.parse(cached);
|
|
cacheItems.push({
|
|
key: key,
|
|
size: cached.length,
|
|
timestamp: parsed.timestamp,
|
|
isPermanent: key.startsWith('htgl_permanent_')
|
|
});
|
|
} catch (e) {
|
|
// 解析失败,删除缓存
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 按优先级排序:永久缓存 > 高优先级 > 中优先级 > 低优先级
|
|
const priorityOrder = {
|
|
'htgl_permanent_': 4, // 永久缓存
|
|
'htgl_client': 3, // 客户相关(高优先级)
|
|
'htgl_supplier': 3, // 供应商相关(高优先级)
|
|
'htgl_supply': 3, // 货源相关(高优先级)
|
|
'htgl_stats': 2, // 统计数据(中优先级)
|
|
'htgl_active': 2, // 活跃数据(中优先级)
|
|
'htgl_logs': 1, // 日志数据(低优先级)
|
|
'htgl_browse': 1 // 浏览记录(低优先级)
|
|
};
|
|
|
|
// 计算优先级
|
|
cacheItems.forEach(item => {
|
|
let priority = 0;
|
|
for (const prefix in priorityOrder) {
|
|
if (item.key.includes(prefix)) {
|
|
priority = priorityOrder[prefix];
|
|
break;
|
|
}
|
|
}
|
|
item.priority = priority;
|
|
});
|
|
|
|
// 排序:优先级低的先清理,同优先级按时间早的先清理
|
|
cacheItems.sort((a, b) => {
|
|
if (a.priority !== b.priority) {
|
|
return a.priority - b.priority;
|
|
}
|
|
return a.timestamp - b.timestamp;
|
|
});
|
|
|
|
// 清理直到有足够空间
|
|
let cleaned = false;
|
|
while (!this.checkStorageCapacity() && cacheItems.length > 0) {
|
|
const item = cacheItems.shift();
|
|
// 不清理永久缓存
|
|
if (!item.isPermanent) {
|
|
localStorage.removeItem(item.key);
|
|
console.log(`清理缓存: ${item.key} (优先级: ${item.priority})`);
|
|
cleaned = true;
|
|
}
|
|
}
|
|
|
|
if (cleaned) {
|
|
console.log('缓存清理完成');
|
|
}
|
|
} catch (e) {
|
|
console.error('按优先级清理缓存失败:', e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 确保所有指定模块都有永久缓存数据
|
|
function ensurePermanentCacheData() {
|
|
// 优先确保活跃统计数据的缓存
|
|
const priorityModules = ['active-stats'];
|
|
const regularModules = [
|
|
'logs', // 操作日志
|
|
'client-stats', // 客户统计
|
|
'business-stats', // 业务统计
|
|
'suppliers', // 供应商管理
|
|
'supply-management', // 货源管理
|
|
'comments', // 留言审核
|
|
'forum-posts', // 论坛动态审核
|
|
'identity-verification' // 身份信息审核
|
|
];
|
|
|
|
const allModules = [...priorityModules, ...regularModules];
|
|
console.log('确保永久缓存数据:', allModules);
|
|
|
|
// 转换模块名到驼峰命名,用于永久缓存键名
|
|
function toCamelCase(str) {
|
|
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
}
|
|
|
|
// 检查localStorage是否可用
|
|
function isLocalStorageAvailable() {
|
|
try {
|
|
const testKey = 'htgl_test_' + Date.now();
|
|
localStorage.setItem(testKey, testKey);
|
|
localStorage.removeItem(testKey);
|
|
return true;
|
|
} catch (e) {
|
|
console.warn('localStorage不可用:', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 检查并加载缺失的永久缓存数据
|
|
const promises = allModules.map(module => {
|
|
return new Promise((resolve) => {
|
|
const permanentKey = toCamelCase(module) + 'Data';
|
|
|
|
// 验证缓存状态,确保数据存在且有效
|
|
let retryCount = 0;
|
|
const maxRetries = 3; // 增加重试次数
|
|
const retryDelay = 1000; // 增加重试延迟
|
|
|
|
function tryLoadData() {
|
|
// 首先检查localStorage是否可用
|
|
if (!isLocalStorageAvailable()) {
|
|
console.warn('localStorage不可用,等待重试:', module);
|
|
retryCount++;
|
|
if (retryCount < maxRetries) {
|
|
setTimeout(tryLoadData, retryDelay);
|
|
} else {
|
|
console.error('localStorage持续不可用,跳过:', module);
|
|
resolve();
|
|
}
|
|
return;
|
|
}
|
|
|
|
let cachedData = cacheManager.getPermanent(permanentKey);
|
|
|
|
if (!cachedData) {
|
|
console.log('永久缓存缺失,加载模块数据:', module, '使用键名:', permanentKey);
|
|
loadModuleData(module)
|
|
.then(() => {
|
|
// 验证缓存是否成功设置
|
|
const afterLoadData = cacheManager.getPermanent(permanentKey);
|
|
if (afterLoadData) {
|
|
console.log('模块数据加载并缓存完成:', module);
|
|
resolve();
|
|
} else {
|
|
console.warn('模块数据加载成功但缓存设置失败,重试:', module);
|
|
retryCount++;
|
|
if (retryCount < maxRetries) {
|
|
setTimeout(tryLoadData, retryDelay);
|
|
} else {
|
|
console.error('模块数据缓存设置多次失败,跳过:', module);
|
|
resolve();
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载模块数据失败:', module, error);
|
|
// 即使API失败,也尝试创建默认数据并设置缓存
|
|
const defaultData = module === 'active-stats' ? {
|
|
overview: { success: false, data: {} },
|
|
daily: { success: false, data: {} },
|
|
totalDuration: { success: false, data: {} }
|
|
} : null;
|
|
|
|
if (defaultData) {
|
|
const setResult = cacheManager.setPermanent(permanentKey, defaultData);
|
|
if (setResult) {
|
|
console.log('为模块设置默认缓存数据:', module);
|
|
}
|
|
}
|
|
resolve(); // 即使失败也继续
|
|
});
|
|
} else {
|
|
console.log('永久缓存已存在,跳过加载:', module, '使用键名:', permanentKey);
|
|
resolve();
|
|
}
|
|
}
|
|
|
|
tryLoadData();
|
|
});
|
|
});
|
|
|
|
return Promise.all(promises).then(() => {
|
|
console.log('所有模块永久缓存数据验证完成');
|
|
|
|
// 额外验证活跃统计数据的缓存状态,增加更强的验证逻辑
|
|
const verifyActiveStatsCache = () => {
|
|
const activeStatsData = cacheManager.getPermanent('activeStatsData');
|
|
if (activeStatsData) {
|
|
console.log('活跃统计永久缓存验证成功');
|
|
return Promise.resolve();
|
|
} else {
|
|
console.log('活跃统计永久缓存验证失败,强制加载');
|
|
return loadModuleData('active-stats')
|
|
.then(() => {
|
|
const afterLoadData = cacheManager.getPermanent('activeStatsData');
|
|
if (afterLoadData) {
|
|
console.log('活跃统计数据强制加载完成');
|
|
} else {
|
|
console.error('活跃统计数据强制加载后缓存仍失败,创建默认数据');
|
|
// 直接创建默认数据并设置缓存
|
|
const defaultData = {
|
|
overview: { success: false, data: {} },
|
|
daily: { success: false, data: {} },
|
|
totalDuration: { success: false, data: {} }
|
|
};
|
|
const setResult = cacheManager.setPermanent('activeStatsData', defaultData);
|
|
if (setResult) {
|
|
console.log('活跃统计默认数据永久缓存设置成功');
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('强制加载活跃统计数据失败:', error);
|
|
// 即使强制加载失败,也创建默认数据
|
|
const defaultData = {
|
|
overview: { success: false, data: {} },
|
|
daily: { success: false, data: {} },
|
|
totalDuration: { success: false, data: {} }
|
|
};
|
|
const setResult = cacheManager.setPermanent('activeStatsData', defaultData);
|
|
if (setResult) {
|
|
console.log('活跃统计默认数据永久缓存设置成功');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
return verifyActiveStatsCache().then(() => {
|
|
console.log('活跃统计缓存最终验证完成');
|
|
});
|
|
});
|
|
}
|
|
|
|
// 初始化数据加载
|
|
initDataLoading();
|
|
|
|
// 显示骨架屏
|
|
function showActiveSkeleton() {
|
|
// 更新总活跃时长统计为骨架屏
|
|
const totalDurationStats = document.getElementById('totalDurationStats');
|
|
if (totalDurationStats) {
|
|
totalDurationStats.innerHTML = `
|
|
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
|
<div style="flex: 1; min-width: 200px;">
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
<div style="flex: 1; min-width: 200px;">
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text skeleton-text-lg"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 更新概览数据为骨架屏(添加元素存在检查)
|
|
const totalActiveClients = document.getElementById('totalActiveClients');
|
|
if (totalActiveClients) {
|
|
totalActiveClients.innerHTML = '<div class="skeleton-text"></div>';
|
|
}
|
|
|
|
const weekActiveClients = document.getElementById('weekActiveClients');
|
|
if (weekActiveClients) {
|
|
weekActiveClients.innerHTML = '<div class="skeleton-text"></div>';
|
|
}
|
|
|
|
const totalActiveDuration = document.getElementById('totalActiveDuration');
|
|
if (totalActiveDuration) {
|
|
totalActiveDuration.innerHTML = '<div class="skeleton-text"></div>';
|
|
}
|
|
|
|
// 更新活跃客户排名为骨架屏
|
|
const tableBody = document.querySelector('#activeUsersTable tbody');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = Array(10).fill().map(() => `
|
|
<tr>
|
|
<td><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td><div class="skeleton-text"></div></td>
|
|
<td><div class="skeleton-text"></div></td>
|
|
<td><div class="skeleton-text skeleton-text-sm"></div></td>
|
|
<td><div class="skeleton-text"></div></td>
|
|
<td>
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text"></div>
|
|
<div class="skeleton-text"></div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
// 刷新活跃统计数据
|
|
function refreshActiveStats(page = 1, pageSize = 10) {
|
|
// 取消之前的请求
|
|
if (activeStatsAbortController) {
|
|
activeStatsAbortController.abort();
|
|
}
|
|
|
|
// 创建新的AbortController
|
|
activeStatsAbortController = new AbortController();
|
|
const signal = activeStatsAbortController.signal;
|
|
|
|
// 增加状态版本
|
|
const currentVersion = ++activeStatsVersion;
|
|
|
|
// 检查是否在活跃统计页面
|
|
const isActiveStatsPage = document.getElementById('activeUsersContainer') !== null;
|
|
|
|
// 检查日期输入框
|
|
const activeStartDate = document.getElementById('activeStartDate');
|
|
const activeEndDate = document.getElementById('activeEndDate');
|
|
const startDate = activeStartDate ? activeStartDate.value : '';
|
|
const endDate = activeEndDate ? activeEndDate.value : '';
|
|
|
|
// 生成缓存键(包含页码信息)
|
|
const params = new URLSearchParams();
|
|
if (startDate) params.append('startDate', startDate);
|
|
if (endDate) params.append('endDate', endDate);
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
const cacheKey = 'active_stats_' + params.toString();
|
|
|
|
// 生成永久缓存键(包含页码信息)
|
|
const permanentCacheKey = 'active_stats_permanent_' + params.toString();
|
|
|
|
// 检查是否包含今日数据
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
const today = formatDate(new Date());
|
|
const containsToday = (startDate && startDate <= today && endDate && endDate >= today);
|
|
|
|
// 首先检查临时缓存(包含页码信息)
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log(`使用临时缓存数据加载客户活跃统计(页码: ${page})`);
|
|
|
|
// 如果在活跃统计页面,立即显示缓存数据
|
|
if (isActiveStatsPage) {
|
|
console.log('在活跃统计页面,显示缓存数据');
|
|
updateActiveStatsUI(cachedData, page, pageSize);
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
}
|
|
|
|
// 无论是否包含今日数据,都后台加载最新数据
|
|
console.log('后台加载最新数据,参数:', { startDate, endDate, page, pageSize });
|
|
loadLatestActiveStats(startDate, endDate, page, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 其次检查永久缓存(包含页码信息)
|
|
const permanentCachedData = cacheManager.getPermanent(permanentCacheKey);
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载客户活跃统计');
|
|
console.log('永久缓存数据结构:', permanentCachedData);
|
|
|
|
// 如果在活跃统计页面,立即显示永久缓存数据
|
|
if (isActiveStatsPage) {
|
|
console.log('在活跃统计页面,显示缓存数据');
|
|
updateActiveStatsUI(permanentCachedData, page, pageSize);
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
} else {
|
|
console.log('不在活跃统计页面,跳过缓存数据显示');
|
|
}
|
|
|
|
// 无论是否包含今日数据,都后台加载最新数据
|
|
console.log('后台加载最新数据,参数:', { startDate, endDate, page, pageSize });
|
|
loadLatestActiveStats(startDate, endDate, page, pageSize);
|
|
return;
|
|
} else {
|
|
console.log('缓存不存在,需要从服务器获取数据');
|
|
}
|
|
|
|
// 如果没有永久缓存,继续检查普通缓存和从服务器获取数据
|
|
if (!activeStartDate) {
|
|
// 如果 DOM 元素不存在,说明客户活跃统计页面还没有加载
|
|
// 只获取数据并缓存,不显示骨架屏和更新 UI
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
const formatDate = (date) => {
|
|
return date.toISOString().split('T')[0];
|
|
};
|
|
|
|
const preloadParams = new URLSearchParams();
|
|
preloadParams.append('startDate', formatDate(startDate));
|
|
preloadParams.append('endDate', formatDate(endDate));
|
|
preloadParams.append('page', page);
|
|
preloadParams.append('pageSize', pageSize);
|
|
|
|
const preloadCacheKey = 'active_stats_' + preloadParams.toString();
|
|
|
|
// 并行请求所有数据
|
|
Promise.all([
|
|
fetch('/api/active-stats?' + preloadParams.toString(), { signal })
|
|
.then(response => response.json()),
|
|
fetch('/api/daily-active-stats?' + preloadParams.toString(), { signal })
|
|
.then(response => response.json()),
|
|
fetch('/api/total-active-duration?' + preloadParams.toString(), { signal })
|
|
.then(response => response.json())
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
// 检查版本是否匹配
|
|
if (currentVersion !== activeStatsVersion) {
|
|
console.log('过期请求,跳过缓存更新');
|
|
return;
|
|
}
|
|
|
|
const combinedData = {
|
|
overview: overviewData,
|
|
daily: dailyData,
|
|
totalDuration: totalDurationData
|
|
};
|
|
// 缓存数据(设置过期时间为8分钟)
|
|
cacheManager.set(preloadCacheKey, combinedData, 480000); // 8分钟过期
|
|
// 更新永久缓存(包含页码信息)
|
|
const preloadPermanentCacheKey = 'active_stats_permanent_' + preloadParams.toString();
|
|
cacheManager.setPermanent(preloadPermanentCacheKey, combinedData);
|
|
})
|
|
.catch(error => {
|
|
if (error.name === 'AbortError') {
|
|
console.log('请求已取消');
|
|
return;
|
|
}
|
|
console.error('预加载客户活跃统计数据失败:', error);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 显示骨架屏
|
|
showActiveSkeleton();
|
|
|
|
// 显示加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, true);
|
|
|
|
// 重新获取日期值(确保使用最新的用户输入)
|
|
const currentStartDate = document.getElementById('activeStartDate').value;
|
|
const currentEndDate = document.getElementById('activeEndDate').value;
|
|
|
|
// 构建查询参数
|
|
const fetchParams = new URLSearchParams();
|
|
if (currentStartDate) fetchParams.append('startDate', currentStartDate);
|
|
if (currentEndDate) fetchParams.append('endDate', currentEndDate);
|
|
fetchParams.append('page', page);
|
|
fetchParams.append('pageSize', pageSize);
|
|
|
|
const fetchCacheKey = 'active_stats_' + fetchParams.toString();
|
|
|
|
// 检查普通缓存
|
|
const fetchCachedData = cacheManager.get(fetchCacheKey);
|
|
if (fetchCachedData) {
|
|
console.log('使用普通缓存数据加载客户活跃统计');
|
|
// 立即显示缓存数据
|
|
updateActiveStatsUI(fetchCachedData, page, pageSize);
|
|
// 隐藏加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
// 后台加载最新数据
|
|
loadLatestActiveStats(currentStartDate, currentEndDate, page, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 从服务器获取数据
|
|
console.log('从服务器获取数据,pageSize=' + pageSize);
|
|
|
|
// 并行请求所有数据
|
|
Promise.all([
|
|
// 获取活跃统计概览
|
|
fetch('/api/active-stats?' + fetchParams.toString(), { signal })
|
|
.then(response => response.json()),
|
|
|
|
// 获取多天活跃统计
|
|
fetch('/api/daily-active-stats?' + fetchParams.toString(), { signal })
|
|
.then(response => response.json()),
|
|
|
|
// 获取总活跃时长统计
|
|
fetch('/api/total-active-duration?' + fetchParams.toString(), { signal })
|
|
.then(response => response.json())
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
// 检查版本是否匹配
|
|
if (currentVersion !== activeStatsVersion) {
|
|
console.log('过期请求,跳过状态更新');
|
|
return;
|
|
}
|
|
|
|
const combinedData = {
|
|
overview: overviewData,
|
|
daily: dailyData,
|
|
totalDuration: totalDurationData
|
|
};
|
|
|
|
// 缓存数据(设置过期时间为8分钟)
|
|
cacheManager.set(fetchCacheKey, combinedData, 480000); // 8分钟过期
|
|
|
|
// 更新永久缓存(包含页码信息)
|
|
const fetchPermanentCacheKey = 'active_stats_permanent_' + fetchParams.toString();
|
|
cacheManager.setPermanent(fetchPermanentCacheKey, combinedData);
|
|
console.log('更新永久缓存数据(包含页码信息)');
|
|
|
|
// 同时更新通用永久缓存(保持向后兼容)
|
|
if (page === 1 || !containsToday) {
|
|
cacheManager.setPermanent('activeStatsData', combinedData);
|
|
console.log('更新通用永久缓存数据');
|
|
}
|
|
|
|
// 更新 UI
|
|
updateActiveStatsUI(combinedData, page, pageSize);
|
|
|
|
// 隐藏加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
|
|
// 预加载相邻页面
|
|
if (totalDurationData.success) {
|
|
preloadAdjacentPages(page, Math.ceil(totalDurationData.data.activeUsers / pageSize), pageSize, currentStartDate, currentEndDate);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (error.name === 'AbortError') {
|
|
console.log('请求已取消');
|
|
return;
|
|
}
|
|
console.error('获取活跃统计数据失败:', error);
|
|
// 显示错误信息
|
|
const tableBody = document.querySelector('#activeUsersTable tbody');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #ff4d4f; padding: 20px;">加载失败,请重试</td></tr>';
|
|
}
|
|
// 隐藏加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
});
|
|
}
|
|
// 从API获取最新客户活跃统计数据
|
|
function loadLatestActiveStats(startDate, endDate, page = 1, pageSize = 10) {
|
|
// 检查是否包含今日数据
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const containsToday = (startDate && startDate <= today && endDate && endDate >= today);
|
|
|
|
console.log(`后台加载最新客户活跃统计数据(页码: ${page}),包含今日数据: ${containsToday}`);
|
|
|
|
// 构建查询参数
|
|
const params = new URLSearchParams();
|
|
if (startDate) params.append('startDate', startDate);
|
|
if (endDate) params.append('endDate', endDate);
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
|
|
const cacheKey = 'active_stats_' + params.toString();
|
|
|
|
// 使用当前的AbortController信号
|
|
const signal = activeStatsAbortController ? activeStatsAbortController.signal : undefined;
|
|
|
|
// 记录当前版本
|
|
const currentVersion = activeStatsVersion;
|
|
|
|
// 并行请求所有数据
|
|
Promise.all([
|
|
// 获取活跃统计概览
|
|
fetch('/api/active-stats?' + params.toString(), { signal })
|
|
.then(response => response.json()),
|
|
|
|
// 获取多天活跃统计
|
|
fetch('/api/daily-active-stats?' + params.toString(), { signal })
|
|
.then(response => response.json()),
|
|
|
|
// 获取总活跃时长统计
|
|
fetch('/api/total-active-duration?' + params.toString(), { signal })
|
|
.then(response => response.json())
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
// 检查版本是否匹配
|
|
if (currentVersion !== activeStatsVersion) {
|
|
console.log('过期请求,跳过状态更新');
|
|
return;
|
|
}
|
|
|
|
console.log('获取活跃统计数据成功:', {
|
|
overviewData: overviewData.success,
|
|
dailyData: dailyData.success,
|
|
totalDurationData: totalDurationData.success,
|
|
page: page
|
|
});
|
|
|
|
const combinedData = {
|
|
overview: overviewData,
|
|
daily: dailyData,
|
|
totalDuration: totalDurationData
|
|
};
|
|
|
|
// 缓存数据(包含页码信息的临时缓存)
|
|
const cacheResult = cacheManager.set(cacheKey, combinedData, 480000); // 8分钟过期
|
|
console.log(`临时缓存设置结果(页码: ${page}):`, cacheResult);
|
|
|
|
// 更新永久缓存(包含页码信息)
|
|
const permanentCacheKey = 'active_stats_permanent_' + params.toString();
|
|
const permanentResult = cacheManager.setPermanent(permanentCacheKey, combinedData);
|
|
console.log(`永久缓存设置结果(页码: ${page}):`, permanentResult);
|
|
|
|
// 同时更新通用永久缓存(保持向后兼容)
|
|
if (page === 1 || !containsToday) {
|
|
const generalPermanentResult = cacheManager.setPermanent('activeStatsData', combinedData);
|
|
console.log('通用永久缓存设置结果:', generalPermanentResult);
|
|
}
|
|
|
|
// 更新分页信息
|
|
if (totalDurationData.success && totalDurationData.data) {
|
|
activeTotalItems = totalDurationData.data.activeUsers || 0;
|
|
activeTotalPages = Math.ceil(activeTotalItems / pageSize);
|
|
console.log('分页信息更新:', { activeTotalItems, activeTotalPages });
|
|
}
|
|
|
|
// 更新UI
|
|
updateActiveStatsUI(combinedData, page, pageSize);
|
|
|
|
// 隐藏加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
|
|
// 预加载相邻页面
|
|
if (totalDurationData.success && totalDurationData.data) {
|
|
preloadAdjacentPages(page, activeTotalPages, pageSize, startDate, endDate);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (error.name === 'AbortError') {
|
|
console.log('请求已取消');
|
|
return;
|
|
}
|
|
console.error('获取最新活跃统计数据失败:', error);
|
|
// 即使出错,也要隐藏加载状态
|
|
updatePaginationButtons(page, activeTotalPages || 1, false);
|
|
});
|
|
}
|
|
|
|
// 预加载相邻页面
|
|
function preloadAdjacentPages(currentPage, totalPages, pageSize, startDate, endDate) {
|
|
// 检查是否包含今日数据
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const containsToday = (startDate && startDate <= today && endDate && endDate >= today);
|
|
|
|
// 如果不包含今日数据,不需要预加载
|
|
if (!containsToday) {
|
|
console.log('仅包含历史数据,无需预加载页面');
|
|
return;
|
|
}
|
|
|
|
// 预加载10页数据,确保前三页优先渲染,其他页在后台加载
|
|
const pagesToPreload = [];
|
|
|
|
// 计算预加载范围:从第1页开始,预加载10页
|
|
const startPage = 1;
|
|
const endPage = Math.min(startPage + 9, totalPages); // 确保不超过总页数
|
|
|
|
// 添加从第1页到第10页的数据
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
pagesToPreload.push(i);
|
|
}
|
|
|
|
// 去重并过滤有效页面,确保页面在合理范围内
|
|
const uniquePages = [...new Set(pagesToPreload)].filter(page => page >= 1);
|
|
|
|
// 优先处理前三页数据,确保其尽快渲染
|
|
const priorityPages = [];
|
|
const backgroundPages = [];
|
|
|
|
uniquePages.forEach(page => {
|
|
if (page <= 3) {
|
|
priorityPages.push(page);
|
|
} else {
|
|
backgroundPages.push(page);
|
|
}
|
|
});
|
|
|
|
// 立即加载前三页数据
|
|
priorityPages.forEach(page => {
|
|
loadPageData(page, pageSize, startDate, endDate);
|
|
});
|
|
|
|
// 后台异步加载其他页面数据
|
|
backgroundPages.forEach(page => {
|
|
// 使用setTimeout延迟加载,避免阻塞主线程
|
|
setTimeout(() => {
|
|
loadPageData(page, pageSize, startDate, endDate);
|
|
}, 100 * (page - 1)); // 按页码顺序延迟加载
|
|
});
|
|
}
|
|
|
|
// 加载单个页面数据的辅助函数
|
|
function loadPageData(page, pageSize, startDate, endDate) {
|
|
// 检查是否包含今日数据
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const containsToday = (startDate && startDate <= today && endDate && endDate >= today);
|
|
|
|
// 构建查询参数
|
|
const params = new URLSearchParams();
|
|
if (startDate) params.append('startDate', startDate);
|
|
if (endDate) params.append('endDate', endDate);
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
|
|
const cacheKey = 'active_stats_' + params.toString();
|
|
|
|
// 检查是否已经缓存
|
|
if (cacheManager.get(cacheKey)) {
|
|
return;
|
|
}
|
|
|
|
// 检查永久缓存是否存在对应数据
|
|
const permanentCachedData = cacheManager.getPermanent('activeStatsData');
|
|
if (permanentCachedData && !containsToday) {
|
|
// 对于历史数据,直接使用永久缓存
|
|
cacheManager.set(cacheKey, permanentCachedData);
|
|
console.log(`使用永久缓存数据预加载页面 ${page}`);
|
|
return;
|
|
}
|
|
|
|
// 如果不包含今日数据,不需要预加载
|
|
if (!containsToday) {
|
|
console.log(`仅包含历史数据,无需预加载页面 ${page}`);
|
|
return;
|
|
}
|
|
|
|
console.log('预加载页面 ' + page);
|
|
|
|
// 并行请求所有数据
|
|
Promise.all([
|
|
fetch('/api/active-stats?' + params.toString())
|
|
.then(response => response.json()),
|
|
fetch('/api/daily-active-stats?' + params.toString())
|
|
.then(response => response.json()),
|
|
fetch('/api/total-active-duration?' + params.toString())
|
|
.then(response => response.json())
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
const combinedData = {
|
|
overview: overviewData,
|
|
daily: dailyData,
|
|
totalDuration: totalDurationData
|
|
};
|
|
|
|
// 缓存数据
|
|
cacheManager.set(cacheKey, combinedData);
|
|
})
|
|
.catch(error => {
|
|
console.error('预加载页面 ' + page + ' 失败:', error);
|
|
});
|
|
}
|
|
|
|
// 更新分页按钮状态,添加加载状态
|
|
function updatePaginationButtons(page, totalPages, isLoading = false) {
|
|
const prevBtn = document.getElementById('activePrevPageBtn');
|
|
const nextBtn = document.getElementById('activeNextPageBtn');
|
|
const pageInfo = document.getElementById('activePageInfo');
|
|
|
|
// 检查元素是否存在并禁用按钮当加载中
|
|
if (prevBtn) {
|
|
prevBtn.disabled = isLoading || page === 1;
|
|
}
|
|
if (nextBtn) {
|
|
nextBtn.disabled = isLoading || page === totalPages;
|
|
}
|
|
|
|
// 更新按钮文本,显示加载状态
|
|
if (isLoading) {
|
|
if (prevBtn) {
|
|
prevBtn.innerHTML = '<span class="loading-spinner"></span> 加载中...';
|
|
}
|
|
if (nextBtn) {
|
|
nextBtn.innerHTML = '<span class="loading-spinner"></span> 加载中...';
|
|
}
|
|
} else {
|
|
if (prevBtn) {
|
|
prevBtn.innerHTML = '上一页';
|
|
}
|
|
if (nextBtn) {
|
|
nextBtn.innerHTML = '下一页';
|
|
}
|
|
}
|
|
|
|
// 更新页面信息
|
|
if (pageInfo) {
|
|
pageInfo.textContent = `第 ${page} 页 / 共 ${totalPages} 页`;
|
|
}
|
|
}
|
|
|
|
// 添加加载动画样式
|
|
function addLoadingStyles() {
|
|
// 检查是否已经添加了样式
|
|
if (!document.getElementById('loading-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'loading-styles';
|
|
style.textContent = `
|
|
.loading-spinner {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 50%;
|
|
border-top-color: #fff;
|
|
animation: spin 1s ease-in-out infinite;
|
|
margin-right: 8px;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
#activePrevPageBtn:disabled,
|
|
#activeNextPageBtn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
// 虚拟滚动实现
|
|
function initVirtualScroll(containerId, data, rowHeight = 80) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '';
|
|
|
|
const totalHeight = data.length * rowHeight;
|
|
const viewportHeight = container.clientHeight;
|
|
|
|
let startIndex = 0;
|
|
let endIndex = Math.min(Math.ceil(viewportHeight / rowHeight) + 1, data.length);
|
|
|
|
const content = document.createElement('div');
|
|
content.style.height = `${totalHeight}px`;
|
|
content.style.position = 'relative';
|
|
|
|
const visibleContent = document.createElement('div');
|
|
visibleContent.style.position = 'absolute';
|
|
visibleContent.style.top = '0';
|
|
visibleContent.style.left = '0';
|
|
visibleContent.style.right = '0';
|
|
|
|
container.appendChild(content);
|
|
content.appendChild(visibleContent);
|
|
|
|
function renderVisibleRows() {
|
|
visibleContent.innerHTML = '';
|
|
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
const item = data[i];
|
|
const row = document.createElement('div');
|
|
row.className = 'virtual-row';
|
|
row.style.transform = `translateY(${i * rowHeight}px)`;
|
|
row.style.position = 'absolute';
|
|
row.style.top = '0';
|
|
row.style.left = '0';
|
|
row.style.right = '0';
|
|
row.style.height = `${rowHeight}px`;
|
|
row.onclick = () => showMultiDayActiveDetails(item.userId);
|
|
|
|
// 格式化时间
|
|
const formatTime = (timeStr) => {
|
|
if (!timeStr) return '暂无';
|
|
const date = new Date(timeStr);
|
|
if (isNaN(date.getTime())) return '暂无';
|
|
return date.toLocaleString('zh-CN');
|
|
};
|
|
|
|
// 解析最后浏览商品
|
|
let lastViewedProduct = '暂无';
|
|
try {
|
|
if (item.lastViewedProduct) {
|
|
let productData = item.lastViewedProduct;
|
|
// 检查是否是字符串,如果是则尝试解析
|
|
if (typeof productData === 'string') {
|
|
try {
|
|
productData = JSON.parse(productData);
|
|
} catch (jsonError) {
|
|
productData = { name: productData };
|
|
}
|
|
}
|
|
const productName = productData.productName || productData.name || '未知商品';
|
|
lastViewedProduct = productName;
|
|
}
|
|
} catch (e) {
|
|
lastViewedProduct = '解析失败';
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<div class="virtual-row-cell">${(startIndex + i + 1)}</div>
|
|
<div class="virtual-row-cell">${item.phoneNumber || '暂无'}</div>
|
|
<div class="virtual-row-cell">${formatTime(item.registerTime)}</div>
|
|
<div class="virtual-row-cell">${(item.duration / 60).toFixed(2)}</div>
|
|
<div class="virtual-row-cell">${formatTime(item.lastActiveTime)}</div>
|
|
<div class="virtual-row-cell">${lastViewedProduct}</div>
|
|
`;
|
|
|
|
visibleContent.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function updateVisibleRows() {
|
|
const scrollTop = container.scrollTop;
|
|
startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - 1);
|
|
endIndex = Math.min(data.length, Math.ceil((scrollTop + viewportHeight) / rowHeight) + 1);
|
|
renderVisibleRows();
|
|
}
|
|
|
|
container.addEventListener('scroll', updateVisibleRows);
|
|
renderVisibleRows();
|
|
}
|
|
|
|
// Web Worker实现
|
|
function createWorker() {
|
|
const workerCode = `
|
|
self.onmessage = function(e) {
|
|
const { task, data } = e.data;
|
|
|
|
switch (task) {
|
|
case 'calculateActiveDuration':
|
|
const totalDuration = calculateActiveDuration(data);
|
|
self.postMessage({ task, result: totalDuration });
|
|
break;
|
|
case 'batchParseJSON':
|
|
const parsedData = batchParseJSON(data);
|
|
self.postMessage({ task, result: parsedData });
|
|
break;
|
|
case 'sortData':
|
|
const sortedData = sortData(data.items, data.key, data.order);
|
|
self.postMessage({ task, result: sortedData });
|
|
break;
|
|
}
|
|
};
|
|
|
|
function calculateActiveDuration(operations) {
|
|
let totalDuration = 0;
|
|
let currentSessionStart = null;
|
|
let lastAction = null;
|
|
let firstOperationTime = null;
|
|
let lastOperationTime = null;
|
|
|
|
for (const op of operations) {
|
|
const operationTime = new Date(op.operationTime);
|
|
const action = op.originalData ? JSON.parse(op.originalData).action : null;
|
|
|
|
if (!firstOperationTime || operationTime < firstOperationTime) {
|
|
firstOperationTime = operationTime;
|
|
}
|
|
if (!lastOperationTime || operationTime > lastOperationTime) {
|
|
lastOperationTime = operationTime;
|
|
}
|
|
|
|
if (action === 'app_hide' && op.originalData && op.originalData.sessionDuration) {
|
|
const durationInSeconds = op.originalData.sessionDuration / 1000;
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
if (action === 'app_show') {
|
|
currentSessionStart = operationTime;
|
|
lastAction = 'app_show';
|
|
} else if (action === 'app_hide') {
|
|
currentSessionStart = null;
|
|
lastAction = 'app_hide';
|
|
} else if (action) {
|
|
lastAction = action;
|
|
}
|
|
}
|
|
|
|
if (currentSessionStart && lastAction === 'app_show') {
|
|
const now = new Date();
|
|
const maxDurationInSeconds = 5 * 60; // 限制为5分钟
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, (now - currentSessionStart) / 1000);
|
|
totalDuration += durationInSeconds;
|
|
}
|
|
|
|
if (totalDuration === 0 && firstOperationTime && lastOperationTime) {
|
|
const maxDurationInSeconds = 5 * 60;
|
|
const durationInSeconds = Math.min(maxDurationInSeconds, Math.max(30, (lastOperationTime - firstOperationTime) / 1000));
|
|
totalDuration = durationInSeconds;
|
|
}
|
|
|
|
const maxUserDurationInSeconds = 24 * 60 * 60;
|
|
return Math.min(maxUserDurationInSeconds, totalDuration);
|
|
}
|
|
|
|
function batchParseJSON(operations) {
|
|
return operations.map(op => {
|
|
try {
|
|
if (typeof op.originalData === 'string') {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: JSON.parse(op.originalData)
|
|
};
|
|
} else {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: op.originalData || {}
|
|
};
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
operationTime: new Date(op.operationTime),
|
|
originalData: {}
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function sortData(items, key, order) {
|
|
return [...items].sort((a, b) => {
|
|
if (order === 'desc') {
|
|
return b[key] - a[key];
|
|
} else {
|
|
return a[key] - b[key];
|
|
}
|
|
});
|
|
}
|
|
`;
|
|
|
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
const workerUrl = URL.createObjectURL(blob);
|
|
return new Worker(workerUrl);
|
|
}
|
|
|
|
// 初始化Web Worker
|
|
function initWorker() {
|
|
if (window.Worker) {
|
|
try {
|
|
worker = createWorker();
|
|
worker.onmessage = function(e) {
|
|
const { task, result } = e.data;
|
|
if (task === 'calculateActiveDuration') {
|
|
console.log('Active duration calculated:', result);
|
|
} else if (task === 'batchParseJSON') {
|
|
console.log('JSON parsed:', result.length, 'items');
|
|
} else if (task === 'sortData') {
|
|
console.log('Data sorted:', result.length, 'items');
|
|
}
|
|
};
|
|
worker.onerror = function(error) {
|
|
console.error('Worker error:', error);
|
|
// 发生错误时销毁worker,防止内存泄漏
|
|
worker.terminate();
|
|
worker = null;
|
|
};
|
|
} catch (error) {
|
|
console.error('Worker initialization error:', error);
|
|
worker = null;
|
|
}
|
|
} else {
|
|
console.log('Web Worker not supported, using fallback implementation');
|
|
}
|
|
}
|
|
|
|
// 使用Web Worker计算活跃时长
|
|
function calculateActiveDurationWithWorker(operations) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!worker) {
|
|
// 降级处理
|
|
const result = calculateActiveDuration(operations);
|
|
resolve(result);
|
|
return;
|
|
}
|
|
|
|
worker.onmessage = function(e) {
|
|
if (e.data.task === 'calculateActiveDuration') {
|
|
resolve(e.data.result);
|
|
}
|
|
};
|
|
|
|
worker.postMessage({ task: 'calculateActiveDuration', data: operations });
|
|
});
|
|
}
|
|
|
|
// 初始化加载样式
|
|
addLoadingStyles();
|
|
|
|
// 初始化Web Worker
|
|
initWorker();
|
|
|
|
// 导航按钮点击状态
|
|
|
|
|
|
// 上一页函数
|
|
function goToPrevPage() {
|
|
if (supplierCurrentPage > 1) {
|
|
supplierCurrentPage--;
|
|
loadSuppliers();
|
|
}
|
|
}
|
|
|
|
// 下一页函数
|
|
function goToNextPage() {
|
|
const totalPages = Math.ceil(supplierTotalCount / supplierPageSize);
|
|
if (supplierCurrentPage < totalPages) {
|
|
supplierCurrentPage++;
|
|
loadSuppliers();
|
|
}
|
|
}
|
|
|
|
// 更新分页状态
|
|
function updatePaginationStatus() {
|
|
const prevBtn = document.getElementById('supplierPrevPageBtn');
|
|
const nextBtn = document.getElementById('supplierNextPageBtn');
|
|
const currentPageEl = document.getElementById('currentPage');
|
|
const supplierCountEl = document.querySelector('.supplier-count');
|
|
|
|
if (prevBtn) prevBtn.disabled = supplierCurrentPage === 1;
|
|
if (nextBtn) nextBtn.disabled = supplierCurrentPage >= Math.ceil(supplierTotalCount / supplierPageSize);
|
|
if (currentPageEl) currentPageEl.textContent = supplierCurrentPage;
|
|
if (supplierCountEl) supplierCountEl.textContent = `(共 ${supplierTotalCount} 条)`;
|
|
}
|
|
|
|
// 切换活跃客户排名页码
|
|
function changeActiveClientPage(direction) {
|
|
// 防止频繁点击
|
|
if (isNavigating) {
|
|
return;
|
|
}
|
|
|
|
const newPage = activeCurrentPage + direction;
|
|
if (newPage >= 1 && newPage <= activeTotalPages) {
|
|
// 设置导航状态为正在导航
|
|
isNavigating = true;
|
|
|
|
// 执行页面导航
|
|
refreshActiveStats(newPage, activePageSize);
|
|
|
|
// 延迟一段时间后恢复导航状态
|
|
setTimeout(() => {
|
|
isNavigating = false;
|
|
}, NAVIGATION_DELAY);
|
|
}
|
|
}
|
|
|
|
// 更改活跃客户排名每页大小
|
|
function changeActiveClientPageSize() {
|
|
const newPageSize = parseInt(document.getElementById('activePageSizeInput').value);
|
|
if (newPageSize >= 1 && newPageSize <= 100) {
|
|
activePageSize = newPageSize;
|
|
refreshActiveStats(1, activePageSize);
|
|
}
|
|
}
|
|
|
|
// 更新活跃统计UI
|
|
function updateActiveStatsUI(data, page, pageSize) {
|
|
// 检查是否在活跃统计页面
|
|
const isActiveStatsPage = document.getElementById('activeUsersContainer') !== null;
|
|
|
|
// 如果不在活跃统计页面,只更新缓存,不更新UI
|
|
if (!isActiveStatsPage) {
|
|
console.log('不在活跃统计页面,跳过UI更新');
|
|
return;
|
|
}
|
|
|
|
// 更新概览数据
|
|
if (data.overview && data.overview.success) {
|
|
const totalActiveClientsEl = document.getElementById('totalActiveClients');
|
|
const weekActiveClientsEl = document.getElementById('weekActiveClients');
|
|
const totalActiveDurationEl = document.getElementById('totalActiveDuration');
|
|
|
|
if (totalActiveClientsEl) {
|
|
totalActiveClientsEl.innerHTML = '';
|
|
totalActiveClientsEl.textContent = data.overview.data.totalActive;
|
|
}
|
|
if (weekActiveClientsEl) {
|
|
weekActiveClientsEl.innerHTML = '';
|
|
weekActiveClientsEl.textContent = data.overview.data.weekActive;
|
|
}
|
|
if (totalActiveDurationEl) {
|
|
totalActiveDurationEl.innerHTML = '';
|
|
const totalDuration = parseFloat(data.overview.data.totalDuration) || 0;
|
|
totalActiveDurationEl.textContent = totalDuration.toFixed(2);
|
|
}
|
|
}
|
|
|
|
// 更新图表数据
|
|
if (data.daily && data.daily.success) {
|
|
const labels = data.daily.data.dailyStats.map(stat => {
|
|
return new Date(stat.date).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
});
|
|
const activeUsers = data.daily.data.dailyStats.map(stat => stat.activeUsers);
|
|
const totalDuration = data.daily.data.dailyStats.map(stat => {
|
|
const duration = parseFloat(stat.totalDuration) || 0;
|
|
return duration.toFixed(2);
|
|
});
|
|
|
|
activeChart.data.labels = labels;
|
|
activeChart.data.datasets[0].data = activeUsers;
|
|
activeChart.data.datasets[1].data = totalDuration;
|
|
activeChart.update();
|
|
}
|
|
|
|
// 更新总活跃时长统计和排名
|
|
if (data.totalDuration && data.totalDuration.success) {
|
|
const totalDurationStats = document.getElementById('totalDurationStats');
|
|
if (totalDurationStats) {
|
|
totalDurationStats.innerHTML = `
|
|
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
|
<div style="flex: 1; min-width: 200px;">
|
|
<h3 style="font-size: 1rem; margin-bottom: 8px;">总活跃时长</h3>
|
|
<p style="font-size: 1.5rem; font-weight: bold; color: #367ee9;">${(parseFloat(data.totalDuration.data.totalDuration) || 0).toFixed(2)} 小时</p>
|
|
</div>
|
|
<div style="flex: 1; min-width: 200px;">
|
|
<h3 style="font-size: 1rem; margin-bottom: 8px;">活跃客户数</h3>
|
|
<p style="font-size: 1.5rem; font-weight: bold; color: #367ee9;">${data.totalDuration.data.activeUsers} 个</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 更新分页数据
|
|
activeCurrentPage = page;
|
|
activePageSize = pageSize;
|
|
activeTotalItems = data.totalDuration.data.activeUsers;
|
|
activeTotalPages = Math.ceil(data.totalDuration.data.activeUsers / pageSize);
|
|
|
|
// 更新分页控件状态
|
|
const prevBtn = document.getElementById('activePrevPageBtn');
|
|
const nextBtn = document.getElementById('activeNextPageBtn');
|
|
const pageInfo = document.getElementById('activePageInfo');
|
|
const pageSizeInput = document.getElementById('activePageSizeInput');
|
|
|
|
if (prevBtn) prevBtn.disabled = activeCurrentPage === 1;
|
|
if (nextBtn) nextBtn.disabled = activeCurrentPage === activeTotalPages;
|
|
if (pageInfo) pageInfo.textContent = `第 ${activeCurrentPage} 页 / 共 ${activeTotalPages} 页 (共 ${activeTotalItems} 条)`;
|
|
|
|
// 更新每页大小输入框的值
|
|
if (pageSizeInput) {
|
|
pageSizeInput.value = activePageSize;
|
|
}
|
|
|
|
// 更新活跃客户排名
|
|
const container = document.getElementById('activeUsersContainer');
|
|
if (container) {
|
|
if (data.totalDuration.data.topActiveUsers && data.totalDuration.data.topActiveUsers.length > 0) {
|
|
console.log('topActiveUsers数组长度:', data.totalDuration.data.topActiveUsers.length);
|
|
console.log('topActiveUsers数组内容:', data.totalDuration.data.topActiveUsers);
|
|
|
|
// 创建表格结构
|
|
const tableHtml = `
|
|
<table id="activeUsersTable" style="width: 100%; border-collapse: collapse;">
|
|
<thead>
|
|
<tr style="background-color: #f8fafc; border-bottom: 1px solid #e8e8e8;">
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">排名</th>
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">电话号码</th>
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">注册时间</th>
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">活跃时长(分钟)</th>
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">最后活跃时间</th>
|
|
<th style="padding: 12px; text-align: left; font-weight: 600;">最后浏览商品</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.totalDuration.data.topActiveUsers.map((user, index) => {
|
|
// 计算实际排名(考虑分页)
|
|
const actualRank = (activeCurrentPage - 1) * activePageSize + index + 1;
|
|
|
|
// 解析最后浏览商品
|
|
let lastViewedProduct = '暂无';
|
|
|
|
try {
|
|
if (user.lastViewedProduct) {
|
|
let productData = user.lastViewedProduct;
|
|
|
|
// 检查是否是字符串,如果是则尝试解析
|
|
if (typeof productData === 'string') {
|
|
try {
|
|
productData = JSON.parse(productData);
|
|
} catch (jsonError) {
|
|
// 如果解析失败,直接使用原数据(可能是字符串)
|
|
productData = { name: productData };
|
|
}
|
|
}
|
|
|
|
// 尝试从不同字段获取商品信息,确保兼容性
|
|
const productId = productData.productId || productData.id ||
|
|
(typeof productData === 'string' ? user.lastViewedProduct : '未知ID');
|
|
const productName = productData.productName || productData.name ||
|
|
(typeof productData === 'string' ? productData : '未知商品');
|
|
const price = productData.price || productData.priceRange || '未知价格';
|
|
const productType = productData.category || productData.type || '未知类型';
|
|
const specification = productData.specification ||
|
|
productData.displaySpecification ||
|
|
productData.specs || '未知规格';
|
|
const quantity = productData.quantity || productData.count || '未知';
|
|
const productContact = productData.productContact || productData.contact || '未知';
|
|
const contactPhone = productData.contactPhone || productData.phone || '未知';
|
|
const creatorNickName = productData.creatorNickName || productData.creator || '未知';
|
|
const creatorPhone = productData.creatorPhone || productData.creatorContact || '未知';
|
|
|
|
// 格式化创建时间,尝试从多个字段获取
|
|
let createTime = '未知';
|
|
const possibleCreateTimeFields = ['createdAt', 'createTime', 'productCreatedAt', 'created_at'];
|
|
for (const field of possibleCreateTimeFields) {
|
|
if (productData[field]) {
|
|
createTime = new Date(productData[field]).toLocaleString('zh-CN');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 整合商品信息,按照要求的顺序排列
|
|
lastViewedProduct = `${productName}<br>ID: ${productId}<br>价格: ${price} 类型: ${productType}<br>规格: ${specification} 件数: ${quantity}<br>商品联系人: ${productContact} ${contactPhone}<br>商品创建人: ${creatorNickName} ${creatorPhone}<br>创建时间: ${createTime}`;
|
|
}
|
|
} catch (e) {
|
|
console.error('解析商品信息失败:', e);
|
|
// 如果发生错误,显示原始数据的前100个字符
|
|
lastViewedProduct = `解析失败: ${user.lastViewedProduct.substring(0, 100)}${user.lastViewedProduct.length > 100 ? '...' : ''}`;
|
|
}
|
|
|
|
// 格式化时间
|
|
const formatTime = (timeStr) => {
|
|
if (!timeStr) return '暂无';
|
|
const date = new Date(timeStr);
|
|
if (isNaN(date.getTime())) return '暂无';
|
|
return date.toLocaleString();
|
|
};
|
|
|
|
return `
|
|
<tr onclick="showMultiDayActiveDetails('${user.userId}')" style="cursor: pointer; transition: background-color 0.2s; border-bottom: 1px solid #f0f0f0;" onmouseover="this.style.backgroundColor='#f8fafc'" onmouseout="this.style.backgroundColor='transparent'">
|
|
<td style="padding: 12px;">${actualRank}</td>
|
|
<td style="padding: 12px;">${user.phoneNumber || '暂无'}</td>
|
|
<td style="padding: 12px;">${formatTime(user.registerTime)}</td>
|
|
<td style="padding: 12px;">${(parseFloat(user.duration || 0) / 60).toFixed(2)}</td>
|
|
<td style="padding: 12px;">${formatTime(user.lastActiveTime)}</td>
|
|
<td style="padding: 12px; white-space: pre-line; line-height: 1.4;">${lastViewedProduct}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
container.innerHTML = tableHtml;
|
|
console.log('渲染完成,表格应该显示', data.totalDuration.data.topActiveUsers.length, '条记录');
|
|
} else {
|
|
container.innerHTML = '<div style="text-align: center; color: #666; padding: 40px;">暂无数据</div>';
|
|
console.log('topActiveUsers数组为空');
|
|
}
|
|
} else {
|
|
console.log('activeUsersContainer元素不存在');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 弹窗样式
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
/* 弹窗背景 */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
/* 商品浏览记录弹窗背景 - 更高的z-index */
|
|
.modal-overlay.product-modal {
|
|
z-index: 1010;
|
|
}
|
|
|
|
/* 弹窗内容 */
|
|
.modal-content {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
width: 90%;
|
|
max-width: 1000px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
/* 弹窗头部 - 固定在顶部 */
|
|
.modal-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: white;
|
|
z-index: 10;
|
|
}
|
|
|
|
/* 弹窗主体 - 可滚动 */
|
|
.modal-body {
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.modal-title {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.close-btn {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
color: #999;
|
|
}
|
|
|
|
.close-btn:hover {
|
|
color: #333;
|
|
}
|
|
|
|
/* 弹窗主体 */
|
|
.modal-body {
|
|
padding: 16px;
|
|
}
|
|
|
|
/* 商品卡片容器 */
|
|
.products-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
/* 商品卡片 */
|
|
.product-card {
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.product-card:hover {
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* 商品图片 */
|
|
.product-image {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
border-radius: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* 商品信息 */
|
|
.product-info {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.product-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
color: #333;
|
|
}
|
|
|
|
.product-details {
|
|
font-size: 14px;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* 浏览时间 */
|
|
.view-time {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 8px;
|
|
text-align: right;
|
|
}
|
|
|
|
/* 无数据提示 */
|
|
.no-data {
|
|
text-align: center;
|
|
color: #999;
|
|
padding: 40px 0;
|
|
grid-column: 1 / -1;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// 创建多天活跃详情弹窗元素
|
|
const activeStatsModal = document.createElement('div');
|
|
activeStatsModal.className = 'modal-overlay';
|
|
activeStatsModal.style.display = 'none';
|
|
activeStatsModal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">客户多天活跃详情</h3>
|
|
<button class="close-btn" onclick="closeActiveStatsModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="no-data">加载中...</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(activeStatsModal);
|
|
|
|
// 创建商品浏览记录弹窗元素
|
|
const productModal = document.createElement('div');
|
|
productModal.className = 'modal-overlay product-modal';
|
|
productModal.style.display = 'none';
|
|
productModal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">浏览商品记录</h3>
|
|
<button class="close-btn" onclick="closeProductModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="products-container" id="dayProductsContainer">
|
|
<div class="no-data">加载中...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(productModal);
|
|
|
|
// 显示多天活跃详情弹窗
|
|
function openActiveStatsModal() {
|
|
activeStatsModal.style.display = 'flex';
|
|
}
|
|
|
|
// 关闭多天活跃详情弹窗
|
|
function closeActiveStatsModal() {
|
|
activeStatsModal.style.display = 'none';
|
|
}
|
|
|
|
// 显示商品浏览记录弹窗
|
|
function openProductModal() {
|
|
productModal.style.display = 'flex';
|
|
}
|
|
|
|
// 关闭商品浏览记录弹窗
|
|
function closeProductModal() {
|
|
productModal.style.display = 'none';
|
|
}
|
|
|
|
// 显示多天活跃详情弹窗
|
|
function showMultiDayActiveDetails(userId) {
|
|
console.log(`[${new Date().toISOString()}] 🟢 showMultiDayActiveDetails 函数被调用`);
|
|
console.log(`[${new Date().toISOString()}] 📋 输入参数: userId=${userId}`);
|
|
|
|
const startDate = document.getElementById('activeStartDate').value;
|
|
const endDate = document.getElementById('activeEndDate').value;
|
|
|
|
console.log(`[${new Date().toISOString()}] 📅 当前活跃统计的时间范围: startDate=${startDate}, endDate=${endDate}`);
|
|
|
|
// 构建查询参数
|
|
const params = new URLSearchParams();
|
|
if (startDate) {
|
|
params.append('startDate', startDate);
|
|
console.log(`[${new Date().toISOString()}] 🔗 添加startDate到查询参数: ${startDate}`);
|
|
}
|
|
if (endDate) {
|
|
params.append('endDate', endDate);
|
|
console.log(`[${new Date().toISOString()}] 🔗 添加endDate到查询参数: ${endDate}`);
|
|
}
|
|
|
|
// 重新设置弹窗HTML结构,确保只显示多天活跃详情
|
|
console.log(`[${new Date().toISOString()}] 🎨 更新多天活跃详情弹窗HTML结构`);
|
|
activeStatsModal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">客户多天活跃详情</h3>
|
|
<button class="close-btn" onclick="closeActiveStatsModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="no-data">加载中...</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 显示弹窗
|
|
console.log(`[${new Date().toISOString()}] 🖼️ 打开多天活跃详情弹窗`);
|
|
openActiveStatsModal();
|
|
|
|
// 调用API获取数据
|
|
const apiUrl = `/api/active-stats/${userId}?${params.toString()}`;
|
|
console.log(`[${new Date().toISOString()}] 📡 调用API获取多天活跃详情: ${apiUrl}`);
|
|
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
console.log(`[${new Date().toISOString()}] 📥 收到API响应,状态码: ${response.status}`);
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log(`[${new Date().toISOString()}] 📊 解析API响应:`, data);
|
|
|
|
if (data.success) {
|
|
console.log(`[${new Date().toISOString()}] ✅ API请求成功,获取到${data.data.dailyStats?.length || 0}天的活跃数据`);
|
|
|
|
// 生成多天活跃统计表格
|
|
const dailyStatsHtml = `
|
|
<div style="margin-top: 20px;">
|
|
<h4 style="margin-bottom: 12px;">多天活跃统计</h4>
|
|
<table style="width: 100%; border-collapse: collapse; margin-top: 8px;">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #e2e8f0;">日期</th>
|
|
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #e2e8f0;">收藏数</th>
|
|
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #e2e8f0;">活跃时长 (分钟)</th>
|
|
<th style="text-align: left; padding: 8px; border-bottom: 1px solid #e2e8f0;">浏览商品数</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.data.dailyStats.length > 0 ? data.data.dailyStats.map(stat => {
|
|
console.log(`[${new Date().toISOString()}] 📋 日期${stat.date}的活跃数据: 操作次数=${stat.operationCount}, 活跃时长=${stat.duration}秒, 浏览商品数=${stat.productCount || 0}`);
|
|
// 创建一个函数来安全地格式化日期,避免时区偏移
|
|
const formatDateToYYYYMMDD = (dateStr) => {
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return dateStr;
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
const safeDate = formatDateToYYYYMMDD(stat.date);
|
|
return `
|
|
<tr style="cursor: pointer; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#f0f4f8'" onmouseout="this.style.backgroundColor='transparent'" onclick="showDayProducts('${data.data.userId}', '${safeDate}')">
|
|
<td style="padding: 8px; border-bottom: 1px solid #f1f5f9; color: #367ee9;">${safeDate}</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #f1f5f9;">${stat.favoriteCount || 0}</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #f1f5f9;">${(parseFloat(stat.duration) / 60).toFixed(2)}</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #f1f5f9;">${stat.productCount || 0}</td>
|
|
</tr>
|
|
`;}).join('') : '<tr><td colspan="4" style="text-align: center; color: #666; padding: 16px;">暂无数据</td></tr>'}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
// 更新弹窗内容
|
|
const modalBody = activeStatsModal.querySelector('.modal-body');
|
|
modalBody.innerHTML = `
|
|
<div style="background-color: #f8fafc; border-radius: 8px; padding: 16px;">
|
|
<h3 style="margin-bottom: 16px;">客户ID: ${data.data.userId} 多天活跃详情</h3>
|
|
|
|
${dailyStatsHtml}
|
|
</div>
|
|
`;
|
|
|
|
console.log(`[${new Date().toISOString()}] 🎨 多天活跃详情弹窗内容更新完成`);
|
|
} else {
|
|
console.error(`[${new Date().toISOString()}] ❌ API返回失败: ${data.message}`);
|
|
const modalBody = activeStatsModal.querySelector('.modal-body');
|
|
modalBody.innerHTML = `<div class="no-data">获取数据失败: ${data.message}</div>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`[${new Date().toISOString()}] 💥 API请求异常:`, error);
|
|
const modalBody = activeStatsModal.querySelector('.modal-body');
|
|
modalBody.innerHTML = '<div class="no-data">获取数据失败,请稍后重试</div>';
|
|
});
|
|
}
|
|
|
|
// 获取某一天的浏览商品记录
|
|
function showDayProducts(userId, date) {
|
|
console.log(`[${new Date().toISOString()}] 🟢 showDayProducts 函数被调用`);
|
|
console.log(`[${new Date().toISOString()}] 📋 输入参数: userId=${userId}, date=${date}, dateType=${typeof date}`);
|
|
|
|
// 确保日期格式为YYYY-MM-DD
|
|
let formattedDate = date;
|
|
if (date instanceof Date) {
|
|
console.log(`[${new Date().toISOString()}] 🔄 日期是Date对象,转换为YYYY-MM-DD格式`);
|
|
// 使用本地时间转换,避免时区偏移
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
formattedDate = `${year}-${month}-${day}`;
|
|
} else if (typeof date === 'string') {
|
|
console.log(`[${new Date().toISOString()}] 🔍 检查日期字符串格式: ${date}`);
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
console.log(`[${new Date().toISOString()}] ✅ 日期已是YYYY-MM-DD格式,直接使用: ${date}`);
|
|
formattedDate = date;
|
|
} else {
|
|
console.log(`[${new Date().toISOString()}] 🔄 日期格式需要转换,尝试解析: ${date}`);
|
|
const dateObj = new Date(date);
|
|
if (!isNaN(dateObj.getTime())) {
|
|
// 使用本地时间转换,避免时区偏移
|
|
const year = dateObj.getFullYear();
|
|
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
|
formattedDate = `${year}-${month}-${day}`;
|
|
console.log(`[${new Date().toISOString()}] ✅ 日期转换成功: ${date} -> ${formattedDate}`);
|
|
} else {
|
|
console.error(`[${new Date().toISOString()}] ❌ 日期转换失败: ${date}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`[${new Date().toISOString()}] 📅 最终使用的日期: ${formattedDate}`);
|
|
|
|
// 重新设置弹窗HTML结构,确保只显示商品浏览记录
|
|
console.log(`[${new Date().toISOString()}] 🎨 更新商品弹窗HTML结构,标题日期: ${formattedDate}`);
|
|
productModal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">${formattedDate} 浏览商品记录</h3>
|
|
<button class="close-btn" onclick="closeProductModal()">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="products-container" id="dayProductsContainer">
|
|
<div class="no-data">加载中...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 显示弹窗
|
|
console.log(`[${new Date().toISOString()}] 🖼️ 打开商品浏览记录弹窗`);
|
|
openProductModal();
|
|
|
|
// 调用API获取数据
|
|
const apiUrl = `/api/user-day-products/${userId}?date=${formattedDate}`;
|
|
console.log(`[${new Date().toISOString()}] 📡 调用API获取商品记录: ${apiUrl}`);
|
|
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
console.log(`[${new Date().toISOString()}] 📥 收到API响应,状态码: ${response.status}`);
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log(`[${new Date().toISOString()}] 📊 解析API响应:`, data);
|
|
const productsContainer = document.getElementById('dayProductsContainer');
|
|
|
|
if (data.success) {
|
|
console.log(`[${new Date().toISOString()}] ✅ API请求成功,商品数量: ${data.data.products?.length || 0}`);
|
|
|
|
if (data.data.products && data.data.products.length > 0) {
|
|
console.log(`[${new Date().toISOString()}] 🎨 渲染商品卡片,共 ${data.data.products.length} 个商品`);
|
|
// 商品分组函数:根据商品ID去重,统计浏览次数和时间
|
|
const groupProductsByID = (products) => {
|
|
const productMap = new Map();
|
|
|
|
products.forEach((product) => {
|
|
// 解析商品数据获取商品ID
|
|
let productId = '';
|
|
try {
|
|
const productData = JSON.parse(product.originalData);
|
|
productId = product.productId || productData.productId || productData.id || '未知ID';
|
|
} catch (e) {
|
|
productId = product.productId || '未知ID';
|
|
}
|
|
|
|
if (productMap.has(productId)) {
|
|
// 如果商品已存在,添加浏览记录
|
|
const existingProduct = productMap.get(productId);
|
|
existingProduct.viewCount += 1;
|
|
existingProduct.viewTimes.push(product.operationTime);
|
|
} else {
|
|
// 如果商品不存在,创建新的商品记录
|
|
productMap.set(productId, {
|
|
...product,
|
|
viewCount: 1,
|
|
viewTimes: [product.operationTime]
|
|
});
|
|
}
|
|
});
|
|
|
|
// 将Map转换为数组并返回
|
|
return Array.from(productMap.values());
|
|
};
|
|
|
|
// 筛选收藏商品和普通浏览商品
|
|
const favoriteProducts = [];
|
|
const regularProducts = [];
|
|
|
|
data.data.products.forEach((product, index) => {
|
|
try {
|
|
const productData = JSON.parse(product.originalData);
|
|
if (productData.action === 'add_favorite') {
|
|
favoriteProducts.push(product);
|
|
} else {
|
|
regularProducts.push(product);
|
|
}
|
|
} catch (e) {
|
|
// 如果解析失败,默认视为普通浏览商品
|
|
regularProducts.push(product);
|
|
}
|
|
});
|
|
|
|
// 对商品进行分组去重
|
|
const groupedFavoriteProducts = groupProductsByID(favoriteProducts);
|
|
const groupedRegularProducts = groupProductsByID(regularProducts);
|
|
|
|
// 渲染商品卡片函数
|
|
const renderProductCard = (product) => {
|
|
// 解析商品数据
|
|
let productData = {};
|
|
try {
|
|
productData = JSON.parse(product.originalData);
|
|
console.log(`[${new Date().toISOString()}] 📦 解析商品数据成功`);
|
|
} catch (e) {
|
|
console.error(`[${new Date().toISOString()}] ❌ 解析商品数据失败:`, e);
|
|
}
|
|
|
|
// 优先使用后端从产品表中查询到的完整信息,只有当这些信息不存在时,才从originalData中提取
|
|
const productId = product.productId || productData.productId || productData.id || '未知ID';
|
|
const productName = product.productName || productData.productName || productData.name || '未知商品';
|
|
const price = product.price || productData.price || '未知价格';
|
|
const productType = product.category || productData.category || '未知类型';
|
|
const specification = product.specification || productData.specification || productData.displaySpecification || '未知规格';
|
|
|
|
// 处理媒体URL(图片或视频)
|
|
let mediaUrl = '';
|
|
let mediaType = 'image'; // 默认是图片
|
|
|
|
// 优先处理后端返回的mediaUrls
|
|
if (product.imageUrls) {
|
|
try {
|
|
const mediaUrls = JSON.parse(product.imageUrls);
|
|
if (mediaUrls && mediaUrls.length > 0) {
|
|
mediaUrl = mediaUrls[0];
|
|
// 判断媒体类型
|
|
const urlLower = mediaUrl.toLowerCase();
|
|
if (urlLower.endsWith('.mp4') || urlLower.endsWith('.avi') || urlLower.endsWith('.mov') || urlLower.endsWith('.wmv')) {
|
|
mediaType = 'video';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(`解析媒体URL失败:`, e);
|
|
}
|
|
} else if (productData.imageUrls && productData.imageUrls.length > 0) {
|
|
mediaUrl = productData.imageUrls[0];
|
|
// 判断媒体类型
|
|
const urlLower = mediaUrl.toLowerCase();
|
|
if (urlLower.endsWith('.mp4') || urlLower.endsWith('.avi') || urlLower.endsWith('.mov') || urlLower.endsWith('.wmv')) {
|
|
mediaType = 'video';
|
|
}
|
|
}
|
|
|
|
// 格式化创建时间,优先使用产品表中的创建时间,其次使用操作时间
|
|
const createTime = product.productCreatedAt ? new Date(product.productCreatedAt).toLocaleString('zh-CN') : (product.createdAt ? new Date(product.createdAt).toLocaleString('zh-CN') : '未知');
|
|
|
|
// 格式化所有浏览时间,按时间倒序排列
|
|
const formattedViewTimes = product.viewTimes
|
|
.map(time => new Date(time).toLocaleString('zh-CN'))
|
|
.sort((a, b) => new Date(b) - new Date(a));
|
|
|
|
console.log(`[${new Date().toISOString()}] 📋 商品信息: ID=${productId}, 名称=${productName}, 浏览次数=${product.viewCount}`);
|
|
|
|
// 根据媒体类型生成对应的HTML
|
|
let mediaHtml = '';
|
|
if (mediaUrl) {
|
|
if (mediaType === 'video') {
|
|
// 视频媒体,默认静音
|
|
mediaHtml = `<video src="${mediaUrl}" alt="${productName}" class="product-media" controls muted style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px; margin-bottom: 12px;"></video>`;
|
|
} else {
|
|
// 图片媒体
|
|
mediaHtml = `<img src="${mediaUrl}" alt="${productName}" class="product-media" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px; margin-bottom: 12px;">`;
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div class="product-card">
|
|
${mediaHtml}
|
|
<div class="product-info">
|
|
<div class="product-name">${productName}</div>
|
|
<div class="product-details">
|
|
<div><strong>ID:</strong> ${productId}</div>
|
|
<div><strong>价格:</strong> ${price} <strong>类型:</strong> ${productType}</div>
|
|
<div><strong>规格:</strong> ${specification} <strong>件数:</strong> ${product.quantity || '未知'}</div>
|
|
<div><strong>商品联系人:</strong> ${product.productContact || '未知'} ${product.contactPhone || '未知'}</div>
|
|
<div><strong>商品创建人:</strong> ${product.creatorNickName || '未知'} ${product.creatorPhone || '未知'}</div>
|
|
<div><strong>创建时间:</strong> ${createTime}</div>
|
|
<div><strong>浏览次数:</strong> ${product.viewCount}次</div>
|
|
<div><strong>浏览时间:</strong></div>
|
|
<div style="margin-left: 8px; color: #666; font-size: 14px;">
|
|
${formattedViewTimes.map(time => `<div style="margin-bottom: 4px;">• ${time}</div>`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
// 添加切换按钮容器
|
|
const productsHtml = `
|
|
<div style="margin-bottom: 24px;">
|
|
<div style="display: flex; gap: 24px; margin-bottom: 16px;">
|
|
<button
|
|
id="favoriteTab"
|
|
style="border: none; background: none; cursor: pointer; font-size: 16px; font-weight: 500; padding: 8px 0; border-bottom: 2px solid #64748b; color: #64748b; transition: all 0.2s;">
|
|
收藏商品 (${groupedFavoriteProducts.length}件)
|
|
</button>
|
|
<button
|
|
id="regularTab"
|
|
style="border: none; background: none; cursor: pointer; font-size: 16px; font-weight: 500; padding: 8px 0; border-bottom: 2px solid #367ee9; color: #367ee9; transition: all 0.2s;">
|
|
普通浏览商品 (${groupedRegularProducts.length}件)
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 收藏商品容器 -->
|
|
<div id="favoriteProductsContainer" style="display: none;">
|
|
<div style="display: grid; grid-template-columns: repeat(3, minmax(300px, 1fr)); gap: 16px;">
|
|
${groupedFavoriteProducts.map(renderProductCard).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 普通商品容器 -->
|
|
<div id="regularProductsContainer" style="display: block;">
|
|
<div style="display: grid; grid-template-columns: repeat(3, minmax(300px, 1fr)); gap: 16px;">
|
|
${groupedRegularProducts.map(renderProductCard).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
productsContainer.innerHTML = productsHtml;
|
|
|
|
// 添加切换事件
|
|
const favoriteTab = document.getElementById('favoriteTab');
|
|
const regularTab = document.getElementById('regularTab');
|
|
const favoriteContainer = document.getElementById('favoriteProductsContainer');
|
|
const regularContainer = document.getElementById('regularProductsContainer');
|
|
|
|
favoriteTab.addEventListener('click', () => {
|
|
// 切换到收藏商品
|
|
favoriteTab.style.borderBottomColor = '#367ee9';
|
|
favoriteTab.style.color = '#367ee9';
|
|
regularTab.style.borderBottomColor = '#64748b';
|
|
regularTab.style.color = '#64748b';
|
|
favoriteContainer.style.display = 'block';
|
|
regularContainer.style.display = 'none';
|
|
});
|
|
|
|
regularTab.addEventListener('click', () => {
|
|
// 切换到普通商品
|
|
regularTab.style.borderBottomColor = '#367ee9';
|
|
regularTab.style.color = '#367ee9';
|
|
favoriteTab.style.borderBottomColor = '#64748b';
|
|
favoriteTab.style.color = '#64748b';
|
|
regularContainer.style.display = 'block';
|
|
favoriteContainer.style.display = 'none';
|
|
});
|
|
|
|
// 如果只有收藏商品,默认显示收藏商品
|
|
if (groupedRegularProducts.length === 0 && groupedFavoriteProducts.length > 0) {
|
|
favoriteTab.click();
|
|
}
|
|
} else {
|
|
console.log(`[${new Date().toISOString()}] 📭 当天没有浏览商品记录`);
|
|
productsContainer.innerHTML = '<div class="no-data">当天没有浏览商品记录</div>';
|
|
}
|
|
} else {
|
|
console.error(`[${new Date().toISOString()}] ❌ API返回失败: ${data.message}`);
|
|
productsContainer.innerHTML = `<div class="no-data">获取数据失败: ${data.message}</div>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`[${new Date().toISOString()}] 💥 API请求异常:`, error);
|
|
const productsContainer = document.getElementById('dayProductsContainer');
|
|
productsContainer.innerHTML = '<div class="no-data">获取数据失败,请稍后重试</div>';
|
|
});
|
|
}
|
|
|
|
// 重置活跃统计筛选条件
|
|
function resetActiveStatsFilters() {
|
|
// 重置时间范围选择器到默认值
|
|
document.getElementById('dateRangeSelect').value = '7days';
|
|
// 设置默认时间范围为最近7天
|
|
setDateRange('7days');
|
|
refreshActiveStats();
|
|
}
|
|
|
|
// 搜索产品生命周期
|
|
function searchProductLifecycle() {
|
|
const productId = document.getElementById('productIdInput').value;
|
|
if (!productId) {
|
|
alert('请输入商品ID');
|
|
return;
|
|
}
|
|
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">加载中...</td></tr>';
|
|
|
|
fetch(`/api/product-lifecycle/${productId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 先展示产品基本信息
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e3f2fd; font-weight: bold; color: #1565c0; border-left: 4px solid #2196f3;">
|
|
产品基本信息
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fafafa;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;">
|
|
<div><strong>商品ID:</strong> ${data.product.productId}</div>
|
|
<div><strong>商品名称:</strong> ${data.product.productName}</div>
|
|
<div><strong>卖家ID:</strong> ${data.product.sellerId}</div>
|
|
<div><strong>卖家名称:</strong> ${data.seller.sellerName || data.seller.nickName}</div>
|
|
<div><strong>卖家电话:</strong> ${data.seller.phoneNumber || '-'}</div>
|
|
<div><strong>状态:</strong> ${mapProductStatus(data.product.status)}</div>
|
|
<div><strong>创建时间:</strong> ${new Date(data.product.created_at).toLocaleString('zh-CN')}</div>
|
|
<div><strong>更新时间:</strong> ${new Date(data.product.updated_at).toLocaleString('zh-CN')}</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e8f5e8; font-weight: bold; color: #2e7d32; border-left: 4px solid #4caf50;">
|
|
产品日志
|
|
</td>
|
|
</tr>`;
|
|
|
|
// 添加产品日志
|
|
if (data.product.product_log) {
|
|
const logs = JSON.parse(data.product.product_log);
|
|
logs.forEach(log => {
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 8px; border-bottom: 1px solid #e9ecef;">
|
|
${log}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #999;">
|
|
暂无日志记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 添加操作历史表头
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fff3e0; font-weight: bold; color: #ef6c00; border-left: 4px solid #ff9800;">
|
|
操作历史
|
|
</td>
|
|
</tr>
|
|
<tr style="background-color: rgba(255, 243, 224, 0.5);">
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">操作时间</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">操作人</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">商品ID</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">卖家ID</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">操作事件</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">操作详情</th>
|
|
</tr>
|
|
`;
|
|
|
|
// 添加操作历史记录
|
|
if (data.lifecycle.length > 0) {
|
|
data.lifecycle.forEach((log, index) => {
|
|
// 为每条日志添加唯一标识,使用时间戳+索引
|
|
const logKey = `lifecycle_${log.id || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: ${index % 2 === 0 ? '#fff8e1' : '#fffef9'}; transition: background-color 0.2s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${log.operationUser || data.product.productName || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.product.productId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.product.sellerId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">
|
|
<button class="btn btn-default" onclick="showProductLifecycleDetail(${JSON.stringify(log).replace(/"/g, '"')})">查看详情</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #fff8e1;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无操作记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
} else {
|
|
tableBody.innerHTML = `<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">${data.message || '查询失败'}</td></tr>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('查询产品生命周期失败:', error);
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">加载失败,请重试</td></tr>';
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// 搜索客户操作历史
|
|
function searchCustomerHistory() {
|
|
const userIdOrPhone = document.getElementById('customerIdInput').value;
|
|
if (!userIdOrPhone) {
|
|
alert('请输入客户ID或电话号码');
|
|
return;
|
|
}
|
|
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">加载中...</td></tr>';
|
|
|
|
fetch(`/api/customer-history/${userIdOrPhone}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 先展示客户基本信息
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e3f2fd; font-weight: bold; color: #1565c0; border-left: 4px solid #2196f3;">
|
|
客户基本信息
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fafafa;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;">
|
|
<div><strong>客户ID:</strong> ${data.customer.userId}</div>
|
|
<div><strong>客户名称:</strong> ${data.customer.nickName}</div>
|
|
<div><strong>电话号码:</strong> ${data.customer.phoneNumber || '-'}</div>
|
|
<div><strong>类型:</strong> ${mapCustomerType(data.customer.type)}</div>
|
|
<div><strong>创建时间:</strong> ${new Date(data.customer.created_at).toLocaleString('zh-CN')}</div>
|
|
<div><strong>更新时间:</strong> ${new Date(data.customer.updated_at).toLocaleString('zh-CN')}</div>
|
|
<div><strong>跟进时间:</strong> ${data.customer.followup_at ? new Date(data.customer.followup_at).toLocaleString('zh-CN') : '未跟进'}</div>
|
|
<div><strong>用户日志:</strong> ${data.customer.userlog || '-'}</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 添加分配历史
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e8f5e8; font-weight: bold; color: #2e7d32; border-left: 4px solid #4caf50;">
|
|
分配历史
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.allocationHistory.length > 0) {
|
|
data.allocationHistory.forEach((allocation, index) => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: ${index % 2 === 0 ? '#f1f8e9' : '#fafffa'}; transition: background-color 0.2s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${new Date(allocation.allocationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">-</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${data.customer.userId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${data.customer.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">分配给${allocation.managerName}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">-</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #f1f8e9;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无分配记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 添加操作历史
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fff3e0; font-weight: bold; color: #ef6c00; border-left: 4px solid #ff9800;">
|
|
操作历史
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.operationHistory.length > 0) {
|
|
data.operationHistory.forEach((log, index) => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: ${index % 2 === 0 ? '#fff8e1' : '#fffef9'}; transition: background-color 0.2s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${log.operationUser || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.customer.userId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.customer.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">-</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #fff8e1;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无操作记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
} else {
|
|
tableBody.innerHTML = `<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">${data.message || '查询失败'}</td></tr>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('查询客户操作历史失败:', error);
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">加载失败,请重试</td></tr>';
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// 搜索业务员操作记录
|
|
function searchAgentOperations() {
|
|
const userName = document.getElementById('agentNameInput').value;
|
|
if (!userName) {
|
|
alert('请输入业务员姓名');
|
|
return;
|
|
}
|
|
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #999;">加载中...</td></tr>';
|
|
|
|
fetch(`/api/agent-operations/${encodeURIComponent(userName)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 先展示业务员基本信息
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e3f2fd; font-weight: bold; color: #1565c0; border-left: 4px solid #2196f3;">
|
|
业务员基本信息
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fafafa;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;">
|
|
<div><strong>业务员姓名:</strong> ${data.agent.userName}</div>
|
|
<div><strong>部门:</strong> ${data.agent.department || data.agent.managerdepartment}</div>
|
|
<div><strong>角色:</strong> ${data.agent.role}</div>
|
|
<div><strong>创建时间:</strong> ${new Date(data.agent.created_at).toLocaleString('zh-CN')}</div>
|
|
<div><strong>更新时间:</strong> ${new Date(data.agent.updated_at).toLocaleString('zh-CN')}</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 添加操作客户列表
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e8f5e8; font-weight: bold; color: #2e7d32; border-left: 4px solid #4caf50;">
|
|
操作客户列表
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.customerOperations.length > 0) {
|
|
data.customerOperations.forEach(customer => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #f1f8e9;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${new Date(customer.firstOperationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${data.agent.userName}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${customer.userId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${customer.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">首次操作客户</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">操作次数: ${customer.operationCount}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #f1f8e9;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无操作客户记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 添加操作货源列表
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fff3e0; font-weight: bold; color: #ef6c00; border-left: 4px solid #ff9800;">
|
|
操作货源列表
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.productOperations.length > 0) {
|
|
data.productOperations.forEach(product => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #fff8e1;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${new Date(product.firstOperationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.agent.userName}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${product.productId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">-</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">首次操作货源</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">操作次数: ${product.operationCount}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #fff8e1;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无操作货源记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 添加详细客户操作日志
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #e8f5e8; font-weight: bold; color: #2e7d32; border-left: 4px solid #4caf50;">
|
|
详细客户操作日志
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.customerLogs.length > 0) {
|
|
data.customerLogs.forEach((log, index) => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: ${index % 2 === 0 ? '#f1f8e9' : '#fafffa'}; transition: background-color 0.2s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${data.agent.userName}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${log.userId || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${log.customerPhone || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(200, 230, 201, 0.8);">
|
|
<button class="btn btn-default" onclick="showAgentLogDetails(${JSON.stringify(log).replace(/"/g, '"')})">查看详情</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #f1f8e9;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无详细客户操作记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 添加详细货源操作日志
|
|
tableBody.innerHTML += `
|
|
<tr>
|
|
<td colspan="6" style="padding: 16px; background-color: #fff3e0; font-weight: bold; color: #ef6c00; border-left: 4px solid #ff9800;">
|
|
详细货源操作日志
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
if (data.productLogs.length > 0) {
|
|
data.productLogs.forEach((log, index) => {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: ${index % 2 === 0 ? '#fff8e1' : '#fffef9'}; transition: background-color 0.2s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${data.agent.userName}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${log.userId || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${log.customerPhone || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(255, 235, 205, 0.8);">
|
|
<button class="btn btn-default" onclick="showAgentLogDetails(${JSON.stringify(log).replace(/"/g, '"')})">查看详情</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableBody.innerHTML += `
|
|
<tr style="background-color: #fff8e1;">
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #666;">
|
|
暂无详细货源操作记录
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
} else {
|
|
tableBody.innerHTML = `<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">${data.message || '查询失败'}</td></tr>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('查询业务员操作记录失败:', error);
|
|
tableBody.innerHTML = '<tr><td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">加载失败,请重试</td></tr>';
|
|
});
|
|
}
|
|
|
|
// 显示客户统计页面
|
|
function showClientStats() {
|
|
// 激活菜单
|
|
activateMenu('client-stats');
|
|
|
|
// 检查永久缓存
|
|
const clientStatsCache = cacheManager.getPermanent('clientStats');
|
|
if (clientStatsCache) {
|
|
console.log('使用客户统计永久缓存数据');
|
|
// 可以在这里直接使用缓存数据
|
|
}
|
|
|
|
// 更新内容区域
|
|
const content = document.querySelector('.content');
|
|
content.innerHTML = `
|
|
<div class="content-header fade-in">
|
|
<h1 class="content-title">客户统计</h1>
|
|
<p class="content-subtitle">查看客户统计数据和分析报告</p>
|
|
</div>
|
|
|
|
<!-- 时间筛选控件 -->
|
|
<div class="card fade-in">
|
|
<h2 class="card-title">时间筛选</h2>
|
|
<div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="btn btn-default" onclick="setTimeRange('today')">今天</button>
|
|
<button class="btn btn-default" onclick="setTimeRange('yesterday')">昨天</button>
|
|
<button class="btn btn-default" onclick="setTimeRange('7days')">近7天</button>
|
|
<button class="btn btn-default" onclick="setTimeRange('30days')">近30天</button>
|
|
<button class="btn btn-default" onclick="setTimeRange('month')">本月</button>
|
|
<button class="btn btn-default" onclick="setTimeRange('lastMonth')">上月</button>
|
|
</div>
|
|
<div style="display: flex; gap: 8px; align-items: center;">
|
|
<input type="date" id="startDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<span>至</span>
|
|
<input type="date" id="endDate" style="padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;">
|
|
<button class="btn btn-primary" onclick="applyDateRange()">应用</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 客户统计卡片 -->
|
|
<div class="card fade-in delay-1">
|
|
<h2 class="card-title">客户统计</h2>
|
|
|
|
<div class="client-stats-grid">
|
|
<div class="client-stat-card" onclick="showNewClientsModal()">
|
|
<div class="client-stat-number" id="todayClients">加载中...</div>
|
|
<div class="client-stat-label">新增客户</div>
|
|
</div>
|
|
<div class="client-stat-card" onclick="showFollowedClientsModal()">
|
|
<div class="client-stat-number" id="followedClients">加载中...</div>
|
|
<div class="client-stat-label">已跟进客户</div>
|
|
</div>
|
|
<div class="client-stat-card" onclick="showNotFollowedClientsModal()">
|
|
<div class="client-stat-number" id="notFollowedClients">加载中...</div>
|
|
<div class="client-stat-label">未跟进客户</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 2rem;">
|
|
<h3 style="font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">客户分配情况</h3>
|
|
<div class="chart-container">
|
|
<canvas id="clientAllocationChart"></canvas>
|
|
</div>
|
|
<div style="margin-top: 1rem; text-align: center;">
|
|
<small style="color: var(--text-muted);">点击图表中的柱子查看对应业务员的客户详情</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 2rem;">
|
|
<h3 style="font-size: 1rem; font-weight: 600; color: var(--text-primary); margin-bottom: 1rem;">客户跟进状态</h3>
|
|
<div class="chart-container">
|
|
<canvas id="followStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 直接调用setTimeRange('today'),使用与点击按钮相同的逻辑
|
|
setTimeRange('today');
|
|
|
|
// 手动为"今天"按钮添加active类,确保进入模块时默认激活
|
|
setTimeout(() => {
|
|
document.querySelectorAll('[onclick^="setTimeRange"]').forEach(btn => {
|
|
if (btn.textContent.trim() === '今天') {
|
|
btn.classList.add('active');
|
|
} else {
|
|
btn.classList.remove('active');
|
|
}
|
|
});
|
|
}, 100);
|
|
|
|
// 初始化全局时间范围变量
|
|
const now = new Date();
|
|
const startDate = new Date(now);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
|
|
// 格式化日期为YYYY-MM-DD(使用本地时间)
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
currentStartDate = formatDate(startDate);
|
|
currentEndDate = formatDate(endDate);
|
|
}
|
|
|
|
// 保存客户统计数据到永久缓存
|
|
function saveClientStatsToCache(stats) {
|
|
const clientStats = cacheManager.getPermanent('clientStats') || {
|
|
overview: {},
|
|
details: {},
|
|
categories: {}
|
|
};
|
|
|
|
// 更新对应部分
|
|
if (stats.overview) {
|
|
clientStats.overview = stats.overview;
|
|
}
|
|
if (stats.details) {
|
|
clientStats.details = stats.details;
|
|
}
|
|
if (stats.categories) {
|
|
clientStats.categories = stats.categories;
|
|
}
|
|
|
|
cacheManager.setPermanent('clientStats', clientStats);
|
|
}
|
|
|
|
// 更新客户详情到永久缓存
|
|
function updateClientDetailToCache(clientId, clientDetail) {
|
|
const clientStats = cacheManager.getPermanent('clientStats') || {
|
|
overview: {},
|
|
details: {},
|
|
categories: {}
|
|
};
|
|
|
|
clientStats.details[clientId] = clientDetail;
|
|
cacheManager.setPermanent('clientStats', clientStats);
|
|
}
|
|
|
|
// 设置时间范围
|
|
function setTimeRange(range) {
|
|
// 移除所有时间范围按钮的active类
|
|
document.querySelectorAll('[onclick^="setTimeRange"]').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// 为对应按钮添加active类
|
|
if (event) {
|
|
event.target.classList.add('active');
|
|
} else {
|
|
// 当通过代码直接调用时,根据range参数找到对应按钮
|
|
document.querySelectorAll('[onclick^="setTimeRange"]').forEach(btn => {
|
|
if (btn.onclick.toString().includes(`setTimeRange('${range}')`)) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
const now = new Date();
|
|
let startDate, endDate;
|
|
|
|
switch(range) {
|
|
case 'today':
|
|
startDate = new Date(now);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999); // 结束时间固定为23:59:59
|
|
break;
|
|
case 'yesterday':
|
|
startDate = new Date(now);
|
|
startDate.setDate(now.getDate() - 1);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(now);
|
|
endDate.setDate(now.getDate() - 1);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case '7days':
|
|
startDate = new Date(now);
|
|
startDate.setDate(now.getDate() - 6);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case '30days':
|
|
startDate = new Date(now);
|
|
startDate.setDate(now.getDate() - 29);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
break;
|
|
case 'month':
|
|
startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
break;
|
|
case 'lastMonth':
|
|
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0);
|
|
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
|
break;
|
|
}
|
|
|
|
// 格式化日期为YYYY-MM-DD(使用本地时间)
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// 对于"today"情况,确保startDate和endDate使用相同的日期
|
|
if (range === 'today') {
|
|
const todayDate = formatDate(startDate);
|
|
document.getElementById('startDate').value = todayDate;
|
|
document.getElementById('endDate').value = todayDate;
|
|
} else {
|
|
document.getElementById('startDate').value = formatDate(startDate);
|
|
document.getElementById('endDate').value = formatDate(endDate);
|
|
}
|
|
|
|
// 应用筛选
|
|
applyDateRange();
|
|
}
|
|
|
|
// 存储当前时间范围的全局变量
|
|
let currentStartDate = '';
|
|
let currentEndDate = '';
|
|
|
|
// 分页相关全局变量
|
|
let currentPage = 1;
|
|
let pageSize = 10;
|
|
let totalPages = 1;
|
|
let totalItems = 0;
|
|
let currentFilterType = 'all'; // 'all' 或 'manager'
|
|
let currentManagerName = '';
|
|
|
|
// 获取时间范围类型
|
|
function getTimeRangeType(startDate, endDate) {
|
|
const now = new Date();
|
|
const today = new Date(now);
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
const yesterday = new Date(now);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
yesterday.setHours(0, 0, 0, 0);
|
|
|
|
const sevenDaysAgo = new Date(now);
|
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6);
|
|
sevenDaysAgo.setHours(0, 0, 0, 0);
|
|
|
|
const thirtyDaysAgo = new Date(now);
|
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29);
|
|
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
|
|
|
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
|
|
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1, 0, 0, 0, 0);
|
|
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
|
|
|
// 格式化日期为YYYY-MM-DD
|
|
const formatDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
const formattedStartDate = startDate;
|
|
const formattedEndDate = endDate;
|
|
|
|
if (formattedStartDate === formatDate(today) && formattedEndDate === formatDate(today)) {
|
|
return 'today';
|
|
} else if (formattedStartDate === formatDate(yesterday) && formattedEndDate === formatDate(yesterday)) {
|
|
return 'yesterday';
|
|
} else if (formattedStartDate === formatDate(sevenDaysAgo) && formattedEndDate === formatDate(today)) {
|
|
return '7days';
|
|
} else if (formattedStartDate === formatDate(thirtyDaysAgo) && formattedEndDate === formatDate(today)) {
|
|
return '30days';
|
|
} else if (formattedStartDate === formatDate(currentMonthStart) && formattedEndDate === formatDate(currentMonthEnd)) {
|
|
return 'month';
|
|
} else if (formattedStartDate === formatDate(lastMonthStart) && formattedEndDate === formatDate(lastMonthEnd)) {
|
|
return 'lastMonth';
|
|
} else {
|
|
return 'custom';
|
|
}
|
|
}
|
|
|
|
// 应用日期范围
|
|
function applyDateRange() {
|
|
const startDate = document.getElementById('startDate').value;
|
|
const endDate = document.getElementById('endDate').value;
|
|
|
|
if (startDate && endDate) {
|
|
console.log('应用日期范围:', startDate, endDate);
|
|
// 保存当前时间范围到全局变量
|
|
currentStartDate = startDate;
|
|
currentEndDate = endDate;
|
|
loadClientStats(startDate, endDate);
|
|
}
|
|
}
|
|
|
|
// 加载操作日志
|
|
function loadLogs(page = 1, pageSize = 10) {
|
|
// 保存当前日志类型
|
|
window.currentLogType = 'system';
|
|
|
|
const searchUserName = document.getElementById('searchUserName')?.value || '';
|
|
const searchUserId = document.getElementById('searchUserId')?.value || '';
|
|
const searchPhone = document.getElementById('searchPhone')?.value || '';
|
|
const searchSystem = document.getElementById('searchSystem')?.value || '';
|
|
const startDate = document.getElementById('startDate')?.value || '';
|
|
const endDate = document.getElementById('endDate')?.value || '';
|
|
|
|
// 构建缓存键
|
|
const cacheKey = `logs_system_${page}_${pageSize}_${searchUserName}_${searchUserId}_${searchPhone}_${searchSystem}_${startDate}_${endDate}`;
|
|
|
|
// 首先检查永久缓存
|
|
const permanentCachedData = cacheManager.getPermanent('logsData');
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载操作日志');
|
|
// 立即显示永久缓存数据
|
|
updateLogsUI(permanentCachedData, page, pageSize);
|
|
// 后台加载最新数据
|
|
loadLatestLogs(page, pageSize, searchUserName, searchUserId, searchPhone, searchSystem, startDate, endDate);
|
|
return;
|
|
}
|
|
|
|
// 检查普通缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用普通缓存数据加载操作日志');
|
|
// 立即显示缓存数据
|
|
updateLogsUI(cachedData, page, pageSize);
|
|
// 后台加载最新数据
|
|
loadLatestLogs(page, pageSize, searchUserName, searchUserId, searchPhone, searchSystem, startDate, endDate);
|
|
return;
|
|
}
|
|
|
|
// 没有缓存,显示加载状态
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: var(--text-muted);">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 从API获取数据
|
|
loadLatestLogs(page, pageSize, searchUserName, searchUserId, searchPhone, searchSystem, startDate, endDate);
|
|
}
|
|
|
|
// 从API获取最新操作日志数据
|
|
function loadLatestLogs(page = 1, pageSize = 10, searchUserName = '', searchUserId = '', searchPhone = '', searchSystem = '', startDate = '', endDate = '') {
|
|
// 构建缓存键
|
|
const cacheKey = `logs_system_${page}_${pageSize}_${searchUserName}_${searchUserId}_${searchPhone}_${searchSystem}_${startDate}_${endDate}`;
|
|
|
|
// 构建API URL
|
|
const params = new URLSearchParams();
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
if (searchUserName) params.append('userName', searchUserName);
|
|
if (searchUserId) params.append('userId', searchUserId);
|
|
if (searchPhone) params.append('phoneNumber', searchPhone);
|
|
if (searchSystem) params.append('system', searchSystem);
|
|
if (startDate) params.append('startDate', startDate);
|
|
if (endDate) params.append('endDate', endDate);
|
|
|
|
// 加载数据
|
|
fetch(`/api/logs?${params.toString()}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 缓存数据
|
|
if (data.success) {
|
|
cacheManager.set(cacheKey, data);
|
|
// 更新永久缓存
|
|
cacheManager.setPermanent('logsData', data);
|
|
}
|
|
|
|
// 更新UI
|
|
updateLogsUI(data, page, pageSize);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载日志失败:', error);
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
加载失败,请重试
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 加载商品日志
|
|
function loadProductLogs(page = 1, pageSize = 10) {
|
|
// 保存当前日志类型
|
|
window.currentLogType = 'product';
|
|
|
|
const searchProductId = document.getElementById('searchProductId')?.value || '';
|
|
const searchSellerId = document.getElementById('searchSellerId')?.value || '';
|
|
const productStartDate = document.getElementById('productStartDate')?.value || '';
|
|
const productEndDate = document.getElementById('productEndDate')?.value || '';
|
|
|
|
// 构建缓存键
|
|
const cacheKey = `logs_product_${page}_${pageSize}_${searchProductId}_${searchSellerId}_${productStartDate}_${productEndDate}`;
|
|
|
|
// 检查缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用缓存数据加载商品日志');
|
|
// 立即显示缓存数据
|
|
updateLogsUI(cachedData, page, pageSize);
|
|
// 后台加载最新数据
|
|
loadLatestProductLogs(page, pageSize, searchProductId, searchSellerId, productStartDate, productEndDate);
|
|
return;
|
|
}
|
|
|
|
// 没有缓存,显示加载状态
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: var(--text-muted);">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 从API获取数据
|
|
loadLatestProductLogs(page, pageSize, searchProductId, searchSellerId, productStartDate, productEndDate);
|
|
}
|
|
|
|
// 从API获取最新商品日志数据
|
|
function loadLatestProductLogs(page = 1, pageSize = 10, searchProductId = '', searchSellerId = '', productStartDate = '', productEndDate = '') {
|
|
// 构建缓存键
|
|
const cacheKey = `logs_product_${page}_${pageSize}_${searchProductId}_${searchSellerId}_${productStartDate}_${productEndDate}`;
|
|
|
|
// 构建API URL
|
|
const params = new URLSearchParams();
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
if (searchProductId) params.append('productId', searchProductId);
|
|
if (searchSellerId) params.append('sellerId', searchSellerId);
|
|
if (productStartDate) params.append('startDate', productStartDate);
|
|
if (productEndDate) params.append('endDate', productEndDate);
|
|
|
|
// 加载数据
|
|
fetch(`/api/product-logs?${params.toString()}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 缓存数据
|
|
if (data.success) {
|
|
cacheManager.set(cacheKey, data);
|
|
}
|
|
|
|
// 更新UI
|
|
updateLogsUI(data, page, pageSize);
|
|
})
|
|
.catch(error => {
|
|
console.error('加载商品日志失败:', error);
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
加载失败,请重试
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 更新日志UI
|
|
function updateLogsUI(data, page, pageSize) {
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
const totalLogs = document.getElementById('totalLogs');
|
|
const currentPage = document.getElementById('currentPage');
|
|
const totalPages = document.getElementById('totalPages');
|
|
const prevPageBtn = document.getElementById('prevPage');
|
|
const nextPageBtn = document.getElementById('nextPage');
|
|
|
|
if (data.success) {
|
|
totalLogs.textContent = data.total;
|
|
currentPage.textContent = page;
|
|
totalPages.textContent = Math.ceil(data.total / pageSize);
|
|
|
|
// 更新分页按钮状态
|
|
prevPageBtn.disabled = page === 1;
|
|
nextPageBtn.disabled = page >= Math.ceil(data.total / pageSize);
|
|
|
|
// 渲染日志数据
|
|
if (data.logs.length === 0) {
|
|
const emptyMessage = window.currentLogType === 'product' ? '暂无商品日志记录' : '暂无日志记录';
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #999;">
|
|
${emptyMessage}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
} else {
|
|
if (window.currentLogType === 'product') {
|
|
// 渲染商品日志
|
|
tableBody.innerHTML = data.logs.map(log => `
|
|
<tr style="transition: all 0.3s ease; cursor: pointer; background-color: rgba(255, 255, 255, 0.4);" onmouseover="this.style.backgroundColor='rgba(245, 245, 245, 0.6)'" onmouseout="this.style.backgroundColor='rgba(255, 255, 255, 0.4)'">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${new Date(log.logTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.productName || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.productId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.sellerId || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${(log.logContent || '-').length > 50 ? (log.logContent.substring(0, 50) + '...') : log.logContent}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5); white-space: nowrap;">
|
|
<button class="btn btn-default" onclick="showProductLogDetails(${log.id})">查看详情</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
// 渲染系统日志
|
|
tableBody.innerHTML = data.logs.map(log => `
|
|
<tr style="transition: all 0.3s ease; cursor: pointer; background-color: rgba(255, 255, 255, 0.4);" onmouseover="this.style.backgroundColor='rgba(245, 245, 245, 0.6)'" onmouseout="this.style.backgroundColor='rgba(255, 255, 255, 0.4)'">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.trauserName || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.userId}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${log.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">
|
|
<button class="btn btn-default" onclick="showLogDetails(${log.id})">查看详情</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
}
|
|
} else {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
${data.message || '加载失败'}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 搜索商品日志
|
|
function searchProductLogs() {
|
|
loadProductLogs(1);
|
|
}
|
|
|
|
// 重置商品日志搜索
|
|
function resetProductSearch() {
|
|
document.getElementById('searchProductId').value = '';
|
|
document.getElementById('searchSellerId').value = '';
|
|
document.getElementById('productStartDate').value = '';
|
|
document.getElementById('productEndDate').value = '';
|
|
loadProductLogs(1);
|
|
}
|
|
|
|
// 查看商品日志详情
|
|
function showProductLogDetails(logId) {
|
|
fetch(`/api/product-logs/${logId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const log = data.log;
|
|
|
|
// 创建模态框HTML
|
|
let modalHTML = `
|
|
<div id="logDetailModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
|
|
<div style="background-color: white; border-radius: 8px; width: 95%; height: 95%; display: flex; flex-direction: column;">
|
|
<div style="padding: 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
|
<h3 style="margin-top: 0; margin-bottom: 0; color: #333;">商品日志详情</h3>
|
|
<button onclick="closeLogDetailModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
</div>
|
|
<div style="padding: 24px; flex-grow: 1; overflow-x: auto; overflow-y: auto;">
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">基本信息</h4>
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold; width: 120px;">日志时间:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${new Date(log.logTime).toLocaleString('zh-CN')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">商品名称:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.productName}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">商品ID:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.productId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">卖家ID:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.sellerId}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">日志内容</h4>
|
|
<div style="background-color: #f9f9f9; border-radius: 4px; padding: 16px; font-size: 14px; line-height: 1.6;">
|
|
${(() => {
|
|
try {
|
|
// 尝试解析日志内容为数组
|
|
const logArray = JSON.parse(log.logContent);
|
|
if (Array.isArray(logArray)) {
|
|
// 如果是数组,渲染为列表
|
|
return `
|
|
<ul style="margin: 0; padding-left: 20px;">
|
|
${logArray.map((logItem, index) => `
|
|
<li style="margin-bottom: 8px; padding: 8px; background-color: ${index % 2 === 0 ? '#ffffff' : '#f5f5f5'}; border-radius: 3px;">
|
|
${logItem}
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
} else {
|
|
// 如果不是数组,直接显示
|
|
return log.logContent;
|
|
}
|
|
} catch (e) {
|
|
// 如果解析失败,直接显示
|
|
return log.logContent;
|
|
}
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div style="padding: 24px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; align-items: center; flex-shrink: 0;">
|
|
<button onclick="closeLogDetailModal()" style="padding: 8px 16px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 添加模态框到页面
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
} else {
|
|
alert('获取商品日志详情失败:' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取商品日志详情失败:', error);
|
|
alert('获取商品日志详情失败,请重试');
|
|
});
|
|
}
|
|
|
|
// 加载客户统计数据
|
|
function loadClientStats(startDate, endDate) {
|
|
// 确定时间范围类型
|
|
const timeRangeType = getTimeRangeType(startDate, endDate);
|
|
|
|
// 构建缓存键,包含followedClientBy参数
|
|
const cacheKey = `client_stats_${startDate || 'all'}_${endDate || 'all'}_followedClientBy=updated_at`;
|
|
|
|
// 首先检查永久缓存
|
|
const permanentCachedData = cacheManager.getPermanent('clientStatsData');
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载客户统计');
|
|
// 检查是否有对应时间范围的数据
|
|
if (permanentCachedData.ranges && permanentCachedData.ranges[timeRangeType]) {
|
|
console.log('使用永久缓存中对应时间范围的数据:', timeRangeType);
|
|
updateClientStatsUI(permanentCachedData.ranges[timeRangeType], startDate, endDate);
|
|
} else {
|
|
// 使用默认数据
|
|
updateClientStatsUI(permanentCachedData, startDate, endDate);
|
|
}
|
|
// 后台加载最新数据
|
|
loadLatestClientStats(startDate, endDate);
|
|
return;
|
|
}
|
|
|
|
// 检查普通缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用缓存数据加载客户统计');
|
|
// 立即显示缓存数据
|
|
updateClientStatsUI(cachedData, startDate, endDate);
|
|
// 后台加载最新数据
|
|
loadLatestClientStats(startDate, endDate);
|
|
return;
|
|
}
|
|
|
|
// 显示加载状态
|
|
const todayClientsEl = document.getElementById('todayClients');
|
|
const followedClientsEl = document.getElementById('followedClients');
|
|
const notFollowedClientsEl = document.getElementById('notFollowedClients');
|
|
|
|
if (todayClientsEl) todayClientsEl.textContent = '加载中...';
|
|
if (followedClientsEl) followedClientsEl.textContent = '加载中...';
|
|
if (notFollowedClientsEl) notFollowedClientsEl.textContent = '加载中...';
|
|
|
|
// 从API获取数据
|
|
loadLatestClientStats(startDate, endDate);
|
|
}
|
|
|
|
// 从API获取最新客户统计数据
|
|
function loadLatestClientStats(startDate, endDate) {
|
|
// 构建缓存键,包含followedClientBy参数
|
|
const cacheKey = `client_stats_${startDate || 'all'}_${endDate || 'all'}_followedClientBy=updated_at`;
|
|
|
|
// 构建API URL,包含时间范围参数,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
let apiUrl = '/api/client-stats';
|
|
if (startDate && endDate) {
|
|
apiUrl += `?startDate=${startDate}&endDate=${endDate}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
}
|
|
|
|
// 并行加载统计数据和客户详情
|
|
Promise.all([
|
|
// 加载统计数据
|
|
fetch(apiUrl)
|
|
.then(response => response.json()),
|
|
|
|
// 同时加载客户详情
|
|
(async () => {
|
|
let detailUrl = '/api/client-details?page=1&pageSize=10';
|
|
if (startDate && endDate) {
|
|
detailUrl += `&startDate=${startDate}&endDate=${endDate}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
}
|
|
const response = await fetch(detailUrl);
|
|
return response.json();
|
|
})()
|
|
])
|
|
.then(([statsData, detailsData]) => {
|
|
// 缓存统计数据
|
|
if (statsData.success) {
|
|
cacheManager.set(cacheKey, statsData);
|
|
|
|
// 更新永久缓存,包含所有预设时间范围
|
|
const existingPermanentData = cacheManager.getPermanent('clientStatsData') || {};
|
|
const timeRangeType = getTimeRangeType(startDate, endDate);
|
|
|
|
// 确保ranges对象存在
|
|
if (!existingPermanentData.ranges) {
|
|
existingPermanentData.ranges = {};
|
|
}
|
|
|
|
// 更新对应时间范围的数据
|
|
existingPermanentData.ranges[timeRangeType] = statsData;
|
|
|
|
// 保存更新后的数据
|
|
cacheManager.setPermanent('clientStatsData', existingPermanentData);
|
|
console.log('永久缓存更新成功,包含时间范围:', timeRangeType);
|
|
}
|
|
|
|
// 缓存客户详情数据
|
|
if (detailsData.success) {
|
|
const detailCacheKey = `client_details_${startDate || 'all'}_${endDate || 'all'}_followedClientBy=updated_at_page=1&pageSize=10`;
|
|
cacheManager.set(detailCacheKey, detailsData);
|
|
}
|
|
|
|
// 更新UI
|
|
updateClientStatsUI(statsData, startDate, endDate, detailsData);
|
|
})
|
|
.catch(error => {
|
|
console.error('获取客户统计数据失败:', error);
|
|
|
|
// 显示错误状态
|
|
const todayClientsEl = document.getElementById('todayClients');
|
|
const followedClientsEl = document.getElementById('followedClients');
|
|
const notFollowedClientsEl = document.getElementById('notFollowedClients');
|
|
|
|
if (todayClientsEl) todayClientsEl.textContent = '加载失败';
|
|
if (followedClientsEl) followedClientsEl.textContent = '加载失败';
|
|
if (notFollowedClientsEl) notFollowedClientsEl.textContent = '加载失败';
|
|
});
|
|
}
|
|
|
|
// 更新客户统计UI
|
|
function updateClientStatsUI(statsData, startDate, endDate, detailsData = null) {
|
|
if (statsData.success) {
|
|
// 更新统计数字
|
|
const todayClientsEl = document.getElementById('todayClients');
|
|
const followedClientsEl = document.getElementById('followedClients');
|
|
const notFollowedClientsEl = document.getElementById('notFollowedClients');
|
|
|
|
if (todayClientsEl) todayClientsEl.textContent = statsData.data.clientCount;
|
|
if (followedClientsEl) followedClientsEl.textContent = statsData.data.followStatus.followed;
|
|
if (notFollowedClientsEl) notFollowedClientsEl.textContent = statsData.data.followStatus.notFollowed;
|
|
|
|
// 渲染客户分配图表
|
|
renderClientAllocationChart(statsData.data.clientAllocation);
|
|
|
|
// 渲染客户跟进状态图表
|
|
renderFollowStatusChart(statsData.data.followStatus);
|
|
|
|
// 默认加载全部客户详情
|
|
if (detailsData) {
|
|
renderClientDetails(detailsData, startDate, endDate);
|
|
} else {
|
|
loadAllClients(startDate, endDate);
|
|
}
|
|
} else {
|
|
console.error('获取客户统计数据失败:', statsData.message);
|
|
}
|
|
}
|
|
|
|
// 加载全部客户详情
|
|
function loadAllClients(startDate, endDate, page = 1, pageSize = 10) {
|
|
// 更新筛选类型
|
|
currentFilterType = 'all';
|
|
currentManagerName = '';
|
|
|
|
// 构建缓存键
|
|
const cacheKey = `client_details_${startDate || 'all'}_${endDate || 'all'}_page=${page}&pageSize=${pageSize}`;
|
|
|
|
// 检查缓存
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
if (cachedData) {
|
|
console.log('使用缓存数据加载客户详情');
|
|
// 立即显示缓存数据
|
|
renderClientDetails(cachedData, startDate, endDate);
|
|
// 后台加载最新数据
|
|
loadLatestAllClients(startDate, endDate, page, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 没有缓存,显示加载状态
|
|
// 更新标题
|
|
const titleElement = document.getElementById('managerClientsTitle');
|
|
if (titleElement) {
|
|
titleElement.textContent = '全部客户详情';
|
|
}
|
|
|
|
// 显示加载状态
|
|
const container = document.getElementById('managerClientsContainer');
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
|
|
if (container && tableBody) {
|
|
container.style.display = 'block';
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #999;">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 从API获取数据
|
|
loadLatestAllClients(startDate, endDate, page, pageSize);
|
|
}
|
|
|
|
// 从API获取最新全部客户详情数据
|
|
function loadLatestAllClients(startDate, endDate, page = 1, pageSize = 10) {
|
|
// 构建缓存键
|
|
const cacheKey = `client_details_${startDate || 'all'}_${endDate || 'all'}_page=${page}&pageSize=${pageSize}`;
|
|
|
|
// 构建API URL,包含时间范围和分页参数,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
let apiUrl = `/api/client-details?page=${page}&pageSize=${pageSize}`;
|
|
if (startDate && endDate) {
|
|
apiUrl += `&startDate=${startDate}&endDate=${endDate}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
}
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 缓存数据
|
|
cacheManager.set(cacheKey, data);
|
|
|
|
// 渲染客户详情
|
|
renderClientDetails(data, startDate, endDate);
|
|
})
|
|
.catch(error => {
|
|
console.error('获取全部客户详情失败:', error);
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
if (tableBody) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取全部客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 渲染客户详情
|
|
function renderClientDetails(data, startDate, endDate) {
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
|
|
if (data.success) {
|
|
// 更新分页信息
|
|
currentPage = data.page || 1;
|
|
this.pageSize = data.pageSize || 10;
|
|
totalItems = data.total;
|
|
totalPages = Math.ceil(totalItems / this.pageSize);
|
|
|
|
// 渲染客户详情
|
|
renderManagerClients(data.data);
|
|
|
|
// 更新分页控件
|
|
updatePaginationControls();
|
|
} else {
|
|
console.error('获取全部客户详情失败:', data.message);
|
|
if (tableBody) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取全部客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
}
|
|
}
|
|
|
|
// 存储图表实例
|
|
let clientAllocationChart = null;
|
|
let followStatusChart = null;
|
|
|
|
// 渲染客户分配图表
|
|
function renderClientAllocationChart(allocationData) {
|
|
const canvas = document.getElementById('clientAllocationChart');
|
|
if (!canvas) {
|
|
console.warn('Canvas element not found: clientAllocationChart');
|
|
return;
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 准备数据
|
|
const labels = allocationData.map(item => {
|
|
// 将未分配改为公海池
|
|
if (item.managerName === '未分配' || !item.managerName) {
|
|
return '公海池';
|
|
}
|
|
return item.managerName;
|
|
});
|
|
const values = allocationData.map(item => item.clientCount);
|
|
|
|
// 生成颜色
|
|
const backgroundColors = generateChartColors(labels.length);
|
|
|
|
// 如果图表实例已存在,销毁它
|
|
if (clientAllocationChart) {
|
|
clientAllocationChart.destroy();
|
|
}
|
|
|
|
// 创建图表
|
|
clientAllocationChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: '客户数量',
|
|
data: values,
|
|
backgroundColor: backgroundColors,
|
|
borderColor: backgroundColors.map(color => color.replace('0.8', '1')),
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
title: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return `客户数量: ${context.parsed.y}`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
precision: 0
|
|
},
|
|
max: Math.max(...values) * 1.2 // 增加Y轴最大值,给标签留出空间
|
|
}
|
|
},
|
|
onClick: function(event, elements) {
|
|
if (elements.length > 0) {
|
|
const index = elements[0].index;
|
|
const managerName = labels[index];
|
|
// 弹出详情弹窗
|
|
showManagerClientsModal(managerName);
|
|
}
|
|
}
|
|
},
|
|
plugins: [{
|
|
id: 'customDataLabels',
|
|
afterDraw: function(chart) {
|
|
const ctx = chart.ctx;
|
|
|
|
chart.data.datasets.forEach((dataset, datasetIndex) => {
|
|
const meta = chart.getDatasetMeta(datasetIndex);
|
|
|
|
if (meta && meta.data) {
|
|
meta.data.forEach((element, index) => {
|
|
const value = dataset.data[index];
|
|
|
|
if (value !== undefined && value !== null) {
|
|
// 获取柱子的位置和尺寸
|
|
const x = element.x;
|
|
const y = element.y;
|
|
|
|
// 计算标注位置,调整为柱子顶部下方一点
|
|
const labelX = x;
|
|
const labelY = y - 10;
|
|
|
|
// 绘制标注
|
|
ctx.save();
|
|
ctx.font = 'bold 14px Arial';
|
|
ctx.fillStyle = '#333';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'bottom';
|
|
ctx.fillText(value, labelX, labelY);
|
|
ctx.restore();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}]
|
|
});
|
|
}
|
|
|
|
// 渲染客户跟进状态图表
|
|
function renderFollowStatusChart(followStatus) {
|
|
const ctx = document.getElementById('followStatusChart').getContext('2d');
|
|
|
|
// 准备数据
|
|
const labels = ['已跟进', '未跟进'];
|
|
const values = [followStatus.followed, followStatus.notFollowed];
|
|
const backgroundColors = ['rgba(16, 185, 129, 0.8)', 'rgba(239, 68, 68, 0.8)'];
|
|
const borderColors = ['rgba(16, 185, 129, 1)', 'rgba(239, 68, 68, 1)'];
|
|
|
|
// 如果图表实例已存在,销毁它
|
|
if (followStatusChart) {
|
|
followStatusChart.destroy();
|
|
}
|
|
|
|
// 创建图表
|
|
followStatusChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: values,
|
|
backgroundColor: backgroundColors,
|
|
borderColor: borderColors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
},
|
|
onClick: function(event, elements) {
|
|
if (elements.length > 0) {
|
|
const index = elements[0].index;
|
|
const status = labels[index];
|
|
|
|
// 根据状态加载对应客户并弹出弹窗
|
|
if (status === '已跟进') {
|
|
showClientsByFollowStatusModal('followed');
|
|
} else if (status === '未跟进') {
|
|
showClientsByFollowStatusModal('notFollowed');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 根据跟进状态加载客户
|
|
function loadClientsByFollowStatus(followStatus, page = 1, pageSize = 10) {
|
|
// 更新筛选类型
|
|
currentFilterType = 'followStatus';
|
|
|
|
// 使用全局变量中保存的当前时间范围
|
|
const startDate = currentStartDate;
|
|
const endDate = currentEndDate;
|
|
|
|
// 构建API URL,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
const apiUrl = `/api/client-details?startDate=${startDate}&endDate=${endDate}&followStatus=${followStatus}&page=${page}&pageSize=${pageSize}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
|
|
// 更新标题
|
|
const statusText = followStatus === 'followed' ? '已跟进' : '未跟进';
|
|
const titleElement = document.getElementById('managerClientsTitle');
|
|
if (titleElement) {
|
|
titleElement.textContent = `${statusText}客户详情`;
|
|
}
|
|
|
|
// 显示加载状态
|
|
const container = document.getElementById('managerClientsContainer');
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
|
|
if (container && tableBody) {
|
|
container.style.display = 'block';
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #999;">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新分页信息
|
|
currentPage = page;
|
|
this.pageSize = pageSize;
|
|
totalItems = data.total;
|
|
totalPages = Math.ceil(totalItems / pageSize);
|
|
|
|
// 渲染客户详情
|
|
renderManagerClients(data.data);
|
|
|
|
// 更新分页控件
|
|
updatePaginationControls();
|
|
} else {
|
|
console.error('获取客户详情失败:', data.message);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取客户详情失败:', error);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
});
|
|
}
|
|
|
|
// 生成图表颜色
|
|
function generateChartColors(count) {
|
|
const colors = [];
|
|
const baseColors = [
|
|
'rgba(54, 126, 233, 0.8)',
|
|
'rgba(127, 86, 217, 0.8)',
|
|
'rgba(16, 185, 129, 0.8)',
|
|
'rgba(245, 158, 11, 0.8)',
|
|
'rgba(239, 68, 68, 0.8)',
|
|
'rgba(59, 130, 246, 0.8)',
|
|
'rgba(168, 85, 247, 0.8)',
|
|
'rgba(236, 72, 153, 0.8)',
|
|
'rgba(249, 115, 22, 0.8)',
|
|
'rgba(10, 148, 134, 0.8)'
|
|
];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
colors.push(baseColors[i % baseColors.length]);
|
|
}
|
|
|
|
return colors;
|
|
}
|
|
|
|
// 客户详情弹窗组件
|
|
function showClientDetailModal(title, data, type = 'default') {
|
|
// 创建模态框HTML
|
|
let modalHTML = `
|
|
<div id="clientDetailModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
|
|
<div style="background-color: white; border-radius: 8px; width: 90%; max-width: 800px; height: 90%; display: flex; flex-direction: column;">
|
|
<div style="padding: 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
|
<h3 style="margin-top: 0; margin-bottom: 0; color: #333;">${title}</h3>
|
|
<button onclick="closeClientDetailModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
</div>
|
|
<div style="padding: 24px; flex-grow: 1; overflow-y: auto;">
|
|
`;
|
|
|
|
// 根据数据类型渲染不同内容
|
|
if (type === 'clientList') {
|
|
// 渲染客户列表
|
|
if (data.length === 0) {
|
|
modalHTML += `
|
|
<div style="text-align: center; padding: 40px; color: #999;">
|
|
暂无客户数据
|
|
</div>
|
|
`;
|
|
} else {
|
|
modalHTML += `
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
<thead>
|
|
<tr style="background-color: #f8fafc;">
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">客户姓名</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">电话号码</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">跟进状态</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">跟进内容</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">跟进时间</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">更新时间</th>
|
|
<th style="padding: 12px; text-align: left; border-bottom: 1px solid #e8e8e8;">负责人</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
data.forEach(client => {
|
|
const statusClass = client.status === '已跟进' ? 'text-success' : 'text-warning';
|
|
const followupTime = client.followup_at ? new Date(client.followup_at).toLocaleString('zh-CN') : '-';
|
|
const updatedTime = client.updated_at ? new Date(client.updated_at).toLocaleString('zh-CN') : '-';
|
|
|
|
modalHTML += `
|
|
<tr style="transition: all 0.3s ease;">
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">${client.nickName || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">${client.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">
|
|
<span class="${statusClass}">${client.status}</span>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: normal; word-wrap: break-word;">${client.followup || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">${followupTime}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">${updatedTime}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid #e8e8e8; white-space: nowrap;">${client.userName || '-'}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
modalHTML += `
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
}
|
|
} else if (type === 'stats') {
|
|
// 渲染统计详情
|
|
modalHTML += `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">统计详情</h4>
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
`;
|
|
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
const label = getStatsLabel(key);
|
|
modalHTML += `
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">${label}</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${value}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
modalHTML += `
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 结束模态框HTML
|
|
modalHTML += `
|
|
</div>
|
|
<div style="padding: 24px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; align-items: center; flex-shrink: 0;">
|
|
<button onclick="closeClientDetailModal()" style="padding: 8px 16px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">关闭</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 添加模态框到页面
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
}
|
|
|
|
// 关闭客户详情模态框
|
|
function closeClientDetailModal() {
|
|
const modal = document.getElementById('clientDetailModal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
// 获取统计标签
|
|
function getStatsLabel(key) {
|
|
const labels = {
|
|
clientCount: '新增客户',
|
|
followed: '已跟进客户',
|
|
notFollowed: '未跟进客户',
|
|
total: '总客户数'
|
|
};
|
|
return labels[key] || key;
|
|
}
|
|
|
|
// 显示业务员客户详情弹窗
|
|
function showManagerClientsModal(managerName) {
|
|
// 使用全局变量中保存的当前时间范围
|
|
const startDate = currentStartDate;
|
|
const endDate = currentEndDate;
|
|
|
|
let apiUrl;
|
|
|
|
// 如果是公海池,调用获取未分配客户的API
|
|
if (managerName === '公海池') {
|
|
// 对于公海池,我们需要获取未分配的客户,即没有在usermanagements表中记录的客户
|
|
apiUrl = `/api/client-details?startDate=${startDate}&endDate=${endDate}&isUnassigned=true&page=1&pageSize=100&newClientBy=created_at&followedClientBy=updated_at`;
|
|
} else {
|
|
// 构建API URL,包含分页参数,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
apiUrl = `/api/manager-clients?managerName=${encodeURIComponent(managerName)}&startDate=${startDate}&endDate=${endDate}&page=1&pageSize=100&newClientBy=created_at&followedClientBy=updated_at`;
|
|
}
|
|
|
|
// 显示加载状态
|
|
showClientDetailModal(`${managerName}的客户详情`, [{status: '加载中...'}], 'clientList');
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
|
|
if (data.success) {
|
|
// 显示客户详情弹窗
|
|
showClientDetailModal(`${managerName}的客户详情`, data.data, 'clientList');
|
|
} else {
|
|
// 显示错误信息
|
|
showClientDetailModal(`${managerName}的客户详情`, [], 'clientList');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取业务员客户详情失败:', error);
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
// 显示错误信息
|
|
showClientDetailModal(`${managerName}的客户详情`, [], 'clientList');
|
|
});
|
|
}
|
|
|
|
// 显示跟进状态客户详情弹窗
|
|
function showClientsByFollowStatusModal(followStatus) {
|
|
// 使用全局变量中保存的当前时间范围
|
|
const startDate = currentStartDate;
|
|
const endDate = currentEndDate;
|
|
|
|
// 构建API URL,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
const apiUrl = `/api/client-details?startDate=${startDate}&endDate=${endDate}&followStatus=${followStatus}&page=1&pageSize=100&newClientBy=created_at&followedClientBy=updated_at`;
|
|
|
|
// 获取状态文本
|
|
const statusText = followStatus === 'followed' ? '已跟进' : '未跟进';
|
|
|
|
// 显示加载状态
|
|
showClientDetailModal(`${statusText}客户详情`, [{status: '加载中...'}], 'clientList');
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
|
|
if (data.success) {
|
|
// 显示客户详情弹窗
|
|
showClientDetailModal(`${statusText}客户详情`, data.data, 'clientList');
|
|
} else {
|
|
// 显示错误信息
|
|
showClientDetailModal(`${statusText}客户详情`, [], 'clientList');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取客户详情失败:', error);
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
// 显示错误信息
|
|
showClientDetailModal(`${statusText}客户详情`, [], 'clientList');
|
|
});
|
|
}
|
|
|
|
// 显示新增客户详情弹窗
|
|
function showNewClientsModal() {
|
|
// 使用全局变量中保存的当前时间范围
|
|
const startDate = currentStartDate;
|
|
const endDate = currentEndDate;
|
|
|
|
// 构建API URL,指定新增客户以创建时间为准
|
|
const apiUrl = `/api/client-details?startDate=${startDate}&endDate=${endDate}&clientType=new&page=1&pageSize=100&newClientBy=created_at`;
|
|
|
|
// 显示加载状态
|
|
showClientDetailModal('新增客户详情', [{status: '加载中...'}], 'clientList');
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
|
|
if (data.success) {
|
|
// 显示客户详情弹窗
|
|
showClientDetailModal('新增客户详情', data.data, 'clientList');
|
|
} else {
|
|
// 显示错误信息
|
|
showClientDetailModal('新增客户详情', [], 'clientList');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取新增客户详情失败:', error);
|
|
// 关闭当前模态框
|
|
closeClientDetailModal();
|
|
// 显示错误信息
|
|
showClientDetailModal('新增客户详情', [], 'clientList');
|
|
});
|
|
}
|
|
|
|
// 显示已跟进客户详情弹窗
|
|
function showFollowedClientsModal() {
|
|
// 直接调用已有的函数
|
|
showClientsByFollowStatusModal('followed');
|
|
}
|
|
|
|
// 显示未跟进客户详情弹窗
|
|
function showNotFollowedClientsModal() {
|
|
// 直接调用已有的函数
|
|
showClientsByFollowStatusModal('notFollowed');
|
|
}
|
|
|
|
|
|
|
|
// 加载指定业务员的客户详情
|
|
function loadManagerClients(managerName, page = 1, pageSize = 10) {
|
|
// 更新筛选类型
|
|
currentFilterType = 'manager';
|
|
currentManagerName = managerName;
|
|
|
|
// 使用全局变量中保存的当前时间范围
|
|
const startDate = currentStartDate;
|
|
const endDate = currentEndDate;
|
|
|
|
let apiUrl;
|
|
|
|
// 如果是公海池,调用获取未分配客户的API
|
|
if (managerName === '公海池') {
|
|
// 对于公海池,我们需要获取未分配的客户,即没有在usermanagements表中记录的客户
|
|
apiUrl = `/api/client-details?startDate=${startDate}&endDate=${endDate}&isUnassigned=true&page=${page}&pageSize=${pageSize}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
} else {
|
|
// 构建API URL,包含分页参数,指定新增客户以创建时间为准,已跟进客户以修改时间为准
|
|
apiUrl = `/api/manager-clients?managerName=${encodeURIComponent(managerName)}&startDate=${startDate}&endDate=${endDate}&page=${page}&pageSize=${pageSize}&newClientBy=created_at&followedClientBy=updated_at`;
|
|
}
|
|
|
|
// 更新标题
|
|
const titleElement = document.getElementById('managerClientsTitle');
|
|
if (titleElement) {
|
|
titleElement.textContent = `${managerName}的客户详情`;
|
|
}
|
|
|
|
// 显示加载状态
|
|
const container = document.getElementById('managerClientsContainer');
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
|
|
if (container && tableBody) {
|
|
container.style.display = 'block';
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #999;">
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 发送请求
|
|
fetch(apiUrl)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新分页信息
|
|
currentPage = page;
|
|
this.pageSize = pageSize;
|
|
totalItems = data.total;
|
|
totalPages = Math.ceil(totalItems / pageSize);
|
|
|
|
// 渲染业务员客户详情
|
|
renderManagerClients(data.data);
|
|
|
|
// 更新分页控件
|
|
updatePaginationControls();
|
|
} else {
|
|
console.error('获取业务员客户详情失败:', data.message);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取业务员客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取业务员客户详情失败:', error);
|
|
if (tableBody) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #ff4d4f;">
|
|
获取业务员客户详情失败
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
// 重置分页控件
|
|
resetPaginationControls();
|
|
});
|
|
}
|
|
|
|
// 渲染业务员客户详情
|
|
function renderManagerClients(clientData) {
|
|
const tableBody = document.getElementById('managerClientsTableBody');
|
|
|
|
// 检查元素是否存在
|
|
if (!tableBody) {
|
|
return;
|
|
}
|
|
|
|
if (clientData.length === 0) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="padding: 40px; text-align: center; color: #999;">
|
|
暂无客户数据
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tableBody.innerHTML = clientData.map(client => {
|
|
const statusClass = client.status === '已跟进' ? 'text-success' : 'text-warning';
|
|
const followupTime = client.followup_at ? new Date(client.followup_at).toLocaleString('zh-CN') : '-';
|
|
const updatedTime = client.updated_at ? new Date(client.updated_at).toLocaleString('zh-CN') : '-';
|
|
|
|
return `
|
|
<tr style="transition: all 0.3s ease; cursor: pointer; background-color: rgba(255, 255, 255, 0.4);" onmouseover="this.style.backgroundColor='rgba(245, 245, 245, 0.6)'" onmouseout="this.style.backgroundColor='rgba(255, 255, 255, 0.4)'">
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${client.nickName || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${client.phoneNumber || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">
|
|
<span class="${statusClass}">${client.status}</span>
|
|
</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${client.followup || '-'}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${followupTime}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${updatedTime}</td>
|
|
<td style="padding: 12px; border-bottom: 1px solid rgba(232, 232, 232, 0.5);">${client.userName || '-'}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 更新分页控件状态
|
|
function updatePaginationControls() {
|
|
const prevBtn = document.getElementById('prevPageBtn');
|
|
const nextBtn = document.getElementById('nextPageBtn');
|
|
const pageInfo = document.getElementById('pageInfo');
|
|
const pageSizeInput = document.getElementById('pageSizeInput');
|
|
|
|
// 检查元素是否存在
|
|
if (!prevBtn || !nextBtn || !pageInfo || !pageSizeInput) {
|
|
return;
|
|
}
|
|
|
|
// 更新页码信息
|
|
pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页 (共 ${totalItems} 条)`;
|
|
|
|
// 更新按钮状态
|
|
prevBtn.disabled = currentPage === 1;
|
|
nextBtn.disabled = currentPage === totalPages;
|
|
|
|
// 更新每页大小输入框
|
|
pageSizeInput.value = pageSize;
|
|
}
|
|
|
|
// 重置分页控件
|
|
function resetPaginationControls() {
|
|
const prevBtn = document.getElementById('prevPageBtn');
|
|
const nextBtn = document.getElementById('nextPageBtn');
|
|
const pageInfo = document.getElementById('pageInfo');
|
|
const pageSizeInput = document.getElementById('pageSizeInput');
|
|
|
|
// 检查元素是否存在
|
|
if (!prevBtn || !nextBtn || !pageInfo || !pageSizeInput) {
|
|
return;
|
|
}
|
|
|
|
// 重置页码信息
|
|
currentPage = 1;
|
|
totalPages = 1;
|
|
totalItems = 0;
|
|
|
|
// 更新控件状态
|
|
pageInfo.textContent = `第 1 页 / 共 1 页 (共 0 条)`;
|
|
prevBtn.disabled = true;
|
|
nextBtn.disabled = true;
|
|
pageSizeInput.value = 10;
|
|
}
|
|
|
|
// 切换页码
|
|
function changeClientPage(direction) {
|
|
const newPage = currentPage + direction;
|
|
if (newPage >= 1 && newPage <= totalPages) {
|
|
// 根据当前筛选类型加载数据
|
|
if (currentFilterType === 'all') {
|
|
loadAllClients(currentStartDate, currentEndDate, newPage, pageSize);
|
|
} else {
|
|
loadManagerClients(currentManagerName, newPage, pageSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 更改每页大小
|
|
function changeClientPageSize() {
|
|
const newPageSize = parseInt(document.getElementById('pageSizeInput').value);
|
|
if (newPageSize >= 1 && newPageSize <= 100) {
|
|
pageSize = newPageSize;
|
|
currentPage = 1; // 重置为第一页
|
|
|
|
// 根据当前筛选类型加载数据
|
|
if (currentFilterType === 'all') {
|
|
loadAllClients(currentStartDate, currentEndDate, currentPage, pageSize);
|
|
} else {
|
|
loadManagerClients(currentManagerName, currentPage, pageSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 搜索日志
|
|
function searchLogs() {
|
|
loadLogs(1);
|
|
}
|
|
|
|
// 重置搜索
|
|
function resetSearch() {
|
|
document.getElementById('searchUserName').value = '';
|
|
document.getElementById('searchUserId').value = '';
|
|
document.getElementById('searchPhone').value = '';
|
|
document.getElementById('searchSystem').value = '';
|
|
document.getElementById('startDate').value = '';
|
|
document.getElementById('endDate').value = '';
|
|
loadLogs(1);
|
|
}
|
|
|
|
// 分页
|
|
function changePage(direction) {
|
|
const currentPage = parseInt(document.getElementById('currentPage').textContent);
|
|
const newPage = currentPage + direction;
|
|
|
|
// 根据当前日志类型调用不同的加载函数
|
|
if (window.currentLogType === 'product') {
|
|
loadProductLogs(newPage);
|
|
} else {
|
|
loadLogs(newPage);
|
|
}
|
|
}
|
|
|
|
// 显示产品生命周期日志详情
|
|
function showProductLifecycleDetail(log) {
|
|
// 创建模态框HTML
|
|
let modalHTML = `
|
|
<div id="logDetailModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
|
|
<div style="background-color: white; border-radius: 8px; width: 95%; height: 95%; display: flex; flex-direction: column;">
|
|
<div style="padding: 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
|
<h3 style="margin-top: 0; margin-bottom: 0; color: #333;">日志详情</h3>
|
|
<button onclick="closeLogDetailModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
</div>
|
|
<div style="padding: 24px; flex-grow: 1; overflow-y: auto;">
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">基本信息</h4>
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作时间</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作人</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.operationUser || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作事件</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">电话号码</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.phoneNumber || '-'}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
${log.originalData && log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">数据对比</h4>
|
|
${renderDataComparison(log.originalData, log.modifiedData, log.changedFields)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${(!log.originalData || !log.modifiedData) ? `
|
|
${log.originalData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">原始数据</h4>
|
|
${renderDataAsTabbedForm(log.originalData)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">修改后数据</h4>
|
|
${renderDataAsTabbedForm(log.modifiedData)}
|
|
</div>
|
|
` : ''}
|
|
` : ''}
|
|
|
|
</div>
|
|
<div style="padding: 24px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; align-items: center; flex-shrink: 0;">
|
|
<button onclick="closeLogDetailModal()" style="padding: 8px 16px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 添加模态框到页面
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
}
|
|
|
|
// 显示业务员操作记录详情
|
|
function showAgentLogDetails(log) {
|
|
// 创建模态框HTML
|
|
let modalHTML = `
|
|
<div id="logDetailModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
|
|
<div style="background-color: white; border-radius: 8px; width: 95%; height: 95%; display: flex; flex-direction: column;">
|
|
<div style="padding: 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
|
<h3 style="margin-top: 0; margin-bottom: 0; color: #333;">日志详情</h3>
|
|
<button onclick="closeLogDetailModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
</div>
|
|
<div style="padding: 24px; flex-grow: 1; overflow-y: auto;">
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">基本信息</h4>
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作时间</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作人</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.trauserName || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">客户ID</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.userId || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">客户电话</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.customerPhone || log.phoneNumber || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px; font-weight: bold;">操作事件</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
${log.originalData && log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">数据对比</h4>
|
|
${renderDataComparison(log.originalData, log.modifiedData, log.changedFields)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${(!log.originalData || !log.modifiedData) ? `
|
|
${log.originalData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">原始数据</h4>
|
|
${renderDataAsTabbedForm(log.originalData)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">修改后数据</h4>
|
|
${renderDataAsTabbedForm(log.modifiedData)}
|
|
</div>
|
|
` : ''}
|
|
` : ''}
|
|
|
|
</div>
|
|
<div style="padding: 24px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; align-items: center; flex-shrink: 0;">
|
|
<button onclick="closeLogDetailModal()" style="padding: 8px 16px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 添加模态框到页面
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
}
|
|
|
|
// 显示日志详情
|
|
function showLogDetails(logId) {
|
|
fetch(`/api/logs/${logId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const log = data.log;
|
|
|
|
// 创建模态框HTML
|
|
let modalHTML = `
|
|
<div id="logDetailModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
|
|
<div style="background-color: white; border-radius: 8px; width: 95%; height: 95%; display: flex; flex-direction: column;">
|
|
<div style="padding: 24px; border-bottom: 1px solid #e8e8e8; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
|
<h3 style="margin-top: 0; margin-bottom: 0; color: #333;">日志详情</h3>
|
|
<button onclick="closeLogDetailModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;">×</button>
|
|
</div>
|
|
<div style="padding: 24px; flex-grow: 1; overflow-y: auto;">
|
|
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">基本信息</h4>
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold; width: 120px;">操作时间:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${new Date(log.operationTime).toLocaleString('zh-CN')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">操作人:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.trauserName || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">公司:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.tracompany || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">部门:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.tradepartment || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">组织:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.traorganization || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">角色:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.trarole || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">客户ID:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.userId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">电话号码:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.phoneNumber || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">操作事件:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${formatOperationEvent(log.operationEvent) || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">操作协助人:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${log.traassistant || '-'}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
${log.originalData && log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">数据对比</h4>
|
|
${renderDataComparison(log.originalData, log.modifiedData, log.changedFields)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${(!log.originalData || !log.modifiedData) ? `
|
|
${log.originalData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">原始数据</h4>
|
|
${renderDataAsTabbedForm(log.originalData)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${log.modifiedData ? `
|
|
<div style="margin-bottom: 20px;">
|
|
<h4 style="margin-top: 0; margin-bottom: 12px; color: #666;">修改后数据</h4>
|
|
${renderDataAsTabbedForm(log.modifiedData)}
|
|
</div>
|
|
` : ''}
|
|
` : ''}
|
|
|
|
</div>
|
|
<div style="padding: 24px; border-top: 1px solid #e8e8e8; display: flex; justify-content: flex-end; align-items: center; flex-shrink: 0;">
|
|
<button onclick="closeLogDetailModal()" style="padding: 8px 16px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 添加模态框到页面
|
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
|
} else {
|
|
alert('获取日志详情失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取日志详情失败:', error);
|
|
alert('获取日志详情失败,请重试');
|
|
});
|
|
}
|
|
|
|
// 关闭日志详情模态框
|
|
function closeLogDetailModal() {
|
|
const modal = document.getElementById('logDetailModal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
}
|
|
|
|
// 解析数据为可读格式
|
|
function parseData(data) {
|
|
try {
|
|
const parsedData = JSON.parse(data);
|
|
return JSON.stringify(parsedData, null, 2);
|
|
} catch (e) {
|
|
return data;
|
|
}
|
|
}
|
|
|
|
// 将数据渲染为可视化表单
|
|
function renderDataAsForm(data) {
|
|
try {
|
|
const parsedData = JSON.parse(data);
|
|
|
|
// 检测是否为产品数据
|
|
if (parsedData.productId && parsedData.productName) {
|
|
return `
|
|
<div style="background-color: #f9f9f9; border-radius: 4px; padding: 16px;">
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
${renderProductField('产品ID', parsedData.productId)}
|
|
${renderProductField('卖家ID', parsedData.sellerId)}
|
|
${renderProductField('产品名称', parsedData.productName)}
|
|
${renderProductField('销售价格', parsedData.price)}
|
|
${renderProductField('采购价格', parsedData.costprice)}
|
|
${renderProductField('品种', parsedData.variety)}
|
|
${renderProductField('产品包装', parsedData.producting)}
|
|
${renderProductField('新鲜程度', parsedData.freshness)}
|
|
${renderProductField('毛重', parsedData.grossWeight)}
|
|
${renderProductField('规格', parsedData.specification)}
|
|
${renderProductField('数量', parsedData.quantity)}
|
|
${renderProductField('状态', parsedData.status)}
|
|
${renderProductField('创建时间', parsedData.created_at)}
|
|
${renderProductField('更新时间', parsedData.updated_at)}
|
|
${renderProductField('蛋黄', parsedData.yolk)}
|
|
${renderProductField('驳回原因', parsedData.rejectReason)}
|
|
${renderProductField('已有几人想要', parsedData.reservedCount)}
|
|
${renderProductField('审核时间', parsedData.audit_time)}
|
|
${renderProductField('商品地区信息', parsedData.region)}
|
|
${renderProductField('商品联系人', parsedData.product_contact)}
|
|
${renderProductField('商品联系人电话号码', parsedData.contact_phone)}
|
|
${renderProductField('货源状态', parsedData.supplyStatus)}
|
|
${renderProductField('货源描述', parsedData.description)}
|
|
${renderProductField('货源类型', parsedData.sourceType)}
|
|
${renderProductField('种类', parsedData.category)}
|
|
${renderProductField('自动下架时间', parsedData.autoOfflineTime)}
|
|
${renderProductField('自动下架天数', parsedData.autoOfflineDays)}
|
|
${renderProductField('自动下架小时数', parsedData.autoOfflineHours)}
|
|
${renderProductField('货源标签是否上锁', parsedData.label === 0 ? '未锁定' : '已锁定')}
|
|
${renderProductField('频率', parsedData.frequency)}
|
|
${renderProductField('产品日志', parsedData.product_log)}
|
|
${renderProductField('电话号码', parsedData.phoneNumber)}
|
|
${renderProductField('昵称', parsedData.nickName)}
|
|
</table>
|
|
${parsedData.imageUrls ? `
|
|
<div style="margin-top: 16px;">
|
|
<h5 style="margin-top: 0; margin-bottom: 8px; color: #666;">商品图片</h5>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
${renderImageUrls(parsedData.imageUrls)}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
} else {
|
|
// 不是产品数据,显示格式化的JSON
|
|
return `
|
|
<div style="background-color: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 13px;">
|
|
${JSON.stringify(parsedData, null, 2)}
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
// 解析失败,显示原始数据
|
|
return `
|
|
<div style="background-color: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 13px;">
|
|
${data}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 字段值中文映射
|
|
function getChineseValue(field, value) {
|
|
const mappings = {
|
|
status: {
|
|
'pending_review': '待审核',
|
|
'published': '已发布',
|
|
'rejected': '已驳回',
|
|
'offline': '已下架'
|
|
},
|
|
freshness: {
|
|
'一般': '一般',
|
|
'fresh': '新鲜',
|
|
'very_fresh': '非常新鲜',
|
|
'一级': '一级',
|
|
'二级': '二级',
|
|
'三级': '三级'
|
|
},
|
|
supplyStatus: {
|
|
'plenty': '充足',
|
|
'limited': '有限',
|
|
'out_of_stock': '缺货',
|
|
'预售': '预售'
|
|
},
|
|
sourceType: {
|
|
'farm_direct': '农场直销',
|
|
'market_purchase': '市场采购',
|
|
'distributor': '经销商',
|
|
'商场直销': '商场直销'
|
|
},
|
|
category: {
|
|
'vegetable': '蔬菜',
|
|
'fruit': '水果',
|
|
'meat': '肉类',
|
|
'seafood': '海鲜',
|
|
'grain': '粮油',
|
|
'dairy': '乳制品',
|
|
'egg': '蛋类',
|
|
'绿壳': '绿壳'
|
|
}
|
|
};
|
|
|
|
if (mappings[field] && mappings[field][value]) {
|
|
return mappings[field][value];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// 渲染产品字段
|
|
function renderProductField(label, value) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return '';
|
|
}
|
|
|
|
// 为特定字段做中文映射
|
|
let displayValue = value;
|
|
if (label === '状态') {
|
|
displayValue = getChineseValue('status', value);
|
|
} else if (label === '新鲜程度') {
|
|
displayValue = getChineseValue('freshness', value);
|
|
} else if (label === '货源状态') {
|
|
displayValue = getChineseValue('supplyStatus', value);
|
|
} else if (label === '货源类型') {
|
|
displayValue = getChineseValue('sourceType', value);
|
|
} else if (label === '种类') {
|
|
displayValue = getChineseValue('category', value);
|
|
}
|
|
|
|
return `
|
|
<tr>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold; width: 150px;">${label}:</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${displayValue}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 渲染图片URLs
|
|
function renderImageUrls(imageUrls) {
|
|
try {
|
|
// 检查imageUrls是否已经是数组
|
|
if (Array.isArray(imageUrls)) {
|
|
return imageUrls.map(url => `
|
|
<div style="width: 100px; height: 100px; overflow: hidden; border-radius: 4px;">
|
|
<img src="${url}" alt="商品图片" style="width: 100%; height: 100%; object-fit: cover;">
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 尝试解析字符串形式的JSON
|
|
const urls = JSON.parse(imageUrls);
|
|
if (Array.isArray(urls)) {
|
|
return urls.map(url => `
|
|
<div style="width: 100px; height: 100px; overflow: hidden; border-radius: 4px;">
|
|
<img src="${url}" alt="商品图片" style="width: 100%; height: 100%; object-fit: cover;">
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
return '';
|
|
}
|
|
} catch (e) {
|
|
console.error('解析图片URLs失败:', e);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// 将数据渲染为标签页形式的表单
|
|
function renderDataAsTabbedForm(data) {
|
|
try {
|
|
const parsedData = JSON.parse(data);
|
|
|
|
// 检测是否为产品数据
|
|
if (parsedData.productId && parsedData.productName) {
|
|
// 处理imageUrls字段,确保它是一个数组
|
|
let imageUrls = [];
|
|
if (parsedData.imageUrls) {
|
|
if (Array.isArray(parsedData.imageUrls)) {
|
|
imageUrls = parsedData.imageUrls;
|
|
} else if (typeof parsedData.imageUrls === 'string') {
|
|
try {
|
|
imageUrls = JSON.parse(parsedData.imageUrls);
|
|
if (!Array.isArray(imageUrls)) {
|
|
imageUrls = [];
|
|
}
|
|
} catch (e) {
|
|
console.error('解析imageUrls字符串失败:', e);
|
|
imageUrls = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div style="background-color: #f9f9f9; border-radius: 4px;">
|
|
<!-- 标签页导航 -->
|
|
<div style="display: flex; border-bottom: 1px solid #e8e8e8; background-color: #f0f0f0;">
|
|
<button class="tab-btn" onclick="switchTab(this, 'basic-info')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px; border-bottom: 2px solid #1890ff; color: #1890ff;">基本信息</button>
|
|
<button class="tab-btn" onclick="switchTab(this, 'product-details')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px;">产品详情</button>
|
|
<button class="tab-btn" onclick="switchTab(this, 'business-info')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px;">业务信息</button>
|
|
<button class="tab-btn" onclick="switchTab(this, 'images')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px;">商品图片</button>
|
|
</div>
|
|
|
|
<!-- 标签页内容 -->
|
|
<div class="tab-content" id="basic-info" style="padding: 16px; display: block;">
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
${renderProductField('产品ID', parsedData.productId)}
|
|
${renderProductField('卖家ID', parsedData.sellerId)}
|
|
${renderProductField('产品名称', parsedData.productName)}
|
|
${renderProductField('状态', parsedData.status)}
|
|
${renderProductField('创建时间', parsedData.created_at)}
|
|
${renderProductField('更新时间', parsedData.updated_at)}
|
|
${renderProductField('审核时间', parsedData.audit_time)}
|
|
${renderProductField('商品地区信息', parsedData.region)}
|
|
${renderProductField('商品联系人', parsedData.product_contact)}
|
|
${renderProductField('商品联系人电话号码', parsedData.contact_phone)}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="tab-content" id="product-details" style="padding: 16px; display: none;">
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
${renderProductField('销售价格', parsedData.price)}
|
|
${renderProductField('采购价格', parsedData.costprice)}
|
|
${renderProductField('品种', parsedData.variety)}
|
|
${renderProductField('产品包装', parsedData.producting)}
|
|
${renderProductField('新鲜程度', parsedData.freshness)}
|
|
${renderProductField('毛重', parsedData.grossWeight)}
|
|
${renderProductField('规格', parsedData.specification)}
|
|
${renderProductField('数量', parsedData.quantity)}
|
|
${renderProductField('蛋黄', parsedData.yolk)}
|
|
${renderProductField('驳回原因', parsedData.rejectReason)}
|
|
${renderProductField('货源状态', parsedData.supplyStatus)}
|
|
${renderProductField('货源描述', parsedData.description)}
|
|
${renderProductField('货源类型', parsedData.sourceType)}
|
|
${renderProductField('种类', parsedData.category)}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="tab-content" id="business-info" style="padding: 16px; display: none;">
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
${renderProductField('已有几人想要', parsedData.reservedCount)}
|
|
${renderProductField('自动下架时间', parsedData.autoOfflineTime)}
|
|
${renderProductField('自动下架天数', parsedData.autoOfflineDays)}
|
|
${renderProductField('自动下架小时数', parsedData.autoOfflineHours)}
|
|
${renderProductField('货源标签是否上锁', parsedData.label === 0 ? '未锁定' : '已锁定')}
|
|
${renderProductField('频率', parsedData.frequency)}
|
|
${renderProductField('产品日志', parsedData.product_log)}
|
|
${renderProductField('电话号码', parsedData.phoneNumber)}
|
|
${renderProductField('昵称', parsedData.nickName)}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="tab-content" id="images" style="padding: 16px; display: none;">
|
|
${imageUrls.length > 0 ? `
|
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
${imageUrls.map(url => {
|
|
// 检测是否为视频URL
|
|
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'];
|
|
const isVideo = videoExtensions.some(ext => url.toLowerCase().includes(ext));
|
|
|
|
if (isVideo) {
|
|
return `
|
|
<div style="width: 100px; height: 100px; overflow: hidden; border-radius: 4px;">
|
|
<video src="${url}" alt="商品视频" style="width: 100%; height: 100%; object-fit: cover;" controls></video>
|
|
</div>
|
|
`;
|
|
} else {
|
|
return `
|
|
<div style="width: 100px; height: 100px; overflow: hidden; border-radius: 4px;">
|
|
<img src="${url}" alt="商品图片" style="width: 100%; height: 100%; object-fit: cover;">
|
|
</div>
|
|
`;
|
|
}
|
|
}).join('')}
|
|
</div>
|
|
` : `
|
|
<div style="color: #999; text-align: center; padding: 40px;">暂无商品图片</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// 不是产品数据,显示格式化的JSON
|
|
return `
|
|
<div style="background-color: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 13px;">
|
|
${JSON.stringify(parsedData, null, 2)}
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
console.error('解析数据失败:', e);
|
|
// 解析失败,显示原始数据
|
|
return `
|
|
<div style="background-color: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 13px;">
|
|
${data}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 切换标签页
|
|
function switchTab(button, tabId) {
|
|
// 移除所有标签页按钮的活跃状态
|
|
const buttons = button.parentElement.querySelectorAll('.tab-btn');
|
|
buttons.forEach(btn => {
|
|
btn.style.borderBottom = 'none';
|
|
btn.style.color = '#666';
|
|
});
|
|
|
|
// 添加当前按钮的活跃状态
|
|
button.style.borderBottom = '2px solid #1890ff';
|
|
button.style.color = '#1890ff';
|
|
|
|
// 隐藏所有标签页内容
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
tabContents.forEach(content => {
|
|
content.style.display = 'none';
|
|
});
|
|
|
|
// 显示当前标签页内容
|
|
const currentTab = document.getElementById(tabId);
|
|
if (currentTab) {
|
|
currentTab.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// 渲染数据对比
|
|
function renderDataComparison(originalData, modifiedData, changedFields) {
|
|
try {
|
|
const original = JSON.parse(originalData);
|
|
const modified = JSON.parse(modifiedData);
|
|
|
|
// 解析变更字段
|
|
let changedFieldsArray = [];
|
|
if (changedFields) {
|
|
try {
|
|
changedFieldsArray = JSON.parse(changedFields);
|
|
if (!Array.isArray(changedFieldsArray)) {
|
|
changedFieldsArray = [];
|
|
}
|
|
} catch (e) {
|
|
console.error('解析变更字段失败:', e);
|
|
changedFieldsArray = [];
|
|
}
|
|
}
|
|
|
|
// 分类字段
|
|
const fieldCategories = {
|
|
'basic-info': [
|
|
{ label: '产品ID', field: 'productId' },
|
|
{ label: '卖家ID', field: 'sellerId' },
|
|
{ label: '产品名称', field: 'productName' },
|
|
{ label: '状态', field: 'status', formatter: mapProductStatus },
|
|
{ label: '创建时间', field: 'created_at' },
|
|
{ label: '更新时间', field: 'updated_at' },
|
|
{ label: '审核时间', field: 'audit_time' },
|
|
{ label: '商品地区信息', field: 'region' },
|
|
{ label: '商品联系人', field: 'product_contact' },
|
|
{ label: '商品联系人电话号码', field: 'contact_phone' }
|
|
],
|
|
'product-details': [
|
|
{ label: '销售价格', field: 'price' },
|
|
{ label: '采购价格', field: 'costprice' },
|
|
{ label: '品种', field: 'variety' },
|
|
{ label: '产品包装', field: 'producting' },
|
|
{ label: '新鲜程度', field: 'freshness' },
|
|
{ label: '毛重', field: 'grossWeight' },
|
|
{ label: '规格', field: 'specification' },
|
|
{ label: '数量', field: 'quantity' },
|
|
{ label: '蛋黄', field: 'yolk' },
|
|
{ label: '驳回原因', field: 'rejectReason' },
|
|
{ label: '货源状态', field: 'supplyStatus' },
|
|
{ label: '货源描述', field: 'description' },
|
|
{ label: '货源类型', field: 'sourceType' },
|
|
{ label: '种类', field: 'category' }
|
|
],
|
|
'business-info': [
|
|
{ label: '已有几人想要', field: 'reservedCount' },
|
|
{ label: '自动下架时间', field: 'autoOfflineTime' },
|
|
{ label: '自动下架天数', field: 'autoOfflineDays' },
|
|
{ label: '自动下架小时数', field: 'autoOfflineHours' },
|
|
{ label: '货源标签是否上锁', field: 'label', formatter: (value) => value === 0 ? '未锁定' : '已锁定' },
|
|
{ label: '频率', field: 'frequency' },
|
|
{ label: '产品日志', field: 'product_log' },
|
|
{ label: '电话号码', field: 'phoneNumber' },
|
|
{ label: '昵称', field: 'nickName' }
|
|
]
|
|
};
|
|
|
|
// 生成对比表格
|
|
let html = `
|
|
<div style="background-color: #f9f9f9; border-radius: 4px;">
|
|
<!-- 标签页导航 -->
|
|
<div style="display: flex; border-bottom: 1px solid #e8e8e8; background-color: #f0f0f0;">
|
|
<button class="tab-btn" onclick="switchTab(this, 'comparison-basic-info')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px; border-bottom: 2px solid #1890ff; color: #1890ff;">基本信息</button>
|
|
<button class="tab-btn" onclick="switchTab(this, 'comparison-product-details')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px;">产品详情</button>
|
|
<button class="tab-btn" onclick="switchTab(this, 'comparison-business-info')" style="padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px;">业务信息</button>
|
|
</div>
|
|
`;
|
|
|
|
// 生成每个标签页的内容
|
|
Object.entries(fieldCategories).forEach(([categoryId, fields]) => {
|
|
html += `
|
|
<div class="tab-content" id="comparison-${categoryId}" style="padding: 16px; ${categoryId === 'basic-info' ? 'display: block;' : 'display: none;'}">
|
|
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
|
<tr style="background-color: #f0f0f0;">
|
|
<th style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 150px;">字段</th>
|
|
<th style="padding: 8px; border-bottom: 1px solid #e8e8e8;">原始值</th>
|
|
<th style="padding: 8px; border-bottom: 1px solid #e8e8e8;">修改后值</th>
|
|
</tr>
|
|
`;
|
|
|
|
fields.forEach(field => {
|
|
const originalValue = original[field.field];
|
|
const modifiedValue = modified[field.field];
|
|
// 比较原始值,不使用格式化后的值进行比较
|
|
const isValueChanged = originalValue !== modifiedValue;
|
|
// 如果提供了changedFieldsArray,则优先使用它来判断是否变更
|
|
const isChanged = changedFieldsArray.length > 0 ? changedFieldsArray.includes(field.field) : isValueChanged;
|
|
|
|
// 格式化值
|
|
let formattedOriginal = originalValue;
|
|
let formattedModified = modifiedValue;
|
|
|
|
if (field.formatter) {
|
|
formattedOriginal = field.formatter(originalValue);
|
|
formattedModified = field.formatter(modifiedValue);
|
|
}
|
|
|
|
// 处理空值
|
|
formattedOriginal = formattedOriginal === undefined || formattedOriginal === null || formattedOriginal === '' ? '-' : formattedOriginal;
|
|
formattedModified = formattedModified === undefined || formattedModified === null || formattedModified === '' ? '-' : formattedModified;
|
|
|
|
// 添加行
|
|
html += `
|
|
<tr ${isChanged ? 'style="background-color: #fff3f3;"' : ''}>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; font-weight: bold;">${field.label}</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">
|
|
${isChanged ? `
|
|
<span style="background-color: #f0f0f0; padding: 4px 8px; border-radius: 4px; color: #999; text-decoration: line-through;">${formattedOriginal}</span>
|
|
` : formattedOriginal}
|
|
</td>
|
|
<td style="padding: 8px; border-bottom: 1px solid #e8e8e8; ${isChanged ? 'color: #52c41a;' : ''}">${formattedModified}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</table>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</div>
|
|
`;
|
|
|
|
return html;
|
|
} catch (e) {
|
|
console.error('渲染数据对比失败:', e);
|
|
return `
|
|
<div style="background-color: #f5f5f5; padding: 12px; border-radius: 4px; text-align: center; color: #ff4d4f;">
|
|
数据对比渲染失败
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 返回登录页面
|
|
function gotoLogin() {
|
|
localStorage.removeItem('loginInfo');
|
|
window.location.href = '/login.html';
|
|
}
|
|
|
|
// 产品状态映射函数
|
|
function mapProductStatus(status) {
|
|
const statusMap = {
|
|
'sold_out': '已下架',
|
|
'published': '已上架',
|
|
'hidden': '已删除'
|
|
};
|
|
return statusMap[status] || status;
|
|
}
|
|
|
|
// 客户类型映射函数
|
|
function mapCustomerType(type) {
|
|
const typeMap = {
|
|
'smalls': '小品种客户',
|
|
'buyer': '大贸易客户',
|
|
'both': '供应商、贸易客户',
|
|
'seller': '供应商'
|
|
};
|
|
return typeMap[type] || type;
|
|
}
|
|
|
|
// 处理操作事件描述,将其中的客户类型映射为中文
|
|
function formatOperationEvent(event) {
|
|
if (!event) return event;
|
|
|
|
// 使用mapCustomerType函数替换事件描述中的客户类型
|
|
let formattedEvent = event;
|
|
const typeMap = {
|
|
'smalls': '小品种客户',
|
|
'buyer': '大贸易客户',
|
|
'both': '供应商、贸易客户',
|
|
'seller': '供应商'
|
|
};
|
|
|
|
// 遍历所有客户类型,替换事件描述中的匹配项
|
|
Object.entries(typeMap).forEach(([englishType, chineseType]) => {
|
|
// 使用正则表达式替换所有匹配的客户类型
|
|
const regex = new RegExp(englishType, 'g');
|
|
formattedEvent = formattedEvent.replace(regex, chineseType);
|
|
});
|
|
|
|
return formattedEvent;
|
|
}
|
|
|
|
// 页面加载完成后添加动画效果
|
|
window.onload = function() {
|
|
const elements = document.querySelectorAll('.fade-in');
|
|
elements.forEach(element => {
|
|
element.style.opacity = '0';
|
|
setTimeout(() => {
|
|
element.style.opacity = '1';
|
|
}, 100);
|
|
});
|
|
|
|
// 初始化加载
|
|
loadData();
|
|
|
|
// 设置定时任务,每10秒更新一次数据
|
|
setInterval(loadData, 10000);
|
|
};
|
|
|
|
// 加载数据函数
|
|
function loadData() {
|
|
// 获取当前活跃的标签页
|
|
const activeTab = document.querySelector('.btn.btn-default.active');
|
|
|
|
// 根据当前活跃的标签页决定加载哪种日志
|
|
if (activeTab && activeTab.id === 'systemLogTab') {
|
|
// 只有当当前显示的是系统操作日志时,才加载系统日志
|
|
// 获取当前页码,避免跳转到第一页
|
|
const currentPageElement = document.getElementById('currentPage');
|
|
const currentPage = currentPageElement ? parseInt(currentPageElement.textContent) || 1 : 1;
|
|
loadLogs(currentPage);
|
|
}
|
|
|
|
// 获取统计数据
|
|
fetch('/api/stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新统计数据,添加null检查
|
|
const totalResourcesEl = document.getElementById('totalResources');
|
|
if (totalResourcesEl) totalResourcesEl.textContent = data.data.total;
|
|
|
|
const pendingResourcesEl = document.getElementById('pendingResources');
|
|
if (pendingResourcesEl) pendingResourcesEl.textContent = data.data.pending;
|
|
|
|
const todayNewResourcesEl = document.getElementById('todayNewResources');
|
|
if (todayNewResourcesEl) todayNewResourcesEl.textContent = data.data.today;
|
|
} else {
|
|
console.error('获取统计数据失败:', data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取统计数据失败:', error);
|
|
});
|
|
|
|
// 加载客户活跃统计数据
|
|
// 设置默认时间范围为最近7天
|
|
const now = new Date();
|
|
const endDate = new Date(now);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
// 格式化日期为YYYY-MM-DD
|
|
const formatDate = (date) => {
|
|
return date.toISOString().split('T')[0];
|
|
};
|
|
|
|
// 构建查询参数
|
|
const params = new URLSearchParams();
|
|
params.append('start_date', formatDate(startDate));
|
|
params.append('end_date', formatDate(endDate));
|
|
params.append('page', 1);
|
|
params.append('page_size', 10);
|
|
|
|
// 检查缓存
|
|
const cacheKey = 'active_stats_' + params.toString();
|
|
const cachedData = cacheManager.get(cacheKey);
|
|
|
|
if (!cachedData) {
|
|
// 如果没有缓存,预加载数据但不影响当前页码状态
|
|
// 直接获取数据并缓存,不调用refreshActiveStats避免重置页码
|
|
Promise.all([
|
|
fetch('/api/active-stats?' + params.toString())
|
|
.then(response => response.json()),
|
|
fetch('/api/daily-active-stats?' + params.toString())
|
|
.then(response => response.json()),
|
|
fetch('/api/total-active-duration?' + params.toString())
|
|
.then(response => response.json())
|
|
])
|
|
.then(([overviewData, dailyData, totalDurationData]) => {
|
|
const combinedData = {
|
|
overview: overviewData,
|
|
daily: dailyData,
|
|
totalDuration: totalDurationData
|
|
};
|
|
cacheManager.set(cacheKey, combinedData);
|
|
})
|
|
.catch(error => {
|
|
console.error('预加载客户活跃统计数据失败:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 粒子特效
|
|
function initParticles() {
|
|
const canvas = document.getElementById('particles-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 设置canvas尺寸
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// 粒子配置
|
|
const particlesConfig = {
|
|
count: 120,
|
|
color: '#367ee9',
|
|
secondaryColor: '#7f56d9',
|
|
radius: 3,
|
|
speed: 0.6,
|
|
opacity: 0.7
|
|
};
|
|
|
|
// 生成粒子
|
|
const particles = [];
|
|
for (let i = 0; i < particlesConfig.count; i++) {
|
|
// 随机选择主色或辅助色
|
|
const isPrimary = Math.random() > 0.5;
|
|
const baseColor = isPrimary ? particlesConfig.color : particlesConfig.secondaryColor;
|
|
const opacity = Math.random() * particlesConfig.opacity + 0.2;
|
|
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * particlesConfig.speed,
|
|
vy: (Math.random() - 0.5) * particlesConfig.speed,
|
|
radius: Math.random() * particlesConfig.radius + 1,
|
|
opacity: opacity,
|
|
color: `rgba(${hexToRgb(baseColor)}, ${opacity})`
|
|
});
|
|
}
|
|
|
|
// 辅助函数:将十六进制颜色转换为RGB
|
|
function hexToRgb(hex) {
|
|
// 移除#号
|
|
hex = hex.replace('#', '');
|
|
|
|
// 解析RGB值
|
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
|
|
return `${r}, ${g}, ${b}`;
|
|
}
|
|
|
|
// 绘制粒子
|
|
function drawParticles() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
particles.forEach(particle => {
|
|
// 添加光晕效果
|
|
const gradient = ctx.createRadialGradient(
|
|
particle.x, particle.y, 0,
|
|
particle.x, particle.y, particle.radius * 4
|
|
);
|
|
gradient.addColorStop(0, particle.color);
|
|
gradient.addColorStop(1, `rgba(${hexToRgb(particlesConfig.color)}, 0)`);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(particle.x, particle.y, particle.radius * 4, 0, Math.PI * 2);
|
|
ctx.fillStyle = gradient;
|
|
ctx.fill();
|
|
|
|
// 绘制粒子核心
|
|
ctx.beginPath();
|
|
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = particle.color;
|
|
ctx.fill();
|
|
|
|
// 更新粒子位置
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
|
|
// 边界检测
|
|
if (particle.x < 0 || particle.x > canvas.width) {
|
|
particle.vx *= -1;
|
|
}
|
|
if (particle.y < 0 || particle.y > canvas.height) {
|
|
particle.vy *= -1;
|
|
}
|
|
});
|
|
|
|
// 绘制粒子间连线
|
|
particles.forEach((particle1, index1) => {
|
|
particles.forEach((particle2, index2) => {
|
|
if (index1 < index2) {
|
|
const dx = particle1.x - particle2.x;
|
|
const dy = particle1.y - particle2.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < 200) {
|
|
const opacity = (1 - distance / 200) * 0.15;
|
|
ctx.beginPath();
|
|
ctx.moveTo(particle1.x, particle1.y);
|
|
ctx.lineTo(particle2.x, particle2.y);
|
|
ctx.strokeStyle = `rgba(${hexToRgb(particlesConfig.color)}, ${opacity})`;
|
|
ctx.lineWidth = 0.6;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
requestAnimationFrame(drawParticles);
|
|
}
|
|
|
|
drawParticles();
|
|
}
|
|
|
|
// 页面加载完成后初始化粒子特效
|
|
window.addEventListener('load', initParticles);
|
|
</script>
|
|
<script>
|
|
// 货源数据缓存
|
|
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟
|
|
|
|
// 登录后立即开始加载货源数据
|
|
window.addEventListener('load', function() {
|
|
// 检查是否已登录
|
|
if (localStorage.getItem('adminToken')) {
|
|
// 预加载货源数据到缓存
|
|
loadSupplyDataWithCache();
|
|
}
|
|
});
|
|
|
|
// 使用缓存的货源数据加载函数
|
|
function loadSupplyDataWithCache(page = 1, searchTerm = '', pageSize = 10) {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 生成缓存键(包含分页、页面大小和搜索词信息)
|
|
const params = new URLSearchParams();
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
if (searchTerm) params.append('searchTerm', searchTerm);
|
|
const cacheKey = 'supply_management_' + params.toString();
|
|
const permanentCacheKey = 'supply_management_permanent_' + params.toString();
|
|
|
|
// 显示加载状态
|
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">加载中...</td></tr>';
|
|
pagination.innerHTML = '';
|
|
|
|
// 检查缓存是否有效
|
|
const now = new Date().getTime();
|
|
if (supplyCacheData.length > 0 && (now - supplyCacheTime) < CACHE_DURATION) {
|
|
// 使用缓存数据
|
|
renderSupplyData(page, searchTerm, pageSize);
|
|
} else {
|
|
// 从服务器获取所有数据
|
|
fetch(`/api/supply-management?getAll=true`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// 更新缓存
|
|
supplyCacheData = data.products;
|
|
supplyCacheTime = now;
|
|
// 渲染数据
|
|
renderSupplyData(page, searchTerm, pageSize);
|
|
} else {
|
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">' + (data.message || '加载失败') + '</td></tr>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取货源数据失败:', error);
|
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">网络错误,请稍后重试</td></tr>';
|
|
});
|
|
}
|
|
}
|
|
|
|
// 渲染货源数据
|
|
function renderSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 过滤数据
|
|
let filteredData = supplyCacheData;
|
|
if (searchTerm) {
|
|
const term = searchTerm.toLowerCase();
|
|
filteredData = supplyCacheData.filter(item =>
|
|
item.productName.toLowerCase().includes(term) ||
|
|
item.specification.toLowerCase().includes(term) ||
|
|
item.region.toLowerCase().includes(term)
|
|
);
|
|
}
|
|
|
|
// 计算分页
|
|
const total = supplyTotalCount || filteredData.length;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const pageData = filteredData.slice(startIndex, endIndex);
|
|
|
|
// 渲染表格
|
|
if (pageData.length === 0) {
|
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">暂无数据</td></tr>';
|
|
} else {
|
|
tableBody.innerHTML = pageData.map(item => `
|
|
<tr>
|
|
<td>${item.productId}</td>
|
|
<td>${item.productName}</td>
|
|
<td>${item.price} 元</td>
|
|
<td>${item.quantity}</td>
|
|
<td>${item.unit || '-'}</td>
|
|
<td>${item.region}</td>
|
|
<td>${item.contactPerson || '-'}</td>
|
|
<td>${item.contact_phone || '-'}</td>
|
|
<td>${item.creator || '-'}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info" onclick="viewSupplyDetails('${item.productId}')">详情</button>
|
|
<button class="btn btn-sm btn-warning" onclick="editSupply('${item.productId}')">编辑</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteSupply('${item.productId}')">删除</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// 渲染分页
|
|
if (totalPages > 1) {
|
|
let paginationHTML = '';
|
|
|
|
// 上一页
|
|
if (page > 1) {
|
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadSupplyData(${page - 1}, '${searchTerm}')">上一页</a></li>`;
|
|
}
|
|
|
|
// 页码
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
paginationHTML += `<li class="page-item ${i === page ? 'active' : ''}"><a class="page-link" href="#" onclick="loadSupplyData(${i}, '${searchTerm}')">${i}</a></li>`;
|
|
}
|
|
|
|
// 下一页
|
|
if (page < totalPages) {
|
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadSupplyData(${page + 1}, '${searchTerm}')">下一页</a></li>`;
|
|
}
|
|
|
|
pagination.innerHTML = paginationHTML;
|
|
} else {
|
|
pagination.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// 从缓存渲染货源数据
|
|
function renderSupplyDataFromCache(page = 1, searchTerm = '', pageSize = 10) {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 过滤数据
|
|
let filteredData = supplyCacheData;
|
|
if (searchTerm) {
|
|
const term = searchTerm.toLowerCase();
|
|
filteredData = supplyCacheData.filter(item =>
|
|
(item.productName || '').toLowerCase().includes(term) ||
|
|
(item.specification || '').toLowerCase().includes(term) ||
|
|
(item.price || '').toString().includes(term) ||
|
|
(item.quantity || '').toString().includes(term) ||
|
|
(item.region || '').toLowerCase().includes(term) ||
|
|
(item.creator || '').toLowerCase().includes(term)
|
|
);
|
|
}
|
|
|
|
// 计算分页
|
|
const total = supplyTotalCount || filteredData.length;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const pageData = filteredData.slice(startIndex, endIndex);
|
|
|
|
// 填充表格数据
|
|
if (pageData.length > 0) {
|
|
tableBody.innerHTML = pageData.map(product => `
|
|
<tr>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.productName || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.specification || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.region || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${formatTime(product.created_at)}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.contactPerson || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.creator || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.viewCount || 0}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick="showViewDetails('${product.productId}')"
|
|
style="margin-right: 0.5rem;"
|
|
>
|
|
查看浏览详细
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">暂无货源数据</div>
|
|
<div style="font-size: 0.875rem;">当前没有商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 生成分页按钮
|
|
generatePagination(pagination, page, totalPages, 'loadSupplyData', pageSize, searchTerm);
|
|
}
|
|
|
|
// 使用传统API加载货源数据(备用)
|
|
function loadSupplyDataWithAPI(page = 1, searchTerm = '', pageSize = 10) {
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 调用API获取货源数据
|
|
let url = `/api/supply-management?page=${page}&pageSize=${pageSize}`;
|
|
if (searchTerm) {
|
|
url += `&search=${encodeURIComponent(searchTerm)}`;
|
|
}
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const products = data.products;
|
|
const total = data.total;
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
|
|
// 填充表格数据
|
|
if (products.length > 0) {
|
|
tableBody.innerHTML = products.map(product => `
|
|
<tr>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.productName || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.specification || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.region || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${formatTime(product.created_at)}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.contactPerson || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.creator || '暂无'}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">${product.viewCount || 0}</td>
|
|
<td style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick="showViewDetails('${product.productId}')"
|
|
style="margin-right: 0.5rem;"
|
|
>
|
|
查看浏览详细
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">暂无货源数据</div>
|
|
<div style="font-size: 0.875rem;">当前没有商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 生成分页按钮
|
|
generatePagination(pagination, page, totalPages, 'loadSupplyData', pageSize);
|
|
} else {
|
|
console.error('获取货源数据失败:', data.message);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载失败</div>
|
|
<div style="font-size: 0.875rem;">${data.message || '获取商品信息失败,请稍后重试'}</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('获取货源数据失败:', error);
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="8" style="text-align: center; padding: 3rem; color: var(--danger-color);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">❌</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">网络错误</div>
|
|
<div style="font-size: 0.875rem;">获取商品信息失败,请检查网络连接</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
// 改进loadAllSupplyData函数,返回Promise
|
|
function loadAllSupplyData() {
|
|
return new Promise((resolve, reject) => {
|
|
if (supplyIsLoading) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
supplyIsLoading = true;
|
|
|
|
// 并行加载前三页数据和所有数据
|
|
Promise.all([
|
|
// 获取前三页数据(快速显示)
|
|
fetch('/api/supply-management?page=1&pageSize=30')
|
|
.then(response => response.json()),
|
|
|
|
// 同时加载所有数据
|
|
fetch('/api/supply-management?getAll=true')
|
|
.then(response => response.json())
|
|
])
|
|
.then(([initialData, fullData]) => {
|
|
// 处理前三页数据
|
|
if (initialData.success) {
|
|
supplyCacheData = initialData.products;
|
|
supplyTotalCount = initialData.total;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新成功,已加载前三页数据,总数据量:', initialData.total);
|
|
|
|
// 存储到永久缓存(包含分页、页面大小和搜索词信息)
|
|
const supplyData = {
|
|
initialData: initialData,
|
|
fullData: fullData
|
|
};
|
|
|
|
// 生成缓存键
|
|
const params = new URLSearchParams();
|
|
params.append('page', 1);
|
|
params.append('pageSize', 30);
|
|
const permanentCacheKey = 'supply_management_permanent_' + params.toString();
|
|
const cacheKey = 'supply_management_' + params.toString();
|
|
|
|
// 存储到永久缓存
|
|
cacheManager.setPermanent(permanentCacheKey, supplyData);
|
|
|
|
// 存储到临时缓存
|
|
cacheManager.set(cacheKey, supplyData, 480000); // 8分钟过期
|
|
|
|
// 同时存储通用永久缓存(保持向后兼容)
|
|
cacheManager.setPermanent('supplyManagementData', supplyData);
|
|
|
|
// 如果当前正在查看货源管理页面,立即渲染
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
loadSupplyData();
|
|
}
|
|
}
|
|
|
|
// 处理完整数据
|
|
if (fullData.success) {
|
|
// 更新为完整缓存数据
|
|
supplyCacheData = fullData.products;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新完成,共', supplyCacheData.length, '条数据');
|
|
|
|
// 如果当前正在查看货源管理页面,重新渲染以显示完整数据
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
loadSupplyData();
|
|
}
|
|
}
|
|
resolve();
|
|
})
|
|
.catch(error => {
|
|
console.error('加载货源数据失败:', error);
|
|
reject(error);
|
|
})
|
|
.finally(() => {
|
|
supplyIsLoading = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
// 修改loadSupplyData函数,使用缓存逻辑
|
|
function loadSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
// 更新全局分页状态
|
|
supplyCurrentPage = page;
|
|
supplyCurrentSearchTerm = searchTerm;
|
|
supplyCurrentPageSize = pageSize;
|
|
|
|
const tableBody = document.getElementById('supplyTableBody');
|
|
const pagination = document.getElementById('supplyPagination');
|
|
|
|
// 生成缓存键(包含分页、页面大小和搜索词信息)
|
|
const params = new URLSearchParams();
|
|
params.append('page', page);
|
|
params.append('pageSize', pageSize);
|
|
if (searchTerm) params.append('searchTerm', searchTerm);
|
|
const cacheKey = 'supply_management_' + params.toString();
|
|
const permanentCacheKey = 'supply_management_permanent_' + params.toString();
|
|
|
|
// 显示加载状态
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; padding: 3rem; color: var(--text-muted);">
|
|
<div style="font-size: 2rem; margin-bottom: 1rem;">📦</div>
|
|
<div style="font-size: 1.125rem; margin-bottom: 0.5rem;">加载货源数据中...</div>
|
|
<div style="font-size: 0.875rem;">请稍候,正在获取商品信息</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
// 首先检查永久缓存(包含分页、页面大小和搜索词信息)
|
|
const permanentCachedData = cacheManager.getPermanent(permanentCacheKey);
|
|
if (permanentCachedData) {
|
|
console.log('使用永久缓存数据加载货源页面 ' + page);
|
|
// 同步永久缓存数据到全局变量
|
|
if (permanentCachedData.initialData && permanentCachedData.initialData.success) {
|
|
supplyCacheData = permanentCachedData.initialData.products;
|
|
supplyTotalCount = permanentCachedData.initialData.total;
|
|
supplyCacheTime = Date.now();
|
|
} else if (permanentCachedData.fullData && permanentCachedData.fullData.success) {
|
|
supplyCacheData = permanentCachedData.fullData.products;
|
|
supplyTotalCount = permanentCachedData.fullData.total;
|
|
supplyCacheTime = Date.now();
|
|
}
|
|
// 立即显示永久缓存数据
|
|
renderSupplyDataFromCache(page, searchTerm, pageSize);
|
|
// 后台加载最新数据
|
|
loadLatestSupplyData(page, searchTerm, pageSize);
|
|
return;
|
|
}
|
|
|
|
// 检查缓存是否有效(5分钟)
|
|
const CACHE_DURATION = 5 * 60 * 1000;
|
|
const now = Date.now();
|
|
|
|
if (supplyCacheData.length > 0 && (now - supplyCacheTime) < CACHE_DURATION) {
|
|
// 使用缓存数据
|
|
renderSupplyDataFromCache(page, searchTerm, pageSize);
|
|
} else {
|
|
// 缓存无效,重新加载所有数据
|
|
loadAllSupplyData().then(() => {
|
|
renderSupplyDataFromCache(page, searchTerm, pageSize);
|
|
}).catch(() => {
|
|
// 加载失败,使用传统分页API
|
|
loadSupplyDataWithAPI(page, searchTerm, pageSize);
|
|
});
|
|
}
|
|
}
|
|
|
|
// 从API获取最新货源数据
|
|
function loadLatestSupplyData(page = 1, searchTerm = '', pageSize = 10) {
|
|
console.log('后台加载最新货源数据:', { page, searchTerm, pageSize });
|
|
|
|
// 构建API请求URL
|
|
let apiUrl = '/api/supply-management?page=' + page + '&pageSize=' + pageSize;
|
|
if (searchTerm) {
|
|
apiUrl += '&search=' + encodeURIComponent(searchTerm);
|
|
}
|
|
|
|
let allDataUrl = '/api/supply-management?getAll=true';
|
|
if (searchTerm) {
|
|
allDataUrl += '&search=' + encodeURIComponent(searchTerm);
|
|
}
|
|
|
|
// 并行请求数据
|
|
Promise.all([
|
|
// 获取当前页数据
|
|
fetch(apiUrl)
|
|
.then(response => response.json()),
|
|
|
|
// 同时加载所有数据
|
|
fetch(allDataUrl)
|
|
.then(response => response.json())
|
|
])
|
|
.then(([initialData, fullData]) => {
|
|
// 处理前三页数据
|
|
if (initialData.success) {
|
|
supplyCacheData = initialData.products;
|
|
supplyTotalCount = initialData.total;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新成功,已加载前三页数据,总数据量:', initialData.total);
|
|
|
|
// 存储到永久缓存
|
|
const supplyData = {
|
|
initialData: initialData,
|
|
fullData: fullData
|
|
};
|
|
cacheManager.setPermanent('supplyManagementData', supplyData);
|
|
|
|
// 如果当前正在查看货源管理页面,立即渲染
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
renderSupplyDataFromCache(page, searchTerm, pageSize);
|
|
}
|
|
}
|
|
|
|
// 处理完整数据
|
|
if (fullData.success) {
|
|
// 更新为完整缓存数据
|
|
supplyCacheData = fullData.products;
|
|
supplyCacheTime = Date.now();
|
|
console.log('货源数据缓存更新完成,共', supplyCacheData.length, '条数据');
|
|
|
|
// 如果当前正在查看货源管理页面,重新渲染以显示完整数据
|
|
if (document.querySelector('.content-title')?.textContent === '货源管理') {
|
|
renderSupplyDataFromCache(page, searchTerm, pageSize);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载最新货源数据失败:', error);
|
|
});
|
|
}
|
|
|
|
// 优化:添加请求防抖函数
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// 优化:为货源管理搜索添加防抖
|
|
const debouncedLoadSupplyData = debounce((page, searchTerm, pageSize) => {
|
|
loadSupplyData(page, searchTerm, pageSize);
|
|
}, 300);
|
|
|
|
// 优化:定期清理过期缓存
|
|
setInterval(() => {
|
|
// 检查缓存是否过期
|
|
const now = Date.now();
|
|
if (supplyCacheTime && (now - supplyCacheTime) > 5 * 60 * 1000) {
|
|
supplyCacheData = [];
|
|
supplyCacheTime = 0;
|
|
console.log('过期货源缓存已清理');
|
|
}
|
|
}, 5 * 60 * 1000); // 每5分钟清理一次
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|