🎨 精简完善系统
This commit is contained in:
305
web-admin/src/features/auth/InitPage.tsx
Normal file
305
web-admin/src/features/auth/InitPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user