初始化

This commit is contained in:
2026-01-23 21:56:02 +08:00
commit 67c85dfb91
25 changed files with 10055 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

1
.htaccess Normal file
View File

@@ -0,0 +1 @@
# 请将伪静态规则或自定义Apache配置填写到此处

View File

@@ -0,0 +1 @@
PQ2oHEhWk4lfJu4mJyLtTUHZehr9t9WRTyMj5El9w0w.zcR77pvvtV00LcqCRWPQnkBuH0F6FaFaWKI0_uGwnn0

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
# CRM Customer Management System
基于 Node.js + SQLite 的轻量级客户管理系统。
## 📋 功能特性
- **多角色管理**:超级管理员、组长 (Leader)、员工。
- **客户管理**:录入、查询、编辑、删除客户信息,支持服务类型和金额统计。
- **过期提醒**:自动标记过期和即将过期的客户。
- **数据统计**
- 管理员:查看全公司数据、各小组(组长)业绩对比。
- 组长:查看本组员工数据及业绩。
- 员工:仅查看个人业绩。
- **权限控制**
- 组长只能管理自己创建的员工。
- 严格的数据隔离。
- 账号封禁/启用功能。
## 🛠️ 技术栈
- **后端**Node.js (Express)
- **数据库**SQLite3
- **加密**bcryptjs (密码), JWT (Token认证)
- **前端**HTML5, CSS3, Vanilla JS (无构建步骤,开箱即用)
## 🚀 快速开始
### 1. 环境准备
确保已安装 Node.js (v14+)。
### 2. 安装依赖
```bash
npm install
```
### 3. 启动应用
此命令会自动初始化数据库(如果不存在)并启动服务器。
```bash
npm start
```
服务器运行在: `http://localhost:3000`
### 4. 默认账号
系统初始化时会自动创建以下默认账号(如果未修改 server.js
| 角色 | 账号 | 密码 | 说明 |
|------|------|------|------|
| **超级管理员** | `admin` | `admin123456` | 拥有所有权限 |
| **员工示例** | `staff1` | `staff123456` | 默认普通员工 |
## 📂 项目结构
```
├── public/ # 前端静态资源 (HTML/CSS/JS)
│ ├── admin.html # 管理员后台
│ ├── leader.html # 组长后台
│ ├── staff.html # 员工后台
│ ├── index.html # 登录页
│ └── css/ # 样式文件
├── server.js # 后端入口 & API 逻辑 & 数据库初始化
├── database.sqlite # SQLite 数据库文件 (自动生成)
└── package.json # 项目配置
```
## 🔧 常见维护
- **重置数据库**:删除 `database.sqlite` 文件重启即可(数据会丢失)。
- **修改端口**:编辑 `server.js` 中的 `PORT` 变量。

BIN
__MACOSX/._.htaccess Normal file

Binary file not shown.

BIN
__MACOSX/._.well-known Normal file

Binary file not shown.

BIN
__MACOSX/._README.md Normal file

Binary file not shown.

BIN
__MACOSX/._package.json Normal file

Binary file not shown.

BIN
__MACOSX/._public Normal file

Binary file not shown.

BIN
__MACOSX/._run.log Normal file

Binary file not shown.

BIN
__MACOSX/._server.js Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data.sqlite Normal file

Binary file not shown.

2435
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "crm-system",
"version": "1.0.0",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js",
"initdb": "node server.js --initdb"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"sqlite3": "^5.1.7"
}
}

2406
public/admin.html Normal file

File diff suppressed because it is too large Load Diff

750
public/index.html Normal file
View File

