mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:26:07 +08:00
[dev]add change password
This commit is contained in:
parent
01e60c5674
commit
98b7a08143
151
src/components/ChangePasswordModal.jsx
Normal file
151
src/components/ChangePasswordModal.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { changePasswordThunk } from '../store/auth/auth.thunk';
|
||||
|
||||
function ChangePasswordModal({ show, onClose }) {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.old_password) {
|
||||
newErrors.old_password = '请输入当前密码';
|
||||
}
|
||||
|
||||
if (!formData.new_password) {
|
||||
newErrors.new_password = '请输入新密码';
|
||||
} else if (formData.new_password.length < 6) {
|
||||
newErrors.new_password = '新密码长度不能少于6个字符';
|
||||
}
|
||||
|
||||
if (!formData.confirm_password) {
|
||||
newErrors.confirm_password = '请确认新密码';
|
||||
} else if (formData.new_password !== formData.confirm_password) {
|
||||
newErrors.confirm_password = '两次输入的密码不一致';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (validateForm()) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await dispatch(
|
||||
changePasswordThunk({
|
||||
old_password: formData.old_password,
|
||||
new_password: formData.new_password,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// 成功后会由thunk中的代码重定向到登录页
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
// 错误处理已经在thunk中完成
|
||||
console.error('Change password failed:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className='modal-dialog modal-dialog-centered'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-header border-0'>
|
||||
<h5 className='modal-title'>修改密码</h5>
|
||||
<button type='button' className='btn-close' onClick={onClose}></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>当前密码</label>
|
||||
<input
|
||||
value={formData.old_password}
|
||||
name='old_password'
|
||||
type='password'
|
||||
className={`form-control${submitted && errors.old_password ? ' is-invalid' : ''}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.old_password && (
|
||||
<div className='invalid-feedback'>{errors.old_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>新密码</label>
|
||||
<input
|
||||
value={formData.new_password}
|
||||
name='new_password'
|
||||
type='password'
|
||||
className={`form-control${submitted && errors.new_password ? ' is-invalid' : ''}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.new_password && (
|
||||
<div className='invalid-feedback'>{errors.new_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>确认新密码</label>
|
||||
<input
|
||||
value={formData.confirm_password}
|
||||
name='confirm_password'
|
||||
type='password'
|
||||
className={`form-control${
|
||||
submitted && errors.confirm_password ? ' is-invalid' : ''
|
||||
}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.confirm_password && (
|
||||
<div className='invalid-feedback'>{errors.confirm_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='modal-footer border-0'>
|
||||
<button type='button' className='btn btn-outline-dark' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type='submit' className='btn btn-dark' disabled={isSubmitting}>
|
||||
{isSubmitting ? '提交中...' : '修改密码'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangePasswordModal;
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateProfileThunk } from '../store/auth/auth.thunk';
|
||||
import ChangePasswordModal from './ChangePasswordModal';
|
||||
|
||||
// 部门和组别的映射关系
|
||||
const departmentGroups = {
|
||||
@ -17,6 +18,7 @@ function UserSettingsModal({ show, onClose }) {
|
||||
const { user, loading } = useSelector((state) => state.auth);
|
||||
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
|
||||
const [formData, setFormData] = useState({});
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
// 可选的组别列表
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
|
||||
@ -198,6 +200,28 @@ function UserSettingsModal({ show, onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<h6 className='text-secondary mb-3'>安全设置</h6>
|
||||
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
|
||||
<div>
|
||||
<div className='d-flex align-items-center gap-2'>
|
||||
<i className='bi bi-key'></i>
|
||||
<span>修改密码</span>
|
||||
</div>
|
||||
<small className='text-secondary'>上次修改:{lastPasswordChange}</small>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-outline-dark btn-sm'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowChangePassword(true);
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='d-none mb-4'>
|
||||
<h6 className='text-secondary mb-3'>安全设置</h6>
|
||||
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
|
||||
@ -260,6 +284,9 @@ function UserSettingsModal({ show, onClose }) {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{showChangePassword && (
|
||||
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { logoutThunk } from '../store/auth/auth.thunk';
|
||||
import UserSettingsModal from '../components/UserSettingsModal';
|
||||
import NotificationCenter from '../components/NotificationCenter';
|
||||
import SvgIcon from '../components/SvgIcon';
|
||||
import ChangePasswordModal from '../components/ChangePasswordModal';
|
||||
import { fetchNotifications } from '../store/notificationCenter/notificationCenter.thunks';
|
||||
|
||||
export default function HeaderWithNav() {
|
||||
@ -14,6 +15,7 @@ export default function HeaderWithNav() {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
|
||||
|
||||
const handleLogout = async () => {
|
||||
@ -135,6 +137,15 @@ export default function HeaderWithNav() {
|
||||
个人设置
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className='dropdown-item'
|
||||
to='#'
|
||||
onClick={() => setShowChangePassword(true)}
|
||||
>
|
||||
修改密码
|
||||
</Link>
|
||||
</li>
|
||||
<li className='d-none'>
|
||||
<hr className='dropdown-divider' />
|
||||
</li>
|
||||
@ -168,6 +179,7 @@ export default function HeaderWithNav() {
|
||||
</nav>
|
||||
<UserSettingsModal show={showSettings} onClose={() => setShowSettings(false)} />
|
||||
<NotificationCenter show={showNotifications} onClose={() => setShowNotifications(false)} />
|
||||
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -159,11 +159,6 @@ const put = async (url, data) => {
|
||||
// Handle DELETE requests with fallback to mock API
|
||||
const del = async (url) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] DELETE ${url}`);
|
||||
return await mockDelete(url);
|
||||
}
|
||||
|
||||
const res = await api.delete(url);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@ -217,36 +212,6 @@ export const switchToRealApi = async () => {
|
||||
// Handle streaming requests
|
||||
const streamRequest = async (url, data, onChunk, onError) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] STREAM ${url}`);
|
||||
// 模拟流式响应
|
||||
setTimeout(
|
||||
() =>
|
||||
onChunk(
|
||||
'{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'
|
||||
),
|
||||
300
|
||||
);
|
||||
setTimeout(
|
||||
() =>
|
||||
onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'),
|
||||
600
|
||||
);
|
||||
setTimeout(
|
||||
() =>
|
||||
onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'),
|
||||
900
|
||||
);
|
||||
setTimeout(
|
||||
() =>
|
||||
onChunk(
|
||||
'{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'
|
||||
),
|
||||
1200
|
||||
);
|
||||
return { success: true, conversation_id: 'mock-1234' };
|
||||
}
|
||||
|
||||
// 获取认证Token
|
||||
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||
let token = '';
|
||||
@ -259,7 +224,6 @@ const streamRequest = async (url, data, onChunk, onError) => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: token ? `Token ${token}` : '',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
@ -819,6 +819,32 @@ export const mockPost = async (url, data, isMultipart = false) => {
|
||||
};
|
||||
}
|
||||
|
||||
// 验证Token
|
||||
if (url === '/auth/verify-token/') {
|
||||
// 在实际应用中,这里会验证请求头中的Token
|
||||
// 由于是mock API,我们假设所有token都是有效的
|
||||
// 并返回第一个用户作为当前用户
|
||||
|
||||
const user = mockUsers[0];
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Token验证成功',
|
||||
data: {
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
department: user.department,
|
||||
group: user.group,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Create knowledge base
|
||||
if (url === '/knowledge-bases/') {
|
||||
const newKnowledgeBase = {
|
||||
@ -978,6 +1004,49 @@ export const mockPost = async (url, data, isMultipart = false) => {
|
||||
};
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
if (url === '/auth/change-password/') {
|
||||
const { old_password, new_password } = data;
|
||||
|
||||
// 从请求头中获取token,找到当前用户
|
||||
const token = 'mock-token'; // 在实际情况下,会从请求头中获取并验证
|
||||
const currentUser = mockUsers.find(user => user.id === 'user-001'); // 假设当前用户是第一个用户
|
||||
|
||||
// 验证旧密码
|
||||
if (!currentUser || currentUser.password !== old_password) {
|
||||
throw {
|
||||
response: {
|
||||
status: 400,
|
||||
data: {
|
||||
code: 400,
|
||||
message: '原密码不正确',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
const userIndex = mockUsers.findIndex(user => user.id === currentUser.id);
|
||||
if (userIndex !== -1) {
|
||||
mockUsers[userIndex].password = new_password;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '密码修改成功',
|
||||
data: { success: true }
|
||||
};
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
if (url === '/auth/logout/') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登出成功',
|
||||
data: { success: true }
|
||||
};
|
||||
}
|
||||
|
||||
// 上传知识库文档
|
||||
if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) {
|
||||
const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1];
|
||||
@ -1001,31 +1070,31 @@ export const mockPost = async (url, data, isMultipart = false) => {
|
||||
// Mark all notifications as read
|
||||
if (url === '/notifications/mark-all-as-read/') {
|
||||
// Update all notifications to be read
|
||||
notifications.forEach(notification => {
|
||||
notifications.forEach((notification) => {
|
||||
notification.is_read = true;
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'All notifications marked as read successfully',
|
||||
data: { success: true }
|
||||
data: { success: true },
|
||||
};
|
||||
}
|
||||
|
||||
// Mark a notification as read
|
||||
if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) {
|
||||
const notificationId = url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)[1];
|
||||
const notificationIndex = notifications.findIndex(n => n.id === notificationId);
|
||||
const notificationIndex = notifications.findIndex((n) => n.id === notificationId);
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
notifications[notificationIndex] = {
|
||||
...notifications[notificationIndex],
|
||||
is_read: true
|
||||
is_read: true,
|
||||
};
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Notification marked as read successfully',
|
||||
data: { success: true, notification: notifications[notificationIndex] }
|
||||
data: { success: true, notification: notifications[notificationIndex] },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ export const initWebSocket = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const wsUrl = `${WS_BASE_URL}/ws/notifications/?token=${token}`;
|
||||
const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${token}`;
|
||||
console.log('正在连接WebSocket...', wsUrl.substring(0, wsUrl.indexOf('?')));
|
||||
|
||||
socket = new WebSocket(wsUrl);
|
||||
|
@ -137,3 +137,43 @@ export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// 修改密码
|
||||
export const changePasswordThunk = createAsyncThunk(
|
||||
'auth/changePassword',
|
||||
async ({ old_password, new_password }, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { message, code } = await post('/auth/change-password/', {
|
||||
old_password,
|
||||
new_password
|
||||
});
|
||||
|
||||
if (code !== 200) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// 显示密码修改成功通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '密码修改成功,请重新登录',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
// 修改成功后清除登录状态
|
||||
sessionStorage.removeItem('token');
|
||||
dispatch(logout());
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || '密码修改失败,请稍后重试';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user