优化UI风格并添加管理员权限系统
主要更改: - 新增管理员权限系统:添加 AdminAuth 中间件和 AdminRoute 组件,限制题库管理功能仅 yanlongqi 用户可访问 - UI 全面改版为白色毛玻璃风格(macOS 风格):应用毛玻璃效果、优化圆角和阴影、统一配色方案 - 登录页优化:将注册功能改为模态框形式,简化登录界面 - 首页优化:题库管理入口仅对管理员用户显示,优化响应式布局和卡片排列 - 移除底部导航栏:简化布局,改善用户体验 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5d1b088e06
commit
c0a280132c
@ -55,3 +55,31 @@ func Auth() gin.HandlerFunc {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminAuth 管理员认证中间件(必须在Auth中间件之后使用)
|
||||||
|
func AdminAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 获取用户名(由 Auth 中间件设置)
|
||||||
|
username, exists := c.Get("username")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "未登录",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为管理员用户
|
||||||
|
if username != "yanlongqi" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无权限访问",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
main.go
13
main.go
@ -46,11 +46,6 @@ func main() {
|
|||||||
auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案
|
auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案
|
||||||
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
||||||
|
|
||||||
// 题库管理API(需要认证)
|
|
||||||
auth.POST("/practice/questions", handlers.CreatePracticeQuestion) // 创建题目
|
|
||||||
auth.PUT("/practice/questions/:id", handlers.UpdatePracticeQuestion) // 更新题目
|
|
||||||
auth.DELETE("/practice/questions/:id", handlers.DeletePracticeQuestion) // 删除题目
|
|
||||||
|
|
||||||
// 错题本相关API
|
// 错题本相关API
|
||||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
||||||
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计
|
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计
|
||||||
@ -59,6 +54,14 @@ func main() {
|
|||||||
auth.PUT("/wrong-questions/:id/mastered", handlers.MarkWrongQuestionMastered) // 标记已掌握
|
auth.PUT("/wrong-questions/:id/mastered", handlers.MarkWrongQuestionMastered) // 标记已掌握
|
||||||
auth.DELETE("/wrong-questions", handlers.ClearWrongQuestions) // 清空错题本
|
auth.DELETE("/wrong-questions", handlers.ClearWrongQuestions) // 清空错题本
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 题库管理API(需要管理员权限)
|
||||||
|
admin := api.Group("", middleware.Auth(), middleware.AdminAuth())
|
||||||
|
{
|
||||||
|
admin.POST("/practice/questions", handlers.CreatePracticeQuestion) // 创建题目
|
||||||
|
admin.PUT("/practice/questions/:id", handlers.UpdatePracticeQuestion) // 更新题目
|
||||||
|
admin.DELETE("/practice/questions/:id", handlers.DeletePracticeQuestion) // 删除题目
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 静态文件服务(必须在 API 路由之后)
|
// 静态文件服务(必须在 API 路由之后)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||||
import TabBarLayout from './components/TabBarLayout'
|
import TabBarLayout from './components/TabBarLayout'
|
||||||
import ProtectedRoute from './components/ProtectedRoute'
|
import ProtectedRoute from './components/ProtectedRoute'
|
||||||
|
import AdminRoute from './components/AdminRoute'
|
||||||
import QuestionPage from './pages/Question'
|
import QuestionPage from './pages/Question'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Home from './pages/Home'
|
import Home from './pages/Home'
|
||||||
@ -21,7 +22,15 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
{/* 不带TabBar的页面,但需要登录保护 */}
|
{/* 不带TabBar的页面,但需要登录保护 */}
|
||||||
<Route path="/wrong-questions" element={<ProtectedRoute><WrongQuestions /></ProtectedRoute>} />
|
<Route path="/wrong-questions" element={<ProtectedRoute><WrongQuestions /></ProtectedRoute>} />
|
||||||
<Route path="/question-management" element={<ProtectedRoute><QuestionManagement /></ProtectedRoute>} />
|
|
||||||
|
{/* 题库管理页面,需要管理员权限 */}
|
||||||
|
<Route path="/question-management" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AdminRoute>
|
||||||
|
<QuestionManagement />
|
||||||
|
</AdminRoute>
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* 不带TabBar的页面,不需要登录保护 */}
|
{/* 不带TabBar的页面,不需要登录保护 */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|||||||
47
web/src/components/AdminRoute.tsx
Normal file
47
web/src/components/AdminRoute.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Navigate } from 'react-router-dom'
|
||||||
|
import { message } from 'antd'
|
||||||
|
|
||||||
|
interface AdminRouteProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminRoute: React.FC<AdminRouteProps> = ({ children }) => {
|
||||||
|
const [isAdmin, setIsAdmin] = useState<boolean | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查用户信息
|
||||||
|
const userStr = localStorage.getItem('user')
|
||||||
|
if (!userStr) {
|
||||||
|
setIsAdmin(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userStr)
|
||||||
|
if (user.username === 'yanlongqi') {
|
||||||
|
setIsAdmin(true)
|
||||||
|
} else {
|
||||||
|
setIsAdmin(false)
|
||||||
|
message.error('无权限访问该页面')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setIsAdmin(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 正在检查权限时,不显示任何内容
|
||||||
|
if (isAdmin === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是管理员,重定向到首页
|
||||||
|
if (!isAdmin) {
|
||||||
|
return <Navigate to="/" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是管理员,显示子组件
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminRoute
|
||||||
@ -1,5 +1,6 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@ -13,8 +14,14 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: white;
|
background: rgba(255, 255, 255, 0.85);
|
||||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 -2px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 -1px 4px rgba(0, 0, 0, 0.02),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border-top: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
|
|||||||
@ -1,61 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React from 'react'
|
||||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { Layout, Menu } from 'antd'
|
import { Layout } from 'antd'
|
||||||
import {
|
|
||||||
HomeOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import styles from './TabBarLayout.module.less'
|
import styles from './TabBarLayout.module.less'
|
||||||
|
|
||||||
const { Footer, Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
const TabBarLayout: React.FC = () => {
|
const TabBarLayout: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setIsMobile(window.innerWidth <= 768)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
return () => window.removeEventListener('resize', handleResize)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{
|
|
||||||
key: '/',
|
|
||||||
icon: <HomeOutlined />,
|
|
||||||
label: '首页',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '/question',
|
|
||||||
icon: <FileTextOutlined />,
|
|
||||||
label: '答题',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleMenuClick = (key: string) => {
|
|
||||||
navigate(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={styles.layout}>
|
<Layout className={styles.layout}>
|
||||||
<Content className={styles.content}>
|
<Content className={styles.content}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Content>
|
</Content>
|
||||||
{isMobile && (
|
|
||||||
<Footer className={styles.footer}>
|
|
||||||
<Menu
|
|
||||||
mode="horizontal"
|
|
||||||
selectedKeys={[location.pathname]}
|
|
||||||
items={menuItems}
|
|
||||||
onClick={({ key }) => handleMenuClick(key)}
|
|
||||||
className={styles.menu}
|
|
||||||
/>
|
|
||||||
</Footer>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fafafa;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,8 +20,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 16px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fafafa;
|
||||||
padding: 24px;
|
padding: 20px;
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,29 +9,35 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 24px;
|
||||||
color: white;
|
|
||||||
|
|
||||||
.headerLeft {
|
.headerLeft {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: white !important;
|
color: #1d1d1f !important;
|
||||||
margin-bottom: 8px !important;
|
margin-bottom: 4px !important;
|
||||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: #6e6e73;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userInfo {
|
.userInfo {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.85);
|
||||||
padding: 12px 20px;
|
padding: 10px 16px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(30px) saturate(180%);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.02),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
.userDetails {
|
.userDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -39,12 +45,13 @@
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
.userNickname {
|
.userNickname {
|
||||||
color: white !important;
|
color: #1d1d1f !important;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userUsername {
|
.userUsername {
|
||||||
color: rgba(255, 255, 255, 0.8) !important;
|
color: #6e6e73 !important;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,119 +59,284 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.statsCard {
|
.statsCard {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 24px;
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 12px rgba(0, 0, 0, 0.05),
|
||||||
|
0 1px 4px rgba(0, 0, 0, 0.03),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-statistic-title {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeSection {
|
.typeSection {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
color: white !important;
|
color: #1d1d1f !important;
|
||||||
margin-bottom: 16px !important;
|
margin-bottom: 16px !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeCard {
|
.typeCard {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
transition: all 0.3s ease;
|
backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 12px rgba(0, 0, 0, 0.05),
|
||||||
|
0 1px 4px rgba(0, 0, 0, 0.03),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||||
|
0 4px 16px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeIcon {
|
.typeIcon {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeTitle {
|
.typeTitle {
|
||||||
margin: 8px 0 !important;
|
margin: 6px 0 4px 0 !important;
|
||||||
|
color: #1d1d1f;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeDesc {
|
.typeDesc {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
color: #6e6e73;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.quickStart {
|
.quickStart {
|
||||||
|
.sectionTitle {
|
||||||
|
color: #1d1d1f !important;
|
||||||
|
margin-bottom: 16px !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.quickCard {
|
.quickCard {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
transition: all 0.3s ease;
|
backdrop-filter: blur(30px) saturate(180%);
|
||||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 12px rgba(0, 0, 0, 0.05),
|
||||||
|
0 1px 4px rgba(0, 0, 0, 0.03),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||||
|
0 4px 16px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计 - 移动端
|
// 响应式设计 - 移动端 (< 768px)
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
padding-bottom: 70px;
|
padding-bottom: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
.headerLeft {
|
.headerLeft {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 24px !important;
|
font-size: 22px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userInfo {
|
.userInfo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-space {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.statsCard {
|
.statsCard {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-statistic-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-statistic-content {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeSection {
|
.typeSection {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 18px !important;
|
font-size: 16px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCard {
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.typeIcon {
|
||||||
|
font-size: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeTitle {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeDesc {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickStart {
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 16px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickCard {
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-space {
|
||||||
|
gap: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计 - 平板
|
// 响应式设计 - 平板 (769px - 1024px)
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 32px;
|
padding: 24px;
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式设计 - PC端
|
|
||||||
@media (min-width: 1025px) {
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px;
|
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
.title {
|
.title {
|
||||||
|
font-size: 28px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCard {
|
||||||
|
.typeIcon {
|
||||||
font-size: 36px !important;
|
font-size: 36px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - PC端宽屏 (> 1025px)
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 40px;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsCard {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeSection {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quickStart {
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 超宽屏 (> 1600px)
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -208,9 +208,9 @@ const Home: React.FC = () => {
|
|||||||
<Title level={4} className={styles.sectionTitle}>
|
<Title level={4} className={styles.sectionTitle}>
|
||||||
<FileTextOutlined /> 选择题型
|
<FileTextOutlined /> 选择题型
|
||||||
</Title>
|
</Title>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[12, 12]}>
|
||||||
{questionTypes.map(type => (
|
{questionTypes.map(type => (
|
||||||
<Col xs={24} sm={12} md={8} lg={8} key={type.key}>
|
<Col xs={12} sm={12} md={8} lg={6} xl={4} key={type.key}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
className={styles.typeCard}
|
className={styles.typeCard}
|
||||||
@ -218,11 +218,11 @@ const Home: React.FC = () => {
|
|||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '24px',
|
padding: '20px 12px',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.typeIcon} style={{ color: type.color, fontSize: '48px' }}>
|
<div className={styles.typeIcon} style={{ color: type.color, fontSize: '40px' }}>
|
||||||
{type.icon}
|
{type.icon}
|
||||||
</div>
|
</div>
|
||||||
<Title level={5} className={styles.typeTitle}>{type.title}</Title>
|
<Title level={5} className={styles.typeTitle}>{type.title}</Title>
|
||||||
@ -235,55 +235,67 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
{/* 快速开始 */}
|
{/* 快速开始 */}
|
||||||
<div className={styles.quickStart}>
|
<div className={styles.quickStart}>
|
||||||
<Card
|
<Title level={4} className={styles.sectionTitle}>
|
||||||
hoverable
|
<RocketOutlined /> 快速开始
|
||||||
className={styles.quickCard}
|
</Title>
|
||||||
onClick={() => navigate('/question')}
|
<Row gutter={[12, 12]}>
|
||||||
>
|
<Col xs={24} sm={24} md={12} lg={8}>
|
||||||
<Space align="center" size="large">
|
<Card
|
||||||
<div className={styles.quickIcon}>
|
hoverable
|
||||||
<RocketOutlined style={{ fontSize: '32px', color: '#722ed1' }} />
|
className={styles.quickCard}
|
||||||
</div>
|
onClick={() => navigate('/question')}
|
||||||
<div>
|
>
|
||||||
<Title level={5} style={{ margin: 0 }}>随机练习</Title>
|
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||||
<Paragraph type="secondary" style={{ margin: 0 }}>从所有题型中随机抽取</Paragraph>
|
<div className={styles.quickIcon}>
|
||||||
</div>
|
<RocketOutlined style={{ fontSize: '32px', color: '#722ed1' }} />
|
||||||
</Space>
|
</div>
|
||||||
</Card>
|
<div style={{ flex: 1 }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>随机练习</Title>
|
||||||
|
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>从所有题型中随机抽取</Paragraph>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Card
|
<Col xs={24} sm={24} md={12} lg={8}>
|
||||||
hoverable
|
<Card
|
||||||
className={styles.quickCard}
|
hoverable
|
||||||
onClick={() => navigate('/wrong-questions')}
|
className={styles.quickCard}
|
||||||
style={{ marginTop: '16px' }}
|
onClick={() => navigate('/wrong-questions')}
|
||||||
>
|
>
|
||||||
<Space align="center" size="large">
|
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||||
<div className={styles.quickIcon}>
|
<div className={styles.quickIcon}>
|
||||||
<BookOutlined style={{ fontSize: '32px', color: '#ff4d4f' }} />
|
<BookOutlined style={{ fontSize: '32px', color: '#ff4d4f' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ flex: 1 }}>
|
||||||
<Title level={5} style={{ margin: 0 }}>错题本</Title>
|
<Title level={5} style={{ margin: 0 }}>错题本</Title>
|
||||||
<Paragraph type="secondary" style={{ margin: 0 }}>复习错题,巩固薄弱知识点</Paragraph>
|
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>复习错题,巩固薄弱知识点</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Card
|
{/* 仅 yanlongqi 用户显示题库管理 */}
|
||||||
hoverable
|
{userInfo?.username === 'yanlongqi' && (
|
||||||
className={styles.quickCard}
|
<Col xs={24} sm={24} md={12} lg={8}>
|
||||||
onClick={() => navigate('/question-management')}
|
<Card
|
||||||
style={{ marginTop: '16px' }}
|
hoverable
|
||||||
>
|
className={styles.quickCard}
|
||||||
<Space align="center" size="large">
|
onClick={() => navigate('/question-management')}
|
||||||
<div className={styles.quickIcon}>
|
>
|
||||||
<SettingOutlined style={{ fontSize: '32px', color: '#13c2c2' }} />
|
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||||
</div>
|
<div className={styles.quickIcon}>
|
||||||
<div>
|
<SettingOutlined style={{ fontSize: '32px', color: '#13c2c2' }} />
|
||||||
<Title level={5} style={{ margin: 0 }}>题库管理</Title>
|
</div>
|
||||||
<Paragraph type="secondary" style={{ margin: 0 }}>添加、编辑和删除题目</Paragraph>
|
<div style={{ flex: 1 }}>
|
||||||
</div>
|
<Title level={5} style={{ margin: 0 }}>题库管理</Title>
|
||||||
</Space>
|
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>添加、编辑和删除题目</Paragraph>
|
||||||
</Card>
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fafafa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,8 +13,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 16px;
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 24px rgba(0, 0, 0, 0.06),
|
||||||
|
0 2px 12px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
@ -30,14 +37,27 @@
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 0 0 8px 0 !important;
|
margin: 0 0 8px 0 !important;
|
||||||
color: #667eea !important;
|
color: #007aff !important;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: block;
|
display: block;
|
||||||
|
color: #6e6e73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerTip {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-typography {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计 - 移动端
|
// 响应式设计 - 移动端
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Form, Input, Button, Card, Tabs, message, Typography } from 'antd'
|
import { Form, Input, Button, Card, Modal, message, Typography } from 'antd'
|
||||||
import { UserOutlined, LockOutlined, IdcardOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined, IdcardOutlined } from '@ant-design/icons'
|
||||||
import styles from './Login.module.less'
|
import styles from './Login.module.less'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text, Link } = Typography
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
@ -21,8 +21,8 @@ interface LoginResponse {
|
|||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [activeTab, setActiveTab] = useState('login')
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [registerModalVisible, setRegisterModalVisible] = useState(false)
|
||||||
const [loginForm] = Form.useForm()
|
const [loginForm] = Form.useForm()
|
||||||
const [registerForm] = Form.useForm()
|
const [registerForm] = Form.useForm()
|
||||||
|
|
||||||
@ -84,6 +84,7 @@ const Login: React.FC = () => {
|
|||||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||||
|
|
||||||
message.success('注册成功')
|
message.success('注册成功')
|
||||||
|
setRegisterModalVisible(false)
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} else {
|
} else {
|
||||||
message.error(data.message || '注册失败')
|
message.error(data.message || '注册失败')
|
||||||
@ -96,94 +97,6 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginTabContent = (
|
|
||||||
<Form
|
|
||||||
form={loginForm}
|
|
||||||
name="login"
|
|
||||||
onFinish={handleLogin}
|
|
||||||
autoComplete="off"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
prefix={<LockOutlined />}
|
|
||||||
placeholder="请输入密码"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
|
|
||||||
const registerTabContent = (
|
|
||||||
<Form
|
|
||||||
form={registerForm}
|
|
||||||
name="register"
|
|
||||||
onFinish={handleRegister}
|
|
||||||
autoComplete="off"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入用户名' },
|
|
||||||
{ min: 3, message: '用户名至少3个字符' }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入密码' },
|
|
||||||
{ min: 6, message: '密码长度至少6位' }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
prefix={<LockOutlined />}
|
|
||||||
placeholder="请输入密码(至少6位)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="nickname"
|
|
||||||
rules={[{ max: 20, message: '昵称最多20个字符' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<IdcardOutlined />}
|
|
||||||
placeholder="请输入昵称(可选)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
|
||||||
注册
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@ -193,25 +106,109 @@ const Login: React.FC = () => {
|
|||||||
<Text type="secondary" className={styles.subtitle}>欢迎使用题库系统</Text>
|
<Text type="secondary" className={styles.subtitle}>欢迎使用题库系统</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Form
|
||||||
activeKey={activeTab}
|
form={loginForm}
|
||||||
onChange={setActiveTab}
|
name="login"
|
||||||
centered
|
onFinish={handleLogin}
|
||||||
items={[
|
autoComplete="off"
|
||||||
{
|
size="large"
|
||||||
key: 'login',
|
>
|
||||||
label: '登录',
|
<Form.Item
|
||||||
children: loginTabContent,
|
name="username"
|
||||||
},
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
{
|
>
|
||||||
key: 'register',
|
<Input
|
||||||
label: '注册',
|
prefix={<UserOutlined />}
|
||||||
children: registerTabContent,
|
placeholder="请输入用户名"
|
||||||
},
|
/>
|
||||||
]}
|
</Form.Item>
|
||||||
/>
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div className={styles.registerTip}>
|
||||||
|
<Text type="secondary">还没有账号?</Text>
|
||||||
|
<Link onClick={() => setRegisterModalVisible(true)}>立即注册</Link>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 注册模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="注册新账号"
|
||||||
|
open={registerModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setRegisterModalVisible(false)
|
||||||
|
registerForm.resetFields()
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={registerForm}
|
||||||
|
name="register"
|
||||||
|
onFinish={handleRegister}
|
||||||
|
autoComplete="off"
|
||||||
|
size="large"
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ min: 3, message: '用户名至少3个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ min: 6, message: '密码长度至少6位' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="请输入密码(至少6位)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="nickname"
|
||||||
|
rules={[{ max: 20, message: '昵称最多20个字符' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<IdcardOutlined />}
|
||||||
|
placeholder="请输入昵称(可选)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||||
|
注册
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fafafa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
@ -17,32 +17,43 @@
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: white !important;
|
color: #1d1d1f !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.questionCard {
|
.questionCard {
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 16px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.questionNumber {
|
.questionNumber {
|
||||||
color: #666;
|
color: #6e6e73;
|
||||||
margin: 8px 0 16px 0 !important;
|
margin: 8px 0 16px 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.questionContent {
|
.questionContent {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #333;
|
color: #1d1d1f;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fillInput {
|
.fillInput {
|
||||||
border-bottom: 2px solid #1677ff !important;
|
border-bottom: 2px solid #007aff !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonGroup {
|
.buttonGroup {
|
||||||
@ -74,7 +85,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.questionCard {
|
.questionCard {
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #fafafa;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,24 +10,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.backButton {
|
.backButton {
|
||||||
color: white;
|
color: #007aff;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: #0051d5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: white !important;
|
color: #1d1d1f !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statsCard {
|
.statsCard {
|
||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 16px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@ -38,14 +46,21 @@
|
|||||||
|
|
||||||
.listCard {
|
.listCard {
|
||||||
margin: 0 20px 20px;
|
margin: 0 20px 20px;
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 16px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.04);
|
||||||
padding-bottom: 60px; // 为底部导航留空间
|
padding-bottom: 60px; // 为底部导航留空间
|
||||||
}
|
}
|
||||||
|
|
||||||
.listItem {
|
.listItem {
|
||||||
padding: 16px 0 !important;
|
padding: 16px 0 !important;
|
||||||
border-bottom: 1px solid #f0f0f0 !important;
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04) !important;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
@ -54,6 +69,7 @@
|
|||||||
|
|
||||||
.questionContent {
|
.questionContent {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
color: #1d1d1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计 - 移动端
|
// 响应式设计 - 移动端
|
||||||
@ -68,7 +84,7 @@
|
|||||||
|
|
||||||
.statsCard {
|
.statsCard {
|
||||||
margin: 0 16px 16px;
|
margin: 0 16px 16px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@ -77,7 +93,7 @@
|
|||||||
|
|
||||||
.listCard {
|
.listCard {
|
||||||
margin: 0 16px 16px;
|
margin: 0 16px 16px;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listItem {
|
.listItem {
|
||||||
|
|||||||
@ -34,8 +34,12 @@ instance.interceptors.response.use(
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
// 未授权,跳转到登录页
|
// 未授权,清除本地存储并跳转到登录页
|
||||||
console.error('未授权,请登录')
|
console.error('Token已过期或未授权,请重新登录')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
// 跳转到登录页
|
||||||
|
window.location.href = '/login'
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
console.error('没有权限访问')
|
console.error('没有权限访问')
|
||||||
|
|||||||
@ -14,11 +14,14 @@ export default defineConfig({
|
|||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
less: {
|
less: {
|
||||||
javascriptEnabled: true,
|
javascriptEnabled: true,
|
||||||
// antd 主题定制
|
// antd 主题定制 - 白色毛玻璃风格
|
||||||
modifyVars: {
|
modifyVars: {
|
||||||
'@primary-color': '#1677ff', // 主色调
|
'@primary-color': '#007aff', // macOS 蓝色
|
||||||
'@link-color': '#1677ff', // 链接色
|
'@link-color': '#007aff', // 链接色
|
||||||
'@border-radius-base': '8px', // 组件圆角
|
'@border-radius-base': '12px', // 组件圆角
|
||||||
|
'@layout-body-background': '#ffffff', // 白色背景
|
||||||
|
'@component-background': 'rgba(255, 255, 255, 0.8)', // 半透明组件背景
|
||||||
|
'@border-color-base': 'rgba(0, 0, 0, 0.06)', // 边框色
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user