From 6ca5e235b48bf1142b4eed37b2644b8acc7176ad Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Tue, 21 Oct 2025 11:40:47 +0800 Subject: [PATCH] feat(settings): add user profile and password management --- src/components/Layout/Header.tsx | 20 +- src/components/Layout/SettingLayout.tsx | 74 ++- src/components/Layout/SettingSidebar.tsx | 136 ++++++ src/constants/common.ts | 0 src/constants/setting.ts | 421 ++++++++++++++++++ src/hooks/setting-hooks.ts | 42 ++ src/hooks/useSnackbar.ts | 11 +- src/hooks/useUserData.ts | 6 +- .../components/ChangePasswordDialog.tsx | 272 +++++++++++ src/pages/setting/components/ProfileForm.tsx | 278 ++++++++++++ src/pages/setting/profile.tsx | 65 ++- src/services/user_service.ts | 7 +- src/utils/request.ts | 18 +- 13 files changed, 1330 insertions(+), 20 deletions(-) create mode 100644 src/components/Layout/SettingSidebar.tsx create mode 100644 src/constants/common.ts create mode 100644 src/constants/setting.ts create mode 100644 src/hooks/setting-hooks.ts create mode 100644 src/pages/setting/components/ChangePasswordDialog.tsx create mode 100644 src/pages/setting/components/ProfileForm.tsx diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index f19b3bd..73575f1 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -10,10 +10,13 @@ import { ListItemIcon, ListItemText } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; -import LogoutIcon from '@mui/icons-material/Logout'; -import PersonIcon from '@mui/icons-material/Person'; +import { + Settings as SettingsIcon, + Search as SearchIcon, + AccountCircle as AccountCircleIcon, + Logout as LogoutIcon, + Person as PersonIcon, +} from '@mui/icons-material'; import LanguageSwitcher from '../LanguageSwitcher'; import { useAuth } from '@/hooks/login-hooks'; import { useNavigate } from 'react-router-dom'; @@ -185,13 +188,20 @@ const Header = () => { - {/* 菜单项 */} + {/* 个人资料 */} 个人资料 + {/* 系统设置 */} + navigate('/setting/system')} sx={{ py: 1 }}> + + + + 系统设置 + diff --git a/src/components/Layout/SettingLayout.tsx b/src/components/Layout/SettingLayout.tsx index fb4e4c7..627aaca 100644 --- a/src/components/Layout/SettingLayout.tsx +++ b/src/components/Layout/SettingLayout.tsx @@ -1,15 +1,85 @@ +import React from 'react'; 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)({ display: 'flex', 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 = { + '/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 ( + + + + ); +} + function SettingLayout() { + return ( - + + + + + + + ); } diff --git a/src/components/Layout/SettingSidebar.tsx b/src/components/Layout/SettingSidebar.tsx new file mode 100644 index 0000000..eb4564a --- /dev/null +++ b/src/components/Layout/SettingSidebar.tsx @@ -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; + 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 ( + + + 设置 + + + + {settingMenuItems.map((item) => { + const IconComponent = item.icon; + const isActive = location.pathname === item.path; + + return ( + 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', + }, + }} + > + + + + ); + })} + + + + © 2025 T-Systems + + + ); +}; + +export default SettingSidebar; \ No newline at end of file diff --git a/src/constants/common.ts b/src/constants/common.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/constants/setting.ts b/src/constants/setting.ts new file mode 100644 index 0000000..24ee0fe --- /dev/null +++ b/src/constants/setting.ts @@ -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', +]; diff --git a/src/hooks/setting-hooks.ts b/src/hooks/setting-hooks.ts new file mode 100644 index 0000000..7c0fb0e --- /dev/null +++ b/src/hooks/setting-hooks.ts @@ -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) => { + 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, + }; +} \ No newline at end of file diff --git a/src/hooks/useSnackbar.ts b/src/hooks/useSnackbar.ts index 017d847..79d5830 100644 --- a/src/hooks/useSnackbar.ts +++ b/src/hooks/useSnackbar.ts @@ -1,12 +1,17 @@ -import { useSnackbar } from '@/components/Provider/SnackbarProvider'; +import { useSnackbar as useSnackbarProvider } from '@/components/Provider/SnackbarProvider'; // 简化的 hooks export const useMessage = () => { - const { showMessage } = useSnackbar(); + const { showMessage } = useSnackbarProvider(); return showMessage; }; export const useNotification = () => { - const { showNotification } = useSnackbar(); + const { showNotification } = useSnackbarProvider(); return showNotification; +}; + +export const useSnackbar = () => { + const { showMessage, showNotification } = useSnackbarProvider(); + return { showMessage, showNotification }; }; \ No newline at end of file diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts index 2185f5e..8302e70 100644 --- a/src/hooks/useUserData.ts +++ b/src/hooks/useUserData.ts @@ -1,6 +1,7 @@ import { useEffect, useCallback } from 'react'; import { useUserStore } from '@/stores/userStore'; import userService from '@/services/user_service'; +import type { IUserInfo } from '@/interfaces/database/user-setting'; /** * 用户数据管理Hook @@ -28,8 +29,9 @@ export const useUserData = () => { try { const response = await userService.getUserInfo(); if (response.data.code === 0) { - setUserInfo(response.data.data); - return response.data.data; + const userInfoData: IUserInfo | null = response.data.data || null; + setUserInfo(userInfoData); + return userInfoData; } else { throw new Error(response.data.message || '获取用户信息失败'); } diff --git a/src/pages/setting/components/ChangePasswordDialog.tsx b/src/pages/setting/components/ChangePasswordDialog.tsx new file mode 100644 index 0000000..ffd192f --- /dev/null +++ b/src/pages/setting/components/ChangePasswordDialog.tsx @@ -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; +} + +interface PasswordFormData { + currentPassword: string; + newPassword: string; + confirmPassword: string; +} + +/** + * 修改密码对话框 + */ +function ChangePasswordDialog({ open, onClose, changeUserPassword }: ChangePasswordDialogProps) { + const { showMessage } = useSnackbar(); + + const [formData, setFormData] = useState({ + currentPassword: '', + newPassword: '', + confirmPassword: '' + }); + + const [showPasswords, setShowPasswords] = useState({ + current: false, + new: false, + confirm: false + }); + + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState>({}); + + // 重置表单 + 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) => { + 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 = {}; + + 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 ( + + + + 修改密码 + + + + + + + + + + 为了您的账户安全,请设置一个强密码。密码长度至少6位,建议包含字母、数字和特殊字符。 + + + {/* 当前密码 */} + + togglePasswordVisibility('current')} + edge="end" + size="small" + > + {showPasswords.current ? : } + + + ) + }} + /> + + {/* 新密码 */} + + togglePasswordVisibility('new')} + edge="end" + size="small" + > + {showPasswords.new ? : } + + + ) + }} + /> + + {/* 确认新密码 */} + + togglePasswordVisibility('confirm')} + edge="end" + size="small" + > + {showPasswords.confirm ? : } + + + ) + }} + /> + + + + + + + + + ); +} + +export default ChangePasswordDialog; \ No newline at end of file diff --git a/src/pages/setting/components/ProfileForm.tsx b/src/pages/setting/components/ProfileForm.tsx new file mode 100644 index 0000000..aa4ebaa --- /dev/null +++ b/src/pages/setting/components/ProfileForm.tsx @@ -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) => Promise; +} + + +/** + * 个人信息表单 + */ +function ProfileForm({ userInfo, onSubmit }: ProfileFormProps) { + const showMessage = useMessage(); + const fileInputRef = useRef(null); + + const [formData, setFormData] = useState>({ + 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) => { + 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) => { + 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 = { + 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 ( + + + 个人资料 + + + + {/* 头像部分 */} + + + + {formData.nickname?.charAt(0)?.toUpperCase()} + + + + 头像 + + + + + + + + + 支持 JPG、PNG 格式,文件大小不超过 2MB + + + + + + {/* 用户名 */} + + + + + {/* 邮箱 (只读) */} + + + + + {/* 语言 */} + + + 语言 + + + + + {/* 时区 */} + + + 时区 + + + + + {/* 保存按钮 */} + + + + + + + + ); +} + +export default ProfileForm; \ No newline at end of file diff --git a/src/pages/setting/profile.tsx b/src/pages/setting/profile.tsx index 3563a10..76d0ddb 100644 --- a/src/pages/setting/profile.tsx +++ b/src/pages/setting/profile.tsx @@ -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() { + 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 ( -
-

