优化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:
yanlongqi 2025-11-05 10:58:43 +08:00
parent 5d1b088e06
commit c0a280132c
15 changed files with 591 additions and 299 deletions

View File

@ -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
View File

@ -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 路由之后)

View File

@ -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 />} />

View 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

View File

@ -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 {

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>
)

View File

@ -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;
}
}
}
// 响应式设计 - 移动端

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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('没有权限访问')

View File

@ -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)', // 边框色
},
},
},