PreferencesSettings.jsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useState, useEffect, useContext } from 'react';
  16. import { Card, Select, Typography, Avatar } from '@douyinfe/semi-ui';
  17. import { Languages } from 'lucide-react';
  18. import { useTranslation } from 'react-i18next';
  19. import { API, showSuccess, showError } from '../../../../helpers';
  20. import { UserContext } from '../../../../context/User';
  21. // Language options with native names and flags
  22. const languageOptions = [
  23. { value: 'zh', label: '中文', flag: '🇨🇳' },
  24. { value: 'en', label: 'English', flag: '🇺🇸' },
  25. { value: 'fr', label: 'Français', flag: '🇫🇷' },
  26. { value: 'ru', label: 'Русский', flag: '🇷🇺' },
  27. { value: 'ja', label: '日本語', flag: '🇯🇵' },
  28. { value: 'vi', label: 'Tiếng Việt', flag: '🇻🇳' },
  29. ];
  30. const PreferencesSettings = ({ t }) => {
  31. const { i18n } = useTranslation();
  32. const [userState, userDispatch] = useContext(UserContext);
  33. const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'zh');
  34. const [loading, setLoading] = useState(false);
  35. // Load saved language preference from user settings
  36. useEffect(() => {
  37. if (userState?.user?.setting) {
  38. try {
  39. const settings = JSON.parse(userState.user.setting);
  40. if (settings.language) {
  41. setCurrentLanguage(settings.language);
  42. // Sync i18n with saved preference
  43. if (i18n.language !== settings.language) {
  44. i18n.changeLanguage(settings.language);
  45. }
  46. }
  47. } catch (e) {
  48. // Ignore parse errors
  49. }
  50. }
  51. }, [userState?.user?.setting, i18n]);
  52. const handleLanguagePreferenceChange = async (lang) => {
  53. if (lang === currentLanguage) return;
  54. setLoading(true);
  55. const previousLang = currentLanguage;
  56. try {
  57. // Update language immediately for responsive UX
  58. setCurrentLanguage(lang);
  59. i18n.changeLanguage(lang);
  60. // Save to backend
  61. const res = await API.put('/api/user/self', {
  62. language: lang,
  63. });
  64. if (res.data.success) {
  65. showSuccess(t('语言偏好已保存'));
  66. // Update user context with new setting
  67. if (userState?.user?.setting) {
  68. try {
  69. const settings = JSON.parse(userState.user.setting);
  70. settings.language = lang;
  71. userDispatch({
  72. type: 'login',
  73. payload: {
  74. ...userState.user,
  75. setting: JSON.stringify(settings),
  76. },
  77. });
  78. } catch (e) {
  79. // Ignore
  80. }
  81. }
  82. } else {
  83. showError(res.data.message || t('保存失败'));
  84. // Revert on error
  85. setCurrentLanguage(previousLang);
  86. i18n.changeLanguage(previousLang);
  87. }
  88. } catch (error) {
  89. showError(t('保存失败,请重试'));
  90. // Revert on error
  91. setCurrentLanguage(previousLang);
  92. i18n.changeLanguage(previousLang);
  93. } finally {
  94. setLoading(false);
  95. }
  96. };
  97. return (
  98. <Card className='!rounded-2xl shadow-sm border-0'>
  99. {/* Card Header */}
  100. <div className='flex items-center mb-4'>
  101. <Avatar size='small' color='violet' className='mr-3 shadow-md'>
  102. <Languages size={16} />
  103. </Avatar>
  104. <div>
  105. <Typography.Text className='text-lg font-medium'>
  106. {t('偏好设置')}
  107. </Typography.Text>
  108. <div className='text-xs text-gray-600 dark:text-gray-400'>
  109. {t('界面语言和其他个人偏好')}
  110. </div>
  111. </div>
  112. </div>
  113. {/* Language Setting Card */}
  114. <Card className='!rounded-xl border dark:border-gray-700'>
  115. <div className='flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4'>
  116. <div className='flex items-start w-full sm:w-auto'>
  117. <div className='w-12 h-12 rounded-full bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center mr-4 flex-shrink-0'>
  118. <Languages
  119. size={20}
  120. className='text-violet-600 dark:text-violet-400'
  121. />
  122. </div>
  123. <div>
  124. <Typography.Title heading={6} className='mb-1'>
  125. {t('语言偏好')}
  126. </Typography.Title>
  127. <Typography.Text type='tertiary' className='text-sm'>
  128. {t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
  129. </Typography.Text>
  130. </div>
  131. </div>
  132. <Select
  133. value={currentLanguage}
  134. onChange={handleLanguagePreferenceChange}
  135. style={{ width: 180 }}
  136. loading={loading}
  137. optionList={languageOptions.map((opt) => ({
  138. value: opt.value,
  139. label: (
  140. <div className='flex items-center gap-2'>
  141. <span>{opt.flag}</span>
  142. <span>{opt.label}</span>
  143. </div>
  144. ),
  145. }))}
  146. renderSelectedItem={(optionNode) => {
  147. const selected = languageOptions.find(
  148. (opt) => opt.value === optionNode.value,
  149. );
  150. return (
  151. <div className='flex items-center gap-2'>
  152. <span>{selected?.flag}</span>
  153. <span>{selected?.label}</span>
  154. </div>
  155. );
  156. }}
  157. />
  158. </div>
  159. </Card>
  160. {/* Additional info */}
  161. <div className='mt-4 text-xs text-gray-500 dark:text-gray-400'>
  162. <Typography.Text type='tertiary'>
  163. {t('提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。')}
  164. </Typography.Text>
  165. </div>
  166. </Card>
  167. );
  168. };
  169. export default PreferencesSettings;