🎨 精简完善系统

This commit is contained in:
2026-04-10 17:57:48 +08:00
parent ee6565371e
commit 82c5020e71
55 changed files with 5785 additions and 9712 deletions

View File

@@ -0,0 +1,305 @@
import { useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Alert, Button, Card, Form, Input, Result, Select, Space, Spin, Typography, message } from 'antd'
import { initApi } from '@/lib/api'
import { useAuthStore } from '@/store/auth'
import type { InitCheckResult, InitDBPayload } from '@/types/system'
const dbTypeOptions: Array<{ label: string; value: InitDBPayload['dbType'] }> = [
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'pgsql' },
{ label: 'SQLite', value: 'sqlite' },
{ label: 'MSSQL', value: 'mssql' },
]
type InitState = 'checking' | 'required' | 'ready'
const dbDefaults: Record<InitDBPayload['dbType'], Partial<InitDBPayload>> = {
mysql: {
host: '127.0.0.1',
port: '3306',
},
pgsql: {
host: '127.0.0.1',
port: '5432',
template: 'template1',
},
sqlite: {
dbPath: 'db',
},
mssql: {
host: '127.0.0.1',
port: '1433',
},
}
function applyDbDefaults(currentValues: InitDBPayload, nextDbType: InitDBPayload['dbType']): InitDBPayload {
return {
...currentValues,
...dbDefaults[nextDbType],
dbType: nextDbType,
}
}
export function InitPage() {
const navigate = useNavigate()
const token = useAuthStore((state) => state.token)
const [form] = Form.useForm<InitDBPayload>()
const [state, setState] = useState<InitState>('checking')
const [checkingError, setCheckingError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
const dbType = Form.useWatch('dbType', form) ?? 'mysql'
const checkInitialization = async () => {
try {
const response = await initApi.checkDB()
const data: InitCheckResult = response.data
if (data.needInit) {
setState('required')
} else {
setState('ready')
}
setCheckingError(null)
} catch (error) {
const messageText = error instanceof Error ? error.message : '初始化状态检测失败'
setCheckingError(messageText)
}
}
useEffect(() => {
void checkInitialization()
}, [])
const helperText = useMemo(() => {
switch (dbType) {
case 'mysql':
return '将创建数据库并写回 MySQL 连接配置。'
case 'pgsql':
return '默认使用 PostgreSQL 公共库建库,可按需指定 template。'
case 'sqlite':
return '将创建本地 SQLite 数据库文件,并写回 sqlite 配置。'
case 'mssql':
return '将按当前连接信息接入 MSSQL并写回 mssql 配置。'
default:
return ''
}
}, [dbType])
const onDbTypeChange = (nextDbType: InitDBPayload['dbType']) => {
const currentValues = form.getFieldsValue()
form.setFieldsValue(applyDbDefaults(currentValues, nextDbType))
}
const submit = async () => {
const values = await form.validateFields()
setSubmitting(true)
try {
await initApi.initDB(values)
message.success('初始化完成,请使用 admin 账户登录')
navigate('/login', { replace: true })
} finally {
setSubmitting(false)
}
}
if (state === 'checking') {
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Project Bootstrap</span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
<br />
</Typography.Title>
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
使
</Typography.Paragraph>
</div>
</section>
<section className="login-form-wrap">
<Card className="glass-panel login-card" bordered={false}>
<div className="fullscreen-status" style={{ minHeight: 360 }}>
<Spin size="large" />
<span>...</span>
{checkingError ? (
<Alert
type="warning"
showIcon
message="检测失败"
description={checkingError}
action={
<Button size="small" onClick={() => void checkInitialization()}>
</Button>
}
/>
) : null}
</div>
</Card>
</section>
</div>
)
}
if (state === 'ready') {
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Project Bootstrap</span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
<br />
</Typography.Title>
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
</Typography.Paragraph>
</div>
</section>
<section className="login-form-wrap">
<Card className="glass-panel login-card" bordered={false}>
<Result
status="success"
title="无需再次初始化"
subTitle="如果需要调整系统配置,请登录后台后在系统配置页修改。"
extra={[
<Button key="login" type="primary" onClick={() => navigate(token ? '/' : '/login', { replace: true })}>
{token ? '进入后台' : '前往登录'}
</Button>,
<Button key="check" onClick={() => void checkInitialization()}>
</Button>,
]}
/>
</Card>
</section>
</div>
)
}
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Project Bootstrap</span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
<br />
</Typography.Title>
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
</Typography.Paragraph>
</div>
</section>
<section className="login-form-wrap">
<Card className="glass-panel login-card" bordered={false}>
<Typography.Title level={2} style={{ marginBottom: 8 }}>
</Typography.Title>
<Typography.Paragraph className="text-muted" style={{ marginBottom: 20 }}>
`admin`使
</Typography.Paragraph>
{checkingError ? (
<Alert
type="warning"
showIcon
message="状态检测存在异常"
description={checkingError}
style={{ marginBottom: 16 }}
/>
) : null}
<Form
form={form}
layout="vertical"
initialValues={{
adminPassword: '',
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
dbName: 'go_web_template',
userName: 'root',
dbPath: 'db',
template: 'template1',
}}
onFinish={submit}
>
<Form.Item name="dbType" label="数据库类型" rules={[{ required: true, message: '请选择数据库类型' }]}>
<Select options={dbTypeOptions} onChange={onDbTypeChange} />
</Form.Item>
<Typography.Paragraph className="text-muted" style={{ marginTop: -8, marginBottom: 16 }}>
{helperText}
</Typography.Paragraph>
<div className="init-form-grid">
{dbType === 'sqlite' ? (
<Form.Item
name="dbPath"
label="数据库目录"
rules={[{ required: true, message: '请输入 SQLite 数据库目录' }]}
>
<Input placeholder="例如 db" />
</Form.Item>
) : (
<>
<Form.Item name="host" label="数据库地址" rules={[{ required: true, message: '请输入数据库地址' }]}>
<Input placeholder="例如 127.0.0.1" />
</Form.Item>
<Form.Item name="port" label="数据库端口" rules={[{ required: true, message: '请输入数据库端口' }]}>
<Input placeholder="例如 3306" />
</Form.Item>
<Form.Item
name="userName"
label="数据库用户名"
rules={[{ required: true, message: '请输入数据库用户名' }]}
>
<Input placeholder="例如 root" />
</Form.Item>
<Form.Item name="password" label="数据库密码">
<Input.Password placeholder="请输入数据库密码" />
</Form.Item>
</>
)}
<Form.Item name="dbName" label="数据库名" rules={[{ required: true, message: '请输入数据库名' }]}>
<Input placeholder={dbType === 'sqlite' ? '例如 go_web_template' : '例如 go_web_template'} />
</Form.Item>
{dbType === 'pgsql' ? (
<Form.Item name="template" label="建库模板">
<Input placeholder="默认 template1" />
</Form.Item>
) : (
<div />
)}
<Form.Item
name="adminPassword"
label="管理员密码"
rules={[
{ required: true, message: '请输入管理员密码' },
{ min: 6, message: '管理员密码至少 6 位' },
]}
>
<Input.Password placeholder="初始化后 admin 账户使用此密码登录" />
</Form.Item>
</div>
<Alert
type="info"
showIcon
style={{ marginBottom: 20 }}
message="初始化完成后,后端会立即持有数据库连接,并将连接配置写回当前配置文件。"
/>
<Space wrap>
<Button type="primary" htmlType="submit" loading={submitting}>
</Button>
<Button onClick={() => void checkInitialization()}></Button>
<Button onClick={() => navigate('/login', { replace: true })}></Button>
</Space>
</Form>
</Card>
</section>
</div>
)
}

