Summary Implement a dedicated, reusable scrolling mechanism for all console-table pages while keeping header and sidebar fixed, plus related layout improvements. Key Changes • Added `.table-scroll-card` utility class – Provides flex column layout and internal vertical scrolling – Desktop height: `calc(100vh - 110px)`; Mobile (<768 px) height: `calc(100vh - 77px)` – Hides scrollbars cross-browser (`-ms-overflow-style`, `scrollbar-width`, `::-webkit-scrollbar`) • Replaced global `.semi-card` scrolling rules with `.table-scroll-card` to avoid affecting non-table cards • Updated table components (Channels, Tokens, Users, Logs, MjLogs, TaskLogs, Redemptions) to use the new class • PageLayout – Footer is now suppressed for all `/console` routes – Confirmed only central content area scrolls; header & sidebar remain fixed • Restored hidden scrollbar rules for `.semi-layout-content` and removed unnecessary global overrides • Minor CSS cleanup & comment improvements for readability Result Console table pages now fill the viewport with smooth, internal scrolling and no visible scrollbars, while other cards and pages remain unaffected.
579 lines
25 KiB
JavaScript
579 lines
25 KiB
JavaScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import {
|
|
Card,
|
|
Form,
|
|
Button,
|
|
Typography,
|
|
Modal,
|
|
Banner,
|
|
Layout,
|
|
Tag,
|
|
} from '@douyinfe/semi-ui';
|
|
import { API, showError, showNotice } from '../../helpers';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
IconHelpCircle,
|
|
IconInfoCircle,
|
|
IconUser,
|
|
IconLock,
|
|
IconSetting,
|
|
IconCheckCircleStroked,
|
|
} from '@douyinfe/semi-icons';
|
|
import { Shield, Rocket, FlaskConical, Database, Layers } from 'lucide-react';
|
|
|
|
const Setup = () => {
|
|
const { t } = useTranslation();
|
|
const [loading, setLoading] = useState(false);
|
|
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
|
|
const [setupStatus, setSetupStatus] = useState({
|
|
status: false,
|
|
root_init: false,
|
|
database_type: '',
|
|
});
|
|
const { Text, Title } = Typography;
|
|
const formRef = useRef(null);
|
|
|
|
const [formData, setFormData] = useState({
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
usageMode: 'external',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchSetupStatus();
|
|
}, []);
|
|
|
|
const fetchSetupStatus = async () => {
|
|
try {
|
|
const res = await API.get('/api/setup');
|
|
const { success, data } = res.data;
|
|
if (success) {
|
|
setSetupStatus(data);
|
|
|
|
// If setup is already completed, redirect to home
|
|
if (data.status) {
|
|
window.location.href = '/';
|
|
}
|
|
} else {
|
|
showError(t('获取初始化状态失败'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch setup status:', error);
|
|
showError(t('获取初始化状态失败'));
|
|
}
|
|
};
|
|
|
|
const handleUsageModeChange = (val) => {
|
|
setFormData({ ...formData, usageMode: val });
|
|
};
|
|
|
|
const onSubmit = () => {
|
|
if (!formRef.current) {
|
|
console.error('Form reference is null');
|
|
showError(t('表单引用错误,请刷新页面重试'));
|
|
return;
|
|
}
|
|
|
|
const values = formRef.current.getValues();
|
|
console.log('Form values:', values);
|
|
|
|
// For root_init=false, validate admin username and password
|
|
if (!setupStatus.root_init) {
|
|
if (!values.username || !values.username.trim()) {
|
|
showError(t('请输入管理员用户名'));
|
|
return;
|
|
}
|
|
|
|
if (!values.password || values.password.length < 8) {
|
|
showError(t('密码长度至少为8个字符'));
|
|
return;
|
|
}
|
|
|
|
if (values.password !== values.confirmPassword) {
|
|
showError(t('两次输入的密码不一致'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Prepare submission data
|
|
const formValues = { ...values };
|
|
formValues.SelfUseModeEnabled = values.usageMode === 'self';
|
|
formValues.DemoSiteEnabled = values.usageMode === 'demo';
|
|
|
|
// Remove usageMode as it's not needed by the backend
|
|
delete formValues.usageMode;
|
|
|
|
console.log('Submitting data to backend:', formValues);
|
|
setLoading(true);
|
|
|
|
// Submit to backend
|
|
API.post('/api/setup', formValues)
|
|
.then((res) => {
|
|
const { success, message } = res.data;
|
|
console.log('API response:', res.data);
|
|
|
|
if (success) {
|
|
showNotice(t('系统初始化成功,正在跳转...'));
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
} else {
|
|
showError(message || t('初始化失败,请重试'));
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('API error:', error);
|
|
showError(t('系统初始化失败,请重试'));
|
|
setLoading(false);
|
|
})
|
|
.finally(() => {
|
|
// setLoading(false);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gray-50 mt-[60px]">
|
|
<Layout>
|
|
<Layout.Content>
|
|
<div className="flex justify-center px-4 py-8">
|
|
<div className="w-full max-w-3xl">
|
|
{/* 主卡片容器 */}
|
|
<Card className="!rounded-2xl shadow-lg border-0">
|
|
{/* 顶部装饰性区域 */}
|
|
<Card
|
|
className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden mb-6"
|
|
style={{
|
|
background: 'linear-gradient(135deg, #f97316 0%, #f59e0b 25%, #f43f5e 50%, #ec4899 75%, #e879f9 100%)',
|
|
position: 'relative'
|
|
}}
|
|
bodyStyle={{ padding: 0 }}
|
|
>
|
|
{/* 装饰性背景元素 */}
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-10 rounded-full"></div>
|
|
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
|
|
</div>
|
|
|
|
<div className="relative py-5 px-6 flex items-center" style={{ color: 'white' }}>
|
|
<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">
|
|
<IconSetting size="large" style={{ color: 'white' }} />
|
|
</div>
|
|
<div className="text-left">
|
|
<Title heading={3} style={{ color: 'white', marginBottom: '2px' }}>
|
|
{t('系统初始化')}
|
|
</Title>
|
|
<Text style={{ color: 'rgba(255, 255, 255, 0.9)', fontSize: '15px' }}>
|
|
{t('欢迎使用,请完成以下设置以开始使用系统')}
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
{/* 数据库警告 */}
|
|
{setupStatus.database_type === 'sqlite' && (
|
|
<div className="px-4">
|
|
<Banner
|
|
type='warning'
|
|
icon={
|
|
<div className="w-12 h-12 rounded-lg bg-orange-50 flex items-center justify-center">
|
|
<Database size={22} className="text-orange-500" />
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
title={
|
|
<div className="flex items-center">
|
|
<span className="font-medium">{t('数据库警告')}</span>
|
|
<Tag color='orange' shape='circle' className="ml-2">
|
|
SQLite
|
|
</Tag>
|
|
</div>
|
|
}
|
|
description={
|
|
<div>
|
|
<p>
|
|
{t(
|
|
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
|
)}
|
|
</p>
|
|
<p className="mt-1">
|
|
<strong>{t(
|
|
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
|
)}</strong>
|
|
</p>
|
|
</div>
|
|
}
|
|
className="!rounded-xl mb-6"
|
|
fullMode={false}
|
|
bordered
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* MySQL数据库提示 */}
|
|
{setupStatus.database_type === 'mysql' && (
|
|
<div className="px-4">
|
|
<Banner
|
|
type='info'
|
|
icon={
|
|
<div className="w-12 h-12 rounded-lg bg-blue-50 flex items-center justify-center">
|
|
<Database size={22} className="text-blue-500" />
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
title={
|
|
<div className="flex items-center">
|
|
<span className="font-medium">{t('数据库信息')}</span>
|
|
<Tag color='blue' shape='circle' className="ml-2">
|
|
MySQL
|
|
</Tag>
|
|
</div>
|
|
}
|
|
description={
|
|
<div>
|
|
<p>
|
|
{t(
|
|
'您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。',
|
|
)}
|
|
</p>
|
|
</div>
|
|
}
|
|
className="!rounded-xl mb-6"
|
|
fullMode={false}
|
|
bordered
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* PostgreSQL数据库提示 */}
|
|
{setupStatus.database_type === 'postgres' && (
|
|
<div className="px-4">
|
|
<Banner
|
|
type='success'
|
|
icon={
|
|
<div className="w-12 h-12 rounded-lg bg-green-50 flex items-center justify-center">
|
|
<Database size={22} className="text-green-500" />
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
title={
|
|
<div className="flex items-center">
|
|
<span className="font-medium">{t('数据库信息')}</span>
|
|
<Tag color='green' shape='circle' className="ml-2">
|
|
PostgreSQL
|
|
</Tag>
|
|
</div>
|
|
}
|
|
description={
|
|
<div>
|
|
<p>
|
|
{t(
|
|
'您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。',
|
|
)}
|
|
</p>
|
|
</div>
|
|
}
|
|
className="!rounded-xl mb-6"
|
|
fullMode={false}
|
|
bordered
|
|
/>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* 主内容区域 */}
|
|
<Form
|
|
getFormApi={(formApi) => {
|
|
formRef.current = formApi;
|
|
console.log('Form API set:', formApi);
|
|
}}
|
|
initValues={formData}
|
|
>
|
|
{/* 管理员账号设置 */}
|
|
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
|
background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
|
|
position: 'relative'
|
|
}}>
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
|
<IconUser size="large" style={{ color: '#ffffff' }} />
|
|
</div>
|
|
<div className="relative">
|
|
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('管理员账号')}</Text>
|
|
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置系统管理员的登录信息')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{setupStatus.root_init ? (
|
|
<>
|
|
<Banner
|
|
type='info'
|
|
icon={
|
|
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center">
|
|
<IconCheckCircleStroked size="large" className="text-blue-500" />
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
description={
|
|
<div className="flex items-center">
|
|
<span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
|
|
</div>
|
|
}
|
|
className="!rounded-lg"
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Form.Input
|
|
field='username'
|
|
label={t('用户名')}
|
|
placeholder={t('请输入管理员用户名')}
|
|
prefix={<IconUser />}
|
|
showClear
|
|
size='large'
|
|
className="mb-4 !rounded-lg"
|
|
noLabel={false}
|
|
validateStatus="default"
|
|
onChange={(value) =>
|
|
setFormData({ ...formData, username: value })
|
|
}
|
|
/>
|
|
<Form.Input
|
|
field='password'
|
|
label={t('密码')}
|
|
placeholder={t('请输入管理员密码')}
|
|
type='password'
|
|
prefix={<IconLock />}
|
|
showClear
|
|
size='large'
|
|
className="mb-4 !rounded-lg"
|
|
noLabel={false}
|
|
mode="password"
|
|
validateStatus="default"
|
|
onChange={(value) =>
|
|
setFormData({ ...formData, password: value })
|
|
}
|
|
/>
|
|
<Form.Input
|
|
field='confirmPassword'
|
|
label={t('确认密码')}
|
|
placeholder={t('请确认管理员密码')}
|
|
type='password'
|
|
prefix={<IconLock />}
|
|
showClear
|
|
size='large'
|
|
className="!rounded-lg"
|
|
noLabel={false}
|
|
mode="password"
|
|
validateStatus="default"
|
|
onChange={(value) =>
|
|
setFormData({ ...formData, confirmPassword: value })
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</Card>
|
|
|
|
{/* 使用模式 */}
|
|
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
|
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
|
position: 'relative'
|
|
}}>
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
|
<Layers size={22} style={{ color: '#ffffff' }} />
|
|
</div>
|
|
<div className="relative">
|
|
<div className="flex items-center">
|
|
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('使用模式')}</Text>
|
|
<Button
|
|
theme='borderless'
|
|
type='tertiary'
|
|
icon={<IconHelpCircle size="small" style={{ color: '#ffffff' }} />}
|
|
size='small'
|
|
onClick={() => setUsageModeInfoVisible(true)}
|
|
className="!rounded-full"
|
|
/>
|
|
</div>
|
|
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('选择适合您使用场景的模式')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Form.RadioGroup
|
|
field='usageMode'
|
|
noLabel={true}
|
|
initValue='external'
|
|
onChange={handleUsageModeChange}
|
|
type='pureCard'
|
|
className="[&_.semi-radio-addon-buttonRadio-wrapper]:!rounded-xl"
|
|
validateStatus="default"
|
|
>
|
|
<div className="space-y-3 mt-2">
|
|
<Form.Radio
|
|
value='external'
|
|
className="!p-4 !rounded-xl hover:!bg-blue-50 transition-colors w-full"
|
|
extra={
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<Rocket size={20} className="text-blue-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-gray-900 mb-1">{t('对外运营模式')}</div>
|
|
<div className="text-sm text-gray-500">{t('适用于为多个用户提供服务的场景')}</div>
|
|
<Tag color='blue' shape='circle' className="mt-2">
|
|
{t('默认模式')}
|
|
</Tag>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
<Form.Radio
|
|
value='self'
|
|
className="!p-4 !rounded-xl hover:!bg-green-50 transition-colors w-full"
|
|
extra={
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<Shield size={20} className="text-green-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-gray-900 mb-1">{t('自用模式')}</div>
|
|
<div className="text-sm text-gray-500">{t('适用于个人使用的场景,不需要设置模型价格')}</div>
|
|
<Tag color='green' shape='circle' className="mt-2">
|
|
{t('无需计费')}
|
|
</Tag>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
<Form.Radio
|
|
value='demo'
|
|
className="!p-4 !rounded-xl hover:!bg-purple-50 transition-colors w-full"
|
|
extra={
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<FlaskConical size={20} className="text-purple-500" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-gray-900 mb-1">{t('演示站点模式')}</div>
|
|
<div className="text-sm text-gray-500">{t('适用于展示系统功能的场景,提供基础功能演示')}</div>
|
|
<Tag color='purple' shape='circle' className="mt-2">
|
|
{t('演示体验')}
|
|
</Tag>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
</Form.RadioGroup>
|
|
</Card>
|
|
</Form>
|
|
|
|
<div className="flex justify-center mt-6">
|
|
<Button
|
|
type='primary'
|
|
onClick={onSubmit}
|
|
loading={loading}
|
|
size='large'
|
|
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"
|
|
icon={<IconCheckCircleStroked />}
|
|
>
|
|
{t('初始化系统')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</Layout.Content>
|
|
</Layout>
|
|
|
|
{/* 使用模式说明模态框 */}
|
|
<Modal
|
|
title={
|
|
<div className="flex items-center">
|
|
<IconInfoCircle className="mr-2 text-blue-500" />
|
|
{t('使用模式说明')}
|
|
</div>
|
|
}
|
|
visible={selfUseModeInfoVisible}
|
|
onOk={() => setUsageModeInfoVisible(false)}
|
|
onCancel={() => setUsageModeInfoVisible(false)}
|
|
closeOnEsc={true}
|
|
okText={t('我已了解')}
|
|
cancelText={null}
|
|
centered={true}
|
|
size='medium'
|
|
className="[&_.semi-modal-body]:!p-6"
|
|
>
|
|
<div className="space-y-6">
|
|
{/* 对外运营模式 */}
|
|
<div className="bg-blue-50 rounded-xl p-4">
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<Rocket size={20} className="text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<Title heading={6} className="text-blue-900 mb-2">{t('对外运营模式')}</Title>
|
|
<div className="space-y-2 text-sm text-gray-700">
|
|
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
|
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
|
<div className="mt-3">
|
|
<Tag color='blue' shape='circle' className="mr-2">{t('计费模式')}</Tag>
|
|
<Tag color='blue' shape='circle'>{t('多用户支持')}</Tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 自用模式 */}
|
|
<div className="bg-green-50 rounded-xl p-4">
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<Shield size={20} className="text-green-600" />
|
|
</div>
|
|
<div>
|
|
<Title heading={6} className="text-green-900 mb-2">{t('自用模式')}</Title>
|
|
<div className="space-y-2 text-sm text-gray-700">
|
|
<p>{t('适用于个人使用的场景。')}</p>
|
|
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
|
<div className="mt-3">
|
|
<Tag color='green' shape='circle' className="mr-2">{t('无需计费')}</Tag>
|
|
<Tag color='green' shape='circle'>{t('个人使用')}</Tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 演示站点模式 */}
|
|
<div className="bg-purple-50 rounded-xl p-4">
|
|
<div className="flex items-start">
|
|
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3 flex-shrink-0">
|
|
<FlaskConical size={20} className="text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<Title heading={6} className="text-purple-900 mb-2">{t('演示站点模式')}</Title>
|
|
<div className="space-y-2 text-sm text-gray-700">
|
|
<p>{t('适用于展示系统功能的场景。')}</p>
|
|
<p>{t('提供基础功能演示,方便用户了解系统特性。')}</p>
|
|
<div className="mt-3">
|
|
<Tag color='purple' shape='circle' className="mr-2">{t('功能演示')}</Tag>
|
|
<Tag color='purple' shape='circle'>{t('体验试用')}</Tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Setup;
|