feat(settings): add user profile and password management
This commit is contained in:
@@ -10,10 +10,13 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText
|
ListItemText
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import {
|
||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
Settings as SettingsIcon,
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
Search as SearchIcon,
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
AccountCircle as AccountCircleIcon,
|
||||||
|
Logout as LogoutIcon,
|
||||||
|
Person as PersonIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
import LanguageSwitcher from '../LanguageSwitcher';
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
import { useAuth } from '@/hooks/login-hooks';
|
import { useAuth } from '@/hooks/login-hooks';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -185,13 +188,20 @@ const Header = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 菜单项 */}
|
{/* 个人资料 */}
|
||||||
<MenuItem onClick={handleProfileClick} sx={{ py: 1 }}>
|
<MenuItem onClick={handleProfileClick} sx={{ py: 1 }}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PersonIcon fontSize="small" />
|
<PersonIcon fontSize="small" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>个人资料</ListItemText>
|
<ListItemText>个人资料</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{/* 系统设置 */}
|
||||||
|
<MenuItem onClick={() => navigate('/setting/system')} sx={{ py: 1 }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SettingsIcon fontSize="small" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>系统设置</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Box, styled } from "@mui/material";
|
import { Box, styled } from "@mui/material";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
|
import SettingSidebar from './SettingSidebar';
|
||||||
|
import { Home as HomeIcon } from '@mui/icons-material';
|
||||||
|
|
||||||
const LayoutContainer = styled(Box)({
|
const LayoutContainer = styled(Box)({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ContentContainer = styled(Box)({
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
const HeaderContainer = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2, 3),
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MainContent = styled(Box)({
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: '24px',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置页面路径映射
|
||||||
|
const settingPathMap: Record<string, string> = {
|
||||||
|
'/setting/profile': '个人资料',
|
||||||
|
'/setting/models': '模型配置',
|
||||||
|
'/setting/system': '系统设置',
|
||||||
|
'/setting/teams': '团队管理',
|
||||||
|
'/setting/mcp': 'MCP配置',
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingHeader() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 生成面包屑导航
|
||||||
|
const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
label: '首页',
|
||||||
|
path: '/',
|
||||||
|
onClick: () => navigate('/'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果当前路径不是设置首页,添加具体页面
|
||||||
|
if (location.pathname !== '/setting' && settingPathMap[location.pathname]) {
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: settingPathMap[location.pathname],
|
||||||
|
isLast: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeaderContainer>
|
||||||
|
<BaseBreadcrumbs
|
||||||
|
items={breadcrumbItems}
|
||||||
|
sx={{ mb: 0 }}
|
||||||
|
/>
|
||||||
|
</HeaderContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SettingLayout() {
|
function SettingLayout() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
|
<SettingSidebar />
|
||||||
|
<ContentContainer>
|
||||||
|
<SettingHeader />
|
||||||
|
<MainContent>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</MainContent>
|
||||||
|
</ContentContainer>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
136
src/components/Layout/SettingSidebar.tsx
Normal file
136
src/components/Layout/SettingSidebar.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material';
|
||||||
|
import {
|
||||||
|
Person as PersonIcon,
|
||||||
|
Computer as ComputerIcon,
|
||||||
|
Group as GroupIcon,
|
||||||
|
SmartToy as SmartToyIcon,
|
||||||
|
Extension as ExtensionIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface SettingMenuItem {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ComponentType<any>;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingMenuItems: SettingMenuItem[] = [
|
||||||
|
{
|
||||||
|
key: 'profile',
|
||||||
|
label: '个人资料',
|
||||||
|
icon: PersonIcon,
|
||||||
|
path: '/setting/profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'models',
|
||||||
|
label: '模型配置',
|
||||||
|
icon: SmartToyIcon,
|
||||||
|
path: '/setting/models',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system',
|
||||||
|
label: '系统设置',
|
||||||
|
icon: ComputerIcon,
|
||||||
|
path: '/setting/system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'teams',
|
||||||
|
label: '团队管理',
|
||||||
|
icon: GroupIcon,
|
||||||
|
path: '/setting/teams',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mcp',
|
||||||
|
label: 'MCP配置',
|
||||||
|
icon: ExtensionIcon,
|
||||||
|
path: '/setting/mcp',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SettingSidebar: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const handleMenuClick = (path: string) => {
|
||||||
|
navigate(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '240px',
|
||||||
|
backgroundColor: '#1E1E24',
|
||||||
|
color: '#DDD',
|
||||||
|
height: '100vh',
|
||||||
|
padding: '1rem 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.05rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '0 1.25rem 1rem',
|
||||||
|
margin: '0 0 0.5rem',
|
||||||
|
color: '#FFF',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
设置
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{settingMenuItems.map((item) => {
|
||||||
|
const IconComponent = item.icon;
|
||||||
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={item.key}
|
||||||
|
onClick={() => handleMenuClick(item.path)}
|
||||||
|
sx={{
|
||||||
|
color: isActive ? '#FFF' : '#B9B9C2',
|
||||||
|
backgroundColor: isActive ? 'rgba(226,0,116,0.12)' : 'transparent',
|
||||||
|
borderLeft: isActive ? '4px solid' : '4px solid transparent',
|
||||||
|
borderLeftColor: isActive ? 'primary.main' : 'transparent',
|
||||||
|
fontWeight: isActive ? 600 : 'normal',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||||
|
color: '#FFF',
|
||||||
|
},
|
||||||
|
'& .MuiListItemText-primary': {
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
marginRight: '12px',
|
||||||
|
fontSize: '1.2rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
padding: '20px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
opacity: 0.7,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
© 2025 T-Systems
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingSidebar;
|
||||||
0
src/constants/common.ts
Normal file
0
src/constants/common.ts
Normal file
421
src/constants/setting.ts
Normal file
421
src/constants/setting.ts
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
|
||||||
|
export const TimezoneList = [
|
||||||
|
'UTC-11\tPacific/Midway',
|
||||||
|
'UTC-11\tPacific/Niue',
|
||||||
|
'UTC-11\tPacific/Pago_Pago',
|
||||||
|
'UTC-10\tAmerica/Adak',
|
||||||
|
'UTC-10\tPacific/Honolulu',
|
||||||
|
'UTC-10\tPacific/Rarotonga',
|
||||||
|
'UTC-10\tPacific/Tahiti',
|
||||||
|
'UTC-9:30\tPacific/Marquesas',
|
||||||
|
'UTC-9\tAmerica/Anchorage',
|
||||||
|
'UTC-9\tAmerica/Juneau',
|
||||||
|
'UTC-9\tAmerica/Metlakatla',
|
||||||
|
'UTC-9\tAmerica/Nome',
|
||||||
|
'UTC-9\tAmerica/Sitka',
|
||||||
|
'UTC-9\tAmerica/Yakutat',
|
||||||
|
'UTC-9\tPacific/Gambier',
|
||||||
|
'UTC-8\tAmerica/Los_Angeles',
|
||||||
|
'UTC-8\tAmerica/Tijuana',
|
||||||
|
'UTC-8\tAmerica/Vancouver',
|
||||||
|
'UTC-8\tPacific/Pitcairn',
|
||||||
|
'UTC-7\tAmerica/Boise',
|
||||||
|
'UTC-7\tAmerica/Cambridge_Bay',
|
||||||
|
'UTC-7\tAmerica/Ciudad_Juarez',
|
||||||
|
'UTC-7\tAmerica/Creston',
|
||||||
|
'UTC-7\tAmerica/Dawson',
|
||||||
|
'UTC-7\tAmerica/Dawson_Creek',
|
||||||
|
'UTC-7\tAmerica/Denver',
|
||||||
|
'UTC-7\tAmerica/Edmonton',
|
||||||
|
'UTC-7\tAmerica/Fort_Nelson',
|
||||||
|
'UTC-7\tAmerica/Hermosillo',
|
||||||
|
'UTC-7\tAmerica/Inuvik',
|
||||||
|
'UTC-7\tAmerica/Mazatlan',
|
||||||
|
'UTC-7\tAmerica/Phoenix',
|
||||||
|
'UTC-7\tAmerica/Whitehorse',
|
||||||
|
'UTC-7\tAmerica/Yellowknife',
|
||||||
|
'UTC-6\tAmerica/Bahia_Banderas',
|
||||||
|
'UTC-6\tAmerica/Belize',
|
||||||
|
'UTC-6\tAmerica/Chicago',
|
||||||
|
'UTC-6\tAmerica/Chihuahua',
|
||||||
|
'UTC-6\tAmerica/Costa_Rica',
|
||||||
|
'UTC-6\tAmerica/El_Salvador',
|
||||||
|
'UTC-6\tAmerica/Guatemala',
|
||||||
|
'UTC-6\tAmerica/Indiana/Knox',
|
||||||
|
'UTC-6\tAmerica/Indiana/Tell_City',
|
||||||
|
'UTC-6\tAmerica/Managua',
|
||||||
|
'UTC-6\tAmerica/Matamoros',
|
||||||
|
'UTC-6\tAmerica/Menominee',
|
||||||
|
'UTC-6\tAmerica/Merida',
|
||||||
|
'UTC-6\tAmerica/Mexico_City',
|
||||||
|
'UTC-6\tAmerica/Monterrey',
|
||||||
|
'UTC-6\tAmerica/North_Dakota/Beulah',
|
||||||
|
'UTC-6\tAmerica/North_Dakota/Center',
|
||||||
|
'UTC-6\tAmerica/North_Dakota/New_Salem',
|
||||||
|
'UTC-6\tAmerica/Ojinaga',
|
||||||
|
'UTC-6\tAmerica/Rankin_Inlet',
|
||||||
|
'UTC-6\tAmerica/Regina',
|
||||||
|
'UTC-6\tAmerica/Resolute',
|
||||||
|
'UTC-6\tAmerica/Swift_Current',
|
||||||
|
'UTC-6\tAmerica/Tegucigalpa',
|
||||||
|
'UTC-6\tAmerica/Winnipeg',
|
||||||
|
'UTC-6\tPacific/Easter',
|
||||||
|
'UTC-6\tPacific/Galapagos',
|
||||||
|
'UTC-5\tAmerica/Atikokan',
|
||||||
|
'UTC-5\tAmerica/Bogota',
|
||||||
|
'UTC-5\tAmerica/Cancun',
|
||||||
|
'UTC-5\tAmerica/Cayman',
|
||||||
|
'UTC-5\tAmerica/Detroit',
|
||||||
|
'UTC-5\tAmerica/Eirunepe',
|
||||||
|
'UTC-5\tAmerica/Grand_Turk',
|
||||||
|
'UTC-5\tAmerica/Guayaquil',
|
||||||
|
'UTC-5\tAmerica/Havana',
|
||||||
|
'UTC-5\tAmerica/Indiana/Indianapolis',
|
||||||
|
'UTC-5\tAmerica/Indiana/Marengo',
|
||||||
|
'UTC-5\tAmerica/Indiana/Petersburg',
|
||||||
|
'UTC-5\tAmerica/Indiana/Vevay',
|
||||||
|
'UTC-5\tAmerica/Indiana/Vincennes',
|
||||||
|
'UTC-5\tAmerica/Indiana/Winamac',
|
||||||
|
'UTC-5\tAmerica/Iqaluit',
|
||||||
|
'UTC-5\tAmerica/Jamaica',
|
||||||
|
'UTC-5\tAmerica/Kentucky/Louisville',
|
||||||
|
'UTC-5\tAmerica/Kentucky/Monticello',
|
||||||
|
'UTC-5\tAmerica/Lima',
|
||||||
|
'UTC-5\tAmerica/Nassau',
|
||||||
|
'UTC-5\tAmerica/New_York',
|
||||||
|
'UTC-5\tAmerica/Panama',
|
||||||
|
'UTC-5\tAmerica/Port-au-Prince',
|
||||||
|
'UTC-5\tAmerica/Rio_Branco',
|
||||||
|
'UTC-5\tAmerica/Toronto',
|
||||||
|
'UTC-4\tAmerica/Anguilla',
|
||||||
|
'UTC-4\tAmerica/Antigua',
|
||||||
|
'UTC-4\tAmerica/Aruba',
|
||||||
|
'UTC-4\tAmerica/Asuncion',
|
||||||
|
'UTC-4\tAmerica/Barbados',
|
||||||
|
'UTC-4\tAmerica/Blanc-Sablon',
|
||||||
|
'UTC-4\tAmerica/Boa_Vista',
|
||||||
|
'UTC-4\tAmerica/Campo_Grande',
|
||||||
|
'UTC-4\tAmerica/Caracas',
|
||||||
|
'UTC-4\tAmerica/Cuiaba',
|
||||||
|
'UTC-4\tAmerica/Curacao',
|
||||||
|
'UTC-4\tAmerica/Dominica',
|
||||||
|
'UTC-4\tAmerica/Glace_Bay',
|
||||||
|
'UTC-4\tAmerica/Goose_Bay',
|
||||||
|
'UTC-4\tAmerica/Grenada',
|
||||||
|
'UTC-4\tAmerica/Guadeloupe',
|
||||||
|
'UTC-4\tAmerica/Guyana',
|
||||||
|
'UTC-4\tAmerica/Halifax',
|
||||||
|
'UTC-4\tAmerica/Kralendijk',
|
||||||
|
'UTC-4\tAmerica/La_Paz',
|
||||||
|
'UTC-4\tAmerica/Lower_Princes',
|
||||||
|
'UTC-4\tAmerica/Manaus',
|
||||||
|
'UTC-4\tAmerica/Marigot',
|
||||||
|
'UTC-4\tAmerica/Martinique',
|
||||||
|
'UTC-4\tAmerica/Moncton',
|
||||||
|
'UTC-4\tAmerica/Montserrat',
|
||||||
|
'UTC-4\tAmerica/Porto_Velho',
|
||||||
|
'UTC-4\tAmerica/Port_of_Spain',
|
||||||
|
'UTC-4\tAmerica/Puerto_Rico',
|
||||||
|
'UTC-4\tAmerica/Santiago',
|
||||||
|
'UTC-4\tAmerica/Santo_Domingo',
|
||||||
|
'UTC-4\tAmerica/St_Barthelemy',
|
||||||
|
'UTC-4\tAmerica/St_Kitts',
|
||||||
|
'UTC-4\tAmerica/St_Lucia',
|
||||||
|
'UTC-4\tAmerica/St_Thomas',
|
||||||
|
'UTC-4\tAmerica/St_Vincent',
|
||||||
|
'UTC-4\tAmerica/Thule',
|
||||||
|
'UTC-4\tAmerica/Tortola',
|
||||||
|
'UTC-4\tAtlantic/Bermuda',
|
||||||
|
'UTC-3:30\tAmerica/St_Johns',
|
||||||
|
'UTC-3\tAmerica/Araguaina',
|
||||||
|
'UTC-3\tAmerica/Argentina/Buenos_Aires',
|
||||||
|
'UTC-3\tAmerica/Argentina/Catamarca',
|
||||||
|
'UTC-3\tAmerica/Argentina/Cordoba',
|
||||||
|
'UTC-3\tAmerica/Argentina/Jujuy',
|
||||||
|
'UTC-3\tAmerica/Argentina/La_Rioja',
|
||||||
|
'UTC-3\tAmerica/Argentina/Mendoza',
|
||||||
|
'UTC-3\tAmerica/Argentina/Rio_Gallegos',
|
||||||
|
'UTC-3\tAmerica/Argentina/Salta',
|
||||||
|
'UTC-3\tAmerica/Argentina/San_Juan',
|
||||||
|
'UTC-3\tAmerica/Argentina/San_Luis',
|
||||||
|
'UTC-3\tAmerica/Argentina/Tucuman',
|
||||||
|
'UTC-3\tAmerica/Argentina/Ushuaia',
|
||||||
|
'UTC-3\tAmerica/Bahia',
|
||||||
|
'UTC-3\tAmerica/Belem',
|
||||||
|
'UTC-3\tAmerica/Cayenne',
|
||||||
|
'UTC-3\tAmerica/Fortaleza',
|
||||||
|
'UTC-3\tAmerica/Maceio',
|
||||||
|
'UTC-3\tAmerica/Miquelon',
|
||||||
|
'UTC-3\tAmerica/Montevideo',
|
||||||
|
'UTC-3\tAmerica/Paramaribo',
|
||||||
|
'UTC-3\tAmerica/Punta_Arenas',
|
||||||
|
'UTC-3\tAmerica/Recife',
|
||||||
|
'UTC-3\tAmerica/Santarem',
|
||||||
|
'UTC-3\tAmerica/Sao_Paulo',
|
||||||
|
'UTC-3\tAntarctica/Palmer',
|
||||||
|
'UTC-3\tAntarctica/Rothera',
|
||||||
|
'UTC-3\tAtlantic/Stanley',
|
||||||
|
'UTC-2\tAmerica/Noronha',
|
||||||
|
'UTC-2\tAmerica/Nuuk',
|
||||||
|
'UTC-2\tAtlantic/South_Georgia',
|
||||||
|
'UTC-1\tAmerica/Scoresbysund',
|
||||||
|
'UTC-1\tAtlantic/Azores',
|
||||||
|
'UTC-1\tAtlantic/Cape_Verde',
|
||||||
|
'UTC+0\tAfrica/Abidjan',
|
||||||
|
'UTC+0\tAfrica/Accra',
|
||||||
|
'UTC+0\tAfrica/Bamako',
|
||||||
|
'UTC+0\tAfrica/Banjul',
|
||||||
|
'UTC+0\tAfrica/Bissau',
|
||||||
|
'UTC+0\tAfrica/Casablanca',
|
||||||
|
'UTC+0\tAfrica/Conakry',
|
||||||
|
'UTC+0\tAfrica/Dakar',
|
||||||
|
'UTC+0\tAfrica/El_Aaiun',
|
||||||
|
'UTC+0\tAfrica/Freetown',
|
||||||
|
'UTC+0\tAfrica/Lome',
|
||||||
|
'UTC+0\tAfrica/Monrovia',
|
||||||
|
'UTC+0\tAfrica/Nouakchott',
|
||||||
|
'UTC+0\tAfrica/Ouagadougou',
|
||||||
|
'UTC+0\tAfrica/Sao_Tome',
|
||||||
|
'UTC+0\tAmerica/Danmarkshavn',
|
||||||
|
'UTC+0\tAntarctica/Troll',
|
||||||
|
'UTC+0\tAtlantic/Canary',
|
||||||
|
'UTC+0\tAtlantic/Faroe',
|
||||||
|
'UTC+0\tAtlantic/Madeira',
|
||||||
|
'UTC+0\tAtlantic/Reykjavik',
|
||||||
|
'UTC+0\tAtlantic/St_Helena',
|
||||||
|
'UTC+0\tEurope/Dublin',
|
||||||
|
'UTC+0\tEurope/Guernsey',
|
||||||
|
'UTC+0\tEurope/Isle_of_Man',
|
||||||
|
'UTC+0\tEurope/Jersey',
|
||||||
|
'UTC+0\tEurope/Lisbon',
|
||||||
|
'UTC+0\tEurope/London',
|
||||||
|
'UTC+1\tAfrica/Algiers',
|
||||||
|
'UTC+1\tAfrica/Bangui',
|
||||||
|
'UTC+1\tAfrica/Brazzaville',
|
||||||
|
'UTC+1\tAfrica/Ceuta',
|
||||||
|
'UTC+1\tAfrica/Douala',
|
||||||
|
'UTC+1\tAfrica/Kinshasa',
|
||||||
|
'UTC+1\tAfrica/Lagos',
|
||||||
|
'UTC+1\tAfrica/Libreville',
|
||||||
|
'UTC+1\tAfrica/Luanda',
|
||||||
|
'UTC+1\tAfrica/Malabo',
|
||||||
|
'UTC+1\tAfrica/Ndjamena',
|
||||||
|
'UTC+1\tAfrica/Niamey',
|
||||||
|
'UTC+1\tAfrica/Porto-Novo',
|
||||||
|
'UTC+1\tAfrica/Tunis',
|
||||||
|
'UTC+1\tAfrica/Windhoek',
|
||||||
|
'UTC+1\tArctic/Longyearbyen',
|
||||||
|
'UTC+1\tEurope/Amsterdam',
|
||||||
|
'UTC+1\tEurope/Andorra',
|
||||||
|
'UTC+1\tEurope/Belgrade',
|
||||||
|
'UTC+1\tEurope/Berlin',
|
||||||
|
'UTC+1\tEurope/Bratislava',
|
||||||
|
'UTC+1\tEurope/Brussels',
|
||||||
|
'UTC+1\tEurope/Budapest',
|
||||||
|
'UTC+1\tEurope/Copenhagen',
|
||||||
|
'UTC+1\tEurope/Gibraltar',
|
||||||
|
'UTC+1\tEurope/Ljubljana',
|
||||||
|
'UTC+1\tEurope/Luxembourg',
|
||||||
|
'UTC+1\tEurope/Madrid',
|
||||||
|
'UTC+1\tEurope/Malta',
|
||||||
|
'UTC+1\tEurope/Monaco',
|
||||||
|
'UTC+1\tEurope/Oslo',
|
||||||
|
'UTC+1\tEurope/Paris',
|
||||||
|
'UTC+1\tEurope/Podgorica',
|
||||||
|
'UTC+1\tEurope/Prague',
|
||||||
|
'UTC+1\tEurope/Rome',
|
||||||
|
'UTC+1\tEurope/San_Marino',
|
||||||
|
'UTC+1\tEurope/Sarajevo',
|
||||||
|
'UTC+1\tEurope/Skopje',
|
||||||
|
'UTC+1\tEurope/Stockholm',
|
||||||
|
'UTC+1\tEurope/Tirane',
|
||||||
|
'UTC+1\tEurope/Vaduz',
|
||||||
|
'UTC+1\tEurope/Vatican',
|
||||||
|
'UTC+1\tEurope/Vienna',
|
||||||
|
'UTC+1\tEurope/Warsaw',
|
||||||
|
'UTC+1\tEurope/Zagreb',
|
||||||
|
'UTC+1\tEurope/Zurich',
|
||||||
|
'UTC+2\tAfrica/Blantyre',
|
||||||
|
'UTC+2\tAfrica/Bujumbura',
|
||||||
|
'UTC+2\tAfrica/Cairo',
|
||||||
|
'UTC+2\tAfrica/Gaborone',
|
||||||
|
'UTC+2\tAfrica/Harare',
|
||||||
|
'UTC+2\tAfrica/Johannesburg',
|
||||||
|
'UTC+2\tAfrica/Juba',
|
||||||
|
'UTC+2\tAfrica/Khartoum',
|
||||||
|
'UTC+2\tAfrica/Kigali',
|
||||||
|
'UTC+2\tAfrica/Lubumbashi',
|
||||||
|
'UTC+2\tAfrica/Lusaka',
|
||||||
|
'UTC+2\tAfrica/Maputo',
|
||||||
|
'UTC+2\tAfrica/Maseru',
|
||||||
|
'UTC+2\tAfrica/Mbabane',
|
||||||
|
'UTC+2\tAfrica/Tripoli',
|
||||||
|
'UTC+2\tAsia/Beirut',
|
||||||
|
'UTC+2\tAsia/Famagusta',
|
||||||
|
'UTC+2\tAsia/Gaza',
|
||||||
|
'UTC+2\tAsia/Hebron',
|
||||||
|
'UTC+2\tAsia/Jerusalem',
|
||||||
|
'UTC+2\tAsia/Nicosia',
|
||||||
|
'UTC+2\tEurope/Athens',
|
||||||
|
'UTC+2\tEurope/Bucharest',
|
||||||
|
'UTC+2\tEurope/Chisinau',
|
||||||
|
'UTC+2\tEurope/Helsinki',
|
||||||
|
'UTC+2\tEurope/Kaliningrad',
|
||||||
|
'UTC+2\tEurope/Kyiv',
|
||||||
|
'UTC+2\tEurope/Mariehamn',
|
||||||
|
'UTC+2\tEurope/Riga',
|
||||||
|
'UTC+2\tEurope/Sofia',
|
||||||
|
'UTC+2\tEurope/Tallinn',
|
||||||
|
'UTC+2\tEurope/Vilnius',
|
||||||
|
'UTC+3\tAfrica/Addis_Ababa',
|
||||||
|
'UTC+3\tAfrica/Asmara',
|
||||||
|
'UTC+3\tAfrica/Dar_es_Salaam',
|
||||||
|
'UTC+3\tAfrica/Djibouti',
|
||||||
|
'UTC+3\tAfrica/Kampala',
|
||||||
|
'UTC+3\tAfrica/Mogadishu',
|
||||||
|
'UTC+3\tAfrica/Nairobi',
|
||||||
|
'UTC+3\tAntarctica/Syowa',
|
||||||
|
'UTC+3\tAsia/Aden',
|
||||||
|
'UTC+3\tAsia/Amman',
|
||||||
|
'UTC+3\tAsia/Baghdad',
|
||||||
|
'UTC+3\tAsia/Bahrain',
|
||||||
|
'UTC+3\tAsia/Damascus',
|
||||||
|
'UTC+3\tAsia/Kuwait',
|
||||||
|
'UTC+3\tAsia/Qatar',
|
||||||
|
'UTC+3\tAsia/Riyadh',
|
||||||
|
'UTC+3\tEurope/Istanbul',
|
||||||
|
'UTC+3\tEurope/Kirov',
|
||||||
|
'UTC+3\tEurope/Minsk',
|
||||||
|
'UTC+3\tEurope/Moscow',
|
||||||
|
'UTC+3\tEurope/Simferopol',
|
||||||
|
'UTC+3\tEurope/Volgograd',
|
||||||
|
'UTC+3\tIndian/Antananarivo',
|
||||||
|
'UTC+3\tIndian/Comoro',
|
||||||
|
'UTC+3\tIndian/Mayotte',
|
||||||
|
'UTC+3:30\tAsia/Tehran',
|
||||||
|
'UTC+4\tAsia/Baku',
|
||||||
|
'UTC+4\tAsia/Dubai',
|
||||||
|
'UTC+4\tAsia/Muscat',
|
||||||
|
'UTC+4\tAsia/Tbilisi',
|
||||||
|
'UTC+4\tAsia/Yerevan',
|
||||||
|
'UTC+4\tEurope/Astrakhan',
|
||||||
|
'UTC+4\tEurope/Samara',
|
||||||
|
'UTC+4\tEurope/Saratov',
|
||||||
|
'UTC+4\tEurope/Ulyanovsk',
|
||||||
|
'UTC+4\tIndian/Mahe',
|
||||||
|
'UTC+4\tIndian/Mauritius',
|
||||||
|
'UTC+4\tIndian/Reunion',
|
||||||
|
'UTC+4:30\tAsia/Kabul',
|
||||||
|
'UTC+5\tAntarctica/Mawson',
|
||||||
|
'UTC+5\tAsia/Aqtau',
|
||||||
|
'UTC+5\tAsia/Aqtobe',
|
||||||
|
'UTC+5\tAsia/Ashgabat',
|
||||||
|
'UTC+5\tAsia/Atyrau',
|
||||||
|
'UTC+5\tAsia/Dushanbe',
|
||||||
|
'UTC+5\tAsia/Karachi',
|
||||||
|
'UTC+5\tAsia/Oral',
|
||||||
|
'UTC+5\tAsia/Qyzylorda',
|
||||||
|
'UTC+5\tAsia/Samarkand',
|
||||||
|
'UTC+5\tAsia/Tashkent',
|
||||||
|
'UTC+5\tAsia/Yekaterinburg',
|
||||||
|
'UTC+5\tIndian/Kerguelen',
|
||||||
|
'UTC+5\tIndian/Maldives',
|
||||||
|
'UTC+5:30\tAsia/Colombo',
|
||||||
|
'UTC+5:30\tAsia/Kolkata',
|
||||||
|
'UTC+5:45\tAsia/Kathmandu',
|
||||||
|
'UTC+6\tAntarctica/Vostok',
|
||||||
|
'UTC+6\tAsia/Almaty',
|
||||||
|
'UTC+6\tAsia/Bishkek',
|
||||||
|
'UTC+6\tAsia/Dhaka',
|
||||||
|
'UTC+6\tAsia/Omsk',
|
||||||
|
'UTC+6\tAsia/Qostanay',
|
||||||
|
'UTC+6\tAsia/Thimphu',
|
||||||
|
'UTC+6\tAsia/Urumqi',
|
||||||
|
'UTC+6\tIndian/Chagos',
|
||||||
|
'UTC+6:30\tAsia/Yangon',
|
||||||
|
'UTC+6:30\tIndian/Cocos',
|
||||||
|
'UTC+7\tAntarctica/Davis',
|
||||||
|
'UTC+7\tAsia/Bangkok',
|
||||||
|
'UTC+7\tAsia/Barnaul',
|
||||||
|
'UTC+7\tAsia/Hovd',
|
||||||
|
'UTC+7\tAsia/Ho_Chi_Minh',
|
||||||
|
'UTC+7\tAsia/Jakarta',
|
||||||
|
'UTC+7\tAsia/Krasnoyarsk',
|
||||||
|
'UTC+7\tAsia/Novokuznetsk',
|
||||||
|
'UTC+7\tAsia/Novosibirsk',
|
||||||
|
'UTC+7\tAsia/Phnom_Penh',
|
||||||
|
'UTC+7\tAsia/Pontianak',
|
||||||
|
'UTC+7\tAsia/Tomsk',
|
||||||
|
'UTC+7\tAsia/Vientiane',
|
||||||
|
'UTC+7\tIndian/Christmas',
|
||||||
|
'UTC+8\tAsia/Brunei',
|
||||||
|
'UTC+8\tAsia/Choibalsan',
|
||||||
|
'UTC+8\tAsia/Hong_Kong',
|
||||||
|
'UTC+8\tAsia/Irkutsk',
|
||||||
|
'UTC+8\tAsia/Kuala_Lumpur',
|
||||||
|
'UTC+8\tAsia/Kuching',
|
||||||
|
'UTC+8\tAsia/Macau',
|
||||||
|
'UTC+8\tAsia/Makassar',
|
||||||
|
'UTC+8\tAsia/Manila',
|
||||||
|
'UTC+8\tAsia/Shanghai',
|
||||||
|
'UTC+8\tAsia/Singapore',
|
||||||
|
'UTC+8\tAsia/Taipei',
|
||||||
|
'UTC+8\tAsia/Ulaanbaatar',
|
||||||
|
'UTC+8\tAustralia/Perth',
|
||||||
|
'UTC+8:45\tAustralia/Eucla',
|
||||||
|
'UTC+9\tAsia/Chita',
|
||||||
|
'UTC+9\tAsia/Dili',
|
||||||
|
'UTC+9\tAsia/Jayapura',
|
||||||
|
'UTC+9\tAsia/Khandyga',
|
||||||
|
'UTC+9\tAsia/Pyongyang',
|
||||||
|
'UTC+9\tAsia/Seoul',
|
||||||
|
'UTC+9\tAsia/Tokyo',
|
||||||
|
'UTC+9\tAsia/Yakutsk',
|
||||||
|
'UTC+9\tPacific/Palau',
|
||||||
|
'UTC+9:30\tAustralia/Adelaide',
|
||||||
|
'UTC+9:30\tAustralia/Broken_Hill',
|
||||||
|
'UTC+9:30\tAustralia/Darwin',
|
||||||
|
'UTC+10\tAntarctica/DumontDUrville',
|
||||||
|
'UTC+10\tAntarctica/Macquarie',
|
||||||
|
'UTC+10\tAsia/Ust-Nera',
|
||||||
|
'UTC+10\tAsia/Vladivostok',
|
||||||
|
'UTC+10\tAustralia/Brisbane',
|
||||||
|
'UTC+10\tAustralia/Hobart',
|
||||||
|
'UTC+10\tAustralia/Lindeman',
|
||||||
|
'UTC+10\tAustralia/Melbourne',
|
||||||
|
'UTC+10\tAustralia/Sydney',
|
||||||
|
'UTC+10\tPacific/Chuuk',
|
||||||
|
'UTC+10\tPacific/Guam',
|
||||||
|
'UTC+10\tPacific/Port_Moresby',
|
||||||
|
'UTC+10\tPacific/Saipan',
|
||||||
|
'UTC+10:30\tAustralia/Lord_Howe',
|
||||||
|
'UTC+11\tAntarctica/Casey',
|
||||||
|
'UTC+11\tAsia/Magadan',
|
||||||
|
'UTC+11\tAsia/Sakhalin',
|
||||||
|
'UTC+11\tAsia/Srednekolymsk',
|
||||||
|
'UTC+11\tPacific/Bougainville',
|
||||||
|
'UTC+11\tPacific/Efate',
|
||||||
|
'UTC+11\tPacific/Guadalcanal',
|
||||||
|
'UTC+11\tPacific/Kosrae',
|
||||||
|
'UTC+11\tPacific/Norfolk',
|
||||||
|
'UTC+11\tPacific/Noumea',
|
||||||
|
'UTC+11\tPacific/Pohnpei',
|
||||||
|
'UTC+12\tAntarctica/McMurdo',
|
||||||
|
'UTC+12\tAsia/Anadyr',
|
||||||
|
'UTC+12\tAsia/Kamchatka',
|
||||||
|
'UTC+12\tPacific/Auckland',
|
||||||
|
'UTC+12\tPacific/Fiji',
|
||||||
|
'UTC+12\tPacific/Funafuti',
|
||||||
|
'UTC+12\tPacific/Kwajalein',
|
||||||
|
'UTC+12\tPacific/Majuro',
|
||||||
|
'UTC+12\tPacific/Nauru',
|
||||||
|
'UTC+12\tPacific/Tarawa',
|
||||||
|
'UTC+12\tPacific/Wake',
|
||||||
|
'UTC+12\tPacific/Wallis',
|
||||||
|
'UTC+12:45\tPacific/Chatham',
|
||||||
|
'UTC+13\tPacific/Apia',
|
||||||
|
'UTC+13\tPacific/Fakaofo',
|
||||||
|
'UTC+13\tPacific/Kanton',
|
||||||
|
'UTC+13\tPacific/Tongatapu',
|
||||||
|
'UTC+14\tPacific/Kiritimati',
|
||||||
|
];
|
||||||
42
src/hooks/setting-hooks.ts
Normal file
42
src/hooks/setting-hooks.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useUserData } from "./useUserData";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import logger from "@/utils/logger";
|
||||||
|
import type { IUserInfo } from "@/interfaces/database/user-setting";
|
||||||
|
import userService from "@/services/user_service";
|
||||||
|
import { rsaPsw } from "../utils/encryption";
|
||||||
|
|
||||||
|
export function useProfileSetting() {
|
||||||
|
const {fetchUserInfo, userInfo} = useUserData();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserInfo();
|
||||||
|
}, [fetchUserInfo]);
|
||||||
|
|
||||||
|
const updateUserInfo = async (newUserInfo: Partial<IUserInfo>) => {
|
||||||
|
try {
|
||||||
|
await userService.updateSetting(newUserInfo);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('更新用户信息失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeUserPassword = async (data: { password: string; new_password: string }) => {
|
||||||
|
try {
|
||||||
|
const newPassword = rsaPsw(data.new_password);
|
||||||
|
const oldPassword = rsaPsw(data.password);
|
||||||
|
const res = await userService.updatePassword({
|
||||||
|
password: oldPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
userInfo,
|
||||||
|
updateUserInfo,
|
||||||
|
changeUserPassword,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import { useSnackbar } from '@/components/Provider/SnackbarProvider';
|
import { useSnackbar as useSnackbarProvider } from '@/components/Provider/SnackbarProvider';
|
||||||
|
|
||||||
// 简化的 hooks
|
// 简化的 hooks
|
||||||
export const useMessage = () => {
|
export const useMessage = () => {
|
||||||
const { showMessage } = useSnackbar();
|
const { showMessage } = useSnackbarProvider();
|
||||||
return showMessage;
|
return showMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useNotification = () => {
|
export const useNotification = () => {
|
||||||
const { showNotification } = useSnackbar();
|
const { showNotification } = useSnackbarProvider();
|
||||||
return showNotification;
|
return showNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSnackbar = () => {
|
||||||
|
const { showMessage, showNotification } = useSnackbarProvider();
|
||||||
|
return { showMessage, showNotification };
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import { useUserStore } from '@/stores/userStore';
|
||||||
import userService from '@/services/user_service';
|
import userService from '@/services/user_service';
|
||||||
|
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户数据管理Hook
|
* 用户数据管理Hook
|
||||||
@@ -28,8 +29,9 @@ export const useUserData = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await userService.getUserInfo();
|
const response = await userService.getUserInfo();
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
setUserInfo(response.data.data);
|
const userInfoData: IUserInfo | null = response.data.data || null;
|
||||||
return response.data.data;
|
setUserInfo(userInfoData);
|
||||||
|
return userInfoData;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || '获取用户信息失败');
|
throw new Error(response.data.message || '获取用户信息失败');
|
||||||
}
|
}
|
||||||
|
|||||||
272
src/pages/setting/components/ChangePasswordDialog.tsx
Normal file
272
src/pages/setting/components/ChangePasswordDialog.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
Typography,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Visibility, VisibilityOff, Close } from '@mui/icons-material';
|
||||||
|
import { useSnackbar } from '@/hooks/useSnackbar';
|
||||||
|
import logger from '@/utils/logger';
|
||||||
|
|
||||||
|
interface ChangePasswordDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
changeUserPassword: (data: { password: string; new_password: string }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordFormData {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码对话框
|
||||||
|
*/
|
||||||
|
function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) {
|
||||||
|
const { showMessage } = useSnackbar();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<PasswordFormData>({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showPasswords, setShowPasswords] = useState({
|
||||||
|
current: false,
|
||||||
|
new: false,
|
||||||
|
confirm: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Partial<PasswordFormData>>({});
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
setShowPasswords({
|
||||||
|
current: false,
|
||||||
|
new: false,
|
||||||
|
confirm: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理输入变化
|
||||||
|
const handleInputChange = (field: keyof PasswordFormData) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 清除对应字段的错误
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换密码可见性
|
||||||
|
const togglePasswordVisibility = (field: 'current' | 'new' | 'confirm') => {
|
||||||
|
setShowPasswords(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: !prev[field]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证表单
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Partial<PasswordFormData> = {};
|
||||||
|
|
||||||
|
if (!formData.currentPassword.trim()) {
|
||||||
|
newErrors.currentPassword = '请输入当前密码';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.newPassword.trim()) {
|
||||||
|
newErrors.newPassword = '请输入新密码';
|
||||||
|
} else if (formData.newPassword.length < 6) {
|
||||||
|
newErrors.newPassword = '新密码长度至少6位';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.confirmPassword.trim()) {
|
||||||
|
newErrors.confirmPassword = '请确认新密码';
|
||||||
|
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = '两次输入的密码不一致';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.currentPassword === formData.newPassword) {
|
||||||
|
newErrors.newPassword = '新密码不能与当前密码相同';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交修改
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await changeUserPassword({
|
||||||
|
password: formData.currentPassword,
|
||||||
|
new_password: formData.newPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
showMessage.success('密码修改成功');
|
||||||
|
handleClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('修改密码失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: { borderRadius: 2 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="h6" component="div">
|
||||||
|
修改密码
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClose}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
||||||
|
<Alert severity="info" sx={{ mb: 1 }}>
|
||||||
|
为了您的账户安全,请设置一个强密码。密码长度至少6位,建议包含字母、数字和特殊字符。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* 当前密码 */}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="当前密码"
|
||||||
|
type={showPasswords.current ? 'text' : 'password'}
|
||||||
|
value={formData.currentPassword}
|
||||||
|
onChange={handleInputChange('currentPassword')}
|
||||||
|
error={!!errors.currentPassword}
|
||||||
|
helperText={errors.currentPassword}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => togglePasswordVisibility('current')}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{showPasswords.current ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 新密码 */}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="新密码"
|
||||||
|
type={showPasswords.new ? 'text' : 'password'}
|
||||||
|
value={formData.newPassword}
|
||||||
|
onChange={handleInputChange('newPassword')}
|
||||||
|
error={!!errors.newPassword}
|
||||||
|
helperText={errors.newPassword || '密码长度至少6位'}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => togglePasswordVisibility('new')}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{showPasswords.new ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 确认新密码 */}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="确认新密码"
|
||||||
|
type={showPasswords.confirm ? 'text' : 'password'}
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange('confirmPassword')}
|
||||||
|
error={!!errors.confirmPassword}
|
||||||
|
helperText={errors.confirmPassword}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => togglePasswordVisibility('confirm')}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{showPasswords.confirm ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: 3, gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outlined"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
variant="contained"
|
||||||
|
disabled={loading}
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
>
|
||||||
|
{loading ? '修改中...' : '确认修改'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangePasswordDialog;
|
||||||
278
src/pages/setting/components/ProfileForm.tsx
Normal file
278
src/pages/setting/components/ProfileForm.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Avatar,
|
||||||
|
Typography,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material';
|
||||||
|
import { PhotoCamera, Edit } from '@mui/icons-material';
|
||||||
|
import { useProfileSetting } from '@/hooks/setting-hooks';
|
||||||
|
import { useMessage } from '@/hooks/useSnackbar';
|
||||||
|
import type { IUserInfo } from '@/interfaces/database/user-setting';
|
||||||
|
import { TimezoneList } from '@/constants/setting';
|
||||||
|
|
||||||
|
// 语言选项
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'Chinese', label: '简体中文' },
|
||||||
|
{ value: 'English', label: 'English' },
|
||||||
|
{ value: 'Spanish', label: 'Español' },
|
||||||
|
{ value: 'French', label: 'Français' },
|
||||||
|
{ value: 'German', label: 'Deutsch' },
|
||||||
|
{ value: 'Japanese', label: '日本語' },
|
||||||
|
{ value: 'Korean', label: '한국어' },
|
||||||
|
{ value: 'Vietnamese', label: 'Tiếng Việt' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 时区选项
|
||||||
|
const timezoneOptions = TimezoneList.map(x => ({ value: x, label: x }));
|
||||||
|
|
||||||
|
interface ProfileFormProps {
|
||||||
|
userInfo: IUserInfo | null;
|
||||||
|
onSubmit: (data: Partial<IUserInfo>) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人信息表单
|
||||||
|
*/
|
||||||
|
function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) {
|
||||||
|
const showMessage = useMessage();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<Partial<IUserInfo>>({
|
||||||
|
nickname: userInfo?.nickname || '',
|
||||||
|
avatar: userInfo?.avatar || null,
|
||||||
|
language: userInfo?.language || 'Chinese',
|
||||||
|
timezone: userInfo?.timezone || 'UTC+8\tAsia/Shanghai',
|
||||||
|
email: userInfo?.email || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新表单数据
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (userInfo) {
|
||||||
|
setFormData({
|
||||||
|
nickname: userInfo.nickname || '',
|
||||||
|
avatar: userInfo.avatar || null,
|
||||||
|
language: userInfo.language || 'Chinese',
|
||||||
|
timezone: userInfo.timezone || 'UTC+8\tAsia/Shanghai',
|
||||||
|
email: userInfo.email || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userInfo]);
|
||||||
|
|
||||||
|
// 处理输入变化
|
||||||
|
const handleInputChange = (field: keyof IUserInfo) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: event.target.value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理选择变化
|
||||||
|
const handleSelectChange = (field: keyof IUserInfo) => (event: any) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: event.target.value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理头像上传
|
||||||
|
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// 检查文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
showMessage.error('请选择图片文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小 (限制为2MB)
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
showMessage.error('图片大小不能超过2MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为base64
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64String = e.target?.result as string;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
avatar: base64String
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 触发文件选择
|
||||||
|
const triggerFileSelect = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (!formData.nickname?.trim()) {
|
||||||
|
showMessage.error('用户名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: Partial<IUserInfo> = {
|
||||||
|
nickname: formData.nickname,
|
||||||
|
avatar: formData.avatar,
|
||||||
|
language: formData.language,
|
||||||
|
timezone: formData.timezone,
|
||||||
|
email: formData.email
|
||||||
|
};
|
||||||
|
|
||||||
|
await onSubmit(updateData);
|
||||||
|
showMessage.success('个人信息更新成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户信息失败:', error);
|
||||||
|
showMessage.error('更新失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0} sx={{ p: 3, backgroundColor: 'transparent' }}>
|
||||||
|
<Typography variant="h6" gutterBottom sx={{ mb: 3, fontWeight: 600 }}>
|
||||||
|
个人资料
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* 头像部分 */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Avatar
|
||||||
|
src={formData.avatar}
|
||||||
|
sx={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'divider'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formData.nickname?.charAt(0)?.toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
头像
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title="上传头像">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={triggerFileSelect}
|
||||||
|
sx={{
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'primary.main',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PhotoCamera />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleAvatarUpload}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
支持 JPG、PNG 格式,文件大小不超过 2MB
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 用户名 */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="用户名"
|
||||||
|
value={formData.nickname}
|
||||||
|
onChange={handleInputChange('nickname')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 邮箱 (只读) */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="邮箱"
|
||||||
|
value={formData.email}
|
||||||
|
variant="outlined"
|
||||||
|
disabled
|
||||||
|
helperText="邮箱地址不可修改"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 语言 */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>语言</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.language}
|
||||||
|
label="语言"
|
||||||
|
onChange={handleSelectChange('language')}
|
||||||
|
>
|
||||||
|
{languageOptions.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 时区 */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>时区</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.timezone}
|
||||||
|
label="时区"
|
||||||
|
onChange={handleSelectChange('timezone')}
|
||||||
|
>
|
||||||
|
{timezoneOptions.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
sx={{ minWidth: 120 }}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileForm;
|
||||||
@@ -1,8 +1,67 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Box, Button, Divider, Typography } from "@mui/material";
|
||||||
|
import { Lock } from "@mui/icons-material";
|
||||||
|
import ProfileForm from "./components/ProfileForm";
|
||||||
|
import ChangePasswordDialog from "./components/ChangePasswordDialog";
|
||||||
|
import { useProfileSetting } from "@/hooks/setting-hooks";
|
||||||
|
import logger from "@/utils/logger";
|
||||||
|
|
||||||
function ProfileSetting() {
|
function ProfileSetting() {
|
||||||
|
const { userInfo, updateUserInfo: updateUserInfoFunc, changeUserPassword: changeUserPasswordFunc } = useProfileSetting();
|
||||||
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
logger.debug('userInfo', userInfo);
|
||||||
|
|
||||||
|
const handleOpenPasswordDialog = () => {
|
||||||
|
setPasswordDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosePasswordDialog = () => {
|
||||||
|
setPasswordDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box sx={{ maxWidth: 800, mx: 'auto', p: 3 }}>
|
||||||
<h1>Profile Setting</h1>
|
{/* 个人资料表单 */}
|
||||||
</div>
|
<ProfileForm userInfo={userInfo} onSubmit={updateUserInfoFunc} />
|
||||||
|
|
||||||
|
{/* 分割线 */}
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
|
||||||
|
{/* 密码修改部分 */}
|
||||||
|
<Box sx={{ p: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom sx={{ mb: 2, fontWeight: 600 }}>
|
||||||
|
账户安全
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
定期更新密码有助于保护您的账户安全
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Lock />}
|
||||||
|
onClick={handleOpenPasswordDialog}
|
||||||
|
sx={{
|
||||||
|
minWidth: 140,
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'primary.main',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
修改密码
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 修改密码对话框 */}
|
||||||
|
<ChangePasswordDialog
|
||||||
|
open={passwordDialogOpen}
|
||||||
|
onClose={handleClosePasswordDialog}
|
||||||
|
changeUserPassword={changeUserPasswordFunc}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,13 @@ const userService = {
|
|||||||
return request.get(api.user_info);
|
return request.get(api.user_info);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 更新用户密码
|
||||||
|
updatePassword: (data: { password: string; new_password: string }) => {
|
||||||
|
return post(api.setting, data);
|
||||||
|
},
|
||||||
|
|
||||||
// 更新用户设置
|
// 更新用户设置
|
||||||
updateSetting: (data: any) => {
|
updateSetting: (data: Partial<IUserInfo>) => {
|
||||||
return post(api.setting, data);
|
return post(api.setting, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import axios from 'axios';
|
|||||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
import { snackbar, notification } from '@/utils/snackbarInstance';
|
import { snackbar, notification } from '@/utils/snackbarInstance';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
const FAILED_TO_FETCH = 'Failed to fetch';
|
const FAILED_TO_FETCH = 'Failed to fetch';
|
||||||
|
|
||||||
@@ -82,6 +83,11 @@ const request: AxiosInstance = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class CustomError extends Error {
|
||||||
|
code?: number;
|
||||||
|
response?: ResponseType;
|
||||||
|
}
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
@@ -128,13 +134,17 @@ request.interceptors.response.use(
|
|||||||
const data: ResponseType = response.data;
|
const data: ResponseType = response.data;
|
||||||
|
|
||||||
// 处理业务错误码
|
// 处理业务错误码
|
||||||
if (data?.code === 100) {
|
if (data?.code === 401) {
|
||||||
snackbar.error(data?.message);
|
|
||||||
} else if (data?.code === 401) {
|
|
||||||
notification.error(data?.message, i18n.t('message.401'));
|
notification.error(data?.message, i18n.t('message.401'));
|
||||||
redirectToLogin();
|
redirectToLogin();
|
||||||
} else if (data?.code !== 0) {
|
} else if (data?.code !== 0) {
|
||||||
snackbar.error(data?.message);
|
// 处理其他业务错误
|
||||||
|
logger.info('请求出现错误:', data?.message);
|
||||||
|
const error = new CustomError(data?.message || '请求出现错误');
|
||||||
|
error.code = data?.code || -1;
|
||||||
|
error.response = data;
|
||||||
|
snackbar.warning(error.message);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user