SetupWizard.jsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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, { useEffect, useState, useRef } from 'react';
  16. import { Card, Divider, Steps, Form } from '@douyinfe/semi-ui';
  17. import { API, showError, showNotice } from '../../helpers';
  18. import { useTranslation } from 'react-i18next';
  19. import StepNavigation from './components/StepNavigation';
  20. import DatabaseStep from './components/steps/DatabaseStep';
  21. import AdminStep from './components/steps/AdminStep';
  22. import UsageModeStep from './components/steps/UsageModeStep';
  23. import CompleteStep from './components/steps/CompleteStep';
  24. const SetupWizard = () => {
  25. const { t } = useTranslation();
  26. const [loading, setLoading] = useState(false);
  27. const [setupStatus, setSetupStatus] = useState({
  28. status: false,
  29. root_init: false,
  30. database_type: '',
  31. });
  32. const [currentStep, setCurrentStep] = useState(0);
  33. const formRef = useRef(null);
  34. const [formData, setFormData] = useState({
  35. username: '',
  36. password: '',
  37. confirmPassword: '',
  38. usageMode: 'external',
  39. });
  40. // 确保默认选中“对外运营模式”,并同步到表单
  41. useEffect(() => {
  42. if (formRef.current) {
  43. formRef.current.setValue('usageMode', 'external');
  44. }
  45. }, []);
  46. // 定义步骤内容
  47. const steps = [
  48. {
  49. title: t('数据库检查'),
  50. description: t('验证数据库连接状态'),
  51. },
  52. {
  53. title: t('管理员账号'),
  54. description: t('设置管理员登录信息'),
  55. },
  56. {
  57. title: t('使用模式'),
  58. description: t('选择系统运行模式'),
  59. },
  60. {
  61. title: t('完成初始化'),
  62. description: t('确认设置并完成初始化'),
  63. },
  64. ];
  65. useEffect(() => {
  66. fetchSetupStatus();
  67. }, []);
  68. const fetchSetupStatus = async () => {
  69. try {
  70. const res = await API.get('/api/setup');
  71. const { success, data } = res.data;
  72. if (success) {
  73. setSetupStatus(data);
  74. // If setup is already completed, redirect to home
  75. if (data.status) {
  76. window.location.href = '/';
  77. return;
  78. }
  79. // 设置当前步骤 - 默认从数据库检查开始
  80. setCurrentStep(0);
  81. } else {
  82. showError(t('获取初始化状态失败'));
  83. }
  84. } catch (error) {
  85. console.error('Failed to fetch setup status:', error);
  86. showError(t('获取初始化状态失败'));
  87. }
  88. };
  89. const handleUsageModeChange = (e) => {
  90. const nextMode = e?.target?.value ?? e;
  91. setFormData((prev) => ({ ...prev, usageMode: nextMode }));
  92. // 同步到表单,便于 getValues() 拿到 usageMode
  93. if (formRef.current) {
  94. formRef.current.setValue('usageMode', nextMode);
  95. }
  96. };
  97. const next = () => {
  98. // 验证当前步骤是否可以继续
  99. if (!canProceedToNext()) {
  100. return;
  101. }
  102. const current = currentStep + 1;
  103. setCurrentStep(current);
  104. };
  105. // 验证是否可以继续到下一步
  106. const canProceedToNext = () => {
  107. switch (currentStep) {
  108. case 0: // 数据库检查步骤
  109. return true; // 数据库检查总是可以继续
  110. case 1: // 管理员账号步骤
  111. if (setupStatus.root_init) {
  112. return true; // 如果已经初始化,可以继续
  113. }
  114. // 检查必填字段
  115. if (
  116. !formData.username ||
  117. !formData.password ||
  118. !formData.confirmPassword
  119. ) {
  120. showError(t('请填写完整的管理员账号信息'));
  121. return false;
  122. }
  123. if (formData.password !== formData.confirmPassword) {
  124. showError(t('两次输入的密码不一致'));
  125. return false;
  126. }
  127. if (formData.password.length < 8) {
  128. showError(t('密码长度至少为8个字符'));
  129. return false;
  130. }
  131. return true;
  132. case 2: // 使用模式步骤
  133. if (!formData.usageMode) {
  134. showError(t('请选择使用模式'));
  135. return false;
  136. }
  137. return true;
  138. default:
  139. return true;
  140. }
  141. };
  142. const prev = () => {
  143. const current = currentStep - 1;
  144. setCurrentStep(current);
  145. };
  146. const onSubmit = () => {
  147. if (!formRef.current) {
  148. console.error('Form reference is null');
  149. showError(t('表单引用错误,请刷新页面重试'));
  150. return;
  151. }
  152. const values = formRef.current.getValues();
  153. // For root_init=false, validate admin username and password
  154. if (!setupStatus.root_init) {
  155. if (!values.username || !values.username.trim()) {
  156. showError(t('请输入管理员用户名'));
  157. return;
  158. }
  159. if (!values.password || values.password.length < 8) {
  160. showError(t('密码长度至少为8个字符'));
  161. return;
  162. }
  163. if (values.password !== values.confirmPassword) {
  164. showError(t('两次输入的密码不一致'));
  165. return;
  166. }
  167. }
  168. // Prepare submission data
  169. const formValues = { ...values };
  170. const usageMode = values.usageMode;
  171. formValues.SelfUseModeEnabled = usageMode === 'self';
  172. formValues.DemoSiteEnabled = usageMode === 'demo';
  173. // Remove usageMode as it's not needed by the backend
  174. delete formValues.usageMode;
  175. // 提交表单至后端
  176. setLoading(true);
  177. // Submit to backend
  178. API.post('/api/setup', formValues)
  179. .then((res) => {
  180. const { success, message } = res.data;
  181. if (success) {
  182. showNotice(t('系统初始化成功,正在跳转...'));
  183. setTimeout(() => {
  184. window.location.reload();
  185. }, 1500);
  186. } else {
  187. showError(message || t('初始化失败,请重试'));
  188. }
  189. })
  190. .catch((error) => {
  191. console.error('API error:', error);
  192. showError(t('系统初始化失败,请重试'));
  193. setLoading(false);
  194. })
  195. .finally(() => {
  196. setLoading(false);
  197. });
  198. };
  199. // 获取步骤内容
  200. const getStepContent = (step) => {
  201. switch (step) {
  202. case 0:
  203. return <DatabaseStep setupStatus={setupStatus} t={t} />;
  204. case 1:
  205. return (
  206. <AdminStep
  207. setupStatus={setupStatus}
  208. formData={formData}
  209. setFormData={setFormData}
  210. formRef={formRef}
  211. t={t}
  212. />
  213. );
  214. case 2:
  215. return (
  216. <UsageModeStep
  217. formData={formData}
  218. handleUsageModeChange={handleUsageModeChange}
  219. t={t}
  220. />
  221. );
  222. case 3:
  223. return (
  224. <CompleteStep setupStatus={setupStatus} formData={formData} t={t} />
  225. );
  226. default:
  227. return null;
  228. }
  229. };
  230. const stepNavigationProps = {
  231. currentStep,
  232. steps,
  233. prev,
  234. next,
  235. onSubmit,
  236. loading,
  237. t,
  238. };
  239. return (
  240. <div className='min-h-screen flex items-center justify-center px-4'>
  241. <div className='w-full max-w-4xl'>
  242. <Card className='!rounded-2xl shadow-sm border-0'>
  243. <div className='mb-4'>
  244. <div className='text-xl font-semibold'>{t('系统初始化')}</div>
  245. <div className='text-xs text-gray-600'>
  246. {t('欢迎使用,请完成以下设置以开始使用系统')}
  247. </div>
  248. </div>
  249. <div className='px-2 py-2'>
  250. <Steps type='basic' current={currentStep}>
  251. {steps.map((item, index) => (
  252. <Steps.Step
  253. key={item.title}
  254. title={
  255. <span className={currentStep === index ? 'shine-text' : ''}>
  256. {item.title}
  257. </span>
  258. }
  259. description={item.description}
  260. />
  261. ))}
  262. </Steps>
  263. </div>
  264. <Divider margin='12px' />
  265. {/* 表单容器 */}
  266. <Form
  267. getFormApi={(formApi) => {
  268. formRef.current = formApi;
  269. }}
  270. initValues={formData}
  271. >
  272. {/* 步骤内容:保持所有字段挂载,仅隐藏非当前步骤 */}
  273. <div className='steps-content'>
  274. {[0, 1, 2, 3].map((idx) => (
  275. <div
  276. key={idx}
  277. style={{ display: currentStep === idx ? 'block' : 'none' }}
  278. >
  279. {React.cloneElement(getStepContent(idx), {
  280. ...stepNavigationProps,
  281. renderNavigationButtons: () => (
  282. <StepNavigation {...stepNavigationProps} />
  283. ),
  284. })}
  285. </div>
  286. ))}
  287. </div>
  288. </Form>
  289. </Card>
  290. </div>
  291. </div>
  292. );
  293. };
  294. export default SetupWizard;