first commit
This commit is contained in:
38
init.sql
Normal file
38
init.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- 创建数据库 (如果不存在)
|
||||||
|
CREATE DATABASE IF NOT EXISTS `exam` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE `exam`;
|
||||||
|
|
||||||
|
-- 1. 题库表 (question)
|
||||||
|
CREATE TABLE IF NOT EXISTS `question` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '题目ID',
|
||||||
|
`title` TEXT NOT NULL COMMENT '题目内容',
|
||||||
|
`type` INT NOT NULL COMMENT '题目类型: 1-选择题, 2-简答题',
|
||||||
|
`answer` TEXT COMMENT '学生答案参考(仅简答题)',
|
||||||
|
`answer_a` VARCHAR(255) COMMENT '选项A(仅选择题)',
|
||||||
|
`answer_b` VARCHAR(255) COMMENT '选项B(仅选择题)',
|
||||||
|
`answer_c` VARCHAR(255) COMMENT '选项C(仅选择题)',
|
||||||
|
`answer_d` VARCHAR(255) COMMENT '选项D(仅选择题)',
|
||||||
|
`r_answer` TEXT COMMENT '正确答案',
|
||||||
|
`score` INT NOT NULL DEFAULT 0 COMMENT '题目分值'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题库表';
|
||||||
|
|
||||||
|
-- 2. 试卷表 (exam_paper)
|
||||||
|
CREATE TABLE IF NOT EXISTS `exam_paper` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '试卷自增ID',
|
||||||
|
`uid` VARCHAR(64) NOT NULL UNIQUE COMMENT '试卷唯一标识(UUID等)',
|
||||||
|
`title` VARCHAR(255) COMMENT '试卷标题',
|
||||||
|
`select` TEXT COMMENT '选择题集合或相关配置',
|
||||||
|
`content` TEXT COMMENT '简答题集合或相关内容'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生成的试卷表';
|
||||||
|
|
||||||
|
-- 3. 答题记录表/批改记录表 (exam_back)
|
||||||
|
CREATE TABLE IF NOT EXISTS `exam_back` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '答题记录自增ID',
|
||||||
|
`uid` VARCHAR(64) NOT NULL UNIQUE COMMENT '答题记录唯一标识',
|
||||||
|
`examId` VARCHAR(64) NOT NULL COMMENT '关联的试卷UID',
|
||||||
|
`title` VARCHAR(255) COMMENT '试卷标题(冗余防丢)',
|
||||||
|
`name` VARCHAR(100) NOT NULL COMMENT '做题人姓名',
|
||||||
|
`content` JSON COMMENT '包含题目和所填答案的JSON数组',
|
||||||
|
`score` VARCHAR(50) COMMENT '总得分/批改状态'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生答卷与批改表';
|
||||||
120
questions_data.sql
Normal file
120
questions_data.sql
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
-- 题库数据插入脚本
|
||||||
|
-- 共100道题,选择题70道(分值2-3分),简答题30道(分值4-5分)
|
||||||
|
-- 组合示例:14道选择题(3分)+6道简答题(5分)=42+30=72分;20道选择题(3分)+8道简答题(5分)=60+40=100分
|
||||||
|
|
||||||
|
USE exam;
|
||||||
|
|
||||||
|
-- 清空已有数据(可选)
|
||||||
|
-- TRUNCATE TABLE question;
|
||||||
|
|
||||||
|
-- 选择题(70道)- 35道3分,35道2分
|
||||||
|
INSERT INTO question (title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score) VALUES
|
||||||
|
('Java中,以下哪个关键字用于定义类?', 1, 'class', 'interface', 'extends', 'implements', 'A', 3),
|
||||||
|
('Python中,列表推导式的语法是?', 1, '[x for x in iterable]', '(x for x in iterable)', '{x for x in iterable}', '<x for x in iterable>', 'A', 2),
|
||||||
|
('HTTP协议默认使用的端口号是?', 1, '80', '8080', '443', '3306', 'A', 3),
|
||||||
|
('MySQL中,用于删除表的命令是?', 1, 'DELETE TABLE', 'DROP TABLE', 'REMOVE TABLE', 'CLEAR TABLE', 'B', 2),
|
||||||
|
('CSS中,用于设置背景颜色的属性是?', 1, 'background-color', 'bgcolor', 'color', 'background', 'A', 3),
|
||||||
|
('JavaScript中,以下哪个不是原始数据类型?', 1, 'String', 'Number', 'Object', 'Boolean', 'C', 2),
|
||||||
|
('Spring Boot中,主类上的核心注解是?', 1, '@SpringApplication', '@SpringBootApplication', '@EnableAutoConfiguration', '@ComponentScan', 'B', 3),
|
||||||
|
('Git中,用于提交代码的命令是?', 1, 'git push', 'git commit', 'git add', 'git pull', 'B', 2),
|
||||||
|
('Linux中,查看当前目录的命令是?', 1, 'ls', 'cd', 'pwd', 'dir', 'C', 3),
|
||||||
|
('HTML中,用于定义超链接的标签是?', 1, '<link>', '<a>', '<href>', '<url>', 'B', 2),
|
||||||
|
('Java中,String类的equals方法用于?', 1, '比较引用', '比较内容', '赋值', '连接字符串', 'B', 3),
|
||||||
|
('数据库中,ACID特性不包括?', 1, 'Atomicity', 'Consistency', 'Isolation', 'Distribution', 'D', 2),
|
||||||
|
('Python中,len函数的作用是?', 1, '计算长度', '计算总和', '计算平均值', '计算最大值', 'A', 3),
|
||||||
|
('Redis是一种?', 1, '关系型数据库', '内存数据库', '文档数据库', '图数据库', 'B', 2),
|
||||||
|
('Docker中,用于构建镜像的命令是?', 1, 'docker run', 'docker build', 'docker pull', 'docker push', 'B', 3),
|
||||||
|
('Java中,以下哪个集合类是线程安全的?', 1, 'ArrayList', 'HashMap', 'Vector', 'HashSet', 'C', 2),
|
||||||
|
('Maven中,用于定义项目依赖的文件是?', 1, 'build.xml', 'pom.xml', 'config.xml', 'project.xml', 'B', 3),
|
||||||
|
('JavaScript中,===运算符的作用是?', 1, '赋值', '相等比较', '严格相等比较', '不等于', 'C', 2),
|
||||||
|
('Linux中,查看文件内容的命令是?', 1, 'open', 'cat', 'view', 'show', 'B', 3),
|
||||||
|
('HTTP状态码404表示?', 1, '服务器错误', '未授权', '未找到', '重定向', 'C', 2),
|
||||||
|
('Java中,final关键字不能用于修饰?', 1, '类', '方法', '变量', '接口', 'D', 3),
|
||||||
|
('Python中,def关键字用于?', 1, '定义变量', '定义函数', '定义类', '定义模块', 'B', 2),
|
||||||
|
('MySQL中,主键的特点是?', 1, '可以为空', '可以重复', '唯一且非空', '自动生成', 'C', 3),
|
||||||
|
('Vue.js中,用于数据绑定的指令是?', 1, 'v-if', 'v-for', 'v-model', 'v-bind', 'C', 2),
|
||||||
|
('Nginx主要用于?', 1, '数据库服务器', 'Web服务器/反向代理', '缓存服务器', '消息队列', 'B', 3),
|
||||||
|
('Java中,异常处理的关键字不包括?', 1, 'try', 'catch', 'throw', 'error', 'D', 2),
|
||||||
|
('CSS中,盒模型不包括?', 1, 'content', 'padding', 'border', 'layer', 'D', 3),
|
||||||
|
('Spring中,依赖注入的方式不包括?', 1, '构造器注入', 'Setter注入', '接口注入', '字段注入', 'C', 2),
|
||||||
|
('Git中,创建新分支的命令是?', 1, 'git branch', 'git checkout', 'git switch', '以上都可以', 'D', 3),
|
||||||
|
('Python中,导入模块的关键字是?', 1, 'include', 'import', 'using', 'require', 'B', 2),
|
||||||
|
('数据库中,用于排序的关键字是?', 1, 'GROUP BY', 'ORDER BY', 'SORT BY', 'ARRANGE BY', 'B', 3),
|
||||||
|
('JavaScript中,数组的push方法作用是?', 1, '删除末尾元素', '在末尾添加元素', '在开头添加元素', '删除开头元素', 'B', 2),
|
||||||
|
('Linux中,修改文件权限的命令是?', 1, 'chown', 'chmod', 'chgrp', 'perm', 'B', 3),
|
||||||
|
('HTML5中,用于本地存储的API是?', 1, 'cookie', 'localStorage', 'session', 'cache', 'B', 2),
|
||||||
|
('Java中,接口可以包含?', 1, '构造方法', '静态方法(Java8+)', '实例变量', '普通方法体(Java7)', 'B', 3),
|
||||||
|
('MongoDB是一种?', 1, '关系型数据库', '文档型NoSQL数据库', '键值对数据库', '列族数据库', 'B', 2),
|
||||||
|
('Kafka主要用于?', 1, '数据存储', '消息队列/流处理', '负载均衡', '缓存', 'B', 3),
|
||||||
|
('Python中,列表和元组的区别是?', 1, '列表有序,元组无序', '列表可变,元组不可变', '列表可以重复,元组不可以', '没有区别', 'B', 2),
|
||||||
|
('CSS中,Flex布局的主轴方向默认是?', 1, 'row', 'column', 'row-reverse', 'column-reverse', 'A', 3),
|
||||||
|
('Java中,synchronized关键字用于?', 1, '继承', '多态', '线程同步', '异常处理', 'C', 2),
|
||||||
|
('MySQL中,JOIN操作的作用是?', 1, '删除数据', '更新数据', '连接多表查询', '插入数据', 'C', 3),
|
||||||
|
('React中,用于状态管理的Hook是?', 1, 'useEffect', 'useState', 'useContext', 'useReducer', 'B', 2),
|
||||||
|
('Linux中,查找文件的命令是?', 1, 'grep', 'find', 'locate', 'search', 'B', 3),
|
||||||
|
('HTTP状态码500表示?', 1, '客户端错误', '服务器内部错误', '未授权', '禁止访问', 'B', 2),
|
||||||
|
('Java中,abstract类不能?', 1, '有构造方法', '有抽象方法', '直接实例化', '有成员变量', 'C', 3),
|
||||||
|
('Python中,__init__方法是?', 1, '构造函数', '析构函数', '普通方法', '静态方法', 'A', 2),
|
||||||
|
('Elasticsearch主要用于?', 1, '关系存储', '全文搜索', '图计算', '事务处理', 'B', 3),
|
||||||
|
('Git中,合并分支的命令是?', 1, 'git combine', 'git merge', 'git join', 'git integrate', 'B', 2),
|
||||||
|
('CSS中,z-index属性用于?', 1, '水平定位', '垂直定位', '层级控制', '透明度控制', 'C', 3),
|
||||||
|
('Spring Boot中,application.yml用于?', 1, '编写业务代码', '配置应用程序', '定义数据模型', '编写测试', 'B', 2),
|
||||||
|
('JavaScript中,typeof null返回?', 1, '"null"', '"undefined"', '"object"', '"number"', 'C', 3),
|
||||||
|
('Redis的数据类型不包括?', 1, 'String', 'List', 'Table', 'Set', 'C', 2),
|
||||||
|
('Docker中,用于查看运行容器的命令是?', 1, 'docker images', 'docker ps', 'docker list', 'docker show', 'B', 3),
|
||||||
|
('MySQL中,索引的作用是?', 1, '增加存储空间', '提高查询效率', '保证数据完整性', '加密数据', 'B', 2),
|
||||||
|
('Vue.js中,v-if和v-show的区别是?', 1, '没有区别', 'v-if是条件渲染,v-show是CSS切换', 'v-if更快', 'v-show只能用于div', 'B', 3),
|
||||||
|
('Linux中,压缩文件的命令是?', 1, 'zip', 'tar', 'gzip', '以上都可以', 'D', 2),
|
||||||
|
('Java中, transient关键字的作用是?', 1, '序列化时忽略该字段', '线程安全', '常量定义', '同步锁', 'A', 3),
|
||||||
|
('Python的装饰器是?', 1, '一种设计模式', '语法糖,用于修改函数', '一种数据结构', '一种算法', 'B', 2),
|
||||||
|
('Nginx配置文件的默认路径是?', 1, '/etc/nginx/nginx.conf', '/usr/local/nginx/conf/nginx.conf', 'A和B都可能', '/var/nginx/nginx.conf', 'C', 3),
|
||||||
|
('React中,props的作用是?', 1, '存储组件内部状态', '父组件向子组件传递数据', '事件处理', '路由跳转', 'B', 2),
|
||||||
|
('SQL中,GROUP BY用于?', 1, '排序', '分组聚合', '筛选', '连接表', 'B', 3),
|
||||||
|
('Java中,volatile关键字用于?', 1, '声明常量', '保证变量可见性', '序列化', '异常处理', 'B', 2),
|
||||||
|
('CSS中,@media用于?', 1, '导入样式', '定义动画', '响应式布局', '字体设置', 'C', 3),
|
||||||
|
('Git中,撤销上次提交的命令是?', 1, 'git reset', 'git revert', 'git undo', 'git rollback', 'A', 2),
|
||||||
|
('Spring Cloud中,服务注册的组件是?', 1, 'Zuul', 'Eureka', 'Ribbon', 'Hystrix', 'B', 3),
|
||||||
|
('Python中,lambda表达式是?', 1, '匿名函数', '装饰器', '生成器', '迭代器', 'A', 2),
|
||||||
|
('Linux中,查看内存使用情况的命令是?', 1, 'df', 'du', 'free', 'top', 'C', 3),
|
||||||
|
('HTTP是无状态协议,解决会话保持的技术是?', 1, 'Cookie/Session', 'WebSocket', 'TCP', 'UDP', 'A', 2),
|
||||||
|
('Java中,默认的类加载器是?', 1, 'Bootstrap ClassLoader', 'Extension ClassLoader', 'Application ClassLoader', 'User ClassLoader', 'C', 3),
|
||||||
|
('Node.js是基于什么语言开发的?', 1, 'Python', 'Java', 'C++', 'JavaScript', 'D', 2),
|
||||||
|
('Java中,==和equals的区别是?', 1, '没有区别', '==比较引用,equals比较内容', 'equals比较引用,==比较内容', '都比较内容', 'B', 3),
|
||||||
|
('Python中,字典的键可以是?', 1, '列表', '字典', '元组', '集合', 'C', 2),
|
||||||
|
('MySQL中,事务的隔离级别不包括?', 1, 'READ UNCOMMITTED', 'READ COMMITTED', 'WRITE COMMITTED', 'SERIALIZABLE', 'C', 3),
|
||||||
|
('JavaScript中,闭包的作用是?', 1, '提高性能', '实现封装和私有变量', '简化代码', '减少内存', 'B', 2),
|
||||||
|
('Linux中,grep命令的作用是?', 1, '查找文件', '过滤文本', '压缩文件', '排序文本', 'B', 3),
|
||||||
|
('Spring中,@Autowired的作用是?', 1, '声明Bean', '自动注入依赖', '配置属性', '定义切面', 'B', 2);
|
||||||
|
|
||||||
|
-- 简答题(30道)- 20道5分,10道4分
|
||||||
|
INSERT INTO question (title, type, answer, r_answer, score) VALUES
|
||||||
|
('简述Java中ArrayList和LinkedList的区别及适用场景。', 2, '', 'ArrayList基于动态数组实现,查询快O(1)、增删慢O(n),适合随机访问多的场景;LinkedList基于双向链表实现,增删快O(1)、查询慢O(n),适合频繁插入删除的场景。', 5),
|
||||||
|
('什么是数据库事务的ACID特性?请分别解释。', 2, '', 'ACID包括:原子性(Atomicity)事务要么全完成要么全不完成;一致性(Consistency)事务前后数据完整性一致;隔离性(Isolation)事务之间互不干扰;持久性(Durability)事务完成后数据永久保存。', 5),
|
||||||
|
('解释Spring框架中的IOC和AOP概念。', 2, '', 'IOC(控制反转)是将对象创建和依赖关系管理交给Spring容器,降低耦合;AOP(面向切面编程)将横切关注点如日志、事务从业务逻辑中分离,通过代理实现功能增强。', 5),
|
||||||
|
('简述Redis的五种基本数据类型及其使用场景。', 2, '', 'String:缓存、计数器;List:消息队列、时间线;Set:去重、集合运算;ZSet:排行榜;Hash:存储对象属性。', 5),
|
||||||
|
('什么是RESTful API?它有哪些特点?', 2, '', 'RESTful是一种软件架构风格,使用URL定位资源,HTTP动词(GET/POST/PUT/DELETE)操作资源。特点:无状态、统一接口、可缓存、分层系统。', 4),
|
||||||
|
('解释Java中的多线程同步机制有哪些?', 2, '', 'synchronized关键字、ReentrantLock显式锁、volatile保证可见性、Atomic原子类、Concurrent并发集合、Semaphore信号量等。', 5),
|
||||||
|
('MySQL中索引失效的常见情况有哪些?', 2, '', '使用!=或<>、对列进行函数操作、LIKE以%开头、类型隐式转换、OR条件无索引、复合索引未用左前缀、IS NOT NULL等。', 5),
|
||||||
|
('简述TCP三次握手的过程。', 2, '', '第一次:客户端发送SYN;第二次:服务端回复SYN+ACK;第三次:客户端回复ACK。目的是确认双方收发能力正常,同步初始序列号。', 4),
|
||||||
|
('什么是JVM内存模型?主要包括哪些区域?', 2, '', 'JVM内存模型定义多线程共享变量的访问规则。运行时数据区包括:堆、方法区、虚拟机栈、本地方法栈、程序计数器。', 5),
|
||||||
|
('简述Vue.js的生命周期钩子函数。', 2, '', '创建前/后:beforeCreate/created;挂载前/后:beforeMount/mounted;更新前/后:beforeUpdate/updated;销毁前/后:beforeUnmount/unmounted。', 5),
|
||||||
|
('什么是Docker?它解决了什么问题?', 2, '', 'Docker是容器化平台,将应用及其依赖打包为镜像,实现一次构建到处运行。解决了环境不一致、部署复杂、资源利用率低等问题。', 4),
|
||||||
|
('解释Git的工作流程和常用分支策略。', 2, '', '工作区→暂存区→本地仓库→远程仓库。常见策略:Git Flow(master/develop/feature/release/hotfix分支)或GitHub Flow(主干开发)。', 5),
|
||||||
|
('什么是微服务架构?它的优缺点是什么?', 2, '', '将单体应用拆分为小型、独立部署的服务。优点:技术异构、独立扩展、故障隔离;缺点:分布式复杂性、运维成本高、数据一致性难保证。', 5),
|
||||||
|
('简述HTTPS的加密流程。', 2, '', '客户端请求证书→服务端发送证书(含公钥)→客户端验证证书→生成随机对称密钥→用公钥加密发送→双方用对称密钥加密通信。', 5),
|
||||||
|
('Java 8引入了哪些重要新特性?', 2, '', 'Lambda表达式、Stream API、Optional类、新日期时间API、默认方法、方法引用、函数式接口、接口中的静态方法等。', 4),
|
||||||
|
('什么是数据库的范式?请列举前三种。', 2, '', '1NF:属性原子性;2NF:消除部分依赖;3NF:消除传递依赖。目的是减少数据冗余,保证数据一致性。', 5),
|
||||||
|
('解释消息队列MQ的主要作用和使用场景。', 2, '', '作用:异步处理、应用解耦、流量削峰、日志处理。场景:订单处理、秒杀系统、日志收集、通知推送等。', 5),
|
||||||
|
('简述Java垃圾回收的基本原理。', 2, '', 'JVM自动回收不再使用的对象内存。常用算法:标记-清除、复制、标记-整理。现代JVM采用分代收集:新生代用复制,老年代用标记-整理。', 5),
|
||||||
|
('什么是CSS盒模型?标准盒模型和IE盒模型有何区别?', 2, '', '盒模型包含content、padding、border、margin。标准盒模型width/height只含content;IE盒模型包含content+padding+border。通过box-sizing切换。', 4),
|
||||||
|
('解释OAuth 2.0的授权流程。', 2, '', '四种模式:授权码模式(最安全)、简化模式、密码凭证、客户端凭证。主流授权码流程:请求授权→获取code→换取token→访问资源。', 5),
|
||||||
|
('什么是设计模式?请列举三种常用创建型模式。', 2, '', '设计模式是解决特定问题的最佳实践。创建型模式:单例模式、工厂模式、建造者模式、原型模式。', 5),
|
||||||
|
('简述Nginx的负载均衡策略有哪些?', 2, '', '轮询(默认)、权重(weight)、IP哈希(ip_hash)、最少连接(least_conn)、一致性哈希等。', 4),
|
||||||
|
('什么是WebSocket?与HTTP有什么区别?', 2, '', 'WebSocket是全双工通信协议,建立连接后可双向实时通信。HTTP是请求-响应模式、无状态、单向。WebSocket适合实时应用如聊天、股票行情。', 5),
|
||||||
|
('解释Spring Boot的自动配置原理。', 2, '', '@EnableAutoConfiguration通过classpath扫描依赖,根据META-INF/spring.factories中定义的自动配置类,结合条件注解如@ConditionalOnClass进行条件装配。', 5),
|
||||||
|
('MySQL中InnoDB和MyISAM存储引擎的区别?', 2, '', 'InnoDB:支持事务、行级锁、外键、MVCC,适合高并发;MyISAM:表级锁、全文索引、速度快,适合读多写少。', 5),
|
||||||
|
('什么是前端路由?React Router的工作原理是什么?', 2, '', '前端路由在不刷新页面的情况下切换视图。React Router通过history API监听URL变化,匹配Route组件渲染对应组件,实现SPA导航。', 4),
|
||||||
|
('简述分布式系统中的CAP理论。', 2, '', 'CAP指一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。分布式系统无法同时满足三者,最多满足两个,通常是AP或CP。', 5),
|
||||||
|
('什么是线程池?Java中如何创建线程池?', 2, '', '线程池复用线程减少创建销毁开销。通过Executors工厂方法或ThreadPoolExecutor构造函数创建,参数包括核心线程数、最大线程数、队列、拒绝策略等。', 5),
|
||||||
|
('解释跨域问题产生的原因及解决方案。', 2, '', '浏览器同源策略限制不同源请求。方案:CORS(服务端设置响应头)、JSONP(只支持GET)、代理服务器、Nginx反向代理、WebSocket等。', 4),
|
||||||
|
('简述Kafka的基本架构和核心概念。', 2, '', 'Kafka是分布式流处理平台。核心概念:Producer生产者、Consumer消费者、Broker服务器、Topic主题、Partition分区、Offset偏移量、Replication副本。', 5);
|
||||||
105
src/main/java/com/baobaot/exam/Controller/AdminController.java
Normal file
105
src/main/java/com/baobaot/exam/Controller/AdminController.java
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package com.baobaot.exam.Controller;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.adminService;
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examPaper;
|
||||||
|
import com.baobaot.exam.dao.question;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class AdminController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private adminService adminService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
@GetMapping("/admin")
|
||||||
|
public String adminRedirect() {
|
||||||
|
return "redirect:/admin/exam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/exam")
|
||||||
|
public String adminPage(Model model) {
|
||||||
|
List<question> questions = adminService.list();
|
||||||
|
model.addAttribute("questions", questions);
|
||||||
|
return "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admin/add/choice")
|
||||||
|
public String addChoiceQuestion(@RequestParam String title,
|
||||||
|
@RequestParam Integer type,
|
||||||
|
@RequestParam(required = false) String answer_a,
|
||||||
|
@RequestParam(required = false) String answer_b,
|
||||||
|
@RequestParam(required = false) String answer_c,
|
||||||
|
@RequestParam(required = false) String answer_d,
|
||||||
|
@RequestParam String r_answer,
|
||||||
|
@RequestParam Integer score) {
|
||||||
|
adminService.addChoiceQuestion(title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score);
|
||||||
|
return "redirect:/admin/exam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admin/add/essay")
|
||||||
|
public String addEssayQuestion(@RequestParam String title,
|
||||||
|
@RequestParam Integer type,
|
||||||
|
@RequestParam(required = false) String answer,
|
||||||
|
@RequestParam(required = false) String r_answer,
|
||||||
|
@RequestParam Integer score) {
|
||||||
|
adminService.addEssayQuestion(title, type, answer, r_answer, score);
|
||||||
|
return "redirect:/admin/exam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/query/type")
|
||||||
|
public String queryByType(@RequestParam Integer type, Model model) {
|
||||||
|
List<question> questions = adminService.getQuestionsByType(type);
|
||||||
|
model.addAttribute("questions", questions);
|
||||||
|
model.addAttribute("queryType", "类型: " + type);
|
||||||
|
return "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/query/score")
|
||||||
|
public String queryByScore(@RequestParam Integer score, Model model) {
|
||||||
|
List<question> questions = adminService.getQuestionsByScore(score);
|
||||||
|
model.addAttribute("questions", questions);
|
||||||
|
model.addAttribute("queryScore", "分数: " + score);
|
||||||
|
return "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admin/delete")
|
||||||
|
public String deleteByTitle(@RequestParam String title) {
|
||||||
|
adminService.deleteQuestionByTitle(title);
|
||||||
|
return "redirect:/admin/exam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/mkexam")
|
||||||
|
public String makeExamPage(Model model) {
|
||||||
|
return "mkexam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/admin/mkexam/create")
|
||||||
|
@ResponseBody
|
||||||
|
public examPaper createExam(@RequestParam(required = false) String title) {
|
||||||
|
return makeExamService.createExamPaper(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/mkexam/detail/{uid}")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getExamDetail(@PathVariable String uid) {
|
||||||
|
return makeExamService.getExamPaperDetail(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/admin/listexam")
|
||||||
|
public String listExamPage(@RequestParam(required = false) String title, Model model) {
|
||||||
|
List<examPaper> examPapers = makeExamService.getExamList(title);
|
||||||
|
model.addAttribute("examPapers", examPapers);
|
||||||
|
model.addAttribute("searchTitle", title);
|
||||||
|
return "listexam";
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/main/java/com/baobaot/exam/Controller/CorrectController.java
Normal file
147
src/main/java/com/baobaot/exam/Controller/CorrectController.java
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package com.baobaot.exam.Controller;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.CorrectService;
|
||||||
|
import com.baobaot.exam.Service.ExamAnswerService;
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dto.AnswerItem;
|
||||||
|
import com.baobaot.exam.dto.CorrectResult;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批改控制器
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/admin/correct")
|
||||||
|
public class CorrectController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CorrectService correctService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExamAnswerService examAnswerService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批改首页 - 搜索界面
|
||||||
|
*/
|
||||||
|
@GetMapping("")
|
||||||
|
public String correctPage() {
|
||||||
|
return "correct";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试卷标题列表(下拉菜单用)
|
||||||
|
*/
|
||||||
|
@GetMapping("/titles")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getExamTitles() {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<String> titles = correctService.getExamTitleList();
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("data", titles);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索答题记录
|
||||||
|
* @param name 做题人姓名(可选)
|
||||||
|
* @param title 试卷标题(可选)
|
||||||
|
*/
|
||||||
|
@GetMapping("/search")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> searchAnswers(
|
||||||
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) String title) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<examBack> answers = correctService.searchAnswers(name, title);
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("data", answers);
|
||||||
|
result.put("count", answers.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取答题详情(用于批改)
|
||||||
|
* @param answerUid 答题记录UID
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail/{answerUid}")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getAnswerDetail(@PathVariable String answerUid) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
CorrectResult detail = correctService.getAnswerDetailForCorrect(answerUid);
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("data", detail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交批改结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/submit/{answerUid}")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> submitCorrect(
|
||||||
|
@PathVariable String answerUid,
|
||||||
|
@RequestBody Map<String, Integer> essayScores) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// essayScores: key = "q_" + qid, value = 得分
|
||||||
|
int totalScore = correctService.submitCorrect(answerUid, essayScores);
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("message", "批改完成");
|
||||||
|
result.put("totalScore", totalScore);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印页面 - 纯文本试卷答案和分数
|
||||||
|
* @param answerUid 答题记录UID
|
||||||
|
*/
|
||||||
|
@GetMapping("/print/{answerUid}")
|
||||||
|
public String printPage(@PathVariable String answerUid, Model model) {
|
||||||
|
try {
|
||||||
|
CorrectResult detail = correctService.getAnswerDetailForCorrect(answerUid);
|
||||||
|
model.addAttribute("data", detail);
|
||||||
|
return "print";
|
||||||
|
} catch (Exception e) {
|
||||||
|
model.addAttribute("error", e.getMessage());
|
||||||
|
return "print";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package com.baobaot.exam.Controller;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.ExamAnswerService;
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dao.examPaper;
|
||||||
|
import com.baobaot.exam.dto.AnswerItem;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ExamAnswerController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExamAnswerService examAnswerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题页面 - 根据试卷UID显示答题界面
|
||||||
|
*/
|
||||||
|
@GetMapping("/{examUid}")
|
||||||
|
public String answerPage(@PathVariable String examUid, Model model) {
|
||||||
|
// 获取试卷详情
|
||||||
|
Map<String, Object> examDetail = makeExamService.getExamPaperDetail(examUid);
|
||||||
|
|
||||||
|
if (examDetail == null) {
|
||||||
|
model.addAttribute("error", "试卷不存在");
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("examUid", examUid);
|
||||||
|
model.addAttribute("title", examDetail.get("title") != null ? examDetail.get("title") : "未命名试卷");
|
||||||
|
model.addAttribute("choiceQuestions", examDetail.get("choiceQuestions"));
|
||||||
|
model.addAttribute("essayQuestions", examDetail.get("essayQuestions"));
|
||||||
|
model.addAttribute("choiceCount", examDetail.get("choiceCount"));
|
||||||
|
model.addAttribute("essayCount", examDetail.get("essayCount"));
|
||||||
|
model.addAttribute("totalScore", examDetail.get("totalScore"));
|
||||||
|
|
||||||
|
return "answer";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于接收前端传来的答卷数据的 DTO 内部类
|
||||||
|
*/
|
||||||
|
public static class SubmitRequest {
|
||||||
|
private String name;
|
||||||
|
private Map<String, String> answers;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAnswers() {
|
||||||
|
return answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnswers(Map<String, String> answers) {
|
||||||
|
this.answers = answers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交答案
|
||||||
|
*/
|
||||||
|
@PostMapping("/{examUid}/submit")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> submitAnswer(@PathVariable String examUid,
|
||||||
|
@RequestBody SubmitRequest request) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String name = request.getName();
|
||||||
|
Map<String, String> answers = request.getAnswers();
|
||||||
|
|
||||||
|
// 验证试卷是否存在
|
||||||
|
examPaper paper = makeExamService.getExamPaperByUid(examUid);
|
||||||
|
if (paper == null) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "试卷不存在");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证姓名
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "请输入做题人姓名");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用服务提交答案
|
||||||
|
String answerUid = examAnswerService.submitAnswer(examUid, name, answers);
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("message", "提交成功");
|
||||||
|
result.put("answerUid", answerUid);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询答题记录详情(根据答题记录UID)
|
||||||
|
*/
|
||||||
|
@GetMapping("/answer/{answerUid}")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getAnswerDetail(@PathVariable String answerUid) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
examBack back = examAnswerService.getAnswerByUid(answerUid);
|
||||||
|
if (back == null) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "答题记录不存在");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析答案内容
|
||||||
|
List<AnswerItem> answers = examAnswerService.parseAnswerContent(back.getContent());
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("uid", back.getUid());
|
||||||
|
result.put("examId", back.getExamId());
|
||||||
|
result.put("name", back.getName());
|
||||||
|
result.put("answers", answers);
|
||||||
|
result.put("submitTime", back.getId()); // 如果有时间字段可以替换
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某试卷的所有答题记录
|
||||||
|
*/
|
||||||
|
@GetMapping("/{examUid}/answers")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getAnswersByExam(@PathVariable String examUid) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证试卷是否存在
|
||||||
|
examPaper paper = makeExamService.getExamPaperByUid(examUid);
|
||||||
|
if (paper == null) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "试卷不存在");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<examBack> answers = examAnswerService.getAnswersByExamId(examUid);
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("examUid", examUid);
|
||||||
|
result.put("examTitle", paper.getTitle());
|
||||||
|
result.put("count", answers.size());
|
||||||
|
result.put("answers", answers);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.baobaot.exam.Controller;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examPaper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/exam")
|
||||||
|
public class ExamController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建试卷
|
||||||
|
*/
|
||||||
|
@PostMapping("/create")
|
||||||
|
public ResponseEntity<Map<String, Object>> createExam(@RequestParam(required = false) String title) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
try {
|
||||||
|
examPaper paper = makeExamService.createExamPaper(title);
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("uid", paper.getUid());
|
||||||
|
result.put("title", paper.getTitle());
|
||||||
|
result.put("select", paper.getSelect());
|
||||||
|
result.put("content", paper.getContent());
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据UID查询试卷
|
||||||
|
*/
|
||||||
|
@GetMapping("/get/{uid}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getExam(@PathVariable String uid) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
examPaper paper = makeExamService.getExamPaperByUid(uid);
|
||||||
|
if (paper != null) {
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("uid", paper.getUid());
|
||||||
|
result.put("title", paper.getTitle());
|
||||||
|
result.put("select", paper.getSelect());
|
||||||
|
result.put("content", paper.getContent());
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
} else {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "试卷不存在");
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/com/baobaot/exam/Service/CorrectService.java
Normal file
42
src/main/java/com/baobaot/exam/Service/CorrectService.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package com.baobaot.exam.Service;
|
||||||
|
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dto.CorrectResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批改服务接口
|
||||||
|
*/
|
||||||
|
public interface CorrectService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索答题记录
|
||||||
|
* @param name 做题人姓名(可选)
|
||||||
|
* @param title 试卷标题(可选)
|
||||||
|
* @return 答题记录列表
|
||||||
|
*/
|
||||||
|
List<examBack> searchAnswers(String name, String title);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有试卷标题列表(去重)
|
||||||
|
* @return 试卷标题列表
|
||||||
|
*/
|
||||||
|
List<String> getExamTitleList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取答题详情(用于批改)
|
||||||
|
* @param answerUid 答题记录UID
|
||||||
|
* @return 批改详情(包含题目、答案、选择题自动批改结果)
|
||||||
|
*/
|
||||||
|
CorrectResult getAnswerDetailForCorrect(String answerUid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交批改结果
|
||||||
|
* @param answerUid 答题记录UID
|
||||||
|
* @param essayScores 简答题得分 Map<qid, score>
|
||||||
|
* @return 总得分
|
||||||
|
*/
|
||||||
|
int submitCorrect(String answerUid, Map<String, Integer> essayScores);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.baobaot.exam.Service;
|
||||||
|
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dto.AnswerItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface ExamAnswerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交答案
|
||||||
|
* @param examUid 试卷UID
|
||||||
|
* @param name 做题人姓名
|
||||||
|
* @param answers 答案Map (key: q_题目ID, value: 答案)
|
||||||
|
* @return 答题记录UID
|
||||||
|
*/
|
||||||
|
String submitAnswer(String examUid, String name, Map<String, String> answers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据答题记录UID查询答案
|
||||||
|
*/
|
||||||
|
examBack getAnswerByUid(String uid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据试卷UID查询所有答题记录
|
||||||
|
*/
|
||||||
|
List<examBack> getAnswersByExamId(String examId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析答题记录的content JSON
|
||||||
|
* @param content examBack.content 字段的JSON字符串
|
||||||
|
* @return 答案项列表
|
||||||
|
*/
|
||||||
|
List<AnswerItem> parseAnswerContent(String content);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据答题记录UID获取解析后的答案列表
|
||||||
|
* @param uid 答题记录UID
|
||||||
|
* @return 答案项列表
|
||||||
|
*/
|
||||||
|
List<AnswerItem> getParsedAnswersByUid(String uid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package com.baobaot.exam.Service.impl;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.CorrectService;
|
||||||
|
import com.baobaot.exam.Service.ExamAnswerService;
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dao.question;
|
||||||
|
import com.baobaot.exam.dto.AnswerItem;
|
||||||
|
import com.baobaot.exam.dto.CorrectDetailItem;
|
||||||
|
import com.baobaot.exam.dto.CorrectResult;
|
||||||
|
import com.baobaot.exam.mapper.examBackMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CorrectServiceImpl implements CorrectService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private examBackMapper examBackMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ExamAnswerService examAnswerService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<examBack> searchAnswers(String name, String title) {
|
||||||
|
LambdaQueryWrapper<examBack> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
// 根据姓名搜索(模糊匹配)
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
wrapper.like(examBack::getName, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据试卷标题搜索(精确匹配)
|
||||||
|
if (StringUtils.hasText(title)) {
|
||||||
|
wrapper.eq(examBack::getTitle, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按ID降序,最新的在前面
|
||||||
|
wrapper.orderByDesc(examBack::getId);
|
||||||
|
|
||||||
|
return examBackMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getExamTitleList() {
|
||||||
|
// 获取所有不重复的试卷标题(使用Java去重,更可靠)
|
||||||
|
LambdaQueryWrapper<examBack> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.isNotNull(examBack::getTitle);
|
||||||
|
wrapper.ne(examBack::getTitle, ""); // 排除空字符串
|
||||||
|
|
||||||
|
List<examBack> list = examBackMapper.selectList(wrapper);
|
||||||
|
|
||||||
|
// 使用Set去重,然后排序
|
||||||
|
Set<String> titleSet = new HashSet<>();
|
||||||
|
for (examBack back : list) {
|
||||||
|
if (StringUtils.hasText(back.getTitle())) {
|
||||||
|
titleSet.add(back.getTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> titles = new ArrayList<>(titleSet);
|
||||||
|
Collections.sort(titles); // 按字母顺序排序
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CorrectResult getAnswerDetailForCorrect(String answerUid) {
|
||||||
|
// 1. 获取答题记录
|
||||||
|
examBack back = examAnswerService.getAnswerByUid(answerUid);
|
||||||
|
if (back == null) {
|
||||||
|
throw new RuntimeException("答题记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取试卷详情
|
||||||
|
Map<String, Object> examDetail = makeExamService.getExamPaperDetail(back.getExamId());
|
||||||
|
if (examDetail == null) {
|
||||||
|
throw new RuntimeException("试卷不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 解析答案(包含得分)
|
||||||
|
List<AnswerItem> answerItems = examAnswerService.parseAnswerContent(back.getContent());
|
||||||
|
Map<Integer, AnswerItem> answerItemMap = new HashMap<>();
|
||||||
|
for (AnswerItem item : answerItems) {
|
||||||
|
answerItemMap.put(item.getQid(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 获取题目列表
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> choiceQuestions = (List<question>) examDetail.get("choiceQuestions");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> essayQuestions = (List<question>) examDetail.get("essayQuestions");
|
||||||
|
|
||||||
|
// 5. 构建批改详情
|
||||||
|
List<CorrectDetailItem> detailItems = new ArrayList<>();
|
||||||
|
int choiceScore = 0;
|
||||||
|
int essayTotalScore = 0;
|
||||||
|
int exmaid = 1;
|
||||||
|
|
||||||
|
// 处理选择题(自动批改)
|
||||||
|
for (question q : choiceQuestions) {
|
||||||
|
AnswerItem answerItem = answerItemMap.get(q.getId());
|
||||||
|
String userAnswer = answerItem != null ? answerItem.getAnswer() : "";
|
||||||
|
String correctAnswer = q.getRAnswer();
|
||||||
|
boolean isCorrect = userAnswer.equalsIgnoreCase(correctAnswer);
|
||||||
|
int score = isCorrect ? q.getScore() : 0;
|
||||||
|
choiceScore += score;
|
||||||
|
|
||||||
|
CorrectDetailItem item = CorrectDetailItem.builder()
|
||||||
|
.exmaid(exmaid++)
|
||||||
|
.qid(q.getId())
|
||||||
|
.type(1)
|
||||||
|
.title(q.getTitle())
|
||||||
|
.score(q.getScore())
|
||||||
|
.userAnswer(userAnswer)
|
||||||
|
.correctAnswer(correctAnswer)
|
||||||
|
.autoCorrect(true)
|
||||||
|
.autoScore(score)
|
||||||
|
.isCorrect(isCorrect)
|
||||||
|
.build();
|
||||||
|
detailItems.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理简答题(需要人工批改)
|
||||||
|
int essayScore = 0;
|
||||||
|
for (question q : essayQuestions) {
|
||||||
|
AnswerItem answerItem = answerItemMap.get(q.getId());
|
||||||
|
String userAnswer = answerItem != null ? answerItem.getAnswer() : "";
|
||||||
|
// 从 answerItem 中获取得分(如果已批改)
|
||||||
|
Integer itemScore = answerItem != null ? answerItem.getScore() : null;
|
||||||
|
essayTotalScore += q.getScore();
|
||||||
|
if (itemScore != null) {
|
||||||
|
essayScore += itemScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrectDetailItem item = CorrectDetailItem.builder()
|
||||||
|
.exmaid(exmaid++)
|
||||||
|
.qid(q.getId())
|
||||||
|
.type(2)
|
||||||
|
.title(q.getTitle())
|
||||||
|
.score(q.getScore())
|
||||||
|
.userAnswer(userAnswer)
|
||||||
|
.correctAnswer(q.getRAnswer())
|
||||||
|
.autoCorrect(false)
|
||||||
|
.autoScore(itemScore != null ? itemScore : 0)
|
||||||
|
.isCorrect(null)
|
||||||
|
.build();
|
||||||
|
detailItems.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 判断是否已批改(exam_back.score字段不为空表示已批改)
|
||||||
|
boolean isCorrected = back.getScore() != null && !back.getScore().isEmpty();
|
||||||
|
int finalScore = isCorrected ? (choiceScore + essayScore) : choiceScore;
|
||||||
|
|
||||||
|
return CorrectResult.builder()
|
||||||
|
.answerUid(answerUid)
|
||||||
|
.studentName(back.getName())
|
||||||
|
.examUid(back.getExamId())
|
||||||
|
.examTitle((String) examDetail.get("title"))
|
||||||
|
.choiceScore(choiceScore)
|
||||||
|
.essayTotalScore(essayTotalScore)
|
||||||
|
.totalScore(choiceScore + essayTotalScore)
|
||||||
|
.isCorrected(isCorrected)
|
||||||
|
.finalScore(finalScore)
|
||||||
|
.questions(detailItems)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int submitCorrect(String answerUid, Map<String, Integer> essayScores) {
|
||||||
|
// 1. 获取答题记录
|
||||||
|
examBack back = examAnswerService.getAnswerByUid(answerUid);
|
||||||
|
if (back == null) {
|
||||||
|
throw new RuntimeException("答题记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取试卷详情以计算选择题得分
|
||||||
|
Map<String, Object> examDetail = makeExamService.getExamPaperDetail(back.getExamId());
|
||||||
|
if (examDetail == null) {
|
||||||
|
throw new RuntimeException("试卷不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 解析答案
|
||||||
|
List<AnswerItem> answerItems = examAnswerService.parseAnswerContent(back.getContent());
|
||||||
|
Map<Integer, AnswerItem> answerItemMap = new HashMap<>();
|
||||||
|
for (AnswerItem item : answerItems) {
|
||||||
|
answerItemMap.put(item.getQid(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 计算选择题得分并设置简答题得分
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> choiceQuestions = (List<question>) examDetail.get("choiceQuestions");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> essayQuestions = (List<question>) examDetail.get("essayQuestions");
|
||||||
|
|
||||||
|
int choiceScore = 0;
|
||||||
|
int essayScore = 0;
|
||||||
|
|
||||||
|
// 设置选择题得分
|
||||||
|
for (question q : choiceQuestions) {
|
||||||
|
AnswerItem item = answerItemMap.get(q.getId());
|
||||||
|
if (item == null) continue;
|
||||||
|
|
||||||
|
String userAnswer = item.getAnswer();
|
||||||
|
if (userAnswer != null && userAnswer.equalsIgnoreCase(q.getRAnswer())) {
|
||||||
|
item.setScore(q.getScore());
|
||||||
|
choiceScore += q.getScore();
|
||||||
|
} else {
|
||||||
|
item.setScore(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置简答题得分
|
||||||
|
for (question q : essayQuestions) {
|
||||||
|
String key = "q_" + q.getId();
|
||||||
|
Integer score = essayScores.get(key);
|
||||||
|
if (score != null) {
|
||||||
|
AnswerItem item = answerItemMap.get(q.getId());
|
||||||
|
if (item != null) {
|
||||||
|
item.setScore(score);
|
||||||
|
essayScore += score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 计算总分
|
||||||
|
int totalScore = choiceScore + essayScore;
|
||||||
|
|
||||||
|
// 6. 更新答题记录(更新content和score)
|
||||||
|
try {
|
||||||
|
back.setContent(objectMapper.writeValueAsString(answerItems));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("保存答案得分失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
back.setScore(String.valueOf(totalScore));
|
||||||
|
examBackMapper.updateById(back);
|
||||||
|
|
||||||
|
return totalScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.baobaot.exam.Service.impl;
|
||||||
|
|
||||||
|
import com.baobaot.exam.Service.ExamAnswerService;
|
||||||
|
import com.baobaot.exam.Service.makeExamService;
|
||||||
|
import com.baobaot.exam.dao.examBack;
|
||||||
|
import com.baobaot.exam.dao.examPaper;
|
||||||
|
import com.baobaot.exam.dao.question;
|
||||||
|
import com.baobaot.exam.dto.AnswerItem;
|
||||||
|
import com.baobaot.exam.mapper.examBackMapper;
|
||||||
|
import com.baobaot.exam.util.UidGenerator;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ExamAnswerServiceImpl implements ExamAnswerService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private makeExamService makeExamService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private examBackMapper examBackMapper;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String submitAnswer(String examUid, String name, Map<String, String> answers) {
|
||||||
|
try {
|
||||||
|
// 获取试卷题目
|
||||||
|
Map<String, Object> examDetail = makeExamService.getExamPaperDetail(examUid);
|
||||||
|
if (examDetail == null) {
|
||||||
|
throw new RuntimeException("试卷不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> choiceQuestions = (List<question>) examDetail.get("choiceQuestions");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<question> essayQuestions = (List<question>) examDetail.get("essayQuestions");
|
||||||
|
|
||||||
|
// 构建答案JSON,格式: [{"exmaid": 1, "qid": 3, "answer": "A"}]
|
||||||
|
List<AnswerItem> answerList = new ArrayList<>();
|
||||||
|
|
||||||
|
// 处理选择题答案
|
||||||
|
int exmaid = 1; // 试卷中的题目序号,从1开始
|
||||||
|
for (question q : choiceQuestions) {
|
||||||
|
AnswerItem item = AnswerItem.builder()
|
||||||
|
.exmaid(exmaid++)
|
||||||
|
.qid(q.getId())
|
||||||
|
.answer(answers.getOrDefault("q_" + q.getId(), ""))
|
||||||
|
.build();
|
||||||
|
answerList.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理简答题答案
|
||||||
|
for (question q : essayQuestions) {
|
||||||
|
AnswerItem item = AnswerItem.builder()
|
||||||
|
.exmaid(exmaid++)
|
||||||
|
.qid(q.getId())
|
||||||
|
.answer(answers.getOrDefault("q_" + q.getId(), ""))
|
||||||
|
.build();
|
||||||
|
answerList.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一UID
|
||||||
|
String uid = UidGenerator.generateUniqueUid(this::isAnswerUidExists);
|
||||||
|
|
||||||
|
// 获取试卷标题
|
||||||
|
examPaper paper = makeExamService.getExamPaperByUid(examUid);
|
||||||
|
String examTitle = paper != null ? paper.getTitle() : "";
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
examBack back = examBack.builder()
|
||||||
|
.uid(uid)
|
||||||
|
.examId(examUid)
|
||||||
|
.title(examTitle)
|
||||||
|
.name(name.trim())
|
||||||
|
.content(objectMapper.writeValueAsString(answerList))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
examBackMapper.insert(back);
|
||||||
|
|
||||||
|
return uid;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("提交失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public examBack getAnswerByUid(String uid) {
|
||||||
|
LambdaQueryWrapper<examBack> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(examBack::getUid, uid);
|
||||||
|
return examBackMapper.selectOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<examBack> getAnswersByExamId(String examId) {
|
||||||
|
LambdaQueryWrapper<examBack> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(examBack::getExamId, examId);
|
||||||
|
return examBackMapper.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查答题记录UID是否已存在
|
||||||
|
*/
|
||||||
|
private boolean isAnswerUidExists(String uid) {
|
||||||
|
LambdaQueryWrapper<examBack> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(examBack::getUid, uid);
|
||||||
|
return examBackMapper.selectCount(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AnswerItem> parseAnswerContent(String content) {
|
||||||
|
try {
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return objectMapper.readValue(content, new TypeReference<List<AnswerItem>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("解析答案内容失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AnswerItem> getParsedAnswersByUid(String uid) {
|
||||||
|
examBack back = getAnswerByUid(uid);
|
||||||
|
if (back == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return parseAnswerContent(back.getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/java/com/baobaot/exam/dto/AnswerItem.java
Normal file
37
src/main/java/com/baobaot/exam/dto/AnswerItem.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.baobaot.exam.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题记录中的单个答案项
|
||||||
|
* 对应 exam_back.content JSON 数组中的每个元素
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AnswerItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试卷中的题目序号(第几题)
|
||||||
|
*/
|
||||||
|
private Integer exmaid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目在题库中的ID
|
||||||
|
*/
|
||||||
|
private Integer qid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户提交的答案
|
||||||
|
*/
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 该题得分(批改后填充)
|
||||||
|
*/
|
||||||
|
private Integer score;
|
||||||
|
}
|
||||||
66
src/main/java/com/baobaot/exam/dto/CorrectDetailItem.java
Normal file
66
src/main/java/com/baobaot/exam/dto/CorrectDetailItem.java
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package com.baobaot.exam.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批改详情中的单个题目项
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CorrectDetailItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试卷中的题目序号
|
||||||
|
*/
|
||||||
|
private Integer exmaid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目在题库中的ID
|
||||||
|
*/
|
||||||
|
private Integer qid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目类型: 1-选择题, 2-简答题
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目内容
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目分值
|
||||||
|
*/
|
||||||
|
private Integer score;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户提交的答案
|
||||||
|
*/
|
||||||
|
private String userAnswer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正确答案(选择题)/参考答案(简答题)
|
||||||
|
*/
|
||||||
|
private String correctAnswer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否自动批改(选择题为true)
|
||||||
|
*/
|
||||||
|
private Boolean autoCorrect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动批改得分(选择题)
|
||||||
|
*/
|
||||||
|
private Integer autoScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否正确(选择题)
|
||||||
|
*/
|
||||||
|
private Boolean isCorrect;
|
||||||
|
}
|
||||||
69
src/main/java/com/baobaot/exam/dto/CorrectResult.java
Normal file
69
src/main/java/com/baobaot/exam/dto/CorrectResult.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.baobaot.exam.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批改结果DTO
|
||||||
|
* 用于返回批改页面的完整数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CorrectResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题记录UID
|
||||||
|
*/
|
||||||
|
private String answerUid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 做题人姓名
|
||||||
|
*/
|
||||||
|
private String studentName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试卷UID
|
||||||
|
*/
|
||||||
|
private String examUid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试卷标题
|
||||||
|
*/
|
||||||
|
private String examTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择题自动批改得分
|
||||||
|
*/
|
||||||
|
private Integer choiceScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简答题总分值
|
||||||
|
*/
|
||||||
|
private Integer essayTotalScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 试卷总分
|
||||||
|
*/
|
||||||
|
private Integer totalScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已批改
|
||||||
|
*/
|
||||||
|
private Boolean isCorrected;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最终得分(已批改时显示)
|
||||||
|
*/
|
||||||
|
private Integer finalScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目详情列表
|
||||||
|
*/
|
||||||
|
private List<CorrectDetailItem> questions;
|
||||||
|
}
|
||||||
37
src/main/java/com/baobaot/exam/util/UidGenerator.java
Normal file
37
src/main/java/com/baobaot/exam/util/UidGenerator.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.baobaot.exam.util;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成16位随机UID工具类
|
||||||
|
* 包含A-Z, a-z, 0-9字符
|
||||||
|
*/
|
||||||
|
public class UidGenerator {
|
||||||
|
|
||||||
|
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
private static final int UID_LENGTH = 16;
|
||||||
|
private static final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成16位随机UID
|
||||||
|
*/
|
||||||
|
public static String generateUid() {
|
||||||
|
StringBuilder sb = new StringBuilder(UID_LENGTH);
|
||||||
|
for (int i = 0; i < UID_LENGTH; i++) {
|
||||||
|
int index = random.nextInt(CHARACTERS.length());
|
||||||
|
sb.append(CHARACTERS.charAt(index));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一的UID(需传入检查唯一性的接口)
|
||||||
|
*/
|
||||||
|
public static String generateUniqueUid(java.util.function.Function<String, Boolean> existsChecker) {
|
||||||
|
String uid;
|
||||||
|
do {
|
||||||
|
uid = generateUid();
|
||||||
|
} while (existsChecker.apply(uid));
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/resources/application.yml
Normal file
9
src/main/resources/application.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: exam
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/exam
|
||||||
|
# url: jdbc:mysql://192.168.1.56:3306/exam?useSSL=true&requireSSL=true&serverTimezone=UTC
|
||||||
|
username: root
|
||||||
|
password: 521707
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
388
src/main/resources/templates/admin.html
Normal file
388
src/main/resources/templates/admin.html
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>题库管理系统</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
label .required {
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
input, select, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
.grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.query-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.query-section .form-group {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tag-choice {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.tag-essay {
|
||||||
|
background: #ffedd5;
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
.tag-answer {
|
||||||
|
background: #fef9c3;
|
||||||
|
color: #a16207;
|
||||||
|
}
|
||||||
|
.score-badge {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.empty-state svg {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>题库管理系统</h1>
|
||||||
|
|
||||||
|
<!-- 添加题目 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>添加题目</h2>
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('choice')">选择题 (Type 1)</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('essay')">简答题 (Type 2)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 选择题表单 -->
|
||||||
|
<div id="choice-tab" class="tab-content active">
|
||||||
|
<form th:action="@{/admin/add/choice}" method="post">
|
||||||
|
<input type="hidden" name="type" value="1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>题目</label>
|
||||||
|
<textarea name="title" placeholder="请输入题目内容" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选项 A</label>
|
||||||
|
<input type="text" name="answer_a" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选项 B</label>
|
||||||
|
<input type="text" name="answer_b" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选项 C</label>
|
||||||
|
<input type="text" name="answer_c" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选项 D</label>
|
||||||
|
<input type="text" name="answer_d" placeholder="选填">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>正确答案<span class="required">*</span></label>
|
||||||
|
<select name="r_answer" required>
|
||||||
|
<option value="">请选择正确答案</option>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
<option value="D">D</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>分值</label>
|
||||||
|
<input type="number" name="score" placeholder="请输入分值" required min="0">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">添加选择题</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简答题表单 -->
|
||||||
|
<div id="essay-tab" class="tab-content">
|
||||||
|
<form th:action="@{/admin/add/essay}" method="post">
|
||||||
|
<input type="hidden" name="type" value="2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>题目</label>
|
||||||
|
<textarea name="title" placeholder="请输入题目内容" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>学生答案参考</label>
|
||||||
|
<textarea name="answer" placeholder="选填"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>参考答案</label>
|
||||||
|
<textarea name="r_answer" placeholder="选填"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>分值</label>
|
||||||
|
<input type="number" name="score" placeholder="请输入分值" required min="0">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">添加简答题</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 查询 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>查询题目</h2>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div>
|
||||||
|
<form th:action="@{/admin/query/type}" method="get" class="query-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>按类型查询</label>
|
||||||
|
<select name="type" required>
|
||||||
|
<option value="1">选择题</option>
|
||||||
|
<option value="2">简答题</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">查询</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form th:action="@{/admin/query/score}" method="get" class="query-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>按分值查询</label>
|
||||||
|
<input type="number" name="score" placeholder="请输入分值" required min="0">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success">查询</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px; display: flex; gap: 12px;">
|
||||||
|
<a th:href="@{/admin/exam}" class="btn btn-primary">显示全部</a>
|
||||||
|
<a th:href="@{/admin/mkexam}" class="btn btn-success">创建试卷</a>
|
||||||
|
<a th:href="@{/admin/listexam}" class="btn btn-success">查看试卷</a>
|
||||||
|
<a th:href="@{/admin/correct}" class="btn btn-success" style="background: #f59e0b;">批改试卷</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 题目列表 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>
|
||||||
|
题目列表
|
||||||
|
<span th:if="${queryType != null}" class="tag tag-choice" th:text="${queryType}" style="margin-left: 10px;"></span>
|
||||||
|
<span th:if="${queryScore != null}" class="tag tag-essay" th:text="${queryScore}" style="margin-left: 10px;"></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div th:if="${#lists.isEmpty(questions)}" class="empty-state">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<p>暂无题目数据</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table th:if="${!#lists.isEmpty(questions)}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>题目</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>选项/学生答案</th>
|
||||||
|
<th>正确答案</th>
|
||||||
|
<th>分值</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="q : ${questions}">
|
||||||
|
<td th:text="${q.id}"></td>
|
||||||
|
<td th:text="${q.title}" style="max-width: 300px; word-wrap: break-word;"></td>
|
||||||
|
<td>
|
||||||
|
<span th:if="${q.type == 1}" class="tag tag-choice">选择题</span>
|
||||||
|
<span th:if="${q.type == 2}" class="tag tag-essay">简答题</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div th:if="${q.type == 1}">
|
||||||
|
<span th:if="${q.answerA != null and q.answerA != ''}" th:text="'A: ' + ${q.answerA}"></span><br th:if="${q.answerA != null and q.answerA != ''}">
|
||||||
|
<span th:if="${q.answerB != null and q.answerB != ''}" th:text="'B: ' + ${q.answerB}"></span><br th:if="${q.answerB != null and q.answerB != ''}">
|
||||||
|
<span th:if="${q.answerC != null and q.answerC != ''}" th:text="'C: ' + ${q.answerC}"></span><br th:if="${q.answerC != null and q.answerC != ''}">
|
||||||
|
<span th:if="${q.answerD != null and q.answerD != ''}" th:text="'D: ' + ${q.answerD}"></span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${q.type == 2}" th:text="${q.answer != null ? q.answer : '无答案'}"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span th:if="${q.rAnswer != null and q.rAnswer != ''}" class="tag tag-answer" th:text="${q.rAnswer}"></span>
|
||||||
|
<span th:if="${q.rAnswer == null or q.rAnswer == ''}" style="color: #999;">-</span>
|
||||||
|
</td>
|
||||||
|
<td><span class="score-badge" th:text="${q.score}"></span></td>
|
||||||
|
<td>
|
||||||
|
<form th:action="@{/admin/delete}" method="post" style="display: inline;">
|
||||||
|
<input type="hidden" name="title" th:value="${q.title}">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('确定要删除这道题吗?')">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function switchTab(type) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
event.target.classList.add('active');
|
||||||
|
document.getElementById(type + '-tab').classList.add('active');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
417
src/main/resources/templates/answer.html
Normal file
417
src/main/resources/templates/answer.html
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title th:text="${title} + ' - 在线答题'">在线答题</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.exam-header {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.exam-title {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.exam-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.student-info {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.student-info label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.student-info input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.student-info input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.question-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.question-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.question-number {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.question-type {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.question-score {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
margin-left: 44px;
|
||||||
|
}
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.option:hover {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
.option input[type="radio"] {
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.option-label {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.essay-input {
|
||||||
|
margin-left: 44px;
|
||||||
|
}
|
||||||
|
.essay-input textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
resize: vertical;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.essay-input textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.submit-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.btn-submit {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.btn-submit:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
.btn-submit:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.modal-content h2 {
|
||||||
|
color: #f59e0b;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.modal-content p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.modal-content .answer-uid {
|
||||||
|
background: #fef3c7;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.btn-close {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
color: #e74c3c;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div th:if="${error}" class="error-message" th:text="${error}"></div>
|
||||||
|
|
||||||
|
<!-- 答题界面 -->
|
||||||
|
<div th:unless="${error}">
|
||||||
|
<!-- 试卷头部 -->
|
||||||
|
<div class="exam-header">
|
||||||
|
<h1 class="exam-title" th:text="${title}">试卷标题</h1>
|
||||||
|
<div class="exam-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">选择题</div>
|
||||||
|
<div class="stat-value" th:text="${choiceCount} + '道'">0道</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">简答题</div>
|
||||||
|
<div class="stat-value" th:text="${essayCount} + '道'">0道</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">总分</div>
|
||||||
|
<div class="stat-value" th:text="${totalScore} + '分'">100分</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="student-info">
|
||||||
|
<label for="studentName">做题人姓名 *</label>
|
||||||
|
<input type="text" id="studentName" placeholder="请输入您的姓名" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 答题表单 -->
|
||||||
|
<form id="answerForm">
|
||||||
|
<input type="hidden" id="examUid" th:value="${examUid}">
|
||||||
|
|
||||||
|
<!-- 选择题 -->
|
||||||
|
<div th:each="q, iterStat : ${choiceQuestions}" class="question-card">
|
||||||
|
<div class="question-header">
|
||||||
|
<span class="question-number" th:text="${iterStat.index + 1}">1</span>
|
||||||
|
<span class="question-title" th:text="${q.title}">题目内容</span>
|
||||||
|
<span class="question-score" th:text="${q.score} + '分'">3分</span>
|
||||||
|
<span class="question-type">选择题</span>
|
||||||
|
</div>
|
||||||
|
<div class="options">
|
||||||
|
<label class="option" th:if="${q.answerA != null and q.answerA != ''}">
|
||||||
|
<input type="radio" th:name="'q_' + ${q.id}" value="A" required>
|
||||||
|
<span class="option-label" th:text="'A. ' + ${q.answerA}">A. 选项A</span>
|
||||||
|
</label>
|
||||||
|
<label class="option" th:if="${q.answerB != null and q.answerB != ''}">
|
||||||
|
<input type="radio" th:name="'q_' + ${q.id}" value="B">
|
||||||
|
<span class="option-label" th:text="'B. ' + ${q.answerB}">B. 选项B</span>
|
||||||
|
</label>
|
||||||
|
<label class="option" th:if="${q.answerC != null and q.answerC != ''}">
|
||||||
|
<input type="radio" th:name="'q_' + ${q.id}" value="C">
|
||||||
|
<span class="option-label" th:text="'C. ' + ${q.answerC}">C. 选项C</span>
|
||||||
|
</label>
|
||||||
|
<label class="option" th:if="${q.answerD != null and q.answerD != ''}">
|
||||||
|
<input type="radio" th:name="'q_' + ${q.id}" value="D">
|
||||||
|
<span class="option-label" th:text="'D. ' + ${q.answerD}">D. 选项D</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简答题 -->
|
||||||
|
<div th:each="q, iterStat : ${essayQuestions}" class="question-card">
|
||||||
|
<div class="question-header">
|
||||||
|
<span class="question-number" th:text="${choiceCount + iterStat.index + 1}">1</span>
|
||||||
|
<span class="question-title" th:text="${q.title}">题目内容</span>
|
||||||
|
<span class="question-score" th:text="${q.score} + '分'">5分</span>
|
||||||
|
<span class="question-type">简答题</span>
|
||||||
|
</div>
|
||||||
|
<div class="essay-input">
|
||||||
|
<textarea th:name="'q_' + ${q.id}" placeholder="请输入您的答案..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="submit-section">
|
||||||
|
<button type="submit" class="btn-submit">提交答案</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成功弹窗 -->
|
||||||
|
<div class="modal" id="successModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>✓ 提交成功</h2>
|
||||||
|
<p>您的答案已保存,答题记录UID:</p>
|
||||||
|
<div class="answer-uid" id="answerUid"></div>
|
||||||
|
<button class="btn-close" onclick="closeModal()">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
document.getElementById('answerForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const name = document.getElementById('studentName').value.trim();
|
||||||
|
if (!name) {
|
||||||
|
alert('请输入做题人姓名');
|
||||||
|
document.getElementById('studentName').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const answers = {};
|
||||||
|
|
||||||
|
// 遍历 FormData 收集答案,排名前面的系统字段
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
if (key !== 'name' && key !== 'examUid') {
|
||||||
|
answers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const examUid = document.getElementById('examUid').value;
|
||||||
|
const submitBtn = document.querySelector('.btn-submit');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = '提交中...';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: name,
|
||||||
|
answers: answers
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/' + examUid + '/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// 填入返回的答案 UID 并显示弹窗
|
||||||
|
const answerUidElement = document.getElementById('answerUid');
|
||||||
|
if (answerUidElement) {
|
||||||
|
answerUidElement.textContent = data.answerUid;
|
||||||
|
}
|
||||||
|
const modalElement = document.getElementById('successModal');
|
||||||
|
if (modalElement) {
|
||||||
|
modalElement.style.display = 'flex';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.message || '提交失败');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = '提交答案';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('提交失败: ' + error.message);
|
||||||
|
console.error('Error:', error);
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = '提交答案';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('successModal').style.display = 'none';
|
||||||
|
// 重置表单
|
||||||
|
document.getElementById('answerForm').reset();
|
||||||
|
document.getElementById('studentName').value = '';
|
||||||
|
document.querySelector('.btn-submit').disabled = false;
|
||||||
|
document.querySelector('.btn-submit').textContent = '提交答案';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
705
src/main/resources/templates/correct.html
Normal file
705
src/main/resources/templates/correct.html
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>批改试卷</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #9ca3af;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
.search-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.search-section .form-group {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tag-corrected {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.tag-pending {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
.score-badge {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
/* 批改详情样式 */
|
||||||
|
.exam-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.exam-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.score-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f59e0b;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.summary-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-label {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.summary-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.question-card {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.question-card:hover {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.question-card.correct {
|
||||||
|
border-color: #fbbf24;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
.question-card.wrong {
|
||||||
|
border-color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
.question-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.question-number {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.question-type {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.type-choice {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.type-essay {
|
||||||
|
background: #ffedd5;
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
.question-score {
|
||||||
|
margin-left: auto;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.answer-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.answer-box {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.answer-box label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.answer-box .answer-text {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.score-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.score-input input {
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.score-input span {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.auto-correct-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.auto-correct-result.correct {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.auto-correct-result.wrong {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
.submit-section {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.modal-content h2 {
|
||||||
|
color: #f59e0b;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="/admin" class="back-link">← 返回管理后台</a>
|
||||||
|
<h1>批改试卷</h1>
|
||||||
|
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<div class="card" id="searchCard">
|
||||||
|
<h2>搜索答题记录</h2>
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>做题人姓名</label>
|
||||||
|
<input type="text" id="searchName" placeholder="输入姓名(可选)">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>试卷标题</label>
|
||||||
|
<select id="searchTitle">
|
||||||
|
<option value="">全部试卷</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="searchAnswers()">搜索</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<div id="searchResults" style="margin-top: 24px; display: none;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>答题记录UID</th>
|
||||||
|
<th>做题人姓名</th>
|
||||||
|
<th>试卷标题</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>得分</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="resultsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="emptyResult" class="empty-state" style="display: none;">
|
||||||
|
未找到匹配的答题记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 批改区域 -->
|
||||||
|
<div class="card" id="correctCard" style="display: none;">
|
||||||
|
<h2>批改详情</h2>
|
||||||
|
|
||||||
|
<!-- 试卷信息 -->
|
||||||
|
<div class="exam-header">
|
||||||
|
<div class="exam-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">答题记录UID</span>
|
||||||
|
<span class="info-value" id="answerUid"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">做题人姓名</span>
|
||||||
|
<span class="info-value" id="studentName"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">试卷标题</span>
|
||||||
|
<span class="info-value" id="examTitle"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 得分汇总 -->
|
||||||
|
<div class="score-summary">
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">选择题得分</div>
|
||||||
|
<div class="summary-value" id="choiceScore">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">简答题满分</div>
|
||||||
|
<div class="summary-value" id="essayTotal">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<div class="summary-label">总分</div>
|
||||||
|
<div class="summary-value" id="totalScore">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 打印按钮 -->
|
||||||
|
<div style="text-align: center; margin-bottom: 24px;">
|
||||||
|
<button class="btn btn-primary" onclick="printAnswer()">打印试卷答案</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 题目列表 -->
|
||||||
|
<div id="questionsList"></div>
|
||||||
|
|
||||||
|
<div style="height: 100px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="submit-section" id="submitSection" style="display: none;">
|
||||||
|
<button class="btn btn-secondary" onclick="backToSearch()">返回搜索</button>
|
||||||
|
<button class="btn btn-success" onclick="submitCorrect()">提交批改</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成功弹窗 -->
|
||||||
|
<div class="modal" id="successModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>✓ 批改完成</h2>
|
||||||
|
<p>试卷批改成功!</p>
|
||||||
|
<p>最终得分:<strong id="finalScore" style="font-size: 24px; color: #f59e0b;"></strong> 分</p>
|
||||||
|
<button class="btn btn-primary" onclick="closeModal()" style="margin-top: 20px;">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentAnswerUid = null;
|
||||||
|
let essayQuestions = [];
|
||||||
|
|
||||||
|
// 页面加载时获取试卷标题列表
|
||||||
|
function loadExamTitles() {
|
||||||
|
fetch('/admin/correct/titles')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const select = document.getElementById('searchTitle');
|
||||||
|
// 保留"全部试卷"选项
|
||||||
|
select.innerHTML = '<option value="">全部试卷</option>';
|
||||||
|
data.data.forEach(title => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = title;
|
||||||
|
option.textContent = title;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('加载试卷标题失败:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', loadExamTitles);
|
||||||
|
|
||||||
|
// 搜索答题记录
|
||||||
|
function searchAnswers() {
|
||||||
|
const name = document.getElementById('searchName').value.trim();
|
||||||
|
const title = document.getElementById('searchTitle').value;
|
||||||
|
|
||||||
|
fetch(`/admin/correct/search?name=${encodeURIComponent(name)}&title=${encodeURIComponent(title)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
displayResults(data.data);
|
||||||
|
} else {
|
||||||
|
alert(data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert('搜索失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示搜索结果
|
||||||
|
function displayResults(answers) {
|
||||||
|
const resultsDiv = document.getElementById('searchResults');
|
||||||
|
const emptyDiv = document.getElementById('emptyResult');
|
||||||
|
const tbody = document.getElementById('resultsBody');
|
||||||
|
|
||||||
|
if (answers.length === 0) {
|
||||||
|
resultsDiv.style.display = 'none';
|
||||||
|
emptyDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsDiv.style.display = 'block';
|
||||||
|
emptyDiv.style.display = 'none';
|
||||||
|
|
||||||
|
tbody.innerHTML = answers.map(a => {
|
||||||
|
const isCorrected = a.score != null && a.score !== '';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${a.uid}</td>
|
||||||
|
<td>${a.name || '-'}</td>
|
||||||
|
<td>${a.title || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<span class="tag ${isCorrected ? 'tag-corrected' : 'tag-pending'}">
|
||||||
|
${isCorrected ? '已批改' : '待批改'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${isCorrected ? `<span class="score-badge">${a.score}分</span>` : '-'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" onclick="loadAnswerDetail('${a.uid}')">
|
||||||
|
${isCorrected ? '查看' : '批改'}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载答题详情
|
||||||
|
function loadAnswerDetail(answerUid) {
|
||||||
|
currentAnswerUid = answerUid;
|
||||||
|
|
||||||
|
fetch(`/admin/correct/detail/${answerUid}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
displayCorrectDetail(data.data);
|
||||||
|
} else {
|
||||||
|
alert(data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert('加载详情失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示批改详情
|
||||||
|
function displayCorrectDetail(data) {
|
||||||
|
document.getElementById('searchCard').style.display = 'none';
|
||||||
|
document.getElementById('correctCard').style.display = 'block';
|
||||||
|
document.getElementById('submitSection').style.display = 'flex';
|
||||||
|
|
||||||
|
document.getElementById('answerUid').textContent = data.answerUid;
|
||||||
|
document.getElementById('studentName').textContent = data.studentName;
|
||||||
|
document.getElementById('examTitle').textContent = data.examTitle;
|
||||||
|
document.getElementById('choiceScore').textContent = data.choiceScore;
|
||||||
|
document.getElementById('essayTotal').textContent = data.essayTotalScore;
|
||||||
|
document.getElementById('totalScore').textContent = data.totalScore;
|
||||||
|
|
||||||
|
const listDiv = document.getElementById('questionsList');
|
||||||
|
essayQuestions = [];
|
||||||
|
|
||||||
|
listDiv.innerHTML = data.questions.map((q, index) => {
|
||||||
|
const isChoice = q.type === 1;
|
||||||
|
|
||||||
|
if (!isChoice) {
|
||||||
|
essayQuestions.push(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="question-card ${isChoice ? (q.isCorrect ? 'correct' : 'wrong') : ''}">
|
||||||
|
<div class="question-header">
|
||||||
|
<span class="question-number">${q.exmaid}</span>
|
||||||
|
<span class="question-type ${isChoice ? 'type-choice' : 'type-essay'}">
|
||||||
|
${isChoice ? '选择题' : '简答题'}
|
||||||
|
</span>
|
||||||
|
<span class="question-score">${q.score}分</span>
|
||||||
|
</div>
|
||||||
|
<div class="question-title">${q.title}</div>
|
||||||
|
|
||||||
|
<div class="answer-section">
|
||||||
|
<div class="answer-box">
|
||||||
|
<label>学生答案</label>
|
||||||
|
<div class="answer-text">${q.userAnswer || '未作答'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="answer-box">
|
||||||
|
<label>${isChoice ? '正确答案' : '参考答案'}</label>
|
||||||
|
<div class="answer-text">${q.correctAnswer || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${isChoice ? `
|
||||||
|
<div class="auto-correct-result ${q.isCorrect ? 'correct' : 'wrong'}">
|
||||||
|
<span>${q.isCorrect ? '✓ 回答正确' : '✗ 回答错误'}</span>
|
||||||
|
<span style="margin-left: auto; font-weight: bold;">得分: ${q.autoScore}/${q.score}</span>
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="score-input">
|
||||||
|
<label>评分:</label>
|
||||||
|
<input type="number"
|
||||||
|
id="score_${q.qid}"
|
||||||
|
min="0"
|
||||||
|
max="${q.score}"
|
||||||
|
value="${data.isCorrected ? q.autoScore : ''}"
|
||||||
|
${data.isCorrected ? 'disabled' : ''}>
|
||||||
|
<span>/ ${q.score} 分</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// 如果已批改,禁用提交按钮
|
||||||
|
if (data.isCorrected) {
|
||||||
|
document.querySelector('#submitSection .btn-success').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回搜索
|
||||||
|
function backToSearch() {
|
||||||
|
document.getElementById('searchCard').style.display = 'block';
|
||||||
|
document.getElementById('correctCard').style.display = 'none';
|
||||||
|
document.getElementById('submitSection').style.display = 'none';
|
||||||
|
currentAnswerUid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交批改
|
||||||
|
function submitCorrect() {
|
||||||
|
if (!currentAnswerUid) return;
|
||||||
|
|
||||||
|
const essayScores = {};
|
||||||
|
for (let q of essayQuestions) {
|
||||||
|
const input = document.getElementById(`score_${q.qid}`);
|
||||||
|
const score = parseInt(input.value) || 0;
|
||||||
|
if (score < 0 || score > q.score) {
|
||||||
|
alert(`第${q.exmaid}题得分必须在0-${q.score}之间`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
essayScores[`q_${q.qid}`] = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/admin/correct/submit/${currentAnswerUid}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(essayScores)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('finalScore').textContent = data.totalScore;
|
||||||
|
document.getElementById('successModal').style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
alert(data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert('提交失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('successModal').style.display = 'none';
|
||||||
|
backToSearch();
|
||||||
|
searchAnswers(); // 刷新列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印试卷答案
|
||||||
|
function printAnswer() {
|
||||||
|
if (!currentAnswerUid) return;
|
||||||
|
window.open(`/admin/correct/print/${currentAnswerUid}`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回车键搜索(仅姓名输入框)
|
||||||
|
document.getElementById('searchName')?.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') searchAnswers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
389
src/main/resources/templates/listexam.html
Normal file
389
src/main/resources/templates/listexam.html
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>查看试卷列表</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.search-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.search-section .form-group {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #9ca3af;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 14px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.modal-header h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.exam-meta {
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fef3c7;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
.question-item {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.question-type {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.question-score {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.answer-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.correct-answer {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #d97706;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #fef3c7;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>试卷管理与浏览</h1>
|
||||||
|
|
||||||
|
<div class="nav-bar">
|
||||||
|
<a href="/admin" class="btn btn-secondary">← 返回题库管理</a>
|
||||||
|
<a href="/admin/mkexam" class="btn btn-success">+ 创建试卷</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>试卷列表</h2>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<form class="search-section" action="/admin/listexam" method="get">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>试卷标题</label>
|
||||||
|
<input type="text" name="title" th:value="${searchTitle}" placeholder="输入试卷标题进行模糊搜索...">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">搜索</button>
|
||||||
|
<a href="/admin/listexam" class="btn btn-secondary">重置</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div th:if="${#lists.isEmpty(examPapers)}" class="empty-state">
|
||||||
|
暂无试卷记录,点击上方"创建试卷"生成一份吧
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table th:unless="${#lists.isEmpty(examPapers)}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="60">ID</th>
|
||||||
|
<th width="280">UID</th>
|
||||||
|
<th>试卷标题</th>
|
||||||
|
<th width="220">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="paper : ${examPapers}">
|
||||||
|
<td th:text="${paper.id}"></td>
|
||||||
|
<td style="font-family: monospace; color: #666; font-size: 13px;" th:text="${paper.uid}"></td>
|
||||||
|
<td th:text="${paper.title != null and paper.title != '' ? paper.title : '未命名试卷'}"></td>
|
||||||
|
<td>
|
||||||
|
<div class="action-btns">
|
||||||
|
<button class="btn btn-primary btn-sm" th:attr="onclick='viewExamDetail(\'' + ${paper.uid} + '\')'">查看内容</button>
|
||||||
|
<a th:href="'/' + ${paper.uid}" target="_blank" class="btn btn-success btn-sm">前往答题</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 试卷内容详情弹窗 -->
|
||||||
|
<div id="detailModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-btn" onclick="closeModal()">×</span>
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="modalTitle">试卷加载中...</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exam-meta">
|
||||||
|
<p><strong>试卷UID:</strong> <span id="modalUid" style="font-family: monospace;"></span></p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
<strong>总分:</strong> <span id="modalTotalScore" style="font-size: 18px; font-weight: bold;">0</span> 分
|
||||||
|
<span style="margin: 0 10px;">|</span>
|
||||||
|
<strong>选择题:</strong> <span id="modalChoiceCount">0</span> 道
|
||||||
|
<span style="margin: 0 10px;">|</span>
|
||||||
|
<strong>简答题:</strong> <span id="modalEssayCount">0</span> 道
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="color: #333; border-left: 4px solid #f59e0b; padding-left: 10px; margin-bottom: 16px;">一、选择题</h3>
|
||||||
|
<div id="choiceContainer" style="margin-bottom: 30px;"></div>
|
||||||
|
|
||||||
|
<h3 style="color: #333; border-left: 4px solid #f59e0b; padding-left: 10px; margin-bottom: 16px;">二、简答题</h3>
|
||||||
|
<div id="essayContainer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function viewExamDetail(uid) {
|
||||||
|
// 打开弹窗
|
||||||
|
document.getElementById('detailModal').style.display = 'block';
|
||||||
|
document.getElementById('modalTitle').textContent = '试卷加载中...';
|
||||||
|
|
||||||
|
fetch('/admin/mkexam/detail/' + uid)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('modalTitle').textContent = data.title || '未命名试卷';
|
||||||
|
document.getElementById('modalUid').textContent = data.uid;
|
||||||
|
document.getElementById('modalTotalScore').textContent = data.totalScore;
|
||||||
|
document.getElementById('modalChoiceCount').textContent = data.choiceCount;
|
||||||
|
document.getElementById('modalEssayCount').textContent = data.essayCount;
|
||||||
|
|
||||||
|
// 渲染选择题
|
||||||
|
let choiceHtml = '';
|
||||||
|
if (data.choiceQuestions && data.choiceQuestions.length > 0) {
|
||||||
|
data.choiceQuestions.forEach((q, index) => {
|
||||||
|
choiceHtml += `
|
||||||
|
<div class="question-item">
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<strong>${index + 1}. ${q.title}</strong>
|
||||||
|
<span class="question-type">选择题</span>
|
||||||
|
<span class="question-score">${q.score}分</span>
|
||||||
|
</div>
|
||||||
|
<div class="answer-content">
|
||||||
|
${q.answerA ? `<div>A. ${q.answerA}</div>` : ''}
|
||||||
|
${q.answerB ? `<div>B. ${q.answerB}</div>` : ''}
|
||||||
|
${q.answerC ? `<div>C. ${q.answerC}</div>` : ''}
|
||||||
|
${q.answerD ? `<div>D. ${q.answerD}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="correct-answer">正确答案:${q.ranswer || q.rAnswer || '-'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
choiceHtml = '<div class="empty-state" style="padding: 20px;">无选择题</div>';
|
||||||
|
}
|
||||||
|
document.getElementById('choiceContainer').innerHTML = choiceHtml;
|
||||||
|
|
||||||
|
// 渲染简答题
|
||||||
|
let essayHtml = '';
|
||||||
|
if (data.essayQuestions && data.essayQuestions.length > 0) {
|
||||||
|
data.essayQuestions.forEach((q, index) => {
|
||||||
|
essayHtml += `
|
||||||
|
<div class="question-item">
|
||||||
|
<div style="margin-bottom: 12px;">
|
||||||
|
<strong>${index + 1}. ${q.title}</strong>
|
||||||
|
<span class="question-type">简答题</span>
|
||||||
|
<span class="question-score">${q.score}分</span>
|
||||||
|
</div>
|
||||||
|
<div class="correct-answer">参考答案:${q.ranswer || q.rAnswer || q.answer || '无'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
essayHtml = '<div class="empty-state" style="padding: 20px;">无简答题</div>';
|
||||||
|
}
|
||||||
|
document.getElementById('essayContainer').innerHTML = essayHtml;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('获取试卷详情失败', err);
|
||||||
|
alert('获取试卷详情失败,请检查网络或后端接口!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击遮罩层关闭弹窗
|
||||||
|
window.onclick = function(event) {
|
||||||
|
let modal = document.getElementById('detailModal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
536
src/main/resources/templates/mkexam.html
Normal file
536
src/main/resources/templates/mkexam.html
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>创建试卷</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #9ca3af;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.exam-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.exam-info h3 {
|
||||||
|
color: #f59e0b;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.uid-display {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
background: #fef3c7;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #d97706;
|
||||||
|
}
|
||||||
|
.question-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.question-section h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.question-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.question-item {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.question-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.question-number {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.question-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.question-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.question-id {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #666;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.question-score {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.question-type {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.loading-spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #f59e0b;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.alert-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.create-section {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
.create-section > p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.create-form {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.create-btn {
|
||||||
|
padding: 14px 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
background: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.exam-title {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>创建试卷</h1>
|
||||||
|
|
||||||
|
<div class="nav-bar">
|
||||||
|
<a th:href="@{/admin/exam}" class="btn btn-secondary">← 返回题库管理</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建按钮区域 -->
|
||||||
|
<div class="card" id="createSection">
|
||||||
|
<h2>生成新试卷</h2>
|
||||||
|
<div class="create-section">
|
||||||
|
<p>系统将自动从题库中随机选择题目的7:3比例组合(选择题:简答题),总分100分</p>
|
||||||
|
<div class="create-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="examTitle">试卷标题</label>
|
||||||
|
<input type="text" id="examTitle" placeholder="请输入试卷标题(可选)" maxlength="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary create-btn" onclick="createExam()">创建试卷</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div class="loading card" id="loadingSection">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>正在生成试卷...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 试卷信息展示 -->
|
||||||
|
<div class="card" id="resultSection" style="display: none;">
|
||||||
|
<h2>试卷生成成功</h2>
|
||||||
|
|
||||||
|
<div class="exam-info">
|
||||||
|
<h3>试卷标题</h3>
|
||||||
|
<div class="exam-title" id="examTitleDisplay">-</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 16px;">试卷UID</h3>
|
||||||
|
<div class="uid-display">
|
||||||
|
<span id="uidDisplay"></span>
|
||||||
|
<button class="copy-btn" onclick="copyUid()">复制</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">选择题数量</div>
|
||||||
|
<div class="stat-value" id="choiceCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">简答题数量</div>
|
||||||
|
<div class="stat-value" id="essayCount">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">总题数</div>
|
||||||
|
<div class="stat-value" id="totalCount">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question-section">
|
||||||
|
<h4>选择题列表</h4>
|
||||||
|
<ul class="question-list" id="choiceList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question-section">
|
||||||
|
<h4>简答题列表</h4>
|
||||||
|
<ul class="question-list" id="essayList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-actions">
|
||||||
|
<button class="btn btn-success" onclick="createAnother()">再创建一份</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div class="alert alert-error" id="errorSection" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function createExam() {
|
||||||
|
// 获取试卷标题
|
||||||
|
const title = document.getElementById('examTitle').value.trim();
|
||||||
|
|
||||||
|
// 显示加载,隐藏创建按钮
|
||||||
|
document.getElementById('createSection').style.display = 'none';
|
||||||
|
document.getElementById('loadingSection').style.display = 'block';
|
||||||
|
document.getElementById('resultSection').style.display = 'none';
|
||||||
|
document.getElementById('errorSection').style.display = 'none';
|
||||||
|
|
||||||
|
// 构建表单数据
|
||||||
|
const formData = new FormData();
|
||||||
|
if (title) {
|
||||||
|
formData.append('title', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/admin/mkexam/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('创建试卷失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// 获取试卷详情(包含题目title)
|
||||||
|
return fetch('/admin/mkexam/detail/' + data.uid);
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(detail => {
|
||||||
|
displayExamDetail(detail);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showError(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayExamDetail(data) {
|
||||||
|
document.getElementById('loadingSection').style.display = 'none';
|
||||||
|
document.getElementById('resultSection').style.display = 'block';
|
||||||
|
|
||||||
|
// 显示试卷标题
|
||||||
|
const titleDisplay = document.getElementById('examTitleDisplay');
|
||||||
|
titleDisplay.textContent = data.title || '未命名试卷';
|
||||||
|
|
||||||
|
// 显示UID
|
||||||
|
document.getElementById('uidDisplay').textContent = data.uid;
|
||||||
|
|
||||||
|
// 更新统计
|
||||||
|
const choiceQuestions = data.choiceQuestions || [];
|
||||||
|
const essayQuestions = data.essayQuestions || [];
|
||||||
|
|
||||||
|
document.getElementById('choiceCount').textContent = choiceQuestions.length;
|
||||||
|
document.getElementById('essayCount').textContent = essayQuestions.length;
|
||||||
|
document.getElementById('totalCount').textContent = choiceQuestions.length + essayQuestions.length;
|
||||||
|
|
||||||
|
// 显示选择题列表
|
||||||
|
const choiceList = document.getElementById('choiceList');
|
||||||
|
choiceList.innerHTML = '';
|
||||||
|
if (choiceQuestions.length === 0) {
|
||||||
|
choiceList.innerHTML = '<li class="question-item"><div class="question-content">暂无选择题</div></li>';
|
||||||
|
} else {
|
||||||
|
choiceQuestions.forEach((q, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'question-item';
|
||||||
|
li.innerHTML = `
|
||||||
|
<span class="question-number">${index + 1}</span>
|
||||||
|
<div class="question-content">
|
||||||
|
<div class="question-title">${escapeHtml(q.title)}</div>
|
||||||
|
<div class="question-meta">
|
||||||
|
<span class="question-id">ID: ${q.id}</span>
|
||||||
|
<span class="question-type">选择题</span>
|
||||||
|
<span class="question-score">${q.score || 0}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
choiceList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示简答题列表
|
||||||
|
const essayList = document.getElementById('essayList');
|
||||||
|
essayList.innerHTML = '';
|
||||||
|
if (essayQuestions.length === 0) {
|
||||||
|
essayList.innerHTML = '<li class="question-item"><div class="question-content">暂无简答题</div></li>';
|
||||||
|
} else {
|
||||||
|
essayQuestions.forEach((q, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'question-item';
|
||||||
|
li.innerHTML = `
|
||||||
|
<span class="question-number">${index + 1}</span>
|
||||||
|
<div class="question-content">
|
||||||
|
<div class="question-title">${escapeHtml(q.title)}</div>
|
||||||
|
<div class="question-meta">
|
||||||
|
<span class="question-id">ID: ${q.id}</span>
|
||||||
|
<span class="question-type">简答题</span>
|
||||||
|
<span class="question-score">${q.score || 0}分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
essayList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
document.getElementById('loadingSection').style.display = 'none';
|
||||||
|
document.getElementById('createSection').style.display = 'block';
|
||||||
|
|
||||||
|
const errorSection = document.getElementById('errorSection');
|
||||||
|
errorSection.textContent = message;
|
||||||
|
errorSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyUid() {
|
||||||
|
const uid = document.getElementById('uidDisplay').textContent;
|
||||||
|
navigator.clipboard.writeText(uid).then(() => {
|
||||||
|
const btn = document.querySelector('.copy-btn');
|
||||||
|
btn.textContent = '已复制';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = '复制';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAnother() {
|
||||||
|
document.getElementById('resultSection').style.display = 'none';
|
||||||
|
document.getElementById('createSection').style.display = 'block';
|
||||||
|
document.getElementById('errorSection').style.display = 'none';
|
||||||
|
// 清空标题输入框
|
||||||
|
document.getElementById('examTitle').value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
300
src/main/resources/templates/print.html
Normal file
300
src/main/resources/templates/print.html
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title th:text="${data != null ? data.examTitle + ' - 试卷答案' : '打印试卷'}">试卷答案</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "SimSun", "宋体", serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
/* 打印样式 */
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.page-break {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 按钮样式(仅屏幕显示) */
|
||||||
|
.print-actions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
margin: 0 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn-print {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-back {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
/* 试卷标题 */
|
||||||
|
.exam-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
}
|
||||||
|
/* 基本信息 */
|
||||||
|
.info-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
/* 分数区域 */
|
||||||
|
.score-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.score-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.score-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.score-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
/* 题目列表 */
|
||||||
|
.questions {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.question-item {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px dashed #ccc;
|
||||||
|
}
|
||||||
|
.question-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.question-number {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.question-type {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.question-score {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
/* 答案区域 */
|
||||||
|
.answer-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-left: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
.answer-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.answer-label {
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 80px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.answer-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.answer-content.correct {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.answer-content.wrong {
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
.score-get {
|
||||||
|
color: #c00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
/* 底部签名区 */
|
||||||
|
.signature-section {
|
||||||
|
margin-top: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.signature-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.signature-line {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
/* 错误提示 */
|
||||||
|
.error-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
color: #c00;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 打印按钮区域(打印时隐藏) -->
|
||||||
|
<div class="print-actions no-print">
|
||||||
|
<button class="btn btn-print" onclick="window.print()">打印试卷</button>
|
||||||
|
<button class="btn btn-back" onclick="history.back()">返回</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div th:if="${error}" class="error-message" th:text="${error}"></div>
|
||||||
|
|
||||||
|
<!-- 试卷内容 -->
|
||||||
|
<div th:if="${data != null}">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="exam-title" th:text="${data.examTitle}">试卷标题</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">姓名:</span>
|
||||||
|
<span class="info-value" th:text="${data.studentName}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">答题记录UID:</span>
|
||||||
|
<span th:text="${data.answerUid}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">日期:</span>
|
||||||
|
<span th:text="${#dates.format(new java.util.Date(), 'yyyy年MM月dd日')}"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分数汇总 -->
|
||||||
|
<div class="score-section">
|
||||||
|
<div class="score-item">
|
||||||
|
<div class="score-label">选择题得分</div>
|
||||||
|
<div class="score-value" th:text="${data.choiceScore} + '分'"></div>
|
||||||
|
</div>
|
||||||
|
<div class="score-item">
|
||||||
|
<div class="score-label">简答题得分</div>
|
||||||
|
<div class="score-value" th:text="${data.finalScore != null ? (data.finalScore - data.choiceScore) : '-'} + '分'"></div>
|
||||||
|
</div>
|
||||||
|
<div class="score-item">
|
||||||
|
<div class="score-label">总得分</div>
|
||||||
|
<div class="score-value" th:text="${data.finalScore != null ? data.finalScore : data.choiceScore} + '/100' + '分'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 题目答案列表 -->
|
||||||
|
<div class="questions">
|
||||||
|
<div th:each="q : ${data.questions}" class="question-item">
|
||||||
|
<div class="question-header">
|
||||||
|
<span class="question-number" th:text="'第' + ${q.exmaid} + '题'">第1题</span>
|
||||||
|
<span class="question-type" th:text="${q.type == 1 ? '选择题' : '简答题'}">选择题</span>
|
||||||
|
<span class="question-score" th:text="${q.score} + '分'">5分</span>
|
||||||
|
</div>
|
||||||
|
<div class="question-title" th:text="${q.title}">题目内容</div>
|
||||||
|
|
||||||
|
<div class="answer-section">
|
||||||
|
<!-- 选择题 -->
|
||||||
|
<div th:if="${q.type == 1}">
|
||||||
|
<div class="answer-row">
|
||||||
|
<span class="answer-label">答案:</span>
|
||||||
|
<span class="answer-content"
|
||||||
|
th:classappend="${q.isCorrect ? 'correct' : 'wrong'}"
|
||||||
|
th:text="${q.userAnswer != null && !q.userAnswer.isEmpty() ? q.userAnswer : '未作答'}">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="answer-row">
|
||||||
|
<span class="answer-label">正确答案:</span>
|
||||||
|
<span class="answer-content correct" th:text="${q.correctAnswer}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="answer-row">
|
||||||
|
<span class="answer-label">得分:</span>
|
||||||
|
<span class="score-get" th:text="${q.autoScore} + '/' + ${q.score} + '分'"></span>
|
||||||
|
<span th:if="${q.isCorrect}" style="color: #fbbf24; margin-left: 10px;">✓ 正确</span>
|
||||||
|
<span th:if="${!q.isCorrect}" style="color: #c00; margin-left: 10px;">✗ 错误</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简答题 -->
|
||||||
|
<div th:if="${q.type == 2}">
|
||||||
|
<div class="answer-row">
|
||||||
|
<span class="answer-label">答案:</span>
|
||||||
|
<span class="answer-content" th:text="${q.userAnswer != null && !q.userAnswer.isEmpty() ? q.userAnswer : '未作答'}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="answer-row" th:if="${data.isCorrected}">
|
||||||
|
<span class="answer-label">得分:</span>
|
||||||
|
<span class="score-get" th:text="${q.autoScore} + '/' + ${q.score} + '分'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部签名区 -->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user