打包成docker镜像
This commit is contained in:
parent
7c407db78b
commit
5d1b088e06
49
.dockerignore
Normal file
49
.dockerignore
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Git 相关
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
|
||||||
|
# 文档
|
||||||
|
*.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# IDE 配置
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Go 相关
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# 前端相关
|
||||||
|
web/node_modules/
|
||||||
|
web/dist/
|
||||||
|
web/.vite/
|
||||||
|
web/yarn-error.log
|
||||||
|
web/package-lock.json
|
||||||
|
|
||||||
|
# 环境变量和配置
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# OS 相关
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker 相关
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
253
DOCKER.md
Normal file
253
DOCKER.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# Docker 部署指南
|
||||||
|
|
||||||
|
本文档说明如何使用 Docker 部署 AnKao 应用。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 使用 Docker Compose (推荐)
|
||||||
|
|
||||||
|
最简单的方式是使用 docker-compose,它会自动启动数据库和应用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建并启动所有服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 停止并删除数据
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
访问应用: http://localhost:8080
|
||||||
|
|
||||||
|
### 手动使用 Docker
|
||||||
|
|
||||||
|
如果你想单独构建和运行容器:
|
||||||
|
|
||||||
|
#### 1. 构建镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t ankao:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 启动 PostgreSQL 数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name ankao-postgres \
|
||||||
|
-e POSTGRES_DB=ankao \
|
||||||
|
-e POSTGRES_USER=postgres \
|
||||||
|
-e POSTGRES_PASSWORD=postgres \
|
||||||
|
-p 5432:5432 \
|
||||||
|
postgres:16-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 启动应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name ankao-app \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e DB_HOST=host.docker.internal \
|
||||||
|
-e DB_PORT=5432 \
|
||||||
|
-e DB_USER=postgres \
|
||||||
|
-e DB_PASSWORD=postgres \
|
||||||
|
-e DB_NAME=ankao \
|
||||||
|
-e DB_SSLMODE=disable \
|
||||||
|
ankao:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 在 Windows 和 Mac 上使用 `host.docker.internal` 连接宿主机的数据库。在 Linux 上需要使用 `--network host` 或创建自定义网络。
|
||||||
|
|
||||||
|
## 环境变量配置
|
||||||
|
|
||||||
|
应用支持以下环境变量配置:
|
||||||
|
|
||||||
|
### 数据库配置
|
||||||
|
|
||||||
|
| 环境变量 | 描述 | 默认值 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| `DB_HOST` | 数据库主机地址 | pgsql.yuchat.top |
|
||||||
|
| `DB_PORT` | 数据库端口 | 5432 |
|
||||||
|
| `DB_USER` | 数据库用户名 | postgres |
|
||||||
|
| `DB_PASSWORD` | 数据库密码 | longqi@1314 |
|
||||||
|
| `DB_NAME` | 数据库名称 | ankao |
|
||||||
|
| `DB_SSLMODE` | SSL 模式 | disable |
|
||||||
|
|
||||||
|
### 应用配置
|
||||||
|
|
||||||
|
| 环境变量 | 描述 | 默认值 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| `GIN_MODE` | Gin 运行模式 (debug/release) | debug |
|
||||||
|
|
||||||
|
## Docker Compose 配置说明
|
||||||
|
|
||||||
|
`docker-compose.yml` 文件定义了两个服务:
|
||||||
|
|
||||||
|
1. **postgres**: PostgreSQL 16 数据库
|
||||||
|
- 端口: 5432
|
||||||
|
- 数据持久化: 使用 Docker volume `postgres_data`
|
||||||
|
- 健康检查: 确保数据库就绪后再启动应用
|
||||||
|
|
||||||
|
2. **app**: AnKao 应用
|
||||||
|
- 端口: 8080
|
||||||
|
- 依赖 postgres 服务
|
||||||
|
- 自动重启策略
|
||||||
|
|
||||||
|
## 生产环境部署建议
|
||||||
|
|
||||||
|
### 1. 修改默认密码
|
||||||
|
|
||||||
|
**重要**: 生产环境中务必修改数据库密码!
|
||||||
|
|
||||||
|
编辑 `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: 你的强密码
|
||||||
|
DB_PASSWORD: 你的强密码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用外部数据库
|
||||||
|
|
||||||
|
如果使用外部 PostgreSQL 数据库,只需启动 app 服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d app
|
||||||
|
```
|
||||||
|
|
||||||
|
并配置相应的环境变量。
|
||||||
|
|
||||||
|
### 3. 配置反向代理
|
||||||
|
|
||||||
|
生产环境建议使用 Nginx 作为反向代理:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 启用 HTTPS
|
||||||
|
|
||||||
|
使用 Let's Encrypt 免费 SSL 证书:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 certbot
|
||||||
|
apt-get install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# 获取证书
|
||||||
|
certbot --nginx -d your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 备份数据
|
||||||
|
|
||||||
|
定期备份 PostgreSQL 数据:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份数据库
|
||||||
|
docker exec ankao-postgres pg_dump -U postgres ankao > backup.sql
|
||||||
|
|
||||||
|
# 恢复数据库
|
||||||
|
cat backup.sql | docker exec -i ankao-postgres psql -U postgres ankao
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 查看应用日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看所有服务日志
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# 查看应用日志
|
||||||
|
docker-compose logs app
|
||||||
|
|
||||||
|
# 查看数据库日志
|
||||||
|
docker-compose logs postgres
|
||||||
|
|
||||||
|
# 实时查看日志
|
||||||
|
docker-compose logs -f app
|
||||||
|
```
|
||||||
|
|
||||||
|
### 进入容器调试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入应用容器
|
||||||
|
docker exec -it ankao-app sh
|
||||||
|
|
||||||
|
# 进入数据库容器
|
||||||
|
docker exec -it ankao-postgres sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 应用无法连接数据库
|
||||||
|
|
||||||
|
检查:
|
||||||
|
- 数据库是否已启动: `docker-compose ps`
|
||||||
|
- 网络配置是否正确
|
||||||
|
- 环境变量配置是否正确
|
||||||
|
|
||||||
|
#### 2. 端口被占用
|
||||||
|
|
||||||
|
修改 `docker-compose.yml` 中的端口映射:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8081:8080" # 将宿主机端口改为 8081
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 构建失败
|
||||||
|
|
||||||
|
清理缓存重新构建:
|
||||||
|
```bash
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## 镜像优化
|
||||||
|
|
||||||
|
当前 Dockerfile 使用多阶段构建,最终镜像基于 Alpine Linux,体积小且安全。
|
||||||
|
|
||||||
|
镜像大小约:
|
||||||
|
- 前端构建阶段: ~200MB (不包含在最终镜像)
|
||||||
|
- 后端构建阶段: ~300MB (不包含在最终镜像)
|
||||||
|
- 最终运行镜像: ~30MB
|
||||||
|
|
||||||
|
## 更新应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 拉取最新代码
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 重新构建并启动
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# 或者分步执行
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 清理资源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止并删除容器
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 删除容器和数据卷
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# 删除镜像
|
||||||
|
docker rmi ankao:latest
|
||||||
|
```
|
||||||
73
Dockerfile
Normal file
73
Dockerfile
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# ============================================
|
||||||
|
# 第一阶段: 构建前端应用
|
||||||
|
# ============================================
|
||||||
|
FROM node:22-alpine AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app/web
|
||||||
|
|
||||||
|
# 复制前端依赖文件
|
||||||
|
COPY web/package.json web/yarn.lock ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
# 复制前端源代码
|
||||||
|
COPY web/ ./
|
||||||
|
|
||||||
|
# 构建前端应用
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 第二阶段: 构建 Go 后端应用
|
||||||
|
# ============================================
|
||||||
|
FROM golang:1.25.1-alpine AS backend-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装必要的构建工具
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# 配置 Go 代理(使用国内镜像加速)
|
||||||
|
ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,direct
|
||||||
|
ENV GOSUMDB=off
|
||||||
|
|
||||||
|
# 复制 Go 依赖文件
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# 下载依赖
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# 复制后端源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建 Go 应用
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server main.go
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 第三阶段: 创建最终运行镜像
|
||||||
|
# ============================================
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装运行时依赖
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
# 设置时区为上海
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 从后端构建阶段复制可执行文件
|
||||||
|
COPY --from=backend-builder /app/server ./server
|
||||||
|
|
||||||
|
# 从前端构建阶段复制构建产物
|
||||||
|
# 根据 main.go 配置:
|
||||||
|
# - r.NoRoute() 返回 ./web/index.html
|
||||||
|
# - r.Static("/assets", "./web/static") 提供静态资源
|
||||||
|
COPY --from=frontend-builder /app/web/dist/index.html ./web/index.html
|
||||||
|
COPY --from=frontend-builder /app/web/dist/assets ./web/static
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["./server"]
|
||||||
15
main.go
15
main.go
@ -23,11 +23,7 @@ func main() {
|
|||||||
r.Use(middleware.CORS())
|
r.Use(middleware.CORS())
|
||||||
r.Use(middleware.Logger())
|
r.Use(middleware.Logger())
|
||||||
|
|
||||||
// 静态文件服务
|
// API路由组(必须在静态文件服务之前注册)
|
||||||
r.Static("/static", "./web/static")
|
|
||||||
r.StaticFile("/", "./web/index.html")
|
|
||||||
|
|
||||||
// API路由组
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
{
|
{
|
||||||
// 健康检查
|
// 健康检查
|
||||||
@ -65,6 +61,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 静态文件服务(必须在 API 路由之后)
|
||||||
|
// 提供静态资源(CSS、JS、图片等)
|
||||||
|
r.Static("/assets", "./web/static")
|
||||||
|
|
||||||
|
// SPA 支持:所有非 API 路由都返回 index.html,让前端路由处理
|
||||||
|
r.NoRoute(func(c *gin.Context) {
|
||||||
|
c.File("./web/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
port := ":8080"
|
port := ":8080"
|
||||||
if err := r.Run(port); err != nil {
|
if err := r.Run(port); err != nil {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DatabaseConfig 数据库配置结构
|
// DatabaseConfig 数据库配置结构
|
||||||
@ -15,17 +17,44 @@ type DatabaseConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDatabaseConfig 获取数据库配置
|
// GetDatabaseConfig 获取数据库配置
|
||||||
|
// 优先使用环境变量,如果没有设置则使用默认值
|
||||||
func GetDatabaseConfig() *DatabaseConfig {
|
func GetDatabaseConfig() *DatabaseConfig {
|
||||||
|
// 从环境变量获取配置,如果未设置则使用默认值
|
||||||
|
host := getEnv("DB_HOST", "pgsql.yuchat.top")
|
||||||
|
port := getEnvAsInt("DB_PORT", 5432)
|
||||||
|
user := getEnv("DB_USER", "postgres")
|
||||||
|
password := getEnv("DB_PASSWORD", "longqi@1314")
|
||||||
|
dbname := getEnv("DB_NAME", "ankao")
|
||||||
|
sslmode := getEnv("DB_SSLMODE", "disable")
|
||||||
|
|
||||||
return &DatabaseConfig{
|
return &DatabaseConfig{
|
||||||
Host: "pgsql.yuchat.top",
|
Host: host,
|
||||||
Port: 5432,
|
Port: port,
|
||||||
User: "postgres",
|
User: user,
|
||||||
Password: "longqi@1314",
|
Password: password,
|
||||||
DBName: "ankao",
|
DBName: dbname,
|
||||||
SSLMode: "disable",
|
SSLMode: sslmode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEnv 获取环境变量,如果不存在则返回默认值
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvAsInt 获取整型环境变量,如果不存在或转换失败则返回默认值
|
||||||
|
func getEnvAsInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
// GetDSN 返回数据库连接字符串
|
// GetDSN 返回数据库连接字符串
|
||||||
func (c *DatabaseConfig) GetDSN() string {
|
func (c *DatabaseConfig) GetDSN() string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
|
|||||||
@ -98,10 +98,10 @@ const QuestionPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 加载题目列表(从第一题开始)
|
// 加载题目列表(从第一题开始)
|
||||||
const loadQuestions = async (type?: string, category?: string) => {
|
const loadQuestions = async (type?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await questionApi.getQuestions({ type, category });
|
const res = await questionApi.getQuestions({ type });
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
setAllQuestions(res.data);
|
setAllQuestions(res.data);
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ const QuestionPage: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 处理判断题答案:将字符串 "true"/"false" 转换为布尔值
|
// 处理判断题答案:将字符串 "true"/"false" 转换为布尔值
|
||||||
let answerToSubmit = selectedAnswer;
|
let answerToSubmit: string | string[] | boolean = selectedAnswer;
|
||||||
if (currentQuestion.type === "true-false" && typeof selectedAnswer === "string") {
|
if (currentQuestion.type === "true-false" && typeof selectedAnswer === "string") {
|
||||||
answerToSubmit = selectedAnswer === "true";
|
answerToSubmit = selectedAnswer === "true";
|
||||||
}
|
}
|
||||||
@ -225,7 +225,6 @@ const QuestionPage: React.FC = () => {
|
|||||||
// 初始化
|
// 初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const typeParam = searchParams.get("type");
|
const typeParam = searchParams.get("type");
|
||||||
const categoryParam = searchParams.get("category");
|
|
||||||
const mode = searchParams.get("mode");
|
const mode = searchParams.get("mode");
|
||||||
|
|
||||||
// 错题练习模式
|
// 错题练习模式
|
||||||
@ -235,7 +234,7 @@ const QuestionPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 普通练习模式 - 从第一题开始
|
// 普通练习模式 - 从第一题开始
|
||||||
loadQuestions(typeParam || undefined, categoryParam || undefined);
|
loadQuestions(typeParam || undefined);
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
// 重试处理
|
// 重试处理
|
||||||
@ -246,8 +245,7 @@ const QuestionPage: React.FC = () => {
|
|||||||
setWrongCount(0);
|
setWrongCount(0);
|
||||||
localStorage.removeItem(getStorageKey());
|
localStorage.removeItem(getStorageKey());
|
||||||
const typeParam = searchParams.get("type");
|
const typeParam = searchParams.get("type");
|
||||||
const categoryParam = searchParams.get("category");
|
loadQuestions(typeParam || undefined);
|
||||||
loadQuestions(typeParam || undefined, categoryParam || undefined);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -290,7 +290,7 @@ const QuestionManagement: React.FC = () => {
|
|||||||
{(fields, { add, remove }, { errors }) => (
|
{(fields, { add, remove }, { errors }) => (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="选项" required>
|
<Form.Item label="选项" required>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field) => (
|
||||||
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...field}
|
{...field}
|
||||||
@ -349,7 +349,7 @@ const QuestionManagement: React.FC = () => {
|
|||||||
{(fields, { add, remove }, { errors }) => (
|
{(fields, { add, remove }, { errors }) => (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="选项" required>
|
<Form.Item label="选项" required>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field) => (
|
||||||
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -108,11 +108,11 @@ const WrongQuestions: React.FC = () => {
|
|||||||
// 获取题型标签颜色
|
// 获取题型标签颜色
|
||||||
const getTypeColor = (type: string) => {
|
const getTypeColor = (type: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
single: 'blue',
|
'multiple-choice': 'blue',
|
||||||
multiple: 'green',
|
'multiple-selection': 'green',
|
||||||
fill: 'cyan',
|
'fill-in-blank': 'cyan',
|
||||||
judge: 'orange',
|
'true-false': 'orange',
|
||||||
short: 'purple',
|
'short-answer': 'purple',
|
||||||
}
|
}
|
||||||
return colorMap[type] || 'default'
|
return colorMap[type] || 'default'
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ const WrongQuestions: React.FC = () => {
|
|||||||
<Space>
|
<Space>
|
||||||
<Text strong>题目 {item.question.question_id || item.question.id}</Text>
|
<Text strong>题目 {item.question.question_id || item.question.id}</Text>
|
||||||
<Tag color={getTypeColor(item.question.type)}>
|
<Tag color={getTypeColor(item.question.type)}>
|
||||||
{item.question.category}
|
{item.question.category || item.question.type}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag color="error">错误 {item.wrong_count} 次</Tag>
|
<Tag color="error">错误 {item.wrong_count} 次</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export interface Question {
|
|||||||
// 提交答案
|
// 提交答案
|
||||||
export interface SubmitAnswer {
|
export interface SubmitAnswer {
|
||||||
question_id: number
|
question_id: number
|
||||||
answer: string | string[]
|
answer: string | string[] | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// 答案结果
|
// 答案结果
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user