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

1768 lines
48 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>