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 = () => {
- {/* 菜单项 */}
+ {/* 个人资料 */}
+ {/* 系统设置 */}
+
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 (
+
+ );
+}
+
+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
-
+
+ {/* 个人资料表单 */}
+
+
+ {/* 分割线 */}
+
+
+ {/* 密码修改部分 */}
+
+
+ 账户安全
+
+
+ 定期更新密码有助于保护您的账户安全
+
+
+ }
+ onClick={handleOpenPasswordDialog}
+ sx={{
+ minWidth: 140,
+ borderColor: 'primary.main',
+ color: 'primary.main',
+ '&:hover': {
+ backgroundColor: 'primary.main',
+ color: 'white'
+ }
+ }}
+ >
+ 修改密码
+
+
+
+ {/* 修改密码对话框 */}
+
+
);
}
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;
},