Compare commits

..

2 Commits

Author SHA1 Message Date
guangfei.zhao
79ee33be7c fix(knowledge): ensure proper id handling in document operations
refactor(teams): update user deletion to use user_id instead of id
refactor(knowledge): optimize data grid locale handling with useCallback
style(knowledge): adjust similarity display format in test chunks
refactor(knowledge): improve document selection logic and typing
2025-11-19 17:44:12 +08:00
guangfei.zhao
83df8a7373 feat(auth): enhance password change flow and error handling
- Return response from useProfileSetting hook after password change
2025-11-19 17:43:45 +08:00
14 changed files with 99 additions and 43 deletions

View File

@@ -120,7 +120,7 @@ const DialogComponent: React.FC<DialogComponentProps> = ({ dialog, onClose }) =>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
{getDialogIcon()} {getDialogIcon()}
<Typography variant="h6" component="span"> <Typography variant="h6" component="span">
{config.title || t('dialog.defaultTitle')} {config.title || t('dialog.confirm')}
</Typography> </Typography>
</Box> </Box>
<IconButton <IconButton

View File

@@ -394,12 +394,12 @@ export const useLoginForm = () => {
password: { password: {
required: t('login.passwordPlaceholder'), required: t('login.passwordPlaceholder'),
minLength: { minLength: {
value: 8, value: 6,
message: t('setting.passwordMinLength') message: t('setting.passwordMinLength')
} }
}, },
confirmPassword: { confirmPassword: {
required: t('confirmPasswordRequired'), required: t('login.confirmPasswordRequired'),
validate: (value: string) => { validate: (value: string) => {
const pwd = registerForm.getValues('password'); const pwd = registerForm.getValues('password');
return value === pwd || t('setting.passwordMismatch'); return value === pwd || t('setting.passwordMismatch');

View File

@@ -42,6 +42,7 @@ export function useProfileSetting() {
password: oldPassword, password: oldPassword,
new_password: newPassword, new_password: newPassword,
}); });
return res;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -262,6 +262,11 @@ export default {
emailInvalid: 'Invalid email address', emailInvalid: 'Invalid email address',
passwordLabel: 'Password', passwordLabel: 'Password',
passwordPlaceholder: 'Please input password', passwordPlaceholder: 'Please input password',
confirmPasswordRequired: 'Please confirm your password',
confirmPassword: 'Confirm password',
confirmPasswordMessage: 'Please confirm your password!',
confirmPasswordNonMatchMessage:
'The confirm password that you entered do not match!',
rememberMe: 'Remember me', rememberMe: 'Remember me',
signInTip: 'Dont have an account?', signInTip: 'Dont have an account?',
signUpTip: 'Already have an account?', signUpTip: 'Already have an account?',
@@ -942,7 +947,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
serverTypeRequired: 'Please select server type', serverTypeRequired: 'Please select server type',
testConnection: 'Test Connection', testConnection: 'Test Connection',
testing: 'Testing...', testing: 'Testing...',
connectionSuccess: 'Connection successful! Found {count} tools', connectionSuccess: 'Connection successful! Found {{count}} tools',
availableTools: 'Available Tools', availableTools: 'Available Tools',
connectionFailed: 'Connection failed', connectionFailed: 'Connection failed',
testFailed: 'Test failed', testFailed: 'Test failed',

View File

@@ -244,6 +244,11 @@ export default {
emailInvalid: '无效的邮箱地址', emailInvalid: '无效的邮箱地址',
passwordLabel: '密码', passwordLabel: '密码',
passwordPlaceholder: '请输入密码', passwordPlaceholder: '请输入密码',
confirmPasswordRequired: '请确认您的密码',
confirmPassword: '确认密码',
confirmPasswordMessage: '请确认您的密码!',
confirmPasswordNonMatchMessage:
'确认密码与密码不匹配!',
rememberMe: '记住我', rememberMe: '记住我',
signInTip: '没有帐户?', signInTip: '没有帐户?',
signUpTip: '已经有帐户?', signUpTip: '已经有帐户?',
@@ -941,7 +946,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
serverTypeRequired: '请选择服务器类型', serverTypeRequired: '请选择服务器类型',
testConnection: '测试连接', testConnection: '测试连接',
testing: '测试中...', testing: '测试中...',
connectionSuccess: '连接成功!发现 {count} 个工具', connectionSuccess: '连接成功!发现 {{count}} 个工具',
availableTools: '可用工具', availableTools: '可用工具',
connectionFailed: '连接失败', connectionFailed: '连接失败',
testFailed: '测试失败', testFailed: '测试失败',

View File

@@ -49,7 +49,7 @@ import {
BugReportOutlined as ProcessIcon, BugReportOutlined as ProcessIcon,
Download as DownloadIcon, Download as DownloadIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid'; import { DataGrid, type GridColDef, type GridRowSelectionModel, type GridRowId } from '@mui/x-data-grid';
import { zhCN, enUS } from '@mui/x-data-grid/locales'; import { zhCN, enUS } from '@mui/x-data-grid/locales';
import type { IKnowledgeFile } from '@/interfaces/database/knowledge'; import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
import type { IDocumentInfoFilter } from '@/interfaces/database/document'; import type { IDocumentInfoFilter } from '@/interfaces/database/document';
@@ -163,7 +163,7 @@ const getRunStatusChip = (run: RunningStatus, progress: number) => {
<CircularProgress size={16} /> <CircularProgress size={16} />
<Box sx={{ minWidth: 80 }}> <Box sx={{ minWidth: 80 }}>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{(progress*100).toFixed(2)}% {(progress * 100).toFixed(2)}%
</Typography> </Typography>
<LinearProgress variant="determinate" value={progress} sx={{ height: 4, borderRadius: 2 }} /> <LinearProgress variant="determinate" value={progress} sx={{ height: 4, borderRadius: 2 }} />
</Box> </Box>
@@ -218,10 +218,10 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
const { i18n, t } = useTranslation(); const { i18n, t } = useTranslation();
// 根据当前语言获取DataGrid的localeText // 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => { const getDataGridLocale = useCallback(() => {
const currentLanguage = i18n.language; const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS; return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
}; }, [i18n]);
const handleFilterSubmit = useCallback(() => { const handleFilterSubmit = useCallback(() => {
const filter = { const filter = {
@@ -346,6 +346,24 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
setSelectedSuffix(typeof value === 'string' ? value.split(',') : value); setSelectedSuffix(typeof value === 'string' ? value.split(',') : value);
}; };
// 选中数量计算(强类型)
const countSelected = (model: GridRowSelectionModel, totalCount: number): number => {
const size = model.ids.size ?? 0;
return model.type === 'exclude' ? Math.max(totalCount - size, 0) : size;
};
// 获取当前页有效选中 ID 列表(强类型)
const getSelectedIdsOnCurrentPage = (model: GridRowSelectionModel, currentFiles: IKnowledgeFile[]): string[] => {
if (model.type === 'include') {
return Array.from(model.ids).map((id) => id.toString());
}
const excluded = model.ids;
return currentFiles
.map((f) => f.id)
.filter((id) => !excluded.has(id))
.map((id) => id.toString());
};
// 处理分页变化 // 处理分页变化
const handlePaginationModelChange = (model: { page: number; pageSize: number }) => { const handlePaginationModelChange = (model: { page: number; pageSize: number }) => {
if (model.page !== page - 1) { // DataGrid的page是0-based我们的是1-based if (model.page !== page - 1) { // DataGrid的page是0-based我们的是1-based
@@ -603,23 +621,22 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
{t('knowledge.uploadFile')} {t('knowledge.uploadFile')}
</Button> </Button>
{rowSelectionModel.ids.size > 0 && ( {countSelected(rowSelectionModel, total) > 0 && (
<> <>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<RefreshIcon />} startIcon={<RefreshIcon />}
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))} onClick={() => onReparse(getSelectedIdsOnCurrentPage(rowSelectionModel, files))}
> >
{t('knowledge.reparse')} ({rowSelectionModel.ids.size}) {t('knowledge.reparse')} ({countSelected(rowSelectionModel, total)})
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
color="error" color="error"
startIcon={<DeleteIcon />} startIcon={<DeleteIcon />}
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))} onClick={() => onDelete(getSelectedIdsOnCurrentPage(rowSelectionModel, files))}
> >
{t('common.delete')} ({rowSelectionModel.ids.size}) {t('common.delete')} ({countSelected(rowSelectionModel, total)})
</Button> </Button>
</> </>
)} )}
@@ -640,7 +657,9 @@ const DocumentListComponent: React.FC<DocumentListComponentProps> = ({
checkboxSelection checkboxSelection
disableRowSelectionOnClick disableRowSelectionOnClick
rowSelectionModel={rowSelectionModel} rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={onRowSelectionModelChange} onRowSelectionModelChange={(model: GridRowSelectionModel) => {
onRowSelectionModelChange(model);
}}
pageSizeOptions={[10, 25, 50, 100]} pageSizeOptions={[10, 25, 50, 100]}
paginationMode="server" paginationMode="server"
rowCount={total} rowCount={total}

View File

@@ -162,20 +162,20 @@ function TestChunkResult(props: TestChunkResultProps) {
</Typography> </Typography>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
<Chip <Chip
label={t('knowledge.similarity', { value: (chunk.similarity * 100).toFixed(1) })} label={`${t('knowledge.similarity')}: ${(chunk.similarity).toFixed(0)}`}
size="small" size="small"
color="primary" color="primary"
/> />
{chunk.vector_similarity !== undefined && ( {chunk.vector_similarity !== undefined && (
<Chip <Chip
label={t('knowledge.vectorSimilarity', { value: (chunk.vector_similarity * 100).toFixed(1) })} label={`${t('knowledge.vectorSimilarity')}: ${(chunk.vector_similarity).toFixed(2)}`}
size="small" size="small"
variant="outlined" variant="outlined"
/> />
)} )}
{chunk.term_similarity !== undefined && ( {chunk.term_similarity !== undefined && (
<Chip <Chip
label={t('knowledge.termSimilarity', { value: (chunk.term_similarity * 100).toFixed(1) })} label={`${t('knowledge.termSimilarity')}: ${(chunk.term_similarity).toFixed(0)}`}
size="small" size="small"
variant="outlined" variant="outlined"
/> />

View File

@@ -134,7 +134,7 @@ function KnowledgeBaseDetail() {
// 删除文件 // 删除文件
const handleDeleteFiles = async () => { const handleDeleteFiles = async () => {
try { try {
await deleteDocuments(Array.from(rowSelectionModel.ids) as string[]); await deleteDocuments(Array.from(rowSelectionModel.ids).map((id) => id.toString()));
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setRowSelectionModel({ setRowSelectionModel({
type: 'include', type: 'include',

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab, Fab, Avatar, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Card, CardContent, Divider } from '@mui/material'; import { Box, Grid, Paper, Typography, IconButton, TextField, Tabs, Tab, Fab, Avatar, Chip, Dialog, DialogTitle, DialogContent, DialogActions, Button, Card, CardContent, Divider } from '@mui/material';
import { DataGrid, type GridColDef } from '@mui/x-data-grid'; import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import { import {
@@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useKnowledgeOverview, useKnowledgeDetail } from '@/hooks/knowledge-hooks'; import { useKnowledgeOverview, useKnowledgeDetail } from '@/hooks/knowledge-hooks';
import logger from '@/utils/logger'; import logger from '@/utils/logger';
import i18n from '@/locales';
import { enUS, zhCN } from '@mui/x-data-grid/locales'; import { enUS, zhCN } from '@mui/x-data-grid/locales';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs'; import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import { LanguageAbbreviation } from '@/constants/common'; import { LanguageAbbreviation } from '@/constants/common';
@@ -38,14 +37,14 @@ interface KnowledgeLogsPageProps {
} }
function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPageProps) { function KnowledgeLogsPage({ embedded = false, kbId: kbIdProp }: KnowledgeLogsPageProps) {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 }); const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 50 });
// 根据当前语言获取DataGrid的localeText // 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => { const getDataGridLocale = useCallback(() => {
const currentLanguage = i18n.language; const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS; return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
}; }, [i18n]);
// 路由参数与数据Hook // 路由参数与数据Hook
const { id: kbIdParam = '' } = useParams(); const { id: kbIdParam = '' } = useParams();

View File

@@ -224,7 +224,7 @@ const Login = () => {
fullWidth fullWidth
id="register-confirm-password" id="register-confirm-password"
type={showConfirmPassword ? 'text' : 'password'} type={showConfirmPassword ? 'text' : 'password'}
placeholder={t('confirmPassword')} placeholder={t('login.confirmPassword')}
autoComplete="new-password" autoComplete="new-password"
{...registerForm.register('confirmPassword', registerValidation.confirmPassword)} {...registerForm.register('confirmPassword', registerValidation.confirmPassword)}
error={!!registerForm.formState.errors.confirmPassword} error={!!registerForm.formState.errors.confirmPassword}

View File

@@ -20,7 +20,8 @@ import logger from '@/utils/logger';
interface ChangePasswordDialogProps { interface ChangePasswordDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
changeUserPassword: (data: { password: string; new_password: string }) => Promise<void>; onSuccess: () => void;
changeUserPassword: (data: { password: string; new_password: string }) => Promise<any>;
} }
interface PasswordFormData { interface PasswordFormData {
@@ -32,7 +33,7 @@ interface PasswordFormData {
/** /**
* 修改密码对话框 * 修改密码对话框
*/ */
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) { function ChangePasswordDialog({ open, onClose, onSuccess, changeUserPassword }: ChangePasswordDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { showMessage } = useSnackbar(); const { showMessage } = useSnackbar();
@@ -133,20 +134,19 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
setLoading(true); setLoading(true);
try { try {
await changeUserPassword({ const res = await changeUserPassword({
password: formData.currentPassword, password: formData.currentPassword,
new_password: formData.newPassword new_password: formData.newPassword
}); });
logger.info('修改密码成功:', res);
showMessage.success(t('setting.passwordChangeSuccess')); showMessage.success(t('setting.passwordChangeSuccess'));
handleClose(); handleClose();
// delay 1000 ms
setTimeout(() => onSuccess(), 1000);
} catch (error: any) { } catch (error: any) {
logger.error('修改密码失败:', error); logger.error('修改密码失败:', error);
if (error.response?.status === 400) {
showMessage.error(t('setting.currentPasswordIncorrect'));
} else {
showMessage.error(t('setting.passwordChangeError'));
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -217,7 +217,7 @@ function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePassw
value={formData.newPassword} value={formData.newPassword}
onChange={handleInputChange('newPassword')} onChange={handleInputChange('newPassword')}
error={!!errors.newPassword} error={!!errors.newPassword}
helperText={errors.newPassword || '密码长度至少6位'} helperText={errors.newPassword}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">

View File

@@ -7,11 +7,14 @@ import ChangePasswordDialog from "./components/ChangePasswordDialog";
import { useProfileSetting } from "@/hooks/setting-hooks"; import { useProfileSetting } from "@/hooks/setting-hooks";
import logger from "@/utils/logger"; import logger from "@/utils/logger";
import { useUserData } from "@/hooks/useUserData"; import { useUserData } from "@/hooks/useUserData";
import { useAuth } from '@/hooks/login-hooks';
import { useNavigate } from "react-router-dom";
function ProfileSetting() { function ProfileSetting() {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting(); const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
const { refreshUserData } = useUserData(); const { refreshUserData } = useUserData();
const { logout } = useAuth();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
logger.debug('userInfo', userInfo); logger.debug('userInfo', userInfo);
@@ -66,6 +69,10 @@ function ProfileSetting() {
<ChangePasswordDialog <ChangePasswordDialog
open={passwordDialogOpen} open={passwordDialogOpen}
onClose={handleClosePasswordDialog} onClose={handleClosePasswordDialog}
onSuccess={() => {
// 使用 useAuth 的 logout确保清除本地存储令牌并跳转登录
logout();
}}
changeUserPassword={changeUserPasswordFunc} changeUserPassword={changeUserPasswordFunc}
/> />
</Box> </Box>

View File

@@ -34,6 +34,7 @@ import {
ExitToApp as ExitToAppIcon ExitToApp as ExitToAppIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useTeamSetting } from '@/hooks/setting-hooks'; import { useTeamSetting } from '@/hooks/setting-hooks';
import { useDialog } from '@/hooks/useDialog';
interface TenantUser { interface TenantUser {
id: string; id: string;
@@ -88,8 +89,15 @@ function TeamsSetting() {
} }
}; };
const dialog = useDialog();
const handleDeleteUser = async (userId: string) => { const handleDeleteUser = async (userId: string) => {
await deleteUser(userId); dialog.confirm({
content: t('setting.sureDelete'),
onConfirm: async () => {
await deleteUser(userId);
}
});
}; };
const handleAgreeTenant = async (tenantId: string) => { const handleAgreeTenant = async (tenantId: string) => {
@@ -170,7 +178,7 @@ function TeamsSetting() {
<IconButton <IconButton
size="small" size="small"
color="error" color="error"
onClick={() => handleDeleteUser(user.id)} onClick={() => handleDeleteUser(user.user_id)}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>

View File

@@ -184,17 +184,29 @@ request.interceptors.response.use(
}); });
const { status, data } = error.response || {}; const { status, data } = error.response || {};
if (status == 401) { if (status == 401 || status == 400) {
logger.info('401', data) logger.info('401 || 400', data)
const detail = data['detail'] const detail = data['detail']
if (detail) { if (detail) {
const error = new CustomError(detail || i18n.t('message.requestError')); const error = new CustomError(detail || i18n.t('message.requestError'));
if (detail.includes('not registered')) { if (status == 401) {
const arr = ['not registered', 'Password error', 'Email and password do not match']
const redirectArr = ['Invalid or expired token', 'No Authorization']
if (arr.some(item => detail.includes(item))) {
error.code = data?.code || -1;
error.response = data;
snackbar.error(detail);
} else if (redirectArr.some(item => detail.includes(item))) {
redirectToLogin();
} else {
error.code = data?.code || -1;
error.response = data;
snackbar.error(detail);
}
} else {
error.code = data?.code || -1; error.code = data?.code || -1;
error.response = data; error.response = data;
snackbar.error(detail); snackbar.error(detail);
} else {
redirectToLogin();
} }
return Promise.reject(error); return Promise.reject(error);
} }