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