Files
CrmSystem/public/admin.html

2407 lines
84 KiB
HTML
Raw Permalink Normal View History

2026-01-23 21:56:02 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户管理系统 - 总后台</title>
<style>
/* ========== 统一主题配色 ========== */
:root {
--primary: #5bbf93;
/* 主色 - 柔和的绿色 */
--primary-light: #8fd6b3;
/* 浅色 */
--primary-dark: #2f8f67;
/* 深色 */
--primary-bg: #f0f9f5;
/* 背景色 */
--primary-gradient: linear-gradient(135deg, #5bbf93 0%, #2f8f67 100%);
--text-primary: #2c3e50;
/* 主要文字 */
--text-secondary: #546e7a;
/* 次要文字 */
--text-muted: #90a4ae;
/* 弱化文字 */
--bg-body: #f8fafc;
/* 页面背景 */
--bg-card: #ffffff;
/* 卡片背景 */
--bg-hover: #f9fafc;
/* 悬停背景 */
--border: #e2e8f0;
/* 边框色 */
--border-light: #f1f5f9;
/* 浅边框 */
--success: #10b981;
/* 成功色 */
--warning: #f59e0b;
/* 警告色 */
--danger: #ef4444;
/* 危险色 */
--info: #3b82f6;
/* 信息色 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--radius-sm: 6px;
--radius: 10px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-full: 9999px;
--transition: all 0.2s ease-in-out;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.admin-container {
display: flex;
min-height: 100vh;
}
/* ========== 侧边栏 ========== */
.sidebar {
width: 280px;
background: linear-gradient(180deg, var(--primary-dark) 0%, #2a7f5f 100%);
color: white;
padding: 30px 0;
position: fixed;
height: 100vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
}
.logo {
padding: 0 25px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 25px;
}
.logo h1 {
font-size: 1.5rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 12px;
}
.logo .subtitle {
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
margin-top: 5px;
margin-left: 40px;
}
.user-info {
padding: 0 25px 25px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 25px;
}
.user-avatar {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
backdrop-filter: blur(5px);
}
.nav-menu {
padding: 0 15px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
border-radius: var(--radius);
margin-bottom: 8px;
cursor: pointer;
transition: var(--transition);
color: rgba(255, 255, 255, 0.8);
border: 1px solid transparent;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
border-color: rgba(255, 255, 255, 0.2);
}
.nav-item.active {
background: rgba(255, 255, 255, 0.15);
color: white;
border-color: rgba(255, 255, 255, 0.3);
font-weight: 600;
}
/* ========== 主内容区 ========== */
.main-content {
flex: 1;
margin-left: 280px;
padding: 30px;
background-color: var(--bg-body);
min-height: 100vh;
}
.header {
background: var(--bg-card);
padding: 24px 30px;
border-radius: var(--radius-lg);
margin-bottom: 30px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
}
.header h2 {
font-size: 1.8rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 12px;
}
.header h2::before {
content: '';
display: block;
width: 4px;
height: 24px;
background: var(--primary);
border-radius: var(--radius-full);
}
.header .date {
color: var(--text-muted);
margin-top: 8px;
font-size: 0.95rem;
}
/* ========== 统计卡片 ========== */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 24px;
margin-bottom: 30px;
}
.stat-card {
background: var(--bg-card);
padding: 28px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border: 1px solid var(--border);
transition: var(--transition);
cursor: pointer;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-light);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--primary);
border-radius: var(--radius-full);
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
font-size: 1.6rem;
background: var(--primary-bg);
color: var(--primary);
}
.stat-value {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-primary);
}
.stat-label {
color: var(--text-muted);
font-size: 0.95rem;
}
/* ========== 员工列表 ========== */
.staff-list {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 30px;
border: 1px solid var(--border);
}
.staff-header {
padding: 22px 28px;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(90deg, var(--primary-bg) 0%, transparent 100%);
}
.staff-header h3 {
font-size: 1.3rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.staff-item {
display: flex;
align-items: center;
padding: 22px 28px;
border-bottom: 1px solid var(--border-light);
cursor: pointer;
transition: var(--transition);
position: relative;
}
.staff-item:hover {
background: var(--bg-hover);
}
.staff-item:last-child {
border-bottom: none;
}
.staff-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.3rem;
margin-right: 18px;
box-shadow: var(--shadow-sm);
}
.staff-info {
flex: 1;
}
.staff-name {
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 4px;
color: var(--text-primary);
}
.staff-details {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 8px;
}
.staff-stats {
display: flex;
gap: 16px;
margin-top: 10px;
}
.stat-badge {
padding: 6px 14px;
border-radius: var(--radius-full);
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
box-shadow: var(--shadow-sm);
}
.stat-badge.total {
background: #e3f2fd;
color: var(--primary);
border: 1px solid rgba(91, 191, 147, 0.3);
}
.stat-badge.expired {
background: #ffebee;
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.stat-badge.expiring {
background: #fff3e0;
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.staff-arrow {
color: var(--text-muted);
font-size: 1.2rem;
transition: transform 0.3s;
}
/* ========== 员工详细统计卡片 ========== */
.staff-stats-grid {
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 16px 28px 20px 28px;
padding: 20px;
background: var(--primary-bg);
border-radius: var(--radius);
animation: slideDown 0.3s ease;
border: 1px solid var(--border);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.staff-stat-card {
background: white;
padding: 20px;
border-radius: var(--radius);
text-align: center;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.staff-stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.staff-stat-card.total:hover {
background: #e3f2fd;
border-color: var(--primary);
}
.staff-stat-card.expired:hover {
background: #ffebee;
border-color: var(--danger);
}
.staff-stat-card.expiring:hover {
background: #fff3e0;
border-color: var(--warning);
}
.staff-stat-card.clients:hover {
background: #e8f5e9;
border-color: var(--success);
}
.staff-stat-value {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 8px;
color: var(--text-primary);
}
.staff-stat-label {
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 500;
}
/* ========== 员工管理页面 ========== */
.staff-management {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 30px;
border: 1px solid var(--border);
}
.management-header {
padding: 22px 28px;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(90deg, var(--primary-bg) 0%, transparent 100%);
}
.management-header h3 {
font-size: 1.3rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.action-buttons {
display: flex;
gap: 12px;
}
.btn {
padding: 10px 22px;
border: none;
border-radius: var(--radius);
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
}
.btn-primary {
background: var(--primary);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
}
.btn-warning {
background: var(--warning);
color: white;
}
.btn-warning:hover {
background: #d97706;
transform: translateY(-2px);
}
/* ========== 员工管理列表 ========== */
.staff-management-list {
padding: 0;
}
.management-staff-item {
display: flex;
align-items: center;
padding: 20px 28px;
border-bottom: 1px solid var(--border-light);
transition: var(--transition);
}
.management-staff-item:hover {
background: var(--bg-hover);
}
.management-staff-item:last-child {
border-bottom: none;
}
.staff-checkbox {
margin-right: 16px;
width: 18px;
height: 18px;
cursor: pointer;
}
.staff-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.btn-small {
padding: 6px 14px;
font-size: 0.9rem;
}
/* ========== 分组样式 ========== */
.group-item {
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 16px;
overflow: hidden;
background: #fff;
}
.group-header {
display: flex;
align-items: center;
padding: 16px 20px;
background: #f8fafc;
cursor: pointer;
transition: var(--transition);
}
.group-header:hover {
background: #f1f5f9;
}
.group-info {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.group-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.group-stats {
display: flex;
gap: 20px;
color: var(--text-muted);
font-size: 0.9rem;
margin-right: 20px;
}
.group-stat-item i {
margin-right: 6px;
}
.group-content {
display: none;
padding: 0;
border-top: 1px solid var(--border);
}
.group-arrow {
color: var(--text-muted);
transition: transform 0.3s;
}
/* ========== 数据表格 ========== */
.data-table {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border);
position: relative;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: var(--bg-hover);
padding: 18px 20px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 2px solid var(--border);
font-size: 0.95rem;
}
td {
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
transition: var(--transition);
}
tr:hover td {
background: var(--bg-hover);
}
/* ========== 筛选栏 ========== */
.filter-bar {
background: var(--bg-card);
padding: 22px;
border-radius: var(--radius-lg);
margin-bottom: 24px;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
box-shadow: var(--shadow);
border: 1px solid var(--border);
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
}
select,
input {
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 0.95rem;
background: white;
color: var(--text-primary);
transition: var(--transition);
}
select:hover,
input:hover {
border-color: var(--primary-light);
}
select:focus,
input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(91, 191, 147, 0.15);
}
/* ========== 模态框 ========== */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(3px);
}
.modal {
background: var(--bg-card);
border-radius: var(--radius-lg);
padding: 32px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
animation: modalAppear 0.3s ease;
border: 1px solid var(--border);
}
@keyframes modalAppear {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-light);
}
.modal-header h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 12px;
}
.modal-header h3::before {
content: '';
display: block;
width: 4px;
height: 24px;
background: var(--primary);
border-radius: var(--radius-full);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-muted);
cursor: pointer;
padding: 8px;
border-radius: var(--radius);
transition: var(--transition);
}
.modal-close:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* ========== 表单 ========== */
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 8px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 1rem;
background: white;
color: var(--text-primary);
transition: var(--transition);
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(91, 191, 147, 0.15);
}
.modal-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 32px;
}
.btn-cancel {
background: var(--bg-hover);
color: var(--text-secondary);
}
.btn-cancel:hover {
background: #e2e8f0;
color: var(--text-primary);
}
/* ========== 员工数据详情表格 ========== */
.staff-client-table {
width: 100%;
border-collapse: collapse;
margin-top: 24px;
}
.staff-client-table th {
background: var(--bg-hover);
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 2px solid var(--border-light);
font-size: 0.9rem;
}
.staff-client-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.empty-data {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-data .icon {
font-size: 3rem;
margin-bottom: 20px;
opacity: 0.3;
}
@media (max-width: 768px) {
.sidebar {
width: 70px;
}
.main-content {
margin-left: 70px;
padding: 20px;
}
.logo h1,
.subtitle,
.user-name,
.nav-text {
display: none;
}
.staff-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.stats-grid {
grid-template-columns: 1fr;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.filter-group {
flex-direction: column;
align-items: flex-start;
}
}
/* ========== 滚动条美化 ========== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* ========== 消息提示 toast和登录页一致 ========== */
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: var(--radius);
font-weight: 600;
z-index: 10000;
color: white;
box-shadow: var(--shadow-lg);
animation: slideIn 0.3s ease;
max-width: 400px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message.success {
background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
}
.message.error {
background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* ========== 按钮 loading ========== */
.btn[disabled] {
opacity: .7;
cursor: not-allowed;
transform: none !important;
}
.loader {
display: inline-block;
width: 18px;
height: 18px;
border: 3px solid rgba(255, 255, 255, 0.35);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="admin-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">
<h1><i class="fas fa-crown"></i> 总后台</h1>
<div class="subtitle">客户管理系统</div>
</div>
<div class="user-info">
<div class="user-avatar"></div>
<div class="user-name">
<div style="font-weight:600;">管理员</div>
<div style="font-size:0.85rem; opacity:0.8;">超级管理员</div>
</div>
</div>
<div class="nav-menu">
<div class="nav-item active" data-page="dashboard">
<span><i class="fas fa-chart-bar"></i></span>
<span class="nav-text">数据总览</span>
</div>
<div class="nav-item" data-page="staff">
<span><i class="fas fa-users"></i></span>
<span class="nav-text">员工管理</span>
</div>
<div class="nav-item" data-page="clients">
<span><i class="fas fa-list"></i></span>
<span class="nav-text">所有客户</span>
</div>
<!-- <div class="nav-item" data-page="reminders">
<span><i class="fas fa-bell"></i></span>
<span class="nav-text">到期提醒</span>
</div> -->
<div class="nav-item" id="logoutBtn">
<span><i class="fas fa-sign-out-alt"></i></span>
<span class="nav-text">退出登录</span>
</div>
</div>
</div>
<!-- 主内容区 - 数据总览 -->
<div class="main-content" id="dashboardPage">
<div class="header">
<h2>数据总览</h2>
<div class="date" id="currentDate"></div>
</div>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" onclick="showAllClients()">
<div class="stat-icon"><i class="fas fa-money-bill-wave"></i></div>
<div class="stat-value" id="totalAmount">¥ 0</div>
<div class="stat-label">总金额</div>
</div>
<div class="stat-card" onclick="showAllClients()">
<div class="stat-icon"><i class="fas fa-users"></i></div>
<div class="stat-value" id="totalClients">0</div>
<div class="stat-label">客户总数</div>
</div>
<div class="stat-card" onclick="showExpiringClients()">
<div class="stat-icon"><i class="fas fa-exclamation-circle"></i></div>
<div class="stat-value" id="expiringCount">0</div>
<div class="stat-label">即将到期</div>
</div>
<div class="stat-card" onclick="showExpiredClients()">
<div class="stat-icon"><i class="fas fa-clock"></i></div>
<div class="stat-value" id="expiredCount">0</div>
<div class="stat-label">已过期</div>
</div>
</div>
<!-- 员工列表 -->
<div class="staff-list">
<div class="staff-header">
<h3><i class="fas fa-user-friends"></i> 员工数据</h3>
<div style="color:var(--text-muted); font-size:0.9rem;">点击员工查看详细统计</div>
</div>
<div id="staffList">
<!-- 员工列表会动态加载 -->
</div>
</div>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-group">
<label><i class="fas fa-user"></i> 员工:</label>
<select id="staffSelect">
<option value="all">所有员工</option>
</select>
</div>
<div class="filter-group">
<label><i class="fas fa-info-circle"></i> 状态:</label>
<select id="statusSelect">
<option value="all">全部</option>
<option value="active">正常</option>
<option value="expiring">即将到期</option>
<option value="expired">已过期</option>
</select>
</div>
<div class="filter-group">
<label><i class="fas fa-calendar"></i> 时间:</label>
<input type="date" id="dateFrom">
<span></span>
<input type="date" id="dateTo">
</div>
<button class="btn btn-primary" onclick="filterData()"><i class="fas fa-filter"></i> 筛选</button>
<button class="btn btn-cancel" onclick="exportData()"><i class="fas fa-download"></i> 导出数据</button>
</div>
<!-- 数据表格 -->
<div class="data-table">
<table>
<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 id="dataTableBody">
<!-- 数据会动态加载 -->
</tbody>
</table>
</div>
</div>
<!-- 员工管理页面 -->
<div class="main-content" id="staffPage" style="display:none;">
<div class="header">
<h2>用户管理</h2>
<div class="date">管理组长和员工账号</div>
</div>
<!-- 组长列表 -->
<div class="staff-management" style="margin-bottom: 30px;">
<div class="management-header">
<h3><i class="fas fa-user-tie"></i> 组长列表</h3>
<div class="action-buttons">
<button class="btn btn-primary" onclick="openUserModal('leader')">
<i class="fas fa-plus"></i> 添加组长
</button>
</div>
</div>
<div class="staff-management-list" id="leaderManagementList">
<!-- 组长列表动态加载 -->
</div>
</div>
<!-- 员工列表 -->
<div class="staff-management">
<div class="management-header">
<h3><i class="fas fa-users"></i> 员工列表</h3>
<div class="action-buttons">
<button class="btn btn-primary" onclick="openUserModal('staff')">
<i class="fas fa-plus"></i> 添加员工
</button>
<button class="btn btn-danger" onclick="deleteSelectedStaff()">
<i class="fas fa-trash"></i> 删除选中
</button>
</div>
</div>
<div class="staff-management-list" id="staffManagementList">
<!-- 员工列表动态加载 -->
</div>
</div>
</div>
<!-- 所有客户页面 -->
<div class="main-content" id="clientsPage" style="display:none;">
<div class="header">
<h2>所有客户</h2>
<div class="date">查看和管理所有客户数据</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label><i class="fas fa-search"></i> 搜索:</label>
<input type="text" id="searchClient" placeholder="客户姓名、电话或备注">
</div>
<div class="filter-group">
<label><i class="fas fa-user"></i> 员工:</label>
<select id="clientStaffSelect">
<option value="all">所有员工</option>
</select>
</div>
<div class="filter-group">
<label><i class="fas fa-concierge-bell"></i> 服务类型:</label>
<select id="serviceTypeSelect">
<option value="all">全部</option>
<option value="TK会员">TK会员</option>
<option value="测试会员">测试会员</option>
</select>
</div>
<button class="btn btn-primary" onclick="searchClients()"><i class="fas fa-search"></i> 搜索</button>
<button class="btn btn-cancel" onclick="exportAllClients()"><i class="fas fa-file-excel"></i>
导出Excel</button>
</div>
<div class="data-table">
<table>
<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 id="allClientsTable">
<!-- 所有客户数据 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 员工详情模态框 -->
<div id="staffDetailModal" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3 id="staffDetailTitle">员工详情</h3>
<button class="modal-close" onclick="closeStaffDetailModal()">&times;</button>
</div>
<div id="staffDetailContent">
<!-- 员工详情内容会动态加载 -->
</div>
</div>
</div>
<!-- 添加员工模态框 -->
<div id="addStaffModal" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3><i class="fas fa-user-plus"></i> 添加用户</h3>
<button class="modal-close" onclick="closeAddStaffModal()">&times;</button>
</div>
<div class="form-group">
<label><i class="fas fa-user"></i> 姓名:</label>
<input type="text" id="staffName" placeholder="请输入姓名">
</div>
<div class="form-group">
<label><i class="fas fa-user-tag"></i> 角色:</label>
<select id="staffRole">
<option value="staff">员工</option>
<option value="leader">组长</option>
</select>
</div>
<div class="form-group" id="staffLeaderGroup" style="display:none;">
<label><i class="fas fa-user-tie"></i> 所属组长:</label>
<select id="staffLeaderSelect">
<option value="">直属 (管理员)</option>
</select>
</div>
<div class="form-group">
<label><i class="fas fa-user-circle"></i> 登录账号:</label>
<input type="text" id="staffUsername" placeholder="设置登录用户名">
</div>
<div class="form-group">
<label><i class="fas fa-lock"></i> 登录密码:</label>
<input type="password" id="staffPassword" placeholder="设置登录密码">
</div>
<div class="form-group">
<label><i class="fas fa-lock"></i> 确认密码:</label>
<input type="password" id="staffConfirmPassword" placeholder="再次输入密码">
</div>
<div class="form-group">
<label><i class="fas fa-phone"></i> 手机号码:</label>
<input type="tel" id="staffPhone" placeholder="员工手机号码">
</div>
<div class="form-group">
<label><i class="fas fa-envelope"></i> 邮箱:</label>
<input type="email" id="staffEmail" placeholder="员工邮箱">
</div>
<div class="modal-actions">
<button class="btn btn-cancel" onclick="closeAddStaffModal()">取消</button>
<button class="btn btn-primary" onclick="saveStaff()">保存员工</button>
</div>
</div>
</div>
<!-- 重置员工密码模态框 -->
<div id="resetPasswordModal" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3><i class="fas fa-key"></i> 重置密码</h3>
<button class="modal-close" onclick="closeResetPasswordModal()">&times;</button>
</div>
<div class="form-group">
<label><i class="fas fa-user"></i> 员工:</label>
<div id="resetStaffName" style="font-weight:600;"></div>
</div>
<div class="form-group">
<label><i class="fas fa-lock"></i> 新密码:</label>
<input type="password" id="resetStaffPassword" placeholder="请输入新密码">
</div>
<div class="form-group">
<label><i class="fas fa-lock"></i> 确认密码:</label>
<input type="password" id="resetStaffConfirmPassword" placeholder="请再次输入新密码">
</div>
<div class="modal-actions">
<button class="btn btn-cancel" onclick="closeResetPasswordModal()">取消</button>
<button class="btn btn-primary" onclick="confirmResetPassword()">确认重置</button>
</div>
</div>
</div>
<script>
let currentResetStaffId = null;
function openResetPasswordModal(staffId, staffName) {
currentResetStaffId = staffId;
$('resetStaffName').textContent = staffName || '-';
$('resetStaffPassword').value = '';
$('resetStaffConfirmPassword').value = '';
$('resetPasswordModal').style.display = 'flex';
}
function closeResetPasswordModal() {
$('resetPasswordModal').style.display = 'none';
currentResetStaffId = null;
}
async function confirmResetPassword() {
if (!currentResetStaffId) return showMessage('请选择员工', 'error');
const btn = document.querySelector('#resetPasswordModal .btn-primary');
const password = $('resetStaffPassword').value;
const confirmPassword = $('resetStaffConfirmPassword').value;
if (!password) return showMessage('请输入新密码', 'error');
if (password !== confirmPassword) return showMessage('两次输入的密码不一致', 'error');
try {
setBtnLoading(btn, true, '重置中...');
await apiFetch(`/api/staff/${currentResetStaffId}/reset_password`, {
method: 'POST',
body: { password }
});
closeResetPasswordModal();
showMessage('密码已重置', 'success');
} catch (e) {
showMessage('重置失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
// =======================
// ✅ 与 login.html 统一的鉴权存储
// =======================
const API_BASE = window.location.origin; // 同域部署:前后端同一个 host/port
const TOKEN_KEY = 'auth_token';
const USER_KEY = 'auth_user';
function $(id) { return document.getElementById(id); }
function showMessage(text, type) {
const msg = $('message');
msg.textContent = text;
msg.className = 'message ' + type;
msg.style.display = 'block';
setTimeout(() => {
msg.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
msg.style.display = 'none';
msg.style.animation = '';
}, 300);
}, 3000);
}
function getToken() {
return localStorage.getItem(TOKEN_KEY) || '';
}
function clearAuth() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
function getAuthUser() {
try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); }
catch { return null; }
}
// 可选:给某个按钮做 loading传入按钮 DOM
function setBtnLoading(btn, loading, text = '处理中...') {
if (!btn) return;
if (loading) {
btn.dataset._oldHtml = btn.innerHTML;
btn.innerHTML = `<div class="loader"></div> <span>${text}</span>`;
btn.disabled = true;
} else {
btn.innerHTML = btn.dataset._oldHtml || btn.innerHTML;
btn.disabled = false;
delete btn.dataset._oldHtml;
}
}
async function apiFetch(path, { method = 'GET', body, headers = {} } = {}) {
const token = getToken();
const res = await fetch(API_BASE + path, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers
},
body: body ? JSON.stringify(body) : undefined
});
// 401token 失效/未登录
if (res.status === 401) {
clearAuth();
showMessage('登录已过期,请重新登录', 'error');
setTimeout(() => location.href = 'index.html', 800); // 你的登录页文件名如果不是 index.html 改一下
throw new Error('Unauthorized');
}
const contentType = res.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
if (!res.ok) throw new Error(`请求失败(${res.status})`);
return res; // 文件流
}
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.message || `请求失败(${res.status})`);
return data;
}
// =======================
// ✅ 进入页面先校验登录
// =======================
async function ensureAuthedOrRedirect() {
const token = getToken();
if (!token) {
location.href = 'index.html'; // 登录页
return null;
}
// 用 /me 再确认一次 token
const me = await apiFetch('/api/auth/me');
// 同步刷新本地 user避免登录后信息变化
localStorage.setItem(USER_KEY, JSON.stringify(me.user));
return me.user;
}
// =======================
// ✅ 全局数据
// =======================
let adminData = {
stats: { totalAmount: 0, totalClients: 0, expiringCount: 0, expiredCount: 0 },
staff: [],
clients: [],
currentSelectedStaff: null,
staff: [],
clients: [],
currentSelectedStaff: null,
leaders: [], // Cache for leaders
me: null
};
// =======================
// ✅ 初始化
// =======================
async function initPage() {
// 当前日期
const currentDate = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric' };
$('currentDate').textContent = currentDate.toLocaleDateString('zh-CN', options);
// 校验登录
adminData.me = await ensureAuthedOrRedirect();
if (!adminData.me) return;
// 左侧用户信息
applySidebarUser(adminData.me);
// 导航
initNavigation();
// 加载数据
await reloadDashboard();
await reloadClientsTable();
// 员工管理admin 才加载)
await loadStaffManagementList();
// 绑定搜索回车
const searchInput = $('searchClient');
if (searchInput) {
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') searchClients();
});
}
}
function applySidebarUser(user) {
try {
const nameEl = document.querySelector('.user-name > div');
if (nameEl) nameEl.textContent = user.name || user.username || '用户';
const avatarEl = document.querySelector('.user-avatar');
if (avatarEl) avatarEl.textContent = (user.name || user.username || 'U').charAt(0);
const roleEl = document.querySelector('.user-name > div:nth-child(2)');
if (roleEl) roleEl.textContent = user.role === 'admin' ? '超级管理员' : '员工';
} catch { }
}
// =======================
// ✅ 后端数据加载
// =======================
async function reloadDashboard() {
const ret = await apiFetch('/api/dashboard/summary?days=7');
adminData.stats = {
totalAmount: ret.stats.totalAmount || 0,
totalClients: ret.stats.totalClients || 0,
expiringCount: ret.stats.expiringCount || 0,
expiredCount: ret.stats.expiredCount || 0
};
const staffStats = ret.staffStats || [];
adminData.staff = staffStats.map(s => ({
id: s.id,
name: s.name || s.username,
username: s.username,
phone: s.phone || '',
email: s.email || '',
total: s.total || 0,
expired: s.expired || 0,
expiring: s.expiring || 0,
clients: s.clients || 0,
joinDate: (s.created_at ? String(s.created_at).slice(0, 10) : ''),
creator_id: s.creator_id,
creator_name: s.creator_name
}));
$('totalAmount').textContent = '¥ ' + formatNumber(adminData.stats.totalAmount);
$('totalClients').textContent = adminData.stats.totalClients;
$('expiringCount').textContent = adminData.stats.expiringCount;
$('expiredCount').textContent = adminData.stats.expiredCount;
loadStaffList();
}
async function reloadClientsTable(params = {}) {
const url = new URL(API_BASE + '/api/clients');
const query = { page: 1, pageSize: 200, ...params };
Object.entries(query).forEach(([k, v]) => {
if (v === undefined || v === null || v === '' || v === 'all') return;
url.searchParams.set(k, v);
});
// 这里要传相对 path 给 apiFetch
const ret = await apiFetch('/api/clients' + url.search);
adminData.clients = (ret.clients || []).map(c => ({
id: c.id,
staffId: c.staff_id,
staff: c.staff_name || c.staff_username || '',
customer: c.customerName,
phone: c.phone || '',
service: c.serviceType || '',
regDate: c.regDate || '',
expireDate: c.expireDate || '',
amount: Number(c.amount || 0),
status: c.status || 'active',
remark: c.remark || ''
}));
renderClientTables(adminData.clients);
}
// =======================
// ✅ 导航
// =======================
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', async function () {
if (this.id === 'logoutBtn') { logout(); return; }
navItems.forEach(nav => nav.classList.remove('active'));
this.classList.add('active');
const pageId = this.getAttribute('data-page') + 'Page';
switchPage(pageId);
if (pageId === 'dashboardPage') {
await reloadDashboard();
await reloadClientsTable();
}
if (pageId === 'staffPage') {
await loadStaffManagementList();
}
if (pageId === 'clientsPage') {
await reloadClientsTable();
}
});
});
}
function switchPage(pageId) {
const pages = ['dashboardPage', 'staffPage', 'clientsPage', 'remindersPage', 'reportsPage', 'settingsPage'];
pages.forEach(page => {
const el = document.getElementById(page);
if (el) el.style.display = 'none';
});
const target = document.getElementById(pageId);
if (target) target.style.display = 'block';
}
// =======================
// ✅ 员工列表
// =======================
function loadStaffList() {
const staffListEl = $('staffList');
const staffSelectEl = $('staffSelect');
const clientStaffSelectEl = $('clientStaffSelect');
if (!staffListEl) return;
staffListEl.innerHTML = '';
if (staffSelectEl) staffSelectEl.innerHTML = '<option value="all">所有员工</option>';
if (clientStaffSelectEl) clientStaffSelectEl.innerHTML = '<option value="all">所有员工</option>';
// 按 Creator 分组
const groups = {};
adminData.staff.forEach(staff => {
const cid = staff.creator_id || 'admin';
const cname = staff.creator_name || '直属员工';
if (!groups[cid]) {
groups[cid] = {
id: cid,
name: cname,
total: 0,
clients: 0,
expiring: 0,
expired: 0,
staff: []
};
}
const g = groups[cid];
g.total += staff.total;
g.clients += staff.clients;
g.expiring += staff.expiring;
g.expired += staff.expired;
g.staff.push(staff);
// Populate Selects
const opt = document.createElement('option');
opt.value = String(staff.id);
opt.textContent = staff.name;
if (staffSelectEl) staffSelectEl.appendChild(opt.cloneNode(true));
if (clientStaffSelectEl) clientStaffSelectEl.appendChild(opt);
});
// 渲染分组
Object.values(groups).forEach(group => {
const groupEl = document.createElement('div');
groupEl.className = 'group-item';
// 计算员工数量
const staffCount = group.staff.length;
groupEl.innerHTML = `
<div class="group-header" onclick="toggleGroup('group-${group.id}')">
<div class="group-info">
<div class="group-avatar">${group.name.charAt(0)}</div>
<div style="font-weight:600; font-size:1.1rem;">${group.name}</div>
</div>
<div class="group-stats">
<span class="group-stat-item" style="color:#6b7280;"><i class="fas fa-user-tie"></i> ${staffCount} 员工</span>
<span class="group-stat-item" style="color:#3b82f6;"><i class="fas fa-users"></i> ${group.clients} 客户</span>
<span class="group-stat-item" style="color:#059669;"><i class="fas fa-money-bill-wave"></i> ¥${formatNumber(group.total)}</span>
<span class="group-stat-item" style="color:#dc2626;"><i class="fas fa-exclamation-circle"></i> ${group.expiring + group.expired} 异常</span>
</div>
<div class="group-arrow" id="arrow-group-${group.id}"></div>
</div>
<div class="group-content" id="group-${group.id}"></div>
`;
staffListEl.appendChild(groupEl);
const groupContentEl = groupEl.querySelector('.group-content');
group.staff.forEach(staff => {
const staffItem = document.createElement('div');
staffItem.className = 'staff-item';
staffItem.onclick = () => toggleStaffStats(staff.id);
staffItem.innerHTML = `
<div class="staff-avatar">${(staff.name || '').charAt(0)}</div>
<div class="staff-info">
<div class="staff-name">${staff.name}</div>
<div class="staff-details">账号: ${staff.username} | 入职: ${staff.joinDate || '-'}</div>
<div class="staff-stats">
<span class="stat-badge total"><i class="fas fa-money-bill-wave"></i> ¥ ${formatNumber(staff.total)}</span>
<span class="stat-badge expired"><i class="fas fa-clock"></i> ${staff.expired}</span>
<span class="stat-badge expiring"><i class="fas fa-exclamation-circle"></i> ${staff.expiring}</span>
</div>
</div>
<div class="staff-arrow" id="arrow-${staff.id}"></div>
`;
groupContentEl.appendChild(staffItem);
const statsContainer = document.createElement('div');
statsContainer.className = 'staff-stats-grid';
statsContainer.id = `stats-${staff.id}`;
statsContainer.innerHTML = `
<div class="staff-stat-card total" onclick="showStaffDetail(${staff.id}, 'total')">
<div class="staff-stat-value">¥ ${formatNumber(staff.total)}</div>
<div class="staff-stat-label">总金额</div>
</div>
<div class="staff-stat-card expired" onclick="showStaffDetail(${staff.id}, 'expired')">
<div class="staff-stat-value">${staff.expired}</div>
<div class="staff-stat-label">已过期</div>
</div>
<div class="staff-stat-card expiring" onclick="showStaffDetail(${staff.id}, 'expiring')">
<div class="staff-stat-value">${staff.expiring}</div>
<div class="staff-stat-label">即将到期</div>
</div>
<div class="staff-stat-card clients" onclick="showStaffDetail(${staff.id}, 'clients')">
<div class="staff-stat-value">${staff.clients}</div>
<div class="staff-stat-label">客户总数</div>
</div>
`;
groupContentEl.appendChild(statsContainer);
});
});
// 如果没有任何分组(空数据),显示空状态
if (Object.keys(groups).length === 0) {
staffListEl.innerHTML = `
<div class="empty-data">
<div class="icon"><i class="fas fa-users"></i></div>
<p>暂无员工数据</p>
</div>
`;
}
}
function toggleGroup(groupId) {
const content = document.getElementById(groupId);
const arrow = document.getElementById(`arrow-${groupId}`);
if (!content || !arrow) return;
if (content.style.display === 'block') {
content.style.display = 'none';
arrow.style.transform = 'rotate(0deg)';
} else {
content.style.display = 'block';
arrow.style.transform = 'rotate(90deg)';
}
}
function toggleStaffStats(staffId) {
const statsContainer = document.getElementById(`stats-${staffId}`);
const arrow = document.getElementById(`arrow-${staffId}`);
if (!statsContainer || !arrow) return;
if (statsContainer.style.display === 'grid') {
statsContainer.style.display = 'none';
arrow.style.transform = 'rotate(0deg)';
} else {
adminData.staff.forEach(staff => {
if (staff.id !== staffId) {
const otherStats = document.getElementById(`stats-${staff.id}`);
const otherArrow = document.getElementById(`arrow-${staff.id}`);
if (otherStats) otherStats.style.display = 'none';
if (otherArrow) otherArrow.style.transform = 'rotate(0deg)';
}
});
statsContainer.style.display = 'grid';
arrow.style.transform = 'rotate(90deg)';
}
}
async function showStaffDetail(staffId, type) {
const staff = adminData.staff.find(s => s.id === staffId);
if (!staff) return;
adminData.currentSelectedStaff = staff;
let status = 'all';
if (type === 'expired') status = 'expired';
if (type === 'expiring') status = 'expiring';
await reloadClientsTable({ staffId, status });
const filteredClients = adminData.clients;
let title = '', description = '';
if (type === 'total' || type === 'clients') {
title = `${staff.name} - 所有客户`;
description = `共 ${filteredClients.length} 个客户,总金额 ¥${formatNumber(staff.total)}`;
} else if (type === 'expired') {
title = `${staff.name} - 已过期客户`;
description = `共 ${filteredClients.length} 个已过期客户`;
} else if (type === 'expiring') {
title = `${staff.name} - 即将到期客户`;
description = `共 ${filteredClients.length} 个即将到期客户`;
}
$('staffDetailTitle').textContent = title;
let content = `
<div style="margin-bottom: 20px; color: var(--text-muted);">
<i class="fas fa-info-circle"></i> ${description}
</div>
`;
if (filteredClients.length === 0) {
content += `
<div class="empty-data">
<div class="icon"><i class="fas fa-database"></i></div>
<p>暂无相关客户数据</p>
</div>
`;
} else {
content += `
<table class="staff-client-table">
<thead>
<tr>
<th>客户姓名</th><th>联系电话</th><th>服务类型</th><th>登记日期</th>
<th>到期时间</th><th>金额</th><th>状态</th><th>备注</th>
</tr>
</thead>
<tbody>
`;
filteredClients.forEach(client => {
const statusInfo = getStatusInfo(client);
content += `
<tr>
<td>${client.customer}</td>
<td>${client.phone}</td>
<td>${client.service}</td>
<td>${client.regDate}</td>
<td>${client.expireDate}</td>
<td style="font-weight:600;">¥ ${formatNumber(client.amount)}</td>
<td style="${statusInfo.style} font-weight:600;">${statusInfo.text}</td>
<td>${client.remark}</td>
</tr>
`;
});
content += `</tbody></table>`;
}
$('staffDetailContent').innerHTML = content;
$('staffDetailModal').style.display = 'flex';
}
function closeStaffDetailModal() {
$('staffDetailModal').style.display = 'none';
reloadDashboard().then(() => reloadClientsTable()).catch(() => { });
}
// =======================
// ✅ 员工管理admin 才能用)
// =======================
async function loadStaffManagementList() {
const leaderEl = $('leaderManagementList');
const staffEl = $('staffManagementList');
if (!leaderEl || !staffEl) return;
if (!adminData.me || adminData.me.role !== 'admin') {
const msg = `
<div class="empty-data">
<div class="icon"><i class="fas fa-lock"></i></div>
<p>只有管理员可以管理用户</p>
</div>
`;
leaderEl.innerHTML = msg;
staffEl.innerHTML = msg;
return;
}
const ret = await apiFetch('/api/staff?includeInactive=true');
const allUsers = ret.staff || [];
const leaders = allUsers.filter(u => u.role === 'leader');
const staffs = allUsers.filter(u => u.role === 'staff');
// 渲染组长列表
leaderEl.innerHTML = '';
if (leaders.length === 0) {
leaderEl.innerHTML = `
<div class="empty-data">
<div class="icon"><i class="fas fa-user-tie"></i></div>
<p>暂无组长,点击"添加组长"创建</p>
</div>
`;
} else {
leaders.forEach(user => renderUserItem(leaderEl, user, '#3b82f6'));
}
// 渲染员工列表
staffEl.innerHTML = '';
if (staffs.length === 0) {
staffEl.innerHTML = `
<div class="empty-data">
<div class="icon"><i class="fas fa-users"></i></div>
<p>暂无员工,点击"添加员工"创建</p>
</div>
`;
} else {
staffs.forEach(user => renderUserItem(staffEl, user, '#10b981'));
}
}
function renderUserItem(container, user, avatarColor) {
const joinDate = user.created_at ? String(user.created_at).slice(0, 10) : '-';
const safeName = String(user.name || user.username || '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'");
const isInactive = user.status === 'inactive';
const statusStyle = isInactive ? 'color:#ef4444;' : 'color:#10b981;';
const statusText = isInactive ? '已禁用' : '正常';
const itemStyle = isInactive ? 'opacity:0.6;background:#fef2f2;' : '';
const toggleBtnClass = isInactive ? 'btn-primary' : 'btn-danger';
const toggleBtnIcon = isInactive ? 'fa-check-circle' : 'fa-ban';
const toggleBtnText = isInactive ? '启用' : '禁用';
const item = document.createElement('div');
item.className = 'management-staff-item';
item.style.cssText = itemStyle;
item.innerHTML = `
<input type="checkbox" class="staff-checkbox" value="${user.id}">
<div class="staff-avatar" style="background:${isInactive ? '#9ca3af' : avatarColor};">${(user.name || user.username || '').charAt(0)}</div>
<div class="staff-info">
<div class="staff-name">${user.name || user.username}${isInactive ? '<span style="background:#ef4444;color:white;padding:2px 8px;border-radius:4px;font-size:0.75rem;margin-left:8px;">已禁用</span>' : ''}</div>
<div class="staff-details">
<span><i class="fas fa-user-circle"></i> ${user.username}</span> |
<span><i class="fas fa-phone"></i> ${user.phone || '-'}</span> |
<span><i class="fas fa-envelope"></i> ${user.email || '-'}</span>
</div>
<div class="staff-details">
<span><i class="fas fa-calendar-alt"></i> 入职: ${joinDate}</span> |
<span style="${statusStyle}"><i class="fas fa-circle"></i> ${statusText}</span>
</div>
</div>
</div>
<div class="staff-actions">
<button class="btn btn-small btn-primary" onclick='openUserModal(${JSON.stringify(user).replace(/'/g, "&#39;")})'>
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-small btn-warning" onclick="openResetPasswordModal(${user.id}, '${safeName}')">
<i class="fas fa-key"></i> 重置密码
</button>
<button class="btn btn-small ${toggleBtnClass}" onclick="toggleUserStatus(${user.id})">
<i class="fas ${toggleBtnIcon}"></i> ${toggleBtnText}
</button>
</div>
`;
container.appendChild(item);
}
let currentEditUserId = null;
function openUserModal(userOrRole = 'staff') {
const modal = $('addStaffModal');
const title = modal.querySelector('.modal-header h3');
modal.style.display = 'flex';
// 重置表单
$('staffName').value = '';
$('staffUsername').value = '';
$('staffPassword').value = '';
$('staffConfirmPassword').value = '';
$('staffPhone').value = '';
$('staffEmail').value = '';
// 只有 admin 可以看到所属组长选项
const leaderGroup = $('staffLeaderGroup');
const leaderSelect = $('staffLeaderSelect');
if (leaderGroup) leaderGroup.style.display = 'none';
if (typeof userOrRole === 'object' && userOrRole !== null) {
// 编辑模式
const user = userOrRole;
currentEditUserId = user.id;
title.innerHTML = '<i class="fas fa-user-edit"></i> 编辑用户';
$('staffRole').value = user.role;
$('staffName').value = user.name || '';
$('staffUsername').value = user.username || '';
$('staffPhone').value = user.phone || '';
$('staffEmail').value = user.email || '';
$('staffPassword').placeholder = '留空则不修改密码';
$('staffConfirmPassword').placeholder = '留空则不修改密码';
// 如果是编辑员工且当前是admin显示修改组长选项
if (user.role === 'staff' && leaderGroup) {
leaderGroup.style.display = 'block';
populateLeaderSelect(user.creator_id);
}
} else {
// 新增模式
currentEditUserId = null;
const role = typeof userOrRole === 'string' ? userOrRole : 'staff';
title.innerHTML = `<i class="fas fa-user-plus"></i> 添加${role === 'leader' ? '组长' : '员工'}`;
$('staffRole').value = role;
$('staffPassword').placeholder = '设置登录密码';
$('staffConfirmPassword').placeholder = '再次输入密码';
// 新增员工时也可以选择组长
if (role === 'staff' && leaderGroup) {
leaderGroup.style.display = 'block';
populateLeaderSelect(null);
}
}
}
async function populateLeaderSelect(currentCreatorId) {
const select = $('staffLeaderSelect');
if (!select) return;
select.innerHTML = '<option value="">加载中...</option>';
try {
// Check cache or fetch
if (!adminData.leaders || adminData.leaders.length === 0) {
const ret = await apiFetch('/api/staff?includeInactive=true');
const all = ret.staff || [];
adminData.leaders = all.filter(u => u.role === 'leader');
}
let html = '<option value="">直属 (管理员)</option>';
adminData.leaders.forEach(l => {
const selected = (currentCreatorId && currentCreatorId === l.id) ? 'selected' : '';
html += `<option value="${l.id}" ${selected}>${l.name || l.username} (组长)</option>`;
});
select.innerHTML = html;
// 如果 currentCreatorId 既不是 admin 也不在列表里(可能被删了),默认选 admin 或保持原样。
// 这里简单处理如果value匹配不上select依然会显示第一项或空所以没大问题。
} catch (e) {
console.error('Fetch leaders failed', e);
select.innerHTML = '<option value="">加载失败</option>';
}
}
function closeAddStaffModal() {
$('addStaffModal').style.display = 'none';
currentEditUserId = null;
}
async function saveStaff() {
const btn = document.querySelector('#addStaffModal .btn-primary');
const role = $('staffRole').value;
const name = $('staffName').value.trim();
const username = $('staffUsername').value.trim();
const password = $('staffPassword').value;
const confirmPassword = $('staffConfirmPassword').value;
const phone = $('staffPhone').value.trim();
const email = $('staffEmail').value.trim();
if (!name || !username) return showMessage('请填写姓名和登录账号', 'error');
// 新增时必须填密码,编辑时可选
if (!currentEditUserId && !password) return showMessage('请设置登录密码', 'error');
if (password && password !== confirmPassword) return showMessage('两次输入的密码不一致', 'error');
try {
setBtnLoading(btn, true, '保存中...');
if (currentEditUserId) {
// 编辑
const body = { role, name, username, phone, email };
if (password) body.password = password;
// 处理组长修改
const leaderSelect = $('staffLeaderSelect');
if (leaderSelect && leaderSelect.offsetParent !== null) { // visible
body.creator_id = leaderSelect.value; // "" for admin, "id" for leader
}
await apiFetch(`/api/staff/${currentEditUserId}`, {
method: 'PUT',
body: body
});
showMessage('用户修改成功', 'success');
} else {
// 新增
const body = { role, name, username, password, phone, email };
// 处理组长选择
const leaderSelect = $('staffLeaderSelect');
if (leaderSelect && leaderSelect.offsetParent !== null) {
body.creator_id = leaderSelect.value;
}
await apiFetch('/api/staff', {
method: 'POST',
body: body
});
showMessage('用户添加成功', 'success');
}
closeAddStaffModal();
await loadStaffManagementList();
} catch (e) {
showMessage('保存失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
async function deleteSelectedStaff() {
const checkboxes = document.querySelectorAll('.staff-checkbox:checked');
if (checkboxes.length === 0) return showMessage('请选择要删除的员工', 'error');
if (!confirm(`确定要删除选中的 ${checkboxes.length} 名员工吗?`)) return;
try {
for (const cb of checkboxes) {
const id = Number(cb.value);
await apiFetch(`/api/staff/${id}`, { method: 'DELETE' });
}
showMessage('删除成功', 'success');
await reloadDashboard();
await loadStaffManagementList();
} catch (e) {
showMessage('删除失败:' + e.message, 'error');
}
}
async function deleteStaff(staffId) {
if (!confirm('确定要禁用此用户吗?')) return;
try {
await apiFetch(`/api/staff/${staffId}`, { method: 'DELETE' });
showMessage('已禁用', 'success');
await reloadDashboard();
await loadStaffManagementList();
} catch (e) {
showMessage('操作失败:' + e.message, 'error');
}
}
async function toggleUserStatus(userId) {
try {
const ret = await apiFetch(`/api/staff/${userId}/toggle_status`, { method: 'POST' });
showMessage(ret.status === 'active' ? '已启用' : '已禁用', 'success');
await loadStaffManagementList();
} catch (e) {
showMessage('操作失败:' + e.message, 'error');
}
}
// =======================
async function deleteClient(clientId) {
if (!confirm('确定要删除该客户吗?')) return;
try {
await apiFetch(`/api/clients/${clientId}`, { method: 'DELETE' });
showMessage('客户已删除', 'success');
await reloadDashboard();
const clientsPage = $('clientsPage');
if (clientsPage && clientsPage.style.display !== 'none') {
const q = $('searchClient').value.trim();
const staffId = $('clientStaffSelect').value;
const serviceType = $('serviceTypeSelect').value;
await reloadClientsTable({ q, staffId, serviceType });
} else {
const staffId = $('staffSelect').value;
const status = $('statusSelect').value;
const dateFrom = $('dateFrom').value;
const dateTo = $('dateTo').value;
await reloadClientsTable({ staffId, status, dateFrom, dateTo });
}
} catch (e) {
showMessage('删除失败:' + e.message, 'error');
}
}
// ✅ 表格渲染 + 筛选/搜索/导出
// =======================
function renderClientTables(list) {
const tbody = $('dataTableBody');
const allClientsTable = $('allClientsTable');
if (tbody) tbody.innerHTML = '';
if (allClientsTable) allClientsTable.innerHTML = '';
list.forEach(client => {
const statusInfo = getStatusInfo(client);
const html = `
<td>${client.staff}</td>
<td>${client.customer}</td>
<td>${client.phone}</td>
<td>${client.service}</td>
<td>${client.regDate}</td>
<td>${client.expireDate}</td>
<td style="font-weight:600;">¥ ${formatNumber(client.amount)}</td>
<td style="${statusInfo.style} font-weight:600;">${statusInfo.text}</td>
<td>
<button class="btn btn-small btn-danger" onclick="deleteClient(${client.id})">
<i class="fas fa-trash"></i> 删除
</button>
</td>
<td>${client.remark}</td>
`;
if (tbody) {
const row = document.createElement('tr');
row.innerHTML = html;
tbody.appendChild(row);
}
if (allClientsTable) {
const row = document.createElement('tr');
row.innerHTML = html;
allClientsTable.appendChild(row);
}
});
}
async function filterData() {
const btn = document.querySelector('#dashboardPage .filter-bar .btn-primary');
const staffId = $('staffSelect').value;
const status = $('statusSelect').value;
const dateFrom = $('dateFrom').value;
const dateTo = $('dateTo').value;
try {
setBtnLoading(btn, true, '筛选中...');
await reloadClientsTable({ staffId, status, dateFrom, dateTo });
showMessage('筛选完成', 'success');
} catch (e) {
showMessage('筛选失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
async function searchClients() {
const btn = document.querySelector('#clientsPage .filter-bar .btn-primary');
const q = $('searchClient').value.trim();
const staffId = $('clientStaffSelect').value;
const serviceType = $('serviceTypeSelect').value;
try {
setBtnLoading(btn, true, '搜索中...');
await reloadClientsTable({ q, staffId, serviceType });
showMessage('搜索完成', 'success');
} catch (e) {
showMessage('搜索失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
function exportData() {
const staffId = $('staffSelect').value;
const status = $('statusSelect').value;
const dateFrom = $('dateFrom').value;
const dateTo = $('dateTo').value;
downloadExport({ staffId, status, dateFrom, dateTo });
}
function exportAllClients() {
const q = $('searchClient').value.trim();
const staffId = $('clientStaffSelect').value;
const serviceType = $('serviceTypeSelect').value;
downloadExport({ q, staffId, serviceType });
}
async function downloadExport(params = {}) {
const btn = document.querySelector('.btn.btn-cancel'); // 简单取一个,你也可分别传入
const url = new URL(API_BASE + '/api/export/clients');
Object.entries(params).forEach(([k, v]) => {
if (v === undefined || v === null || v === '' || v === 'all') return;
url.searchParams.set(k, v);
});
try {
setBtnLoading(btn, true, '导出中...');
// 这里用 fetch + blob 才能带 Authorization
const res = await apiFetch('/api/export/clients' + url.search);
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `clients_${Date.now()}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
showMessage('导出成功', 'success');
} catch (e) {
showMessage('导出失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
function getStatusInfo(client) {
const today = new Date();
const expireDate = client.expireDate ? new Date(client.expireDate) : null;
if (!expireDate || Number.isNaN(expireDate.getTime())) {
return { text: '正常', style: 'color: var(--success);' };
}
const diffDays = Math.ceil((expireDate - today) / (1000 * 60 * 60 * 24));
if (diffDays < 0) return { text: '已过期', style: 'color: var(--danger);' };
if (diffDays <= 7) return { text: `${diffDays}天后到期`, style: 'color: var(--warning);' };
return { text: '正常', style: 'color: var(--success);' };
}
function formatNumber(num) {
const n = Number(num || 0);
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function logout() {
if (!confirm('确定要退出登录吗?')) return;
clearAuth();
showMessage('已退出登录', 'success');
setTimeout(() => location.href = 'index.html', 600);
}
function showAllClients() {
document.querySelector('.nav-item[data-page="clients"]').click();
}
async function showExpiringClients() {
document.querySelector('.nav-item[data-page="clients"]').click();
await reloadClientsTable({ status: 'expiring' });
}
async function showExpiredClients() {
document.querySelector('.nav-item[data-page="clients"]').click();
await reloadClientsTable({ status: 'expired' });
}
// 页面加载
document.addEventListener('DOMContentLoaded', () => {
initPage().catch(e => {
if (String(e.message || '').includes('Unauthorized')) return;
showMessage('初始化失败:' + e.message, 'error');
});
});
</script>
<!-- Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<div id="message" class="message" style="display:none;"></div>
</body>
</html>