Merge pull request #132 from legeling/feature/frontend-refactor-vben-admin
fix: 优化登录注册验证流程和修复多个前端问题,以及修改密码失败的问题
This commit is contained in:
commit
05778d8082
@ -22,6 +22,7 @@ import { Logs } from '@/pages/admin/Logs'
|
||||
import { RiskLogs } from '@/pages/admin/RiskLogs'
|
||||
import { DataManagement } from '@/pages/admin/DataManagement'
|
||||
import { verifyToken } from '@/api/auth'
|
||||
import { Toast } from '@/components/common/Toast'
|
||||
|
||||
// Protected route wrapper
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
@ -96,6 +97,8 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
{/* 全局 Toast 组件 */}
|
||||
<Toast />
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { get, post, put, del } from '@/utils/request'
|
||||
import type { ApiResponse, NotificationChannel, MessageNotification } from '@/types'
|
||||
import { del, get, post, put } from '@/utils/request'
|
||||
import type { ApiResponse, MessageNotification, NotificationChannel } from '@/types'
|
||||
|
||||
// ========== 通知渠道 ==========
|
||||
|
||||
@ -90,9 +90,10 @@ export const deleteNotificationChannel = (channelId: string): Promise<ApiRespons
|
||||
return del(`/notification-channels/${channelId}`)
|
||||
}
|
||||
|
||||
// 测试通知渠道
|
||||
export const testNotificationChannel = (channelId: string): Promise<ApiResponse> => {
|
||||
return post(`/notification-channels/${channelId}/test`)
|
||||
// 测试通知渠道 - 后端暂未实现此接口
|
||||
export const testNotificationChannel = async (_channelId: string): Promise<ApiResponse> => {
|
||||
// TODO: 后端暂未实现 POST /notification-channels/{id}/test 接口
|
||||
return { success: false, message: '通知渠道测试功能暂未实现' }
|
||||
}
|
||||
|
||||
// ========== 消息通知 ==========
|
||||
|
||||
@ -20,20 +20,28 @@ export const getSystemSettings = async (): Promise<{ success: boolean; data?: Sy
|
||||
// 更新系统设置
|
||||
export const updateSystemSettings = async (data: Partial<SystemSettings>): Promise<ApiResponse> => {
|
||||
// 逐个更新设置项,确保 value 是字符串
|
||||
const promises = Object.entries(data).map(([key, value]) => {
|
||||
// 将布尔值和数字转换为字符串
|
||||
let stringValue: string
|
||||
if (typeof value === 'boolean') {
|
||||
stringValue = value ? 'true' : 'false'
|
||||
} else if (typeof value === 'number') {
|
||||
stringValue = String(value)
|
||||
} else {
|
||||
stringValue = value as string
|
||||
}
|
||||
return put(`/system-settings/${key}`, { value: stringValue })
|
||||
})
|
||||
await Promise.all(promises)
|
||||
return { success: true, message: '设置已保存' }
|
||||
const promises = Object.entries(data)
|
||||
.filter(([, value]) => value !== undefined && value !== null) // 过滤掉空值
|
||||
.map(([key, value]) => {
|
||||
// 将布尔值和数字转换为字符串
|
||||
let stringValue: string
|
||||
if (typeof value === 'boolean') {
|
||||
stringValue = value ? 'true' : 'false'
|
||||
} else if (typeof value === 'number') {
|
||||
stringValue = String(value)
|
||||
} else {
|
||||
stringValue = String(value ?? '')
|
||||
}
|
||||
return put(`/system-settings/${key}`, { value: stringValue })
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
return { success: true, message: '设置已保存' }
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error)
|
||||
return { success: false, message: '保存设置失败' }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 AI 设置
|
||||
@ -59,8 +67,11 @@ export const testAIConnection = async (cookieId?: string): Promise<ApiResponse>
|
||||
return { success: true, message: `AI 回复: ${result.reply}` }
|
||||
}
|
||||
return { success: result.success ?? true, message: result.message || 'AI 连接测试成功' }
|
||||
} catch (error) {
|
||||
return { success: false, message: 'AI 连接测试失败' }
|
||||
} catch (error: unknown) {
|
||||
// 提取后端返回的错误信息
|
||||
const axiosError = error as { response?: { data?: { detail?: string; message?: string } } }
|
||||
const detail = axiosError.response?.data?.detail || axiosError.response?.data?.message
|
||||
return { success: false, message: detail || 'AI 连接测试失败' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,9 +94,9 @@ export const testEmailSend = async (_email: string): Promise<ApiResponse> => {
|
||||
return { success: false, message: '邮件测试功能暂未实现,请检查 SMTP 配置后直接保存' }
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
// 修改密码(管理员)
|
||||
export const changePassword = async (data: { current_password: string; new_password: string }): Promise<ApiResponse> => {
|
||||
return post('/change-password', data)
|
||||
return post('/change-admin-password', data)
|
||||
}
|
||||
|
||||
// 获取备份文件列表(管理员)
|
||||
|
||||
@ -36,6 +36,7 @@ export function Login() {
|
||||
const [sessionId] = useState(() => `session_${Math.random().toString(36).substr(2, 9)}_${Date.now()}`)
|
||||
const [captchaVerified, setCaptchaVerified] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
const [emailError, setEmailError] = useState('')
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
@ -113,11 +114,12 @@ export function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleVerifyCaptcha = async () => {
|
||||
if (captchaCode.length !== 4) return
|
||||
const handleVerifyCaptcha = async (code?: string) => {
|
||||
const codeToVerify = code || captchaCode
|
||||
if (codeToVerify.length !== 4) return
|
||||
|
||||
try {
|
||||
const result = await verifyCaptcha(sessionId, captchaCode)
|
||||
const result = await verifyCaptcha(sessionId, codeToVerify)
|
||||
if (result.success) {
|
||||
setCaptchaVerified(true)
|
||||
addToast({ type: 'success', message: '验证码验证成功' })
|
||||
@ -131,8 +133,33 @@ export function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
// 邮箱格式校验
|
||||
const isValidEmail = (emailStr: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailStr)
|
||||
|
||||
// 邮箱输入处理
|
||||
const handleEmailChange = (value: string) => {
|
||||
setEmailForCode(value)
|
||||
if (value && !isValidEmail(value)) {
|
||||
setEmailError('请输入正确的邮箱格式')
|
||||
} else {
|
||||
setEmailError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!captchaVerified || !emailForCode || countdown > 0) return
|
||||
if (!emailForCode) {
|
||||
setEmailError('请输入邮箱地址')
|
||||
return
|
||||
}
|
||||
if (!isValidEmail(emailForCode)) {
|
||||
setEmailError('请输入正确的邮箱格式')
|
||||
return
|
||||
}
|
||||
if (!captchaVerified) {
|
||||
addToast({ type: 'warning', message: '请先完成图形验证码验证' })
|
||||
return
|
||||
}
|
||||
if (countdown > 0) return
|
||||
|
||||
try {
|
||||
const result = await sendVerificationCode(emailForCode, 'login', sessionId)
|
||||
@ -395,11 +422,12 @@ export function Login() {
|
||||
<input
|
||||
type="email"
|
||||
value={emailForCode}
|
||||
onChange={(e) => setEmailForCode(e.target.value)}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
placeholder="name@example.com"
|
||||
className="input-ios pl-9"
|
||||
className={cn('input-ios pl-9', emailError && 'border-red-500 focus:border-red-500')}
|
||||
/>
|
||||
</div>
|
||||
{emailError && <p className="text-xs text-red-500 mt-1">{emailError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Captcha */}
|
||||
@ -410,14 +438,20 @@ export function Login() {
|
||||
type="text"
|
||||
value={captchaCode}
|
||||
onChange={(e) => {
|
||||
setCaptchaCode(e.target.value)
|
||||
if (e.target.value.length === 4) {
|
||||
setTimeout(handleVerifyCaptcha, 100)
|
||||
const val = e.target.value.toUpperCase()
|
||||
setCaptchaCode(val)
|
||||
// 输入4位后自动验证
|
||||
if (val.length === 4 && !captchaVerified) {
|
||||
handleVerifyCaptcha(val)
|
||||
}
|
||||
}}
|
||||
placeholder="输入验证码"
|
||||
maxLength={4}
|
||||
className="input-ios flex-1"
|
||||
className={cn(
|
||||
'input-ios flex-1',
|
||||
captchaVerified && 'border-green-500 bg-green-50 dark:bg-green-900/20'
|
||||
)}
|
||||
disabled={captchaVerified}
|
||||
/>
|
||||
<img
|
||||
src={captchaImage}
|
||||
@ -430,7 +464,7 @@ export function Login() {
|
||||
'text-xs',
|
||||
captchaVerified ? 'text-green-600' : 'text-gray-400'
|
||||
)}>
|
||||
{captchaVerified ? '✓ 验证成功' : '点击图片更换验证码'}
|
||||
{captchaVerified ? '✓ 验证成功,可以发送邮箱验证码' : '输入4位验证码后自动验证'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -452,7 +486,7 @@ export function Login() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendCode}
|
||||
disabled={!captchaVerified || !emailForCode || countdown > 0}
|
||||
disabled={countdown > 0}
|
||||
className="btn-ios-secondary whitespace-nowrap"
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : '发送'}
|
||||
|
||||
@ -27,6 +27,7 @@ export function Register() {
|
||||
const [sessionId] = useState(() => `session_${Math.random().toString(36).substr(2, 9)}_${Date.now()}`)
|
||||
const [captchaVerified, setCaptchaVerified] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
const [emailError, setEmailError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getRegistrationStatus()
|
||||
@ -63,11 +64,12 @@ export function Register() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleVerifyCaptcha = async () => {
|
||||
if (captchaCode.length !== 4) return
|
||||
const handleVerifyCaptcha = async (code?: string) => {
|
||||
const codeToVerify = code || captchaCode
|
||||
if (codeToVerify.length !== 4) return
|
||||
|
||||
try {
|
||||
const result = await verifyCaptcha(sessionId, captchaCode)
|
||||
const result = await verifyCaptcha(sessionId, codeToVerify)
|
||||
if (result.success) {
|
||||
setCaptchaVerified(true)
|
||||
addToast({ type: 'success', message: '验证码验证成功' })
|
||||
@ -81,8 +83,33 @@ export function Register() {
|
||||
}
|
||||
}
|
||||
|
||||
// 邮箱格式校验
|
||||
const isValidEmail = (emailStr: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailStr)
|
||||
|
||||
// 邮箱输入处理
|
||||
const handleEmailChange = (value: string) => {
|
||||
setEmail(value)
|
||||
if (value && !isValidEmail(value)) {
|
||||
setEmailError('请输入正确的邮箱格式')
|
||||
} else {
|
||||
setEmailError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!captchaVerified || !email || countdown > 0) return
|
||||
if (!email) {
|
||||
setEmailError('请输入邮箱地址')
|
||||
return
|
||||
}
|
||||
if (!isValidEmail(email)) {
|
||||
setEmailError('请输入正确的邮箱格式')
|
||||
return
|
||||
}
|
||||
if (!captchaVerified) {
|
||||
addToast({ type: 'warning', message: '请先完成图形验证码验证' })
|
||||
return
|
||||
}
|
||||
if (countdown > 0) return
|
||||
|
||||
try {
|
||||
const result = await sendVerificationCode(email, 'register', sessionId)
|
||||
@ -194,11 +221,12 @@ export function Register() {
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={(e) => handleEmailChange(e.target.value)}
|
||||
placeholder="name@example.com"
|
||||
className="input-ios pl-9"
|
||||
className={cn('input-ios pl-9', emailError && 'border-red-500 focus:border-red-500')}
|
||||
/>
|
||||
</div>
|
||||
{emailError && <p className="text-xs text-red-500 mt-1">{emailError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
@ -246,14 +274,19 @@ export function Register() {
|
||||
type="text"
|
||||
value={captchaCode}
|
||||
onChange={(e) => {
|
||||
setCaptchaCode(e.target.value)
|
||||
if (e.target.value.length === 4) {
|
||||
setTimeout(handleVerifyCaptcha, 100)
|
||||
const val = e.target.value.toUpperCase()
|
||||
setCaptchaCode(val)
|
||||
if (val.length === 4 && !captchaVerified) {
|
||||
handleVerifyCaptcha(val)
|
||||
}
|
||||
}}
|
||||
disabled={captchaVerified}
|
||||
placeholder="输入验证码"
|
||||
maxLength={4}
|
||||
className="input-ios flex-1"
|
||||
className={cn(
|
||||
'input-ios flex-1',
|
||||
captchaVerified && 'border-green-500 bg-green-50 dark:bg-green-900/20',
|
||||
)}
|
||||
/>
|
||||
<img
|
||||
src={captchaImage}
|
||||
@ -266,7 +299,7 @@ export function Register() {
|
||||
'text-xs',
|
||||
captchaVerified ? 'text-green-600 dark:text-green-400' : 'text-slate-400'
|
||||
)}>
|
||||
{captchaVerified ? '✓ 验证成功' : '点击图片更换验证码'}
|
||||
{captchaVerified ? '✓ 验证成功,可以发送邮箱验证码' : '输入4位验证码后自动验证'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -288,7 +321,7 @@ export function Register() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendCode}
|
||||
disabled={!captchaVerified || !email || countdown > 0}
|
||||
disabled={countdown > 0}
|
||||
className="btn-ios-secondary whitespace-nowrap"
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : '发送'}
|
||||
|
||||
@ -345,11 +345,11 @@ export function Items() {
|
||||
</tr>
|
||||
) : (
|
||||
filteredItems.map((item) => (
|
||||
<tr key={item.id} className={selectedIds.has(item.id) ? 'bg-blue-50' : ''}>
|
||||
<tr key={item.id} className={selectedIds.has(item.id) ? 'bg-blue-50 dark:bg-blue-900/30' : ''}>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => toggleSelect(item.id)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
{selectedIds.has(item.id) ? (
|
||||
<CheckSquare className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
|
||||
@ -92,6 +92,12 @@ export default defineConfig(({ command }) => ({
|
||||
'/register': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
bypass: (req) => {
|
||||
// 浏览器直接访问时返回前端页面,只有 POST 请求才代理到后端
|
||||
if (req.method === 'GET' && req.headers.accept?.includes('text/html')) {
|
||||
return '/index.html'
|
||||
}
|
||||
},
|
||||
},
|
||||
'/itemReplays': {
|
||||
target: 'http://localhost:8080',
|
||||
@ -149,6 +155,18 @@ export default defineConfig(({ command }) => ({
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/change-admin-password': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/logout': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/user-settings': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/search': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
@ -158,8 +176,12 @@ export default defineConfig(({ command }) => ({
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
bypass: (req) => {
|
||||
// 浏览器直接访问(Accept 包含 text/html)时,让前端路由处理
|
||||
if (req.headers.accept?.includes('text/html')) {
|
||||
// 只有浏览器直接访问 /items 路径时才返回前端页面
|
||||
// API 请求通常是 /items/xxx 或带有 application/json
|
||||
const isApiRequest = req.url !== '/items' ||
|
||||
req.headers.accept?.includes('application/json') ||
|
||||
req.headers['content-type']?.includes('application/json')
|
||||
if (!isApiRequest && req.headers.accept?.includes('text/html')) {
|
||||
return '/index.html'
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user