Files
CrmSystem/public/leader.html
2026-01-23 21:56:02 +08:00

1567 lines
44 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户管理系统 - 组长管理</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, #3b82f6 0%, #1d4ed8 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(--info);
border-radius: var(--radius-full);
}
.header .date {
color: var(--text-muted);
margin-top: 8px;
font-size: 0.95rem;
}
/* ========== 员工管理 ========== */
.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, #eff6ff 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(--info);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
background: #2563eb;
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);
}
.btn-small {
padding: 6px 14px;
font-size: 0.9rem;
}
.btn-cancel {
background: var(--bg-hover);
color: var(--text-secondary);
}
.btn-cancel:hover {
background: #e2e8f0;
color: var(--text-primary);
}
.btn[disabled] {
opacity: .7;
cursor: not-allowed;
transform: none !important;
}
/* ========== 员工列表 ========== */
.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-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--info);
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: 4px;
}
.staff-actions {
display: flex;
gap: 8px;
margin-left: auto;
}
/* ========== 模态框 ========== */
.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: 500px;
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(--info);
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(--info);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.modal-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 32px;
}
/* ========== 空数据提示 ========== */
.empty-data {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-data .icon {
font-size: 3rem;
margin-bottom: 20px;
opacity: 0.3;
}
/* ========== 消息提示 ========== */
.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;
}
}
.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);
}
}
/* ========== 响应式 ========== */
@media (max-width: 768px) {
.sidebar {
width: 70px;
}
.main-content {
margin-left: 70px;
padding: 20px;
}
.logo h1,
.subtitle,
.user-name,
.nav-text {
display: none;
}
}
/* ========== 滚动条 ========== */
::-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;
}
/* ========== 数据总览卡片 ========== */
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow);
border: 1px solid var(--border);
display: flex;
align-items: center;
gap: 16px;
cursor: pointer;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--info);
border-radius: var(--radius-full);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--info);
}
.stat-card.amount::before {
background: #10b981;
}
.stat-card.clients::before {
background: #3b82f6;
}
.stat-card.expiring::before {
background: #f59e0b;
}
.stat-card.expired::before {
background: #ef4444;
}
.stat-card.staff::before {
background: #8b5cf6;
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.stat-icon.blue {
background: #dbeafe;
color: #3b82f6;
}
.stat-icon.green {
background: #d1fae5;
color: #10b981;
}
.stat-icon.yellow {
background: #fef3c7;
color: #f59e0b;
}
.stat-icon.red {
background: #fee2e2;
color: #ef4444;
}
.stat-icon.purple {
background: #ede9fe;
color: #8b5cf6;
}
.stat-info h3 {
font-size: 1.8rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-info p {
color: var(--text-muted);
font-size: 0.9rem;
}
.staff-stats {
display: flex;
gap: 16px;
margin-top: 8px;
flex-wrap: wrap;
}
.staff-stat-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.staff-stat-badge.clients {
background: #dbeafe;
color: #3b82f6;
}
.staff-stat-badge.amount {
background: #d1fae5;
color: #059669;
}
.staff-stat-badge.expiring {
background: #fef3c7;
color: #d97706;
}
.staff-stat-badge.expired {
background: #fee2e2;
color: #dc2626;
}
/* ========== 员工详细统计卡片 ========== */
.staff-stats-grid {
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 0 28px 20px 28px;
padding: 20px;
background: #eff6ff;
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: 18px;
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);
border-color: var(--info);
}
.staff-stat-value {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 6px;
color: var(--text-primary);
}
.staff-stat-label {
font-size: 0.85rem;
color: var(--text-muted);
}
.staff-arrow {
color: var(--text-muted);
font-size: 1rem;
transition: transform 0.3s;
margin-left: auto;
}
/* ========== 客户详情模态框 ========== */
.modal.wide {
max-width: 900px;
width: 95%;
}
.staff-client-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.staff-client-table th,
.staff-client-table td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid var(--border-light);
}
.staff-client-table th {
background: #f8fafc;
font-weight: 600;
color: var(--text-secondary);
}
.staff-client-table tr:hover {
background: var(--bg-hover);
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
.staff-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.staff-stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="admin-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">
<h1><i class="fas fa-user-tie"></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;">Team Leader</div>
</div>
</div>
<div class="nav-menu">
<div class="nav-item active" data-page="staff">
<span><i class="fas fa-users"></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="staffPage">
<div class="header">
<h2>员工管理</h2>
<div class="date">创建和管理我的员工账号</div>
</div>
<!-- 数据总览 -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card amount">
<div class="stat-icon green"><i class="fas fa-money-bill-wave"></i></div>
<div class="stat-info">
<h3 id="totalAmount">¥ 0</h3>
<p>总金额</p>
</div>
</div>
<div class="stat-card staff">
<div class="stat-icon purple"><i class="fas fa-users"></i></div>
<div class="stat-info">
<h3 id="totalStaff">0</h3>
<p>员工总数</p>
</div>
</div>
<div class="stat-card clients">
<div class="stat-icon blue"><i class="fas fa-address-book"></i></div>
<div class="stat-info">
<h3 id="totalClients">0</h3>
<p>客户总数</p>
</div>
</div>
<div class="stat-card expiring">
<div class="stat-icon yellow"><i class="fas fa-clock"></i></div>
<div class="stat-info">
<h3 id="totalExpiring">0</h3>
<p>即将到期</p>
</div>
</div>
<div class="stat-card expired">
<div class="stat-icon red"><i class="fas fa-exclamation-circle"></i></div>
<div class="stat-info">
<h3 id="totalExpired">0</h3>
<p>已到期</p>
</div>
</div>
</div>
<div class="staff-management">
<div class="management-header">
<h3><i class="fas fa-user-plus"></i> 员工列表</h3>
<div class="action-buttons">
<button class="btn btn-primary" onclick="openStaffModal()">
<i class="fas fa-plus"></i> 添加员工
</button>
</div>
</div>
<div class="staff-management-list" id="staffManagementList">
<!-- 员工列表动态加载 -->
</div>
</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-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>
<!-- 员工客户详情模态框 -->
<div id="staffDetailModal" class="modal-overlay" style="display:none;">
<div class="modal wide">
<div class="modal-header">
<h3 id="staffDetailTitle"><i class="fas fa-user"></i> 员工详情</h3>
<button class="modal-close" onclick="closeStaffDetailModal()">&times;</button>
</div>
<div id="staffDetailContent">
<!-- 动态加载 -->
</div>
</div>
</div>
<div id="message" class="message" style="display:none;"></div>
<!-- Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script>
// =======================
// 配置
// =======================
const API_BASE = window.location.origin;
const TOKEN_KEY = 'auth_token';
const USER_KEY = 'auth_user';
let currentResetStaffId = null;
let currentEditStaffId = null;
let leaderData = { me: null, staff: [] };
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; }
}
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
});
if (res.status === 401) {
clearAuth();
showMessage('登录已过期,请重新登录', 'error');
setTimeout(() => location.href = 'index.html', 800);
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;
}
const me = await apiFetch('/api/auth/me');
// 验证是否为 leader 角色
if (me.user.role !== 'leader') {
showMessage('无权访问此页面', 'error');
clearAuth();
setTimeout(() => location.href = 'index.html', 800);
return null;
}
localStorage.setItem(USER_KEY, JSON.stringify(me.user));
return me.user;
}
// =======================
// 初始化
// =======================
async function initPage() {
leaderData.me = await ensureAuthedOrRedirect();
if (!leaderData.me) return;
applySidebarUser(leaderData.me);
initNavigation();
await loadStaffManagementList();
}
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 || '组').charAt(0);
} catch { }
}
function initNavigation() {
const logoutBtn = $('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', logout);
}
}
// =======================
// 员工管理
// =======================
async function loadStaffManagementList() {
const el = $('staffManagementList');
if (!el) return;
try {
const ret = await apiFetch('/api/staff?includeInactive=true');
const staffList = ret.staff || [];
leaderData.staff = staffList; // 保存供后续使用
// 更新数据总览
const totalStaff = staffList.filter(s => s.status === 'active').length;
const totalClients = staffList.reduce((sum, s) => sum + (s.clientsCount || 0), 0);
const totalAmount = staffList.reduce((sum, s) => sum + (s.totalAmount || 0), 0);
const totalExpiring = staffList.reduce((sum, s) => sum + (s.expiringCount || 0), 0);
const totalExpired = staffList.reduce((sum, s) => sum + (s.expiredCount || 0), 0);
$('totalAmount').textContent = '¥ ' + totalAmount.toLocaleString();
$('totalStaff').textContent = totalStaff;
$('totalClients').textContent = totalClients;
$('totalExpiring').textContent = totalExpiring;
$('totalExpired').textContent = totalExpired;
el.innerHTML = '';
if (staffList.length === 0) {
el.innerHTML = `
<div class="empty-data">
<div class="icon"><i class="fas fa-users"></i></div>
<p>暂无员工数据,点击"添加员工"创建第一位员工</p>
</div>
`;
return;
}
staffList.forEach(staff => {
const safeStaffName = String(staff.name || staff.username || '')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'");
const isInactive = staff.status === 'inactive';
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 staffItem = document.createElement('div');
staffItem.className = 'management-staff-item';
staffItem.style.cssText = itemStyle + 'cursor:pointer;';
staffItem.onclick = () => toggleStaffStats(staff.id);
staffItem.innerHTML = `
<div class="staff-avatar" style="background:${isInactive ? '#9ca3af' : '#3b82f6'}">${(staff.name || staff.username || '').charAt(0)}</div>
<div class="staff-info">
<div class="staff-name">${staff.name || staff.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-stats">
<span class="staff-stat-badge amount"><i class="fas fa-yen-sign"></i> ¥${(staff.totalAmount || 0).toLocaleString()}</span>
<span class="staff-stat-badge clients"><i class="fas fa-address-book"></i> 客户 ${staff.clientsCount || 0}</span>
${staff.expiringCount ? `<span class="staff-stat-badge expiring"><i class="fas fa-clock"></i> 即将到期 ${staff.expiringCount}</span>` : ''}
${staff.expiredCount ? `<span class="staff-stat-badge expired"><i class="fas fa-exclamation-circle"></i> 已到期 ${staff.expiredCount}</span>` : ''}
</div>
</div>
<div class="staff-arrow" id="arrow-${staff.id}">▶</div>
<div class="staff-actions" onclick="event.stopPropagation()">
<button class="btn btn-small btn-primary" onclick='openStaffModal(${JSON.stringify(staff).replace(/'/g, "&#39;")})'>
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-small btn-warning" onclick="openResetPasswordModal(${staff.id}, '${safeStaffName}')">
<i class="fas fa-key"></i> 重置密码
</button>
<button class="btn btn-small ${toggleBtnClass}" onclick="toggleUserStatus(${staff.id})">
<i class="fas ${toggleBtnIcon}"></i> ${toggleBtnText}
</button>
</div>
`;
el.appendChild(staffItem);
// 可展开统计卡片
const statsContainer = document.createElement('div');
statsContainer.className = 'staff-stats-grid';
statsContainer.id = `stats-${staff.id}`;
statsContainer.innerHTML = `
<div class="staff-stat-card" onclick="showStaffDetail(${staff.id}, 'total')">
<div class="staff-stat-value">¥ ${(staff.totalAmount || 0).toLocaleString()}</div>
<div class="staff-stat-label">总金额</div>
</div>
<div class="staff-stat-card" onclick="showStaffDetail(${staff.id}, 'clients')">
<div class="staff-stat-value">${staff.clientsCount || 0}</div>
<div class="staff-stat-label">客户总数</div>
</div>
<div class="staff-stat-card" onclick="showStaffDetail(${staff.id}, 'expiring')">
<div class="staff-stat-value">${staff.expiringCount || 0}</div>
<div class="staff-stat-label">即将到期</div>
</div>
<div class="staff-stat-card" onclick="showStaffDetail(${staff.id}, 'expired')">
<div class="staff-stat-value">${staff.expiredCount || 0}</div>
<div class="staff-stat-label">已到期</div>
</div>
`;
el.appendChild(statsContainer);
});
} catch (e) {
el.innerHTML = `
<div class="empty-data">
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
<p>加载失败:${e.message}</p>
</div>
`;
}
}
// =======================
// 添加员工
// =======================
// =======================
// 添加/编辑员工
// =======================
function openStaffModal(staff = null) {
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 = '';
if (staff) {
// 编辑模式
currentEditStaffId = staff.id;
title.innerHTML = '<i class="fas fa-user-edit"></i> 编辑员工';
$('staffName').value = staff.name || '';
$('staffUsername').value = staff.username || '';
$('staffPhone').value = staff.phone || '';
$('staffEmail').value = staff.email || '';
$('staffPassword').placeholder = '留空则不修改密码';
$('staffConfirmPassword').placeholder = '留空则不修改密码';
} else {
// 新增模式
currentEditStaffId = null;
title.innerHTML = '<i class="fas fa-user-plus"></i> 添加员工';
$('staffPassword').placeholder = '设置登录密码';
$('staffConfirmPassword').placeholder = '再次输入密码';
}
}
function closeAddStaffModal() {
$('addStaffModal').style.display = 'none';
currentEditStaffId = null;
}
async function saveStaff() {
const btn = document.querySelector('#addStaffModal .btn-primary');
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 (!currentEditStaffId && !password) return showMessage('请设置登录密码', 'error');
if (password && password !== confirmPassword) return showMessage('两次输入的密码不一致', 'error');
try {
setBtnLoading(btn, true, '保存中...');
if (currentEditStaffId) {
// 编辑
const body = { name, username, phone, email };
if (password) body.password = password;
await apiFetch(`/api/staff/${currentEditStaffId}`, { method: 'PUT', body });
showMessage('员工修改成功', 'success');
} else {
// 新增
await apiFetch('/api/staff', { method: 'POST', body: { name, username, password, phone, email } });
showMessage('员工添加成功', 'success');
}
closeAddStaffModal();
await loadStaffManagementList();
} catch (e) {
showMessage('添加失败:' + e.message, 'error');
} finally {
setBtnLoading(btn, false);
}
}
// =======================
// 重置密码
// =======================
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);
}
}
// =======================
// 启用/禁用员工
// =======================
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');
}
}
// =======================
// 展开/收起员工统计
// =======================
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 {
// 关闭其他展开的
leaderData.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 = leaderData.staff.find(s => s.id === staffId);
if (!staff) return;
let status = 'all';
if (type === 'expired') status = 'expired';
if (type === 'expiring') status = 'expiring';
try {
const ret = await apiFetch(`/api/clients?staffId=${staffId}&status=${status}`);
const clients = ret.clients || [];
let title = '', description = '';
if (type === 'total' || type === 'clients') {
title = `${staff.name || staff.username} - 所有客户`;
description = `${clients.length} 个客户,总金额 ¥${(staff.totalAmount || 0).toLocaleString()}`;
} else if (type === 'expired') {
title = `${staff.name || staff.username} - 已过期客户`;
description = `${clients.length} 个已过期客户`;
} else if (type === 'expiring') {
title = `${staff.name || staff.username} - 即将到期客户`;
description = `${clients.length} 个即将到期客户`;
}
$('staffDetailTitle').innerHTML = `<i class="fas fa-user"></i> ${title}`;
let content = `
<div style="margin-bottom: 20px; color: var(--text-muted);">
<i class="fas fa-info-circle"></i> ${description}
</div>
`;
if (clients.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>
`;
clients.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;">¥ ${(client.amount || 0).toLocaleString()}</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';
} catch (e) {
showMessage('加载客户数据失败:' + e.message, 'error');
}
}
function getStatusInfo(client) {
if (!client.expireDate) return { text: '未知', style: 'color:#9ca3af;' };
const now = new Date();
const exp = new Date(client.expireDate);
const diffDays = Math.ceil((exp - now) / (1000 * 60 * 60 * 24));
if (diffDays < 0) return { text: '已过期', style: 'color:#ef4444;' };
if (diffDays <= 7) return { text: '即将到期', style: 'color:#f59e0b;' };
return { text: '正常', style: 'color:#10b981;' };
}
function closeStaffDetailModal() {
$('staffDetailModal').style.display = 'none';
}
// =======================
// 退出登录
// =======================
function logout() {
if (!confirm('确定要退出登录吗?')) return;
clearAuth();
showMessage('已退出登录', 'success');
setTimeout(() => location.href = 'index.html', 600);
}
// =======================
// 页面加载
// =======================
document.addEventListener('DOMContentLoaded', () => {
initPage().catch(e => {
if (String(e.message || '').includes('Unauthorized')) return;
showMessage('初始化失败:' + e.message, 'error');
});
});
</script>
</body>
</html>