Files
CrmSystem/public/staff.html

1768 lines
48 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>
/* 字体图标备用方案 */
@font-face {
font-family: 'FontAwesome';
src: url('data:application/x-font-woff2;charset=utf-8;base64,...') format('woff2');
font-weight: normal;
font-style: normal;
}
/* 使用Unicode字符作为图标备用 */
.icon-address-book:before {
content: "📒";
}
.icon-user-plus:before {
content: "";
}
.icon-list:before {
content: "📋";
}
.icon-calendar:before {
content: "📅";
}
.icon-user:before {
content: "👤";
}
.icon-phone:before {
content: "📱";
}
.icon-clock:before {
content: "⏰";
}
.icon-money:before {
content: "💰";
}
.icon-bell:before {
content: "🔔";
}
.icon-calculator:before {
content: "🧮";
}
.icon-plus:before {
content: "";
}
.icon-rotate:before {
content: "🔄";
}
.icon-database:before {
content: "🗄️";
}
.icon-info:before {
content: "";
}
.icon-calendar-plus:before {
content: "📆➕";
}
.icon-logout:before {
content: "🚪";
}
:root {
--primary: #8fd6b3;
--primary-dark: #5bbf93;
--primary-deep: #2f8f67;
--danger: #ef4444;
--success: #10b981;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
header {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-deep) 100%);
color: #fff;
padding: 25px 30px;
position: relative;
overflow: hidden;
}
header::before {
content: '';
position: absolute;
top: -50px;
right: -50px;
width: 150px;
height: 150px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 70%);
border-radius: 50%;
}
h1 {
font-size: 2.2rem;
margin-bottom: 8px;
font-weight: 600;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
font-weight: 300;
}
.user-info-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
}
.user-badge {
display: flex;
align-items: center;
gap: 12px;
}
.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;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
font-size: 1.1rem;
}
.user-role {
font-size: 0.85rem;
opacity: 0.9;
}
.logout-btn {
padding: 8px 20px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 999px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.content {
display: flex;
flex-direction: column;
padding: 25px;
}
.form-section {
background-color: #f9fafc;
border-radius: 10px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid #eaeef5;
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #eaeef5;
font-size: 1.5rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 8px;
font-weight: 600;
color: #3a506b;
font-size: 0.95rem;
}
input,
select,
textarea {
padding: 12px 15px;
border: 1px solid #d1d9e6;
border-radius: 8px;
font-size: 1rem;
transition: all .3s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-dark);
box-shadow: 0 0 0 3px rgba(91, 191, 147, 0.15);
}
.btn-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
button {
padding: 12px 25px;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all .3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background-color: var(--primary-dark);
color: #fff;
}
.btn-primary:hover {
background-color: var(--primary-deep);
transform: translateY(-2px);
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-success {
background-color: #28a745;
color: #fff;
}
.btn-success:hover {
background-color: #218838;
}
.btn-danger {
background-color: #dc3545;
color: #fff;
}
.btn-danger:hover {
background-color: #c82333;
}
.table-section {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
min-width: 800px;
}
th {
background-color: #f1f5fd;
color: #2c3e50;
padding: 16px 12px;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #eaeef5;
}
td {
padding: 14px 12px;
border-bottom: 1px solid #eaeef5;
}
tr:hover {
background-color: #f9fafc;
}
.amount-cell {
font-weight: 600;
color: #2c3e50;
}
.expired {
color: #dc3545;
font-weight: 600;
}
.expiring-soon {
color: #ff9800;
font-weight: 600;
}
.actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 10px;
font-size: 0.85rem;
border-radius: 5px;
}
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-top: 30px;
}
.stat-card {
background-color: #fff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05);
border-left: 5px solid var(--primary-dark);
cursor: pointer;
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
font-size: 1rem;
color: #6c757d;
margin-bottom: 10px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #2c3e50;
}
.total-amount {
color: #28a745;
}
.footer {
text-align: center;
padding: 20px;
color: #6c757d;
font-size: 0.9rem;
border-top: 1px solid #eaeef5;
margin-top: 20px;
}
.no-data {
text-align: center;
padding: 40px;
color: #6c757d;
}
@media (max-width:768px) {
.form-grid {
grid-template-columns: 1fr;
}
.btn-group {
justify-content: center;
}
header {
padding: 20px 15px;
}
.content {
padding: 15px;
}
.user-info-bar {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
}
/* 启动页Splash */
#splash {
position: fixed;
inset: 0;
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-deep) 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
#splash .panel {
width: min(520px, 92vw);
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 18px;
padding: 26px 22px;
color: #fff;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(10px);
}
#splash .brand {
display: flex;
gap: 14px;
align-items: center;
}
#splash .logo {
width: 52px;
height: 52px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.18);
display: grid;
place-items: center;
}
#splash .logo .icon {
font-size: 24px;
}
#splash .title {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
}
#splash .desc {
opacity: 0.92;
margin-top: 6px;
font-weight: 300;
}
#splash .bar {
margin-top: 18px;
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
overflow: hidden;
}
#splash .bar::after {
content: "";
display: block;
height: 100%;
width: 35%;
border-radius: 999px;
background: rgba(255, 255, 255, 0.85);
animation: splashMove 1.2s infinite ease-in-out;
}
@keyframes splashMove {
0% {
transform: translateX(-110%);
}
100% {
transform: translateX(300%);
}
}
#splash.hide {
opacity: 0;
pointer-events: none;
transition: opacity 220ms ease;
}
/* 登录检查遮罩 */
#loginCheck {
position: fixed;
inset: 0;
background: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 20px;
z-index: 9998;
}
#loginCheck .spinner {
width: 40px;
height: 40px;
border: 4px solid var(--primary-bg);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 续费弹窗样式 */
.renew-modal {
position: absolute;
z-index: 1000;
background: white;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
padding: 20px;
width: 300px;
border: 1px solid #eaeef5;
display: none;
}
.renew-modal.show {
display: block;
}
.renew-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eaeef5;
}
.renew-modal-title {
font-weight: 600;
color: #2c3e50;
font-size: 1.1rem;
}
.renew-modal-close {
background: none;
border: none;
font-size: 1.2rem;
color: #6c757d;
cursor: pointer;
padding: 5px;
}
.renew-modal-body {
margin-bottom: 20px;
}
.renew-field {
margin-bottom: 15px;
}
.renew-field label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #3a506b;
font-size: 0.9rem;
}
.renew-field input {
width: 100%;
padding: 10px;
border: 1px solid #d1d9e6;
border-radius: 6px;
font-size: 0.95rem;
}
.renew-modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.renew-btn-small {
padding: 8px 16px;
font-size: 0.9rem;
}
/* 到期详情弹窗 */
.status-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(2px);
}
.status-modal {
width: min(900px, 92vw);
max-height: 80vh;
overflow: auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
border: 1px solid #eaeef5;
}
.status-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 22px;
border-bottom: 1px solid #eaeef5;
}
.status-modal-title {
font-weight: 700;
color: #2c3e50;
}
.status-modal-close {
background: none;
border: none;
font-size: 1.2rem;
color: #6c757d;
cursor: pointer;
}
.status-modal-body {
padding: 18px 22px 22px;
}
.status-table {
width: 100%;
border-collapse: collapse;
min-width: 700px;
}
.status-table th {
background-color: #f1f5fd;
padding: 12px 10px;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #eaeef5;
}
.status-table td {
padding: 12px 10px;
border-bottom: 1px solid #eaeef5;
}
/* 备注输入框样式 */
.remark-input {
width: 100%;
min-height: 36px;
resize: vertical;
border-radius: 8px;
border: 1px solid #d1d9e6;
padding: 8px 10px;
font-size: 0.9rem;
font-family: inherit;
transition: all 0.3s;
}
.remark-input:focus {
outline: none;
border-color: var(--primary-dark);
box-shadow: 0 0 0 3px rgba(91, 191, 147, 0.15);
}
/* 续费按钮样式 */
.renew-btn {
padding: 6px 12px;
background-color: var(--primary-dark);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
white-space: nowrap;
}
.renew-btn:hover {
background-color: var(--primary-deep);
transform: translateY(-1px);
}
/* 图标样式 */
.icon {
display: inline-block;
margin-right: 5px;
}
/* 消息提示 */
.staff-message {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: 10px;
font-weight: 600;
z-index: 10000;
color: white;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
animation: slideIn 0.3s ease;
max-width: 400px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.staff-message.success {
background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
}
.staff-message.error {
background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%);
}
.staff-message.info {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-deep) 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;
}
}
/* 数据加载提示 */
.data-loading {
text-align: center;
padding: 40px;
color: #6c757d;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #eaeef5;
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 刷新按钮 */
.refresh-btn {
position: absolute;
top: 25px;
right: 25px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
</style>
</head>
<body>
<!-- 登录检查 -->
<div id="loginCheck">
<div style="font-size:1.2rem; color:#2c3e50;">正在验证登录状态...</div>
<div class="spinner"></div>
</div>
<!-- 启动页 -->
<div id="splash" aria-hidden="true">
<div class="panel">
<div class="brand">
<div class="logo"><span class="icon icon-address-book"></span></div>
<div>
<div class="title">客户登记管理系统</div>
<div class="desc">正在加载数据...</div>
</div>
</div>
<div class="bar" role="progressbar" aria-label="loading"></div>
</div>
</div>
<div class="container" id="mainContent" style="display:none;">
<header>
<h1><span class="icon icon-address-book"></span> 客户登记管理系统</h1>
<p class="subtitle">员工工作台 - 管理您的客户</p>
<div class="user-info-bar">
<div class="user-badge">
<div class="user-avatar" id="staffAvatar"></div>
<div class="user-details">
<div class="user-name" id="staffName">员工姓名</div>
<div class="user-role" id="staffRole">员工账号</div>
</div>
</div>
<button class="logout-btn" id="logoutBtn">
<span class="icon icon-logout"></span> 退出登录
</button>
</div>
<button class="refresh-btn" onclick="loadClients()" title="刷新数据">
<span class="icon icon-rotate"></span> 刷新数据
</button>
</header>
<div class="content">
<section class="form-section">
<h2><span class="icon icon-user-plus"></span> 添加新客户</h2>
<div class="form-grid">
<div class="form-group">
<label for="regDate"><span class="icon icon-calendar"></span> 登记日期</label>
<input type="date" id="regDate" required>
</div>
<div class="form-group">
<label for="customerName"><span class="icon icon-user"></span> 客户姓名</label>
<input type="text" id="customerName" placeholder="请输入客户姓名" required>
</div>
<div class="form-group">
<label for="phone"><span class="icon icon-phone"></span> 联系电话</label>
<input type="tel" id="phone" placeholder="请输入联系电话" required>
</div>
<div class="form-group">
<label for="expireDate"><span class="icon icon-clock"></span> 到期时间</label>
<input type="date" id="expireDate" required>
</div>
<div class="form-group">
<label for="amount"><span class="icon icon-money"></span> 金额(元)</label>
<input type="number" id="amount" placeholder="请输入金额" min="0" step="0.01" required>
</div>
<div class="form-group">
<label for="serviceType"><span class="icon icon-bell"></span> 服务类型</label>
<select id="serviceType">
<option value="TK会员">TK会员</option>
<option value="测试会员">测试会员</option>
</select>
</div>
</div>
<div class="btn-group">
<button id="addBtn" class="btn-primary"><span class="icon icon-plus"></span> 添加客户</button>
<!-- <button id="calculateTotalBtn" class="btn-success"><span class="icon icon-calculator"></span> 计算总金额</button> -->
<!-- <button class="btn-secondary" onclick="clearForm()"><span class="icon icon-rotate"></span> 清空表单</button> -->
</div>
</section>
<section class="table-section">
<h2><span class="icon icon-list"></span> 我的客户列表</h2>
<div id="filterBar" style="margin-top:10px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
<span style="font-weight:600; color:#3a506b;">当前筛选:</span>
<span id="currentFilterTag"
style="background:#eef7f1; color:var(--primary-deep); border:1px solid rgba(47,143,103,0.25); padding:6px 10px; border-radius:999px; font-weight:700;">全部</span>
<button id="clearFilterBtn" class="btn-secondary" style="padding:8px 14px; border-radius:999px;"><span
class="icon icon-rotate"></span> 查看全部</button>
<span id="filterHint" style="opacity:.75; font-size:0.92rem;">提示:点击下方"即将到期/已过期"卡片可快速筛选</span>
</div>
<div id="tableContainer">
<!-- 数据加载中提示 -->
<div id="dataLoading" class="data-loading" style="display:none;">
<div class="loading-spinner"></div>
<p>正在加载客户数据...</p>
</div>
<table id="clientTable">
<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="tableBody"></tbody>
<tfoot>
<tr>
<td colspan="7" style="text-align:right;font-weight:600;">总计:</td>
<td id="totalAmount" class="amount-cell">0.00</td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
<div id="noDataMessage" class="no-data" style="display:none;">
<span class="icon icon-database" style="font-size:2rem; margin-bottom:15px; opacity:0.5;"></span>
<p>暂无客户数据,请添加第一条客户记录</p>
<p style="margin-top:10px; font-size:0.9rem; color:#6c757d;">或者等待管理员分配客户</p>
</div>
</div>
</section>
<section class="stats-section">
<div class="stat-card" onclick="setFilter('all')">
<h3>客户总数</h3>
<div class="stat-value" id="totalClients">0</div>
</div>
<div class="stat-card" onclick="setFilter('all')">
<h3>总金额</h3>
<div class="stat-value total-amount" id="totalAmountStat">¥0.00</div>
</div>
<div class="stat-card" onclick="openStatusModal('expiring')" id="expiringCard">
<h3>即将到期 (&lt; 7天)</h3>
<div class="stat-value" id="expiringSoonCount">0</div>
</div>
<div class="stat-card" onclick="openStatusModal('expired')" id="expiredCard">
<h3>已过期</h3>
<div class="stat-value expired" id="expiredCount">0</div>
</div>
</section>
</div>
<div class="footer">
<p>客户登记管理系统 - 员工端 &copy; 2024 | 数据来源:管理员分配</p>
<p style="font-size:0.8rem; margin-top:5px;">最后更新: <span id="lastUpdateTime">-</span></p>
</div>
</div>
<!-- 续费弹窗 -->
<div id="renewModal" class="renew-modal">
<div class="renew-modal-header">
<div class="renew-modal-title"><span class="icon icon-calendar-plus"></span> 客户续费</div>
<button class="renew-modal-close" id="renewModalClose">&times;</button>
</div>
<div class="renew-modal-body">
<div class="renew-field">
<label for="renewExpireDateInput"><span class="icon icon-calendar"></span> 新到期时间</label>
<input type="date" id="renewExpireDateInput">
</div>
<div class="renew-field">
<label for="renewAmountInput"><span class="icon icon-money"></span> 续费金额(元)</label>
<input type="number" id="renewAmountInput" min="0" step="0.01" placeholder="请输入续费金额">
</div>
<div style="font-size:0.85rem; color:#6c757d; margin-top:10px;">
<span class="icon icon-info"></span> 更新后,该客户的到期时间和金额将被修改
</div>
</div>
<div class="renew-modal-footer">
<button class="btn-secondary renew-btn-small" id="renewModalCancel">取消</button>
<button class="btn-success renew-btn-small" id="renewModalConfirm">确认续费</button>
</div>
</div>
<!-- 到期详情弹窗 -->
<div id="statusModal" class="status-modal-overlay">
<div class="status-modal">
<div class="status-modal-header">
<div class="status-modal-title" id="statusModalTitle">到期详情</div>
<button class="status-modal-close" onclick="closeStatusModal()">&times;</button>
</div>
<div class="status-modal-body">
<div id="statusModalSummary" style="margin-bottom:12px; color:#6c757d;"></div>
<div style="overflow:auto;">
<table class="status-table">
<thead>
<tr>
<th>客户姓名</th>
<th>联系电话</th>
<th>服务类型</th>
<th>登记日期</th>
<th>到期时间</th>
<th>金额</th>
</tr>
</thead>
<tbody id="statusModalBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 消息提示 -->
<div id="staffMessage" class="staff-message" style="display:none;"></div>
<script>
// ========== API ==========
const API_BASE_URL = window.location.origin;
const API_ENDPOINTS = {
me: '/api/auth/me',
staffClients: '/api/staff/clients',
staffClientById: (id) => `/api/staff/clients/${id}`
};
function getAuthToken() {
return localStorage.getItem('auth_token') || '';
}
async function apiRequest(path, options = {}) {
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
const token = getAuthToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${API_BASE_URL}${path}`, { ...options, headers });
const data = await res.json().catch(() => ({}));
if (!res.ok) {
if (res.status === 401) handleAuthExpired();
throw new Error(data.message || '请求失败');
}
return data;
}
function handleAuthExpired() {
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_token');
showMessage('登录已失效,请重新登录', 'error');
setTimeout(() => {
window.location.href = 'index.html';
}, 800);
}
function normalizeClient(row) {
return {
id: row.id,
regDate: row.regDate || '',
customerName: row.customerName || row.customer || '',
customer: row.customerName || row.customer || '',
phone: row.phone || '',
expireDate: row.expireDate || '',
amount: row.amount || 0,
serviceType: row.serviceType || row.service || '',
service: row.serviceType || row.service || '',
remark: row.remark || '',
lastRenewAt: row.lastRenewAt || null
};
}
// ========== 登录检查 ==========
document.addEventListener('DOMContentLoaded', function () {
const loginCheck = document.getElementById('loginCheck');
const mainContent = document.getElementById('mainContent');
const splash = document.getElementById('splash');
void (async () => {
const authUser = localStorage.getItem('auth_user');
const authToken = localStorage.getItem('auth_token');
if (!authUser || !authToken) {
alert('请先登录');
window.location.href = 'index.html';
return;
}
try {
const user = JSON.parse(authUser);
if (user.role !== 'staff') {
alert('权限不足,请使用员工账号登录');
window.location.href = 'index.html';
return;
}
const me = await apiRequest(API_ENDPOINTS.me, { method: 'GET' });
if (!me.user || me.user.role !== 'staff') {
handleAuthExpired();
return;
}
localStorage.setItem('auth_user', JSON.stringify(me.user));
document.getElementById('staffName').textContent = me.user.name || '员工';
document.getElementById('staffRole').textContent = me.user.username ? `账号: ${me.user.username}` : '员工账号';
document.getElementById('staffAvatar').textContent = (me.user.name || '员').charAt(0);
window.currentStaff = me.user;
setTimeout(() => {
loginCheck.style.display = 'none';
mainContent.style.display = 'block';
initPage();
setTimeout(() => {
if (splash) {
splash.classList.add('hide');
setTimeout(() => splash.remove(), 300);
}
}, 500);
}, 600);
} catch (e) {
console.error('登录检查失败:', e);
handleAuthExpired();
}
})();
});
// ========== 全局变量 ==========
let clients = [];
let currentFilter = 'all';
const remarkSaveTimers = new Map();
// DOM元素引用
const regDateInput = document.getElementById('regDate');
const customerNameInput = document.getElementById('customerName');
const phoneInput = document.getElementById('phone');
const expireDateInput = document.getElementById('expireDate');
const amountInput = document.getElementById('amount');
const serviceTypeInput = document.getElementById('serviceType');
const addBtn = document.getElementById('addBtn');
const calculateTotalBtn = document.getElementById('calculateTotalBtn');
const tableBody = document.getElementById('tableBody');
const totalAmountCell = document.getElementById('totalAmount');
const totalAmountStat = document.getElementById('totalAmountStat');
const totalClientsStat = document.getElementById('totalClients');
const expiringSoonCount = document.getElementById('expiringSoonCount');
const expiredCount = document.getElementById('expiredCount');
const noDataMessage = document.getElementById('noDataMessage');
const expiringCard = document.getElementById('expiringCard');
const expiredCard = document.getElementById('expiredCard');
const currentFilterTag = document.getElementById('currentFilterTag');
const clearFilterBtn = document.getElementById('clearFilterBtn');
const logoutBtn = document.getElementById('logoutBtn');
const dataLoading = document.getElementById('dataLoading');
const lastUpdateTimeEl = document.getElementById('lastUpdateTime');
const statusModal = document.getElementById('statusModal');
const statusModalBody = document.getElementById('statusModalBody');
const statusModalTitle = document.getElementById('statusModalTitle');
const statusModalSummary = document.getElementById('statusModalSummary');
// 续费弹窗元素
const renewModal = document.getElementById('renewModal');
const renewExpireDateInput = document.getElementById('renewExpireDateInput');
const renewAmountInput = document.getElementById('renewAmountInput');
const renewModalClose = document.getElementById('renewModalClose');
const renewModalCancel = document.getElementById('renewModalCancel');
const renewModalConfirm = document.getElementById('renewModalConfirm');
let renewClientId = null;
let renewButtonPosition = { top: 0, left: 0 };
// ========== 初始化页面 ==========
function initPage() {
// 设置默认日期
const today = new Date().toISOString().split('T')[0];
regDateInput.value = today;
expireDateInput.value = getDateAfterDays(30);
// 加载客户数据
loadClients();
// 初始化事件监听
initEventListeners();
}
// ========== 获取n天后的日期 ==========
function getDateAfterDays(days) {
const d = new Date();
d.setDate(d.getDate() + days);
return d.toISOString().split('T')[0];
}
// ========== 初始化事件监听 ==========
function initEventListeners() {
// 添加客户按钮
if (addBtn) addBtn.addEventListener('click', addClient);
// 计算总金额按钮
if (calculateTotalBtn) calculateTotalBtn.addEventListener('click', calculateTotal);
// 回车键添加客户
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addClient();
});
// 筛选卡片点击事件
// 清除筛选按钮
if (clearFilterBtn) clearFilterBtn.addEventListener('click', () => setFilter('all'));
// 续费弹窗事件
if (renewModalClose) renewModalClose.addEventListener('click', closeRenewModal);
if (renewModalCancel) renewModalCancel.addEventListener('click', closeRenewModal);
if (renewModalConfirm) renewModalConfirm.addEventListener('click', confirmRenew);
// 点击弹窗外部关闭
document.addEventListener('click', (e) => {
if (renewModal.classList.contains('show') &&
!renewModal.contains(e.target) &&
!e.target.classList.contains('renew-btn')) {
closeRenewModal();
}
});
// ESC键关闭弹窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (renewModal.classList.contains('show')) closeRenewModal();
if (statusModal && statusModal.style.display === 'flex') closeStatusModal();
}
});
// 退出登录按钮
if (logoutBtn) logoutBtn.addEventListener('click', logout);
// 点击遮罩关闭详情弹窗
if (statusModal) {
statusModal.addEventListener('click', (e) => {
if (e.target === statusModal) closeStatusModal();
});
}
}
// ========== 加载客户数据 ==========
async function loadClients() {
try {
if (dataLoading) dataLoading.style.display = 'flex';
if (noDataMessage) noDataMessage.style.display = 'none';
const data = await apiRequest(API_ENDPOINTS.staffClients, { method: 'GET' });
clients = Array.isArray(data.clients) ? data.clients.map(normalizeClient) : [];
if (lastUpdateTimeEl) {
lastUpdateTimeEl.textContent = new Date().toLocaleString('zh-CN');
}
renderTable();
updateStats();
if (clients.length === 0) {
showMessage('您目前没有客户数据,请联系管理员分配或自行添加客户', 'info');
}
} catch (error) {
console.error('加载客户数据失败:', error);
showMessage('加载数据失败: ' + error.message, 'error');
clients = [];
renderTable();
updateStats();
} finally {
if (dataLoading) dataLoading.style.display = 'none';
}
}
// ========== 渲染表格 ==========
function renderTable() {
const view = getFilteredClients();
if (view.length === 0) {
tableBody.innerHTML = '';
if (noDataMessage) noDataMessage.style.display = 'block';
totalAmountCell.textContent = '¥0.00';
return;
}
if (noDataMessage) noDataMessage.style.display = 'none';
tableBody.innerHTML = '';
view.forEach((client, index) => {
const row = document.createElement('tr');
const status = getExpirationStatus(client.expireDate);
row.innerHTML = `
<td>${index + 1}</td>
<td>${client.regDate}</td>
<td>${client.customerName || client.customer || ""}</td>
<td>${formatPhone(client.phone)}</td>
<td>${client.serviceType || client.service || ""}</td>
<td>${client.expireDate}</td>
<td class="amount-cell">¥${formatAmount(client.amount)}</td>
<td class="${status.class}">${status.text}</td>
<td>
<button class="renew-btn" onclick="openRenewModal(${client.id}, event)" title="续费/延长到期">
续费
</button>
</td>
<td>
<textarea class="remark-input"
placeholder="填写客户备注..."
oninput="saveRemark(${client.id}, this.value)"
onblur="saveRemark(${client.id}, this.value)">${client.remark || ''}</textarea>
</td>
`;
tableBody.appendChild(row);
});
updateTotalAmount();
}
// ========== 获取筛选后的客户 ==========
function getFilteredClients() {
if (currentFilter === 'expiring') {
return clients.filter(c => getExpirationStatus(c.expireDate).class === 'expiring-soon');
}
if (currentFilter === 'expired') {
return clients.filter(c => getExpirationStatus(c.expireDate).class === 'expired');
}
return clients;
}
// ========== 设置筛选 ==========
function setFilter(next) {
currentFilter = next;
if (currentFilterTag) {
currentFilterTag.textContent = (next === 'expiring') ? '即将到期(<7天' : (next === 'expired' ? '已过期' : '全部');
}
renderTable();
updateTotalAmount();
}
// ========== 格式化电话 ==========
function formatPhone(phone) {
return String(phone || '');
}
// ========== 格式化金额 ==========
function formatAmount(amount) {
return parseFloat(amount).toFixed(2);
}
// ========== 获取到期状态 ==========
function getExpirationStatus(expireDate) {
if (!expireDate) return { text: '正常', class: '' };
const now = new Date();
const expire = new Date(expireDate);
if (Number.isNaN(expire.getTime())) return { text: '正常', class: '' };
const diffTime = expire - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) return { text: '已过期', class: 'expired' };
if (diffDays <= 7) return { text: `${diffDays}天后到期`, class: 'expiring-soon' };
return { text: '正常', class: '' };
}
// ========== 更新总金额 ==========
function updateTotalAmount() {
const view = getFilteredClients();
const totalView = view.reduce((sum, client) => {
const st = getExpirationStatus(client.expireDate).class;
if (currentFilter === 'all' && st === 'expired') return sum;
return sum + parseFloat(client.amount || 0);
}, 0);
totalAmountCell.textContent = `¥${formatAmount(totalView)}`;
const totalActive = clients.reduce((sum, client) => {
const st = getExpirationStatus(client.expireDate).class;
if (st === 'expired') return sum;
return sum + parseFloat(client.amount || 0);
}, 0);
totalAmountStat.textContent = `¥${formatAmount(totalActive)}`;
}
// ========== 更新统计 ==========
function updateStats() {
totalClientsStat.textContent = clients.length;
updateTotalAmount();
let expiringSoon = 0;
let expired = 0;
clients.forEach(client => {
const status = getExpirationStatus(client.expireDate);
if (status.class === 'expiring-soon') expiringSoon++;
if (status.class === 'expired') expired++;
});
expiringSoonCount.textContent = expiringSoon;
expiredCount.textContent = expired;
}
// ========== 添加客户 ==========
async function addClient() {
if (!validateForm()) return;
const payload = {
regDate: regDateInput.value,
customerName: customerNameInput.value.trim(),
phone: phoneInput.value.trim(),
expireDate: expireDateInput.value,
amount: parseFloat(amountInput.value),
serviceType: serviceTypeInput.value
};
try {
const data = await apiRequest(API_ENDPOINTS.staffClients, {
method: 'POST',
body: JSON.stringify(payload)
});
const created = data.client ? normalizeClient(data.client) : normalizeClient(payload);
clients.unshift(created);
clearForm();
renderTable();
updateStats();
showMessage('\u5ba2\u6237\u6dfb\u52a0\u6210\u529f', 'success');
} catch (error) {
console.error('\u6dfb\u52a0\u5ba2\u6237\u5931\u8d25:', error);
showMessage('加载数据失败: ' + error.message, 'error');
}
}
// ========== 保存客户到本地存储 ==========
// ========== 验证表单 ==========
function validateForm() {
if (!customerNameInput.value.trim()) {
showMessage('请输入客户姓名', 'error');
customerNameInput.focus();
return false;
}
if (!phoneInput.value.trim()) {
showMessage('请输入联系电话', 'error');
phoneInput.focus();
return false;
}
if (!amountInput.value || parseFloat(amountInput.value) < 0) {
showMessage('请输入有效的金额', 'error');
amountInput.focus();
return false;
}
if (new Date(expireDateInput.value) < new Date(regDateInput.value)) {
showMessage('到期时间不能早于登记日期', 'error');
expireDateInput.focus();
return false;
}
return true;
}
// ========== 清空表单 ==========
function clearForm() {
customerNameInput.value = '';
phoneInput.value = '';
amountInput.value = '';
serviceTypeInput.value = 'TK会员';
const today = new Date().toISOString().split('T')[0];
regDateInput.value = today;
expireDateInput.value = getDateAfterDays(30);
customerNameInput.focus();
}
// ========== 计算总金额 ==========
function calculateTotal() {
updateTotalAmount();
showMessage(`总金额已计算:${totalAmountCell.textContent}`, 'success');
}
// ========== 显示消息 ==========
function showMessage(message, type) {
const msgEl = document.getElementById('staffMessage');
msgEl.textContent = message;
msgEl.className = 'staff-message ' + type;
msgEl.style.display = 'block';
setTimeout(() => {
msgEl.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
msgEl.style.display = 'none';
msgEl.style.animation = '';
}, 300);
}, 3000);
}
// ========== 打开续费弹窗 ==========
function openRenewModal(id, event) {
const client = clients.find(c => c.id === id);
if (!client) return;
renewClientId = id;
// 定位弹窗
if (event) {
const buttonRect = event.target.getBoundingClientRect();
renewButtonPosition = {
top: buttonRect.top + window.scrollY,
left: buttonRect.left + window.scrollX
};
renewModal.style.position = 'absolute';
renewModal.style.top = (renewButtonPosition.top + 40) + 'px';
renewModal.style.left = Math.max(20, renewButtonPosition.left - 150) + 'px';
}
// 设置默认值为当前值
renewExpireDateInput.value = client.expireDate || '';
renewAmountInput.value = 0;
renewModal.classList.add('show');
// 自动聚焦
setTimeout(() => renewExpireDateInput.focus(), 50);
}
// ========== 关闭续费弹窗 ==========
function closeRenewModal() {
renewClientId = null;
renewModal.classList.remove('show');
}
// ========== 确认续费 ==========
// ========== 确认续费 ==========
async function confirmRenew() {
if (renewClientId == null) return;
const client = clients.find(c => c.id === renewClientId);
if (!client) return closeRenewModal();
const newExpire = renewExpireDateInput.value.trim();
const addAmountStr = renewAmountInput.value.toString().trim(); // 本次续费增加金额
if (!newExpire) {
showMessage('请填写新到期时间', 'error');
renewExpireDateInput.focus();
return;
}
if (!addAmountStr || isNaN(Number(addAmountStr)) || Number(addAmountStr) < 0) {
showMessage('请填写正确的续费金额', 'error');
renewAmountInput.focus();
return;
}
// ✅ 关键:本次输入金额 + 原本金额
const oldAmount = Number(client.amount || 0);
const addAmount = Number(addAmountStr);
const nextAmount = oldAmount + addAmount;
try {
const payload = {
expireDate: newExpire,
amount: nextAmount, // ✅ 更新为累加后的金额
lastRenewAt: new Date().toISOString()
};
const data = await apiRequest(API_ENDPOINTS.staffClientById(renewClientId), {
method: 'PUT',
body: JSON.stringify(payload)
});
if (data.client) {
const updated = normalizeClient(data.client);
const idx = clients.findIndex(c => c.id === renewClientId);
if (idx >= 0) clients[idx] = updated;
} else {
client.expireDate = newExpire;
client.amount = nextAmount; // ✅ 本地也更新为累加后的金额
client.lastRenewAt = payload.lastRenewAt;
}
closeRenewModal();
renderTable();
updateStats();
showMessage('客户续费成功', 'success');
} catch (error) {
console.error('续费失败:', error);
showMessage('续费失败: ' + error.message, 'error');
}
}
// ========== 打开到期详情弹窗 ==========
function openStatusModal(type) {
if (!statusModal || !statusModalBody || !statusModalTitle || !statusModalSummary) return;
const isExpired = type === 'expired';
const label = isExpired ? '已过期' : '即将到期';
const list = clients.filter(c => {
const st = getExpirationStatus(c.expireDate).class;
return isExpired ? st === 'expired' : st === 'expiring-soon';
});
statusModalTitle.textContent = `${label}客户`;
statusModalSummary.textContent = `共 ${list.length} 位客户`;
statusModalBody.innerHTML = '';
if (list.length === 0) {
statusModalBody.innerHTML = `<tr><td colspan="6" style="text-align:center; color:#6c757d;">暂无数据</td></tr>`;
} else {
list.forEach(client => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${client.customerName || client.customer || ''}</td>
<td>${formatPhone(client.phone)}</td>
<td>${client.serviceType || client.service || ''}</td>
<td>${client.regDate || ''}</td>
<td>${client.expireDate || ''}</td>
<td>¥${formatAmount(client.amount)}</td>
`;
statusModalBody.appendChild(row);
});
}
statusModal.style.display = 'flex';
}
function closeStatusModal() {
if (!statusModal) return;
statusModal.style.display = 'none';
}
// ========== 保存备注 ==========
function saveRemark(id, remark) {
const client = clients.find(c => c.id === id);
if (!client) return;
client.remark = remark;
if (remarkSaveTimers.has(id)) {
clearTimeout(remarkSaveTimers.get(id));
}
const timer = setTimeout(async () => {
try {
const data = await apiRequest(API_ENDPOINTS.staffClientById(id), {
method: 'PUT',
body: JSON.stringify({ remark })
});
if (data.client) {
const updated = normalizeClient(data.client);
const idx = clients.findIndex(c => c.id === id);
if (idx >= 0) clients[idx] = updated;
}
} catch (error) {
console.error('\u4fdd\u5b58\u5907\u6ce8\u5931\u8d25:', error);
showMessage('加载数据失败: ' + error.message, 'error');
}
}, 500);
remarkSaveTimers.set(id, timer);
}
// ========== 退出登录 ==========
function logout() {
if (confirm('确定要退出登录吗?')) {
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_token');
window.location.href = 'index.html';
}
}
// ========== 全局函数导出 ==========
window.openRenewModal = openRenewModal;
window.openStatusModal = openStatusModal;
window.closeStatusModal = closeStatusModal;
window.saveRemark = saveRemark;
window.setFilter = setFilter;
window.loadClients = loadClients;
window.clearForm = clearForm;
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@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; } }
`;
document.head.appendChild(style);
</script>
</body>
</html>