View File

@@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { Alert, Button, Card, Form, Input, Typography, message } from 'antd'
import { authApi, menuApi } from '@/lib/api'
import { Alert, Button, Card, Form, Input, Result, Spin, Typography, message } from 'antd'
import { authApi, initApi, menuApi } from '@/lib/api'
import { buildFullMenus, findDefaultRoute } from '@/lib/menu'
import { useAuthStore } from '@/store/auth'
import type { CaptchaInfo } from '@/types/system'
import type { CaptchaInfo, InitCheckResult } from '@/types/system'
type LoginForm = {
username: string
@@ -34,6 +34,9 @@ export function LoginPage() {
const [captcha, setCaptcha] = useState<CaptchaInfo | null>(null)
const [submitting, setSubmitting] = useState(false)
const [loadCaptchaError, setLoadCaptchaError] = useState<string | null>(null)
const [checkingInit, setCheckingInit] = useState(true)
const [needInit, setNeedInit] = useState(false)
const [loadInitError, setLoadInitError] = useState<string | null>(null)
const redirectTarget = useMemo(() => {
const state = location.state as { redirectTo?: string } | null
@@ -51,8 +54,38 @@ export function LoginPage() {
}
}
const checkInitialization = async () => {
try {
setCheckingInit(true)
const response = await initApi.checkDB()
const data: InitCheckResult = response.data
setNeedInit(data.needInit)
setLoadInitError(null)
return data.needInit
} catch (error) {
const messageText = error instanceof Error ? error.message : '初始化状态检测失败'
setLoadInitError(messageText)
return false
} finally {
setCheckingInit(false)
}
}
useEffect(() => {
fetchCaptcha()
let ignore = false
const prepare = async () => {
const required = await checkInitialization()
if (!required && !ignore) {
await fetchCaptcha()
}
}
void prepare()
return () => {
ignore = true
}
}, [])
const submit = async () => {
@@ -87,10 +120,84 @@ export function LoginPage() {
}
}
if (checkingInit) {
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Gin-React-Admin · React </span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
<br />
</Typography.Title>
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
</Typography.Paragraph>
</div>
</section>
<section className="login-form-wrap">
<Card className="glass-panel login-card" bordered={false}>
<div className="fullscreen-status" style={{ minHeight: 360 }}>
<Spin size="large" />
<span>...</span>
</div>
</Card>
</section>
</div>
)
}
if (needInit) {
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Gin-React-Admin · React </span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
<br />
</Typography.Title>
<Typography.Paragraph style={{ color: 'rgba(255,255,255,0.78)', fontSize: 18, lineHeight: 1.8 }}>
使
</Typography.Paragraph>
</div>
</section>
<section className="login-form-wrap">
<Card className="glass-panel login-card" bordered={false}>
<Result
status="warning"
title="检测到项目尚未初始化"
subTitle="当前登录流程不可用。请先进入初始化页,完成数据库和管理员账号初始化。"
extra={[
<Button key="init" type="primary" onClick={() => navigate('/init', { replace: true })}>
</Button>,
<Button key="retry" onClick={() => void checkInitialization()}>
</Button>,
]}
/>
{loadInitError ? (
<Alert
type="warning"
showIcon
message="初始化状态检测异常"
description={loadInitError}
style={{ marginTop: 16 }}
/>
) : null}
</Card>
</section>
</div>
)
}
return (
<div className="login-shell">
<section className="login-hero">
<span className="capsule">Gin-Vue-Admin · React </span>
<span className="capsule">Gin-React-Admin · React </span>
<div style={{ maxWidth: 620, marginTop: 72 }}>
<Typography.Title style={{ color: 'rgba(255,255,255,0.96)', fontSize: 52, marginBottom: 18 }}>
@@ -110,6 +217,20 @@ export function LoginPage() {
<Typography.Paragraph className="text-muted">
沿
</Typography.Paragraph>
{loadInitError ? (
<Alert
type="warning"
showIcon
message="初始化状态检测失败"
description={loadInitError}
action={
<Button size="small" onClick={() => void checkInitialization()}>
</Button>
}
style={{ marginBottom: 16 }}
/>
) : null}
{loadCaptchaError ? (
<Alert
type="warning"