@@ -0,0 +1,750 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>客户管理系统 - 登录</title>
<style>
/* ========== 主题配色 ========== */
:root {
--primary: #5bbf93;
--primary-light: #8fd6b3;
--primary-dark: #2f8f67;
--primary-bg: #f0f9f5;
--primary-gradient: linear-gradient(135deg, #5bbf93 0%, #2f8f67 100%);
--text-primary: #2c3e50;
--text-secondary: #546e7a;
--text-muted: #90a4ae;
--bg-body: #f8fafc;
--bg-card: #ffffff;
--border: #e2e8f0;
--border-light: #f1f5f9;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--radius: 10px;
--radius-lg: 16px;
--radius-full: 9999px;
--transition: all 0.2s ease-in-out;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image: url('data:image/svg+xml,<svg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h60v60H0z" fill="none"/><path d="M30 10c-11.05 0-20 8.95-20 20s8.95 20 20 20 20-8.95 20-20-8.95-20-20-20zm0 36c-8.82 0-16-7.18-16-16s7.18-16 16-16 16 7.18 16 16-7.18 16-16 16z" fill="%235bbf93" opacity="0.05"/></svg>');
}
/* ========== 登录容器 ========== */
.login-container {
width: 100%;
max-width: 440px;
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
border: 1px solid var(--border);
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ========== 登录头部 ========== */
.login-header {
background: var(--primary-gradient);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
overflow: hidden;
}
.login-header::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
border-radius: 50%;
transform: translate(30%, -30%);
}
.login-header::after {
content: '';
position: absolute;
bottom: -50px;
left: -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%;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.logo-icon {
width: 60px;
height: 60px;
background: rgba(255,255,255,0.2);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
backdrop-filter: blur(5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.logo-text h1 {
font-size: 1.8rem;
font-weight: 700;
text-align: left;
line-height: 1.2;
}
.logo-text .subtitle {
font-size: 0.9rem;
opacity: 0.9;
font-weight: 300;
text-align: left;
}
.login-header .welcome {
font-size: 1.1rem;
opacity: 0.95;
margin-top: 10px;
font-weight: 400;
}
/* ========== 用户类型切换 ========== */
.user-type-toggle {
display: flex;
background: rgba(255,255,255,0.1);
border-radius: var(--radius);
padding: 5px;
margin-top: 20px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255,255,255,0.2);
}
.user-type-btn {
flex: 1;
padding: 10px;
border: none;
background: transparent;
color: rgba(255,255,255,0.8);
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: var(--transition);
border-radius: calc(var(--radius) - 2px);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.user-type-btn.active {
background: white;
color: var(--primary-dark);
box-shadow: var(--shadow);
}
/* ========== 登录表单 ========== */
.login-form {
padding: 40px 35px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 8px;
}
.form-group .input-wrapper {
position: relative;
}
.form-group input, .form-group select {
width: 100%;
padding: 16px 50px 16px 20px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 1rem;
background: white;
color: var(--text-primary);
transition: var(--transition);
font-family: inherit;
}
.form-group input:hover, .form-group select:hover {
border-color: var(--primary-light);
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(91, 191, 147, 0.15);
}
.form-group input::placeholder {
color: var(--text-muted);
}
.input-icon {
position: absolute;
right: 18px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 1.1rem;
}
/* ========== 员工选择 ========== */
.staff-select {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2390a4ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1em;
padding-right: 3rem;
}
/* ========== 记住我选项 ========== */
.remember-me {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 25px;
cursor: pointer;
}
.remember-me input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--primary);
}
.remember-me label {
margin: 0;
font-size: 0.9rem;
color: var(--text-secondary);
cursor: pointer;
font-weight: 500;
}
/* ========== 登录按钮 ========== */
.login-btn {
width: 100%;
padding: 16px;
border: none;
border-radius: var(--radius);
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: var(--transition);
background: var(--primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-family: inherit;
box-shadow: var(--shadow);
}
.login-btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.login-btn:active {
transform: translateY(0);
}
.login-btn:disabled {
background: var(--text-muted);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* ========== 安全提示 ========== */
.security-tip {
margin-top: 15px;
padding: 12px 16px;
background: #fff3e0;
border-radius: var(--radius);
border: 1px solid rgba(245, 158, 11, 0.3);
font-size: 0.85rem;
color: #7c2d12;
display: flex;
align-items: center;
gap: 10px;
}
.security-tip i {
color: #f59e0b;
}
/* ========== 员工信息卡片 ========== */
.staff-info-card {
margin-top: 15px;
padding: 15px;
background: var(--primary-bg);
border-radius: var(--radius);
border: 1px solid var(--border);
display: none;
animation: fadeIn 0.3s ease;
}
.staff-info-card.show {
display: block;
}
.staff-info-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.staff-avatar {
width: 40px;
height: 40px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
}
.staff-name {
font-weight: 600;
color: var(--text-primary);
}
.staff-details {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.5;
}
/* ========== 消息提示 ========== */
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: var(--radius);
font-weight: 600;
z-index: 10000;
color: white;
box-shadow: var(--shadow-lg);
animation: slideIn 0.3s ease;
max-width: 400px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message.success {
background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
}
.message.error {
background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* ========== 加载动画 ========== */
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== 底部信息 ========== */
.login-footer {
text-align: center;
padding: 25px;
color: var(--text-muted);
font-size: 0.85rem;
background: var(--bg-body);
border-top: 1px solid var(--border);
line-height: 1.5;
}
/* ========== 响应式设计 ========== */
@media (max-width: 480px) {
.login-container {
max-width: 100%;
border-radius: 12px;
}
.login-header {
padding: 30px 20px;
}
.login-form {
padding: 30px 20px;
}
.logo {
flex-direction: column;
text-align: center;
}
.logo-text h1 {
text-align: center;
}
.logo-text .subtitle {
text-align: center;
}
}
/* ========== 登录说明 ========== */
.login-instructions {
margin-bottom: 20px;
padding: 12px 16px;
background: #e8f6f3;
border-radius: var(--radius);
border: 1px solid rgba(39, 174, 96, 0.3);
font-size: 0.85rem;
color: #145a32;
display: flex;
align-items: center;
gap: 10px;
}
.login-instructions i {
color: var(--success);
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="logo">
<div class="logo-icon">📋</div>
<div class="logo-text">
<h1>客户管理系统</h1>
<div class="subtitle">统一登录入口</div>
</div>
</div>
<div class="welcome">请登录您的账号</div>
</div>
<div class="login-form">
<form id="loginForm" autocomplete="on">
<div class="form-group">
<label for="username"><i class="fas fa-user"></i> 账号</label>
<div class="input-wrapper">
<input id="username" name="username" type="text" placeholder="请输入账号" required>
<span class="input-icon"><i class="fas fa-user"></i></span>
</div>
</div>
<div class="form-group">
<label for="password"><i class="fas fa-lock"></i> 密码</label>
<div class="input-wrapper">
<input id="password" name="password" type="password" placeholder="请输入密码" required>
<span class="input-icon"><i class="fas fa-lock"></i></span>
</div>
</div>
<div class="remember-me">
<input type="checkbox" id="rememberMe">
<label for="rememberMe">记住账号</label>
</div>
<button type="submit" class="login-btn" id="loginBtn">
<span>登录系统</span>
<i class="fas fa-sign-in-alt"></i>
</button>
<!--<div class="login-instructions">-->
<!-- <i class="fas fa-info-circle"></i>-->
<!-- <div>-->
<!-- <strong>测试账号:</strong><br>-->
<!-- 管理员admin / niu995228...<br>-->
<!-- 员工staff1 / staff123456-->
<!-- </div>-->
<!--</div>-->
<div class="security-tip">
<i class="fas fa-shield-alt"></i>
登录信息将保存7天
</div>
</form>
</div>
<div class="login-footer">
<p>客户管理系统 © 2024 | 服务器后端连接</p>
<p style="font-size:0.8rem; margin-top:5px;">技术支持:如有问题请联系管理员</p>
</div>
</div>
<div id="message" class="message" style="display:none;"></div>
<script>
// ========== 配置 ==========
const API_BASE_URL = window.location.origin; // 根据当前页面自动获取后端地址
const API_ENDPOINTS = {
login: '/api/auth/login',
me: '/api/auth/me'
};
// 获取DOM元素
function $(id) { return document.getElementById(id); }
// 显示消息
function showMessage(text, type) {
const msg = $('message');
msg.textContent = text;
msg.className = 'message ' + type;
msg.style.display = 'block';
setTimeout(() => {
msg.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
msg.style.display = 'none';
msg.style.animation = '';
}, 300);
}, 3000);
}
// 设置按钮加载状态
function setButtonLoading(loading) {
const btn = $('loginBtn');
if (loading) {
btn.innerHTML = '<div class="loader"></div> <span>登录中...</span>';
btn.disabled = true;
} else {
btn.innerHTML = '<span>登录系统</span> <i class="fas fa-sign-in-alt"></i>';
btn.disabled = false;
}
}
// 加载记住的账号
function loadRemembered() {
const remembered = localStorage.getItem('remember_username');
if (remembered) {
$('username').value = remembered;
$('rememberMe').checked = true;
}
}
// 检查是否已登录
function checkAlreadyLoggedIn() {
const authUser = localStorage.getItem('auth_user');
const authToken = localStorage.getItem('auth_token');
if (authUser && authToken) {
// 验证token是否有效
fetch(`${API_BASE_URL}${API_ENDPOINTS.me}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
})
.then(response => {
if (response.ok) {
response.json().then(data => {
// 自动跳转到对应页面
if (data.user.role === 'admin') {
window.location.href = 'admin.html';
} else if (data.user.role === 'leader') {
window.location.href = 'leader.html';
} else if (data.user.role === 'staff') {
window.location.href = 'staff.html';
}
});
} else {
// token无效清除本地存储
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_token');
}
})
.catch(() => {
// 网络错误,清除本地存储
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_token');
});
}
}
// 登录函数 - 调用后端API
async function login(username, password) {
try {
setButtonLoading(true);
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.login}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '登录失败');
}
return data;
} catch (error) {
console.error('登录错误:', error);
throw error;
} finally {
setButtonLoading(false);
}
}
// 表单提交事件
$('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const username = $('username').value.trim();
const password = $('password').value;
if (!username || !password) {
showMessage('请输入账号和密码', 'error');
return;
}
try {
const loginData = await login(username, password);
// 记住账号
if ($('rememberMe').checked) {
localStorage.setItem('remember_username', username);
} else {
localStorage.removeItem('remember_username');
}
// 保存登录信息
localStorage.setItem('auth_token', loginData.token);
localStorage.setItem('auth_user', JSON.stringify(loginData.user));
// 显示成功消息
showMessage('登录成功,正在跳转...', 'success');
// 根据角色跳转
setTimeout(() => {
if (loginData.user.role === 'admin') {
window.location.href = 'admin.html';
} else if (loginData.user.role === 'leader') {
window.location.href = 'leader.html';
} else if (loginData.user.role === 'staff') {
window.location.href = 'staff.html';
}
}, 1000);
} catch (error) {
console.error('登录失败:', error);
showMessage(error.message || '登录失败,请检查账号密码', 'error');
}
});
// 页面加载
document.addEventListener('DOMContentLoaded', function() {
loadRemembered();
// checkAlreadyLoggedIn();
});
</script>
<!-- Font Awesome图标 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</body>
</html>

1566
public/leader.html Normal file

File diff suppressed because it is too large Load Diff

1768
public/staff.html Normal file

File diff suppressed because it is too large Load Diff

2
run.log Normal file
View File

@@ -0,0 +1,2 @@
nohup: ignoring input
🚀 CRM running on http://0.0.0.0:3000

1044
server.js Normal file

File diff suppressed because it is too large Load Diff