优化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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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
|
||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
||||
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计
|
||||
@ -59,6 +54,14 @@ func main() {
|
||||
auth.PUT("/wrong-questions/:id/mastered", handlers.MarkWrongQuestionMastered) // 标记已掌握
|
||||
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 路由之后)
|
||||
|
||||
@ -2,6 +2,7 @@ import React from 'react'
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import TabBarLayout from './components/TabBarLayout'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
import AdminRoute from './components/AdminRoute'
|
||||
import QuestionPage from './pages/Question'
|
||||
import Login from './pages/Login'
|
||||
import Home from './pages/Home'
|
||||
@ -21,7 +22,15 @@ const App: React.FC = () => {
|
||||
|
||||
{/* 不带TabBar的页面,但需要登录保护 */}
|
||||
<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的页面,不需要登录保护 */}
|
||||
<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 {
|
||||
min-height: 100vh;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -13,8 +14,14 @@
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
padding: 0;
|
||||
background: white;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
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 {
|
||||
|
||||
@ -1,61 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom'
|
||||
import { Layout, Menu } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Layout } from 'antd'
|
||||
import styles from './TabBarLayout.module.less'
|
||||
|
||||
const { Footer, Content } = Layout
|
||||
const { Content } = Layout
|
||||
|
||||
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 (
|
||||
<Layout className={styles.layout}>
|
||||
<Content className={styles.content}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
{isMobile && (
|
||||
<Footer className={styles.footer}>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
className={styles.menu}
|
||||
/>
|
||||
</Footer>
|
||||
)}
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #fafafa;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@ -20,8 +20,15 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 20px;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -40,7 +47,7 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
padding: 20px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
@ -9,29 +9,35 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32px;
|
||||
color: white;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.headerLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white !important;
|
||||
margin-bottom: 8px !important;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
color: #1d1d1f !important;
|
||||
margin-bottom: 4px !important;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: #6e6e73;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
padding: 10px 16px;
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
-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 {
|
||||
display: flex;
|
||||
@ -39,12 +45,13 @@
|
||||
gap: 2px;
|
||||
|
||||
.userNickname {
|
||||
color: white !important;
|
||||
font-size: 16px;
|
||||
color: #1d1d1f !important;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.userUsername {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
color: #6e6e73 !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@ -52,119 +59,284 @@
|
||||
}
|
||||
|
||||
.statsCard {
|
||||
margin-bottom: 32px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 24px;
|
||||
border-radius: 20px;
|
||||
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 {
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.sectionTitle {
|
||||
color: white !important;
|
||||
color: #1d1d1f !important;
|
||||
margin-bottom: 16px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.typeCard {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
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);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.typeTitle {
|
||||
margin: 8px 0 !important;
|
||||
margin: 6px 0 4px 0 !important;
|
||||
color: #1d1d1f;
|
||||
font-weight: 600;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.typeDesc {
|
||||
margin: 0 !important;
|
||||
color: #6e6e73;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.quickStart {
|
||||
.sectionTitle {
|
||||
color: #1d1d1f !important;
|
||||
margin-bottom: 16px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.quickCard {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||
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);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
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) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.headerLeft {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px !important;
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
|
||||
:global {
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.statsCard {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
:global {
|
||||
.ant-statistic-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-statistic-content {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typeSection {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.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) {
|
||||
.container {
|
||||
padding: 32px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - PC端
|
||||
@media (min-width: 1025px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
padding: 24px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.header {
|
||||
.title {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.typeCard {
|
||||
.typeIcon {
|
||||
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}>
|
||||
<FileTextOutlined /> 选择题型
|
||||
</Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={[12, 12]}>
|
||||
{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
|
||||
hoverable
|
||||
className={styles.typeCard}
|
||||
@ -218,11 +218,11 @@ const Home: React.FC = () => {
|
||||
styles={{
|
||||
body: {
|
||||
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}
|
||||
</div>
|
||||
<Title level={5} className={styles.typeTitle}>{type.title}</Title>
|
||||
@ -235,55 +235,67 @@ const Home: React.FC = () => {
|
||||
|
||||
{/* 快速开始 */}
|
||||
<div className={styles.quickStart}>
|
||||
<Title level={4} className={styles.sectionTitle}>
|
||||
<RocketOutlined /> 快速开始
|
||||
</Title>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.quickCard}
|
||||
onClick={() => navigate('/question')}
|
||||
>
|
||||
<Space align="center" size="large">
|
||||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||
<div className={styles.quickIcon}>
|
||||
<RocketOutlined style={{ fontSize: '32px', color: '#722ed1' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={5} style={{ margin: 0 }}>随机练习</Title>
|
||||
<Paragraph type="secondary" style={{ margin: 0 }}>从所有题型中随机抽取</Paragraph>
|
||||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>从所有题型中随机抽取</Paragraph>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.quickCard}
|
||||
onClick={() => navigate('/wrong-questions')}
|
||||
style={{ marginTop: '16px' }}
|
||||
>
|
||||
<Space align="center" size="large">
|
||||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||
<div className={styles.quickIcon}>
|
||||
<BookOutlined style={{ fontSize: '32px', color: '#ff4d4f' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={5} style={{ margin: 0 }}>错题本</Title>
|
||||
<Paragraph type="secondary" style={{ margin: 0 }}>复习错题,巩固薄弱知识点</Paragraph>
|
||||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>复习错题,巩固薄弱知识点</Paragraph>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 仅 yanlongqi 用户显示题库管理 */}
|
||||
{userInfo?.username === 'yanlongqi' && (
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.quickCard}
|
||||
onClick={() => navigate('/question-management')}
|
||||
style={{ marginTop: '16px' }}
|
||||
>
|
||||
<Space align="center" size="large">
|
||||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||
<div className={styles.quickIcon}>
|
||||
<SettingOutlined style={{ fontSize: '32px', color: '#13c2c2' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={5} style={{ margin: 0 }}>题库管理</Title>
|
||||
<Paragraph type="secondary" style={{ margin: 0 }}>添加、编辑和删除题目</Paragraph>
|
||||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>添加、编辑和删除题目</Paragraph>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #fafafa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@ -13,8 +13,15 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
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 {
|
||||
.ant-card-body {
|
||||
@ -30,14 +37,27 @@
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px 0 !important;
|
||||
color: #667eea !important;
|
||||
color: #007aff !important;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 15px;
|
||||
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 { 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 styles from './Login.module.less'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Title, Text, Link } = Typography
|
||||
|
||||
interface LoginResponse {
|
||||
success: boolean
|
||||
@ -21,8 +21,8 @@ interface LoginResponse {
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState('login')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [registerModalVisible, setRegisterModalVisible] = useState(false)
|
||||
const [loginForm] = Form.useForm()
|
||||
const [registerForm] = Form.useForm()
|
||||
|
||||
@ -84,6 +84,7 @@ const Login: React.FC = () => {
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||
|
||||
message.success('注册成功')
|
||||
setRegisterModalVisible(false)
|
||||
navigate('/')
|
||||
} else {
|
||||
message.error(data.message || '注册失败')
|
||||
@ -96,7 +97,15 @@ const Login: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loginTabContent = (
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Title level={2} className={styles.title}>AnKao</Title>
|
||||
<Text type="secondary" className={styles.subtitle}>欢迎使用题库系统</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={loginForm}
|
||||
name="login"
|
||||
@ -129,16 +138,33 @@ const Login: React.FC = () => {
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
|
||||
const registerTabContent = (
|
||||
<div className={styles.registerTip}>
|
||||
<Text type="secondary">还没有账号?</Text>
|
||||
<Link onClick={() => setRegisterModalVisible(true)}>立即注册</Link>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</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"
|
||||
@ -182,36 +208,7 @@ const Login: React.FC = () => {
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Title level={2} className={styles.title}>AnKao</Title>
|
||||
<Text type="secondary" className={styles.subtitle}>欢迎使用题库系统</Text>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
centered
|
||||
items={[
|
||||
{
|
||||
key: 'login',
|
||||
label: '登录',
|
||||
children: loginTabContent,
|
||||
},
|
||||
{
|
||||
key: 'register',
|
||||
label: '注册',
|
||||
children: registerTabContent,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #fafafa;
|
||||
padding: 20px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
@ -17,32 +17,43 @@
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
color: white !important;
|
||||
color: #1d1d1f !important;
|
||||
margin: 0 !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.questionCard {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 20px;
|
||||
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;
|
||||
}
|
||||
|
||||
.questionNumber {
|
||||
color: #666;
|
||||
color: #6e6e73;
|
||||
margin: 8px 0 16px 0 !important;
|
||||
}
|
||||
|
||||
.questionContent {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
color: #1d1d1f;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.fillInput {
|
||||
border-bottom: 2px solid #1677ff !important;
|
||||
border-bottom: 2px solid #007aff !important;
|
||||
border-radius: 0 !important;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
@ -74,7 +85,7 @@
|
||||
}
|
||||
|
||||
.questionCard {
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #fafafa;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@ -10,24 +10,32 @@
|
||||
}
|
||||
|
||||
.backButton {
|
||||
color: white;
|
||||
color: #007aff;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: #0051d5;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white !important;
|
||||
color: #1d1d1f !important;
|
||||
margin: 0 !important;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.statsCard {
|
||||
margin: 0 20px 20px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 20px;
|
||||
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 {
|
||||
@ -38,14 +46,21 @@
|
||||
|
||||
.listCard {
|
||||
margin: 0 20px 20px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 20px;
|
||||
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; // 为底部导航留空间
|
||||
}
|
||||
|
||||
.listItem {
|
||||
padding: 16px 0 !important;
|
||||
border-bottom: 1px solid #f0f0f0 !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04) !important;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none !important;
|
||||
@ -54,6 +69,7 @@
|
||||
|
||||
.questionContent {
|
||||
margin-top: 8px;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
// 响应式设计 - 移动端
|
||||
@ -68,7 +84,7 @@
|
||||
|
||||
.statsCard {
|
||||
margin: 0 16px 16px;
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@ -77,7 +93,7 @@
|
||||
|
||||
.listCard {
|
||||
margin: 0 16px 16px;
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
|
||||
@ -34,8 +34,12 @@ instance.interceptors.response.use(
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,跳转到登录页
|
||||
console.error('未授权,请登录')
|
||||
// 未授权,清除本地存储并跳转到登录页
|
||||
console.error('Token已过期或未授权,请重新登录')
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
// 跳转到登录页
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
console.error('没有权限访问')
|
||||
|
||||
@ -14,11 +14,14 @@ export default defineConfig({
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true,
|
||||
// antd 主题定制
|
||||
// antd 主题定制 - 白色毛玻璃风格
|
||||
modifyVars: {
|
||||
'@primary-color': '#1677ff', // 主色调
|
||||
'@link-color': '#1677ff', // 链接色
|
||||
'@border-radius-base': '8px', // 组件圆角
|
||||
'@primary-color': '#007aff', // macOS 蓝色
|
||||
'@link-color': '#007aff', // 链接色
|
||||
'@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