Profile Setting

-
+ + {/* 个人资料表单 */} + + + {/* 分割线 */} + + + {/* 密码修改部分 */} + + + 账户安全 + + + 定期更新密码有助于保护您的账户安全 + + + + + + {/* 修改密码对话框 */} + + ); } diff --git a/src/services/user_service.ts b/src/services/user_service.ts index f93499d..3f0c834 100644 --- a/src/services/user_service.ts +++ b/src/services/user_service.ts @@ -26,8 +26,13 @@ const userService = { return request.get(api.user_info); }, + // 更新用户密码 + updatePassword: (data: { password: string; new_password: string }) => { + return post(api.setting, data); + }, + // 更新用户设置 - updateSetting: (data: any) => { + updateSetting: (data: Partial) => { return post(api.setting, data); }, diff --git a/src/utils/request.ts b/src/utils/request.ts index c94d963..3c3c419 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,6 +5,7 @@ import axios from 'axios'; import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import { snackbar, notification } from '@/utils/snackbarInstance'; +import logger from './logger'; 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( (config: InternalAxiosRequestConfig) => { @@ -128,13 +134,17 @@ request.interceptors.response.use( const data: ResponseType = response.data; // 处理业务错误码 - if (data?.code === 100) { - snackbar.error(data?.message); - } else if (data?.code === 401) { + if (data?.code === 401) { notification.error(data?.message, i18n.t('message.401')); redirectToLogin(); } 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; },