1768 lines
48 KiB
HTML
1768 lines
48 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>
|
|||
|
|
/* 字体图标备用方案 */
|
|||
|
|
@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>即将到期 (< 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>客户登记管理系统 - 员工端 © 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">×</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()">×</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>
|