diff --git a/.DS_Store b/.DS_Store
index d4c681b..e9df439 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/Dockerfile b/Dockerfile
index 6e389bb..5eddf73 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/deploy-prod-nobuild.sh b/deploy-prod-nobuild.sh
new file mode 100755
index 0000000..e2d4340
--- /dev/null
+++ b/deploy-prod-nobuild.sh
@@ -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 " ✅ 进程监控和健康检查"
\ No newline at end of file
diff --git a/dist.zip b/dist.zip
new file mode 100644
index 0000000..e6fa54c
Binary files /dev/null and b/dist.zip differ
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index f3bc752..32bce59 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -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
diff --git a/server.js b/server.js
index ff45212..92e3db2 100644
--- a/server.js
+++ b/server.js
@@ -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"));
});
diff --git a/src/App.jsx b/src/App.jsx
index cce8f5a..88ffb2c 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -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() {
:
} />
+ :
+ } />
diff --git a/src/components/admin/SystemConfiguration.jsx b/src/components/admin/SystemConfiguration.jsx
new file mode 100644
index 0000000..85e3253
--- /dev/null
+++ b/src/components/admin/SystemConfiguration.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+const SystemConfiguration = () => {
+ return (
+
+ );
+};
+
+export default SystemConfiguration;
diff --git a/src/components/admin/UserManagement.css b/src/components/admin/UserManagement.css
new file mode 100644
index 0000000..5aa8f13
--- /dev/null
+++ b/src/components/admin/UserManagement.css
@@ -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);
+}
diff --git a/src/components/admin/UserManagement.jsx b/src/components/admin/UserManagement.jsx
new file mode 100644
index 0000000..f634b2c
--- /dev/null
+++ b/src/components/admin/UserManagement.jsx
@@ -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 (
+
+
+
用户列表
+
+
+ {loading &&
加载中...
}
+ {error &&
{error}
}
+ {!loading && !error && (
+ <>
+
+
+
+ | ID |
+ 用户名 |
+ 姓名 |
+ 邮箱 |
+ 角色 |
+ 创建时间 |
+ 操作 |
+
+
+
+ {users.map(user => (
+
+ | {user.user_id} |
+ {user.username} |
+ {user.caption} |
+ {user.email} |
+ {user.role_id === 1 ? '管理员' : '普通用户'} |
+ {new Date(user.created_at).toLocaleString()} |
+
+
+
+
+ |
+
+ ))}
+
+
+
+
总计 {total} 条
+
+
+ 第 {page} 页 / {Math.ceil(total / pageSize)}
+
+
+
+ >
+ )}
+
+ {showAddUserModal && (
+
+ )}
+
+ {showEditUserModal && editingUser && (
+
+ )}
+
+ {showDeleteConfirm && processingUser && (
+
+
+
确认删除
+
您确定要删除用户 {processingUser.caption} 吗?此操作无法撤销。
+
+
+
+
+
+
+ )}
+
+ {showResetConfirm && processingUser && (
+
+
+
确认重置密码
+
您确定要重置用户 {processingUser.caption} 的密码吗?
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default UserManagement;
diff --git a/src/config/api.js b/src/config/api.js
index eeb337d..69d0c3b 100644
--- a/src/config/api.js
+++ b/src/config/api.js
@@ -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',
diff --git a/src/pages/AdminManagement.css b/src/pages/AdminManagement.css
new file mode 100644
index 0000000..a11ce56
--- /dev/null
+++ b/src/pages/AdminManagement.css
@@ -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;
+ }
+}
diff --git a/src/pages/AdminManagement.jsx b/src/pages/AdminManagement.jsx
new file mode 100644
index 0000000..81e6d3b
--- /dev/null
+++ b/src/pages/AdminManagement.jsx
@@ -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 (
+
+
+
+
+
+ iMeeting
+
+
平台管理
+
+
+
+
+
+
+
+
+
+ {activeTab === 'userManagement' && }
+ {activeTab === 'systemConfiguration' && }
+
+
+
+
+ );
+};
+
+export default AdminManagement;
\ No newline at end of file
diff --git a/src/pages/Dashboard.css b/src/pages/Dashboard.css
index 0accce9..aaa1266 100644
--- a/src/pages/Dashboard.css
+++ b/src/pages/Dashboard.css
@@ -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;
}
\ No newline at end of file
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index aeb3aa2..ee3dce5 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -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 }) => {
iMeeting
-
欢迎,{userInfo?.caption}
-
+
+
setDropdownOpen(!dropdownOpen)}>
+ 欢迎,{userInfo?.caption}
+
+
+ {dropdownOpen && (
+
+
+ {user.role_id === 1 && (
+ setDropdownOpen(false)}> 平台管理
+ )}
+
+
+ )}
+
@@ -186,6 +249,34 @@ const Dashboard = ({ user, onLogout }) => {
/>
+
+ {showChangePasswordModal && (
+
+ )}
);
};
diff --git a/src/pages/HomePage.css b/src/pages/HomePage.css
index bfe1abb..56c670d 100644
--- a/src/pages/HomePage.css
+++ b/src/pages/HomePage.css
@@ -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;
}
\ No newline at end of file
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index ebbc143..00f9f87 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -46,7 +46,7 @@ const HomePage = ({ onLogin }) => {
+
+
+ {/* Footer */}
+
+
{/* Login Modal */}
{showLoginModal && (
@@ -160,8 +168,8 @@ const HomePage = ({ onLogin }) => {
-
开通账号:
-
请联系:18980500203
+
开通业务账号:
+
请联系平台管理员