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

<!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, '&quot;')})" 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, '&quot;')})" 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()">&times;</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()">&times;</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()">&times;</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()">&times;</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, '&quot;')})">查看详情</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, '&quot;')})">查看详情</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, '&quot;')})">查看详情</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;">&times;</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;">&times;</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;">&times;</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;">&times;</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;">&times;</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>