index.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import {
  3. Card,
  4. Form,
  5. Button,
  6. Typography,
  7. Modal,
  8. Banner,
  9. Layout,
  10. Tag,
  11. } from '@douyinfe/semi-ui';
  12. import { API, showError, showNotice } from '../../helpers';
  13. import { useTranslation } from 'react-i18next';
  14. import {
  15. IconHelpCircle,
  16. IconInfoCircle,
  17. IconUser,
  18. IconLock,
  19. IconSetting,
  20. IconCheckCircleStroked,
  21. } from '@douyinfe/semi-icons';
  22. import { Shield, Rocket, FlaskConical, Database, Layers } from 'lucide-react';
  23. const Setup = () => {
  24. const { t } = useTranslation();
  25. const [loading, setLoading] = useState(false);
  26. const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
  27. const [setupStatus, setSetupStatus] = useState({
  28. status: false,
  29. root_init: false,
  30. database_type: '',
  31. });
  32. const { Text, Title } = Typography;
  33. const formRef = useRef(null);
  34. const [formData, setFormData] = useState({
  35. username: '',
  36. password: '',
  37. confirmPassword: '',
  38. usageMode: 'external',
  39. });
  40. useEffect(() => {
  41. fetchSetupStatus();
  42. }, []);
  43. const fetchSetupStatus = async () => {
  44. try {
  45. const res = await API.get('/api/setup');
  46. const { success, data } = res.data;
  47. if (success) {
  48. setSetupStatus(data);
  49. // If setup is already completed, redirect to home
  50. if (data.status) {
  51. window.location.href = '/';
  52. }
  53. } else {
  54. showError(t('获取初始化状态失败'));
  55. }
  56. } catch (error) {
  57. console.error('Failed to fetch setup status:', error);
  58. showError(t('获取初始化状态失败'));
  59. }
  60. };
  61. const handleUsageModeChange = (val) => {
  62. setFormData({ ...formData, usageMode: val });
  63. };
  64. const onSubmit = () => {
  65. if (!formRef.current) {
  66. console.error('Form reference is null');
  67. showError(t('表单引用错误,请刷新页面重试'));
  68. return;
  69. }
  70. const values = formRef.current.getValues();
  71. console.log('Form values:', values);
  72. // For root_init=false, validate admin username and password
  73. if (!setupStatus.root_init) {
  74. if (!values.username || !values.username.trim()) {
  75. showError(t('请输入管理员用户名'));
  76. return;
  77. }
  78. if (!values.password || values.password.length < 8) {
  79. showError(t('密码长度至少为8个字符'));
  80. return;
  81. }
  82. if (values.password !== values.confirmPassword) {
  83. showError(t('两次输入的密码不一致'));
  84. return;
  85. }
  86. }
  87. // Prepare submission data
  88. const formValues = { ...values };
  89. formValues.SelfUseModeEnabled = values.usageMode === 'self';
  90. formValues.DemoSiteEnabled = values.usageMode === 'demo';
  91. // Remove usageMode as it's not needed by the backend
  92. delete formValues.usageMode;
  93. console.log('Submitting data to backend:', formValues);
  94. setLoading(true);
  95. // Submit to backend
  96. API.post('/api/setup', formValues)
  97. .then((res) => {
  98. const { success, message } = res.data;
  99. console.log('API response:', res.data);
  100. if (success) {
  101. showNotice(t('系统初始化成功,正在跳转...'));
  102. setTimeout(() => {
  103. window.location.reload();
  104. }, 1500);
  105. } else {
  106. showError(message || t('初始化失败,请重试'));
  107. }
  108. })
  109. .catch((error) => {
  110. console.error('API error:', error);
  111. showError(t('系统初始化失败,请重试'));
  112. setLoading(false);
  113. })
  114. .finally(() => {
  115. // setLoading(false);
  116. });
  117. };
  118. return (
  119. <div className="bg-gray-50 mt-[60px]">
  120. <Layout>
  121. <Layout.Content>
  122. <div className="flex justify-center px-4 py-8">
  123. <div className="w-full max-w-3xl">
  124. {/* 主卡片容器 */}
  125. <Card className="!rounded-2xl shadow-lg border-0">
  126. {/* 顶部装饰性区域 */}
  127. <Card
  128. className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden mb-6"
  129. style={{
  130. background: 'linear-gradient(135deg, #f97316 0%, #f59e0b 25%, #f43f5e 50%, #ec4899 75%, #e879f9 100%)',
  131. position: 'relative'
  132. }}
  133. bodyStyle={{ padding: 0 }}
  134. >
  135. {/* 装饰性背景元素 */}
  136. <div className="absolute inset-0 overflow-hidden">
  137. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-10 rounded-full"></div>
  138. <div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-5 rounded-full"></div>
  139. <div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
  140. </div>
  141. <div className="relative py-5 px-6 flex items-center" style={{ color: 'white' }}>
  142. <div className="w-14 h-14 rounded-full bg-white bg-opacity-20 flex items-center justify-center mr-5 shadow-lg flex-shrink-0">
  143. <IconSetting size="large" style={{ color: 'white' }} />
  144. </div>
  145. <div className="text-left">
  146. <Title heading={3} style={{ color: 'white', marginBottom: '2px' }}>
  147. {t('系统初始化')}
  148. </Title>
  149. <Text style={{ color: 'rgba(255, 255, 255, 0.9)', fontSize: '15px' }}>
  150. {t('欢迎使用,请完成以下设置以开始使用系统')}
  151. </Text>
  152. </div>
  153. </div>
  154. {/* 数据库警告 */}
  155. {setupStatus.database_type === 'sqlite' && (
  156. <div className="px-4">
  157. <Banner
  158. type='warning'
  159. icon={
  160. <div className="w-12 h-12 rounded-lg bg-orange-50 flex items-center justify-center">
  161. <Database size={22} className="text-orange-500" />
  162. </div>
  163. }
  164. closeIcon={null}
  165. title={
  166. <div className="flex items-center">
  167. <span className="font-medium">{t('数据库警告')}</span>
  168. <Tag color='orange' shape='circle' className="ml-2">
  169. SQLite
  170. </Tag>
  171. </div>
  172. }
  173. description={
  174. <div>
  175. <p>
  176. {t(
  177. '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
  178. )}
  179. </p>
  180. <p className="mt-1">
  181. <strong>{t(
  182. '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
  183. )}</strong>
  184. </p>
  185. </div>
  186. }
  187. className="!rounded-xl mb-6"
  188. fullMode={false}
  189. bordered
  190. />
  191. </div>
  192. )}
  193. {/* MySQL数据库提示 */}
  194. {setupStatus.database_type === 'mysql' && (
  195. <div className="px-4">
  196. <Banner
  197. type='info'
  198. icon={
  199. <div className="w-12 h-12 rounded-lg bg-blue-50 flex items-center justify-center">
  200. <Database size={22} className="text-blue-500" />
  201. </div>
  202. }
  203. closeIcon={null}
  204. title={
  205. <div className="flex items-center">
  206. <span className="font-medium">{t('数据库信息')}</span>
  207. <Tag color='blue' shape='circle' className="ml-2">
  208. MySQL
  209. </Tag>
  210. </div>
  211. }
  212. description={
  213. <div>
  214. <p>
  215. {t(
  216. '您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。',
  217. )}
  218. </p>
  219. </div>
  220. }
  221. className="!rounded-xl mb-6"
  222. fullMode={false}
  223. bordered
  224. />
  225. </div>
  226. )}
  227. {/* PostgreSQL数据库提示 */}
  228. {setupStatus.database_type === 'postgres' && (
  229. <div className="px-4">
  230. <Banner
  231. type='success'
  232. icon={
  233. <div className="w-12 h-12 rounded-lg bg-green-50 flex items-center justify-center">
  234. <Database size={22} className="text-green-500" />
  235. </div>
  236. }
  237. closeIcon={null}
  238. title={
  239. <div className="flex items-center">
  240. <span className="font-medium">{t('数据库信息')}</span>
  241. <Tag color='green' shape='circle' className="ml-2">
  242. PostgreSQL
  243. </Tag>
  244. </div>
  245. }
  246. description={
  247. <div>
  248. <p>
  249. {t(
  250. '您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。',
  251. )}
  252. </p>
  253. </div>
  254. }
  255. className="!rounded-xl mb-6"
  256. fullMode={false}
  257. bordered
  258. />
  259. </div>
  260. )}
  261. </Card>
  262. {/* 主内容区域 */}
  263. <Form
  264. getFormApi={(formApi) => {
  265. formRef.current = formApi;
  266. console.log('Form API set:', formApi);
  267. }}
  268. initValues={formData}
  269. >
  270. {/* 管理员账号设置 */}
  271. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  272. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  273. background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
  274. position: 'relative'
  275. }}>
  276. <div className="absolute inset-0 overflow-hidden">
  277. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  278. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  279. </div>
  280. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  281. <IconUser size="large" style={{ color: '#ffffff' }} />
  282. </div>
  283. <div className="relative">
  284. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('管理员账号')}</Text>
  285. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置系统管理员的登录信息')}</div>
  286. </div>
  287. </div>
  288. {setupStatus.root_init ? (
  289. <>
  290. <Banner
  291. type='info'
  292. icon={
  293. <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center">
  294. <IconCheckCircleStroked size="large" className="text-blue-500" />
  295. </div>
  296. }
  297. closeIcon={null}
  298. description={
  299. <div className="flex items-center">
  300. <span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
  301. </div>
  302. }
  303. className="!rounded-lg"
  304. />
  305. </>
  306. ) : (
  307. <>
  308. <Form.Input
  309. field='username'
  310. label={t('用户名')}
  311. placeholder={t('请输入管理员用户名')}
  312. prefix={<IconUser />}
  313. showClear
  314. size='large'
  315. className="mb-4 !rounded-lg"
  316. noLabel={false}
  317. validateStatus="default"
  318. onChange={(value) =>
  319. setFormData({ ...formData, username: value })
  320. }
  321. />
  322. <Form.Input
  323. field='password'
  324. label={t('密码')}
  325. placeholder={t('请输入管理员密码')}
  326. type='password'
  327. prefix={<IconLock />}
  328. showClear
  329. size='large'
  330. className="mb-4 !rounded-lg"
  331. noLabel={false}
  332. mode="password"
  333. validateStatus="default"
  334. onChange={(value) =>
  335. setFormData({ ...formData, password: value })
  336. }
  337. />
  338. <Form.Input
  339. field='confirmPassword'
  340. label={t('确认密码')}
  341. placeholder={t('请确认管理员密码')}
  342. type='password'
  343. prefix={<IconLock />}
  344. showClear
  345. size='large'
  346. className="!rounded-lg"
  347. noLabel={false}
  348. mode="password"
  349. validateStatus="default"
  350. onChange={(value) =>
  351. setFormData({ ...formData, confirmPassword: value })
  352. }
  353. />
  354. </>
  355. )}
  356. </Card>
  357. {/* 使用模式 */}
  358. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  359. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  360. background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
  361. position: 'relative'
  362. }}>
  363. <div className="absolute inset-0 overflow-hidden">
  364. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  365. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  366. </div>
  367. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  368. <Layers size={22} style={{ color: '#ffffff' }} />
  369. </div>
  370. <div className="relative">
  371. <div className="flex items-center">
  372. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('使用模式')}</Text>
  373. <Button
  374. theme='borderless'
  375. type='tertiary'
  376. icon={<IconHelpCircle size="small" style={{ color: '#ffffff' }} />}
  377. size='small'
  378. onClick={() => setUsageModeInfoVisible(true)}
  379. className="!rounded-full"
  380. />
  381. </div>
  382. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('选择适合您使用场景的模式')}</div>
  383. </div>
  384. </div>
  385. <Form.RadioGroup
  386. field='usageMode'
  387. noLabel={true}
  388. initValue='external'
  389. onChange={handleUsageModeChange}
  390. type='pureCard'
  391. className="[&_.semi-radio-addon-buttonRadio-wrapper]:!rounded-xl"
  392. validateStatus="default"
  393. >
  394. <div className="space-y-3 mt-2">
  395. <Form.Radio
  396. value='external'
  397. className="!p-4 !rounded-xl hover:!bg-blue-50 transition-colors w-full"
  398. extra={
  399. <div className="flex items-start">
  400. <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3 flex-shrink-0">
  401. <Rocket size={20} className="text-blue-500" />
  402. </div>
  403. <div className="flex-1">
  404. <div className="font-medium text-gray-900 mb-1">{t('对外运营模式')}</div>
  405. <div className="text-sm text-gray-500">{t('适用于为多个用户提供服务的场景')}</div>
  406. <Tag color='blue' shape='circle' className="mt-2">
  407. {t('默认模式')}
  408. </Tag>
  409. </div>
  410. </div>
  411. }
  412. />
  413. <Form.Radio
  414. value='self'
  415. className="!p-4 !rounded-xl hover:!bg-green-50 transition-colors w-full"
  416. extra={
  417. <div className="flex items-start">
  418. <div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3 flex-shrink-0">
  419. <Shield size={20} className="text-green-500" />
  420. </div>
  421. <div className="flex-1">
  422. <div className="font-medium text-gray-900 mb-1">{t('自用模式')}</div>
  423. <div className="text-sm text-gray-500">{t('适用于个人使用的场景,不需要设置模型价格')}</div>
  424. <Tag color='green' shape='circle' className="mt-2">
  425. {t('无需计费')}
  426. </Tag>
  427. </div>
  428. </div>
  429. }
  430. />
  431. <Form.Radio
  432. value='demo'
  433. className="!p-4 !rounded-xl hover:!bg-purple-50 transition-colors w-full"
  434. extra={
  435. <div className="flex items-start">
  436. <div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3 flex-shrink-0">
  437. <FlaskConical size={20} className="text-purple-500" />
  438. </div>
  439. <div className="flex-1">
  440. <div className="font-medium text-gray-900 mb-1">{t('演示站点模式')}</div>
  441. <div className="text-sm text-gray-500">{t('适用于展示系统功能的场景,提供基础功能演示')}</div>
  442. <Tag color='purple' shape='circle' className="mt-2">
  443. {t('演示体验')}
  444. </Tag>
  445. </div>
  446. </div>
  447. }
  448. />
  449. </div>
  450. </Form.RadioGroup>
  451. </Card>
  452. </Form>
  453. <div className="flex justify-center mt-6">
  454. <Button
  455. type='primary'
  456. onClick={onSubmit}
  457. loading={loading}
  458. size='large'
  459. className="!rounded-lg !bg-gradient-to-r !from-orange-500 !to-pink-500 hover:!from-orange-600 hover:!to-pink-600 !border-0 !px-8"
  460. icon={<IconCheckCircleStroked />}
  461. >
  462. {t('初始化系统')}
  463. </Button>
  464. </div>
  465. </Card>
  466. </div>
  467. </div>
  468. </Layout.Content>
  469. </Layout>
  470. {/* 使用模式说明模态框 */}
  471. <Modal
  472. title={
  473. <div className="flex items-center">
  474. <IconInfoCircle className="mr-2 text-blue-500" />
  475. {t('使用模式说明')}
  476. </div>
  477. }
  478. visible={selfUseModeInfoVisible}
  479. onOk={() => setUsageModeInfoVisible(false)}
  480. onCancel={() => setUsageModeInfoVisible(false)}
  481. closeOnEsc={true}
  482. okText={t('我已了解')}
  483. cancelText={null}
  484. centered={true}
  485. size='medium'
  486. className="[&_.semi-modal-body]:!p-6"
  487. >
  488. <div className="space-y-6">
  489. {/* 对外运营模式 */}
  490. <div className="bg-blue-50 rounded-xl p-4">
  491. <div className="flex items-start">
  492. <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3 flex-shrink-0">
  493. <Rocket size={20} className="text-blue-600" />
  494. </div>
  495. <div>
  496. <Title heading={6} className="text-blue-900 mb-2">{t('对外运营模式')}</Title>
  497. <div className="space-y-2 text-sm text-gray-700">
  498. <p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
  499. <p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
  500. <div className="mt-3">
  501. <Tag color='blue' shape='circle' className="mr-2">{t('计费模式')}</Tag>
  502. <Tag color='blue' shape='circle'>{t('多用户支持')}</Tag>
  503. </div>
  504. </div>
  505. </div>
  506. </div>
  507. </div>
  508. {/* 自用模式 */}
  509. <div className="bg-green-50 rounded-xl p-4">
  510. <div className="flex items-start">
  511. <div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3 flex-shrink-0">
  512. <Shield size={20} className="text-green-600" />
  513. </div>
  514. <div>
  515. <Title heading={6} className="text-green-900 mb-2">{t('自用模式')}</Title>
  516. <div className="space-y-2 text-sm text-gray-700">
  517. <p>{t('适用于个人使用的场景。')}</p>
  518. <p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
  519. <div className="mt-3">
  520. <Tag color='green' shape='circle' className="mr-2">{t('无需计费')}</Tag>
  521. <Tag color='green' shape='circle'>{t('个人使用')}</Tag>
  522. </div>
  523. </div>
  524. </div>
  525. </div>
  526. </div>
  527. {/* 演示站点模式 */}
  528. <div className="bg-purple-50 rounded-xl p-4">
  529. <div className="flex items-start">
  530. <div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3 flex-shrink-0">
  531. <FlaskConical size={20} className="text-purple-600" />
  532. </div>
  533. <div>
  534. <Title heading={6} className="text-purple-900 mb-2">{t('演示站点模式')}</Title>
  535. <div className="space-y-2 text-sm text-gray-700">
  536. <p>{t('适用于展示系统功能的场景。')}</p>
  537. <p>{t('提供基础功能演示,方便用户了解系统特性。')}</p>
  538. <div className="mt-3">
  539. <Tag color='purple' shape='circle' className="mr-2">{t('功能演示')}</Tag>
  540. <Tag color='purple' shape='circle'>{t('体验试用')}</Tag>
  541. </div>
  542. </div>
  543. </div>
  544. </div>
  545. </div>
  546. </div>
  547. </Modal>
  548. </div>
  549. );
  550. };
  551. export default Setup;