v1.0.3
parent
eb035d6d35
commit
c8b568bd5a
|
|
@ -1,5 +1,5 @@
|
|||
# 生产环境服务器 - PM2版本
|
||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/node:18-alpine
|
||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/node:18-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "🚀 开始部署iMeeting前端服务(PM2模式)..."
|
||||
|
||||
# 手动构建dist目录
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "❌ 构建失败!dist目录未生成"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 前端构建完成,开始Docker部署..."
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 停止并删除现有容器
|
||||
echo "📦 停止现有容器..."
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# 构建新镜像
|
||||
echo "🔨 构建Docker镜像..."
|
||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
||||
|
||||
# 启动服务
|
||||
echo "▶️ 启动PM2服务..."
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 检查服务状态
|
||||
echo "🔍 检查服务状态..."
|
||||
sleep 15
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 检查PM2进程状态
|
||||
echo "🔄 检查PM2进程状态..."
|
||||
docker exec imeeting-frontend pm2 list
|
||||
|
||||
# 检查健康状态
|
||||
echo "🏥 检查健康状态..."
|
||||
curl -f http://localhost:3001/health && echo "✅ 前端服务健康检查通过" || echo "❌ 前端服务健康检查失败"
|
||||
|
||||
echo ""
|
||||
echo "🎉 部署完成!"
|
||||
echo "📱 前端服务访问地址: http://localhost:3001"
|
||||
echo "📊 查看日志: docker-compose -f docker-compose.prod.yml logs -f"
|
||||
echo "📈 查看PM2状态: docker exec imeeting-frontend pm2 monit"
|
||||
echo "📋 查看PM2进程: docker exec imeeting-frontend pm2 list"
|
||||
echo "🛑 停止服务: docker-compose -f docker-compose.prod.yml down"
|
||||
echo ""
|
||||
echo "💡 提示:PM2模式特性:"
|
||||
echo " ✅ 集群模式(2个实例)"
|
||||
echo " ✅ 自动重启和故障恢复"
|
||||
echo " ✅ 内存限制保护(1GB)"
|
||||
echo " ✅ 详细日志管理"
|
||||
echo " ✅ 进程监控和健康检查"
|
||||
|
|
@ -3,7 +3,6 @@ services:
|
|||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: imeeting-frontend
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
|
|
@ -14,6 +13,9 @@ services:
|
|||
# 挂载日志目录到宿主机
|
||||
- ./logs:/app/logs
|
||||
restart: unless-stopped
|
||||
container_name: imeeting-frontend
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health", "||", "exit", "1"]
|
||||
interval: 30s
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const app = express();
|
|||
const PORT = process.env.PORT;
|
||||
const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8000";
|
||||
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
|
|
@ -39,6 +40,7 @@ app.use(express.static(path.join(__dirname, "dist"), {
|
|||
|
||||
// SPA路由处理 - 必须放在最后
|
||||
app.get("*", (req, res) => {
|
||||
console.log("Proxying request to:", BACKEND_URL);
|
||||
res.sendFile(path.join(__dirname, "dist", "index.html"));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Dashboard from './pages/Dashboard';
|
|||
import MeetingDetails from './pages/MeetingDetails';
|
||||
import CreateMeeting from './pages/CreateMeeting';
|
||||
import EditMeeting from './pages/EditMeeting';
|
||||
import AdminManagement from './pages/AdminManagement';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
|
|
@ -77,6 +78,9 @@ function App() {
|
|||
<Route path="/meetings/edit/:meeting_id" element={
|
||||
user ? <EditMeeting user={user} /> : <Navigate to="/" />
|
||||
} />
|
||||
<Route path="/admin/management" element={
|
||||
user && user.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" />
|
||||
} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
const SystemConfiguration = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2>系统配置</h2>
|
||||
<p>系统相关配置项。</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemConfiguration;
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
/* UserManagement.css */
|
||||
.user-management .toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.users-table th, .users-table td {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.users-table .action-btn {
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.3s ease;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.users-table .action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.users-table .action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.users-table .btn-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.users-table .btn-danger:hover {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.users-table .btn-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.users-table .btn-warning:hover {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
white-space: nowrap;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
margin: 0 1rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #1e293b;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-content .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-content .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.info-note {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-note p {
|
||||
margin: 0;
|
||||
color: #0369a1;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Button styles for confirmations */
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
||||
import { Plus, Edit, Trash2, KeyRound } from 'lucide-react';
|
||||
import './UserManagement.css';
|
||||
|
||||
const UserManagement = () => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
||||
const [showEditUserModal, setShowEditUserModal] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const [processingUser, setProcessingUser] = useState(null);
|
||||
const [newUser, setNewUser] = useState({ username: '', caption: '', email: '', role_id: 2 });
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [page, pageSize]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=${page}&size=${pageSize}`));
|
||||
setUsers(response.data.users);
|
||||
setTotal(response.data.total);
|
||||
} catch (err) {
|
||||
setError('无法加载用户列表');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUser = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.CREATE), newUser);
|
||||
setShowAddUserModal(false);
|
||||
setNewUser({ username: '', caption: '', email: '', role_id: 2 });
|
||||
setError(''); // Clear any previous errors
|
||||
fetchUsers(); // Refresh user list
|
||||
} catch (err) {
|
||||
console.error('Error adding user:', err);
|
||||
setError(err.response?.data?.detail || '新增用户失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateUser = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
// 只发送模型期望的字段
|
||||
const updateData = {
|
||||
caption: editingUser.caption,
|
||||
email: editingUser.email,
|
||||
role_id: editingUser.role_id
|
||||
};
|
||||
|
||||
// 只有当用户名被修改时才发送
|
||||
if (editingUser.username && editingUser.username.trim()) {
|
||||
updateData.username = editingUser.username;
|
||||
}
|
||||
|
||||
console.log('Sending update data:', updateData); // 调试用
|
||||
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE(editingUser.user_id)), updateData);
|
||||
setShowEditUserModal(false);
|
||||
setError(''); // Clear any previous errors
|
||||
fetchUsers(); // Refresh user list
|
||||
} catch (err) {
|
||||
console.error('Error updating user:', err);
|
||||
console.error('Error response:', err.response?.data); // 调试用
|
||||
setError(err.response?.data?.detail || '修改用户失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
try {
|
||||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.USERS.DELETE(processingUser.user_id)));
|
||||
setShowDeleteConfirm(false);
|
||||
fetchUsers(); // Refresh user list
|
||||
} catch (err) {
|
||||
console.error('Error deleting user:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
try {
|
||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.USERS.RESET_PASSWORD(processingUser.user_id)));
|
||||
setShowResetConfirm(false);
|
||||
} catch (err) {
|
||||
console.error('Error resetting password:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const openEditModal = (user) => {
|
||||
setEditingUser({ ...user });
|
||||
setShowEditUserModal(true);
|
||||
};
|
||||
|
||||
const openDeleteConfirm = (user) => {
|
||||
setProcessingUser(user);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const openResetConfirm = (user) => {
|
||||
setProcessingUser(user);
|
||||
setShowResetConfirm(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="user-management">
|
||||
<div className="toolbar">
|
||||
<h2>用户列表</h2>
|
||||
<button className="btn btn-primary" onClick={() => setShowAddUserModal(true)}><Plus size={16} /> 新增用户</button>
|
||||
</div>
|
||||
{loading && <p>加载中...</p>}
|
||||
{error && <p className="error-message">{error}</p>}
|
||||
{!loading && !error && (
|
||||
<>
|
||||
<table className="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>姓名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(user => (
|
||||
<tr key={user.user_id}>
|
||||
<td>{user.user_id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.caption}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.role_id === 1 ? '管理员' : '普通用户'}</td>
|
||||
<td>{new Date(user.created_at).toLocaleString()}</td>
|
||||
<td className="action-cell">
|
||||
<button className="action-btn" onClick={() => openEditModal(user)} title="修改"><Edit size={16} />修改</button>
|
||||
<button className="action-btn btn-danger" onClick={() => openDeleteConfirm(user)} title="删除"><Trash2 size={16} />删除</button>
|
||||
<button className="action-btn btn-warning" onClick={() => openResetConfirm(user)} title="重置密码"><KeyRound size={16} />重置</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pagination">
|
||||
<span>总计 {total} 条</span>
|
||||
<div>
|
||||
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>上一页</button>
|
||||
<span>第 {page} 页 / {Math.ceil(total / pageSize)}</span>
|
||||
<button onClick={() => setPage(p => p + 1)} disabled={page * pageSize >= total}>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showAddUserModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handleAddUser}>
|
||||
<h2>新增用户</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<div className="form-group">
|
||||
<label>用户名</label>
|
||||
<input className="form-input" type="text" value={newUser.username} onChange={(e) => setNewUser({...newUser, username: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>姓名</label>
|
||||
<input className="form-input" type="text" value={newUser.caption} onChange={(e) => setNewUser({...newUser, caption: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>邮箱</label>
|
||||
<input className="form-input" type="email" value={newUser.email} onChange={(e) => setNewUser({...newUser, email: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>角色</label>
|
||||
<select className="form-input" value={newUser.role_id} onChange={(e) => setNewUser({...newUser, role_id: parseInt(e.target.value)})}>
|
||||
<option value={2}>普通用户</option>
|
||||
<option value={1}>管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="info-note">
|
||||
<p>注:新用户的默认密码为系统配置的默认密码,用户可登录后自行修改</p>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => {setShowAddUserModal(false); setError('');}}>取消</button>
|
||||
<button type="submit" className="btn btn-primary">确认新增</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditUserModal && editingUser && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handleUpdateUser}>
|
||||
<h2>修改用户</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<div className="form-group">
|
||||
<label>用户名</label>
|
||||
<input className="form-input" type="text" value={editingUser.username} onChange={(e) => setEditingUser({...editingUser, username: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>姓名</label>
|
||||
<input className="form-input" type="text" value={editingUser.caption} onChange={(e) => setEditingUser({...editingUser, caption: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>邮箱</label>
|
||||
<input className="form-input" type="email" value={editingUser.email} onChange={(e) => setEditingUser({...editingUser, email: e.target.value})} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>角色</label>
|
||||
<select className="form-input" value={editingUser.role_id} onChange={(e) => setEditingUser({...editingUser, role_id: parseInt(e.target.value)})}>
|
||||
<option value={2}>普通用户</option>
|
||||
<option value={1}>管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => {setShowEditUserModal(false); setError('');}}>取消</button>
|
||||
<button type="submit" className="btn btn-primary">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDeleteConfirm && processingUser && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<h2>确认删除</h2>
|
||||
<p>您确定要删除用户 <strong>{processingUser.caption}</strong> 吗?此操作无法撤销。</p>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowDeleteConfirm(false)}>取消</button>
|
||||
<button type="button" className="btn btn-danger" onClick={handleDeleteUser}>确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showResetConfirm && processingUser && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<h2>确认重置密码</h2>
|
||||
<p>您确定要重置用户 <strong>{processingUser.caption}</strong> 的密码吗?</p>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowResetConfirm(false)}>取消</button>
|
||||
<button type="button" className="btn btn-warning" onClick={handleResetPassword}>确认重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
|
|
@ -11,7 +11,12 @@ const API_CONFIG = {
|
|||
},
|
||||
USERS: {
|
||||
LIST: '/api/users',
|
||||
DETAIL: (userId) => `/api/users/${userId}`
|
||||
CREATE: '/api/users',
|
||||
UPDATE: (userId) => `/api/users/${userId}`,
|
||||
DELETE: (userId) => `/api/users/${userId}`,
|
||||
RESET_PASSWORD: (userId) => `/api/users/${userId}/reset-password`,
|
||||
DETAIL: (userId) => `/api/users/${userId}`,
|
||||
UPDATE_PASSWORD: (userId) => `/api/users/${userId}/password`
|
||||
},
|
||||
MEETINGS: {
|
||||
LIST: '/api/meetings',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
/* AdminManagement.css */
|
||||
.admin-management-page {
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.admin-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-header .header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-header .logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.admin-header .logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.admin-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.admin-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.admin-wrapper {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
position: relative;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #475569;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-btn:active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-btn:focus {
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 2rem;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.admin-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.admin-header .header-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React, { useState } from 'react';
|
||||
import { MessageSquare, Settings, Users } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import UserManagement from '../components/admin/UserManagement';
|
||||
import SystemConfiguration from '../components/admin/SystemConfiguration';
|
||||
import './AdminManagement.css';
|
||||
|
||||
const AdminManagement = () => {
|
||||
const [activeTab, setActiveTab] = useState('userManagement');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogoClick = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-management-page">
|
||||
<header className="admin-header">
|
||||
<div className="header-content">
|
||||
<div className="logo" onClick={handleLogoClick} style={{ cursor: 'pointer' }}>
|
||||
<MessageSquare className="logo-icon" />
|
||||
<span className="logo-text">iMeeting</span>
|
||||
</div>
|
||||
<h1>平台管理</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="admin-content">
|
||||
<div className="admin-wrapper">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'userManagement' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('userManagement')}
|
||||
>
|
||||
<Users size={18} style={{ marginRight: '0.5rem' }} />
|
||||
用户管理
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'systemConfiguration' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('systemConfiguration')}
|
||||
>
|
||||
<Settings size={18} style={{ marginRight: '0.5rem' }} />
|
||||
系统配置
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{activeTab === 'userManagement' && <UserManagement />}
|
||||
{activeTab === 'systemConfiguration' && <SystemConfiguration />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminManagement;
|
||||
|
|
@ -301,4 +301,112 @@
|
|||
.welcome-text {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* User Menu */
|
||||
.user-menu-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
width: 160px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dropdown-menu button,
|
||||
.dropdown-menu a {
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown-menu button:hover,
|
||||
.dropdown-menu a:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-content .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-content .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-content .form-group input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: green;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus } from 'lucide-react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield } from 'lucide-react';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||
|
|
@ -11,11 +11,31 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
const [meetings, setMeetings] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [passwordChangeError, setPasswordChangeError] = useState('');
|
||||
const [passwordChangeSuccess, setPasswordChangeSuccess] = useState('');
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserData();
|
||||
}, [user.user_id]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [dropdownRef]);
|
||||
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -49,6 +69,39 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = async (e) => {
|
||||
e.preventDefault();
|
||||
if (newPassword !== confirmPassword) {
|
||||
setPasswordChangeError('新密码不匹配');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
setPasswordChangeError('新密码长度不能少于6位');
|
||||
return;
|
||||
}
|
||||
|
||||
setPasswordChangeError('');
|
||||
setPasswordChangeSuccess('');
|
||||
|
||||
try {
|
||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.USERS.UPDATE_PASSWORD(user.user_id)), {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
setPasswordChangeSuccess('密码修改成功!');
|
||||
// 清空输入框并准备关闭模态框
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setTimeout(() => {
|
||||
setShowChangePasswordModal(false);
|
||||
setPasswordChangeSuccess('');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
setPasswordChangeError(err.response?.data?.detail || '密码修改失败');
|
||||
}
|
||||
};
|
||||
|
||||
const groupMeetingsByDate = (meetings) => {
|
||||
return meetings.reduce((acc, meeting) => {
|
||||
const date = new Date(meeting.meeting_time || meeting.created_at).toISOString().split('T')[0];
|
||||
|
|
@ -104,11 +157,21 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
<span className="logo-text">iMeeting</span>
|
||||
</div>
|
||||
<div className="user-actions">
|
||||
<span className="welcome-text">欢迎,{userInfo?.caption}</span>
|
||||
<button className="logout-btn" onClick={onLogout}>
|
||||
<LogOut size={18} />
|
||||
退出
|
||||
</button>
|
||||
<div className="user-menu-container" ref={dropdownRef}>
|
||||
<div className="user-menu-trigger" onClick={() => setDropdownOpen(!dropdownOpen)}>
|
||||
<span className="welcome-text">欢迎,{userInfo?.caption}</span>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
{dropdownOpen && (
|
||||
<div className="dropdown-menu">
|
||||
<button onClick={() => { setShowChangePasswordModal(true); setDropdownOpen(false); }}><KeyRound size={16} /> 修改密码</button>
|
||||
{user.role_id === 1 && (
|
||||
<Link to="/admin/management" onClick={() => setDropdownOpen(false)}><Shield size={16} /> 平台管理</Link>
|
||||
)}
|
||||
<button onClick={onLogout}><LogOut size={16} /> 退出</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -186,6 +249,34 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{showChangePasswordModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handlePasswordChange}>
|
||||
<h2>修改密码</h2>
|
||||
{passwordChangeError && <p className="error-message">{passwordChangeError}</p>}
|
||||
{passwordChangeSuccess && <p className="success-message">{passwordChangeSuccess}</p>}
|
||||
<div className="form-group">
|
||||
<label>旧密码</label>
|
||||
<input type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>新密码</label>
|
||||
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required />
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowChangePasswordModal(false)}>取消</button>
|
||||
<button type="submit" className="btn btn-primary">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
color: #ffd700;
|
||||
}
|
||||
|
||||
.logo-text-white {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
|
|
@ -305,4 +309,17 @@
|
|||
.login-modal {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.homepage-footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.homepage-footer p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ const HomePage = ({ onLogin }) => {
|
|||
<div className="header-content">
|
||||
<div className="logo">
|
||||
<Brain className="logo-icon" />
|
||||
<span className="logo-text">iMeeting</span>
|
||||
<span className="logo-text-white">iMeeting</span>
|
||||
</div>
|
||||
<button
|
||||
className="login-btn"
|
||||
|
|
@ -102,6 +102,14 @@ const HomePage = ({ onLogin }) => {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="homepage-footer">
|
||||
<p>© 2025 紫光汇智信息技术有限公司. All rights reserved.</p>
|
||||
<p>备案号:渝ICP备2023007695号-11</p>
|
||||
</footer>
|
||||
|
||||
{/* Login Modal */}
|
||||
{showLoginModal && (
|
||||
<div className="modal-overlay" onClick={closeModal}>
|
||||
|
|
@ -160,8 +168,8 @@ const HomePage = ({ onLogin }) => {
|
|||
</form>
|
||||
|
||||
<div className="demo-info">
|
||||
<p>开通账号:</p>
|
||||
<p>请联系:18980500203</p>
|
||||
<p>开通业务账号:</p>
|
||||
<p>请联系平台管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue