一些修改

This commit is contained in:
Yakumo Hokori
2026-03-06 18:34:37 +08:00
parent f56733b54b
commit 350ebcef11
9 changed files with 871 additions and 153 deletions

View File

@@ -5,10 +5,12 @@ import com.baobaot.exam.Service.makeExamService;
import com.baobaot.exam.dao.examPaper; import com.baobaot.exam.dao.examPaper;
import com.baobaot.exam.dao.question; import com.baobaot.exam.dao.question;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -28,13 +30,34 @@ public class AdminController {
@GetMapping("/admin/exam") @GetMapping("/admin/exam")
public String adminPage(Model model) { public String adminPage(Model model) {
List<question> questions = adminService.list();
model.addAttribute("questions", questions);
return "admin"; return "admin";
} }
// API: 获取题目列表(支持全部、按类型、按分值)
@GetMapping("/admin/api/questions")
@ResponseBody
public ResponseEntity<Map<String, Object>> getQuestions(
@RequestParam(required = false) Integer type,
@RequestParam(required = false) Integer score) {
Map<String, Object> result = new HashMap<>();
List<question> questions;
if (type != null) {
questions = adminService.getQuestionsByType(type);
} else if (score != null) {
questions = adminService.getQuestionsByScore(score);
} else {
questions = adminService.list();
}
result.put("success", true);
result.put("data", questions);
return ResponseEntity.ok(result);
}
@PostMapping("/admin/add/choice") @PostMapping("/admin/add/choice")
public String addChoiceQuestion(@RequestParam String title, @ResponseBody
public ResponseEntity<Map<String, Object>> addChoiceQuestion(@RequestParam String title,
@RequestParam Integer type, @RequestParam Integer type,
@RequestParam(required = false) String answer_a, @RequestParam(required = false) String answer_a,
@RequestParam(required = false) String answer_b, @RequestParam(required = false) String answer_b,
@@ -42,40 +65,50 @@ public class AdminController {
@RequestParam(required = false) String answer_d, @RequestParam(required = false) String answer_d,
@RequestParam String r_answer, @RequestParam String r_answer,
@RequestParam Integer score) { @RequestParam Integer score) {
Map<String, Object> result = new HashMap<>();
try {
adminService.addChoiceQuestion(title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score); adminService.addChoiceQuestion(title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score);
return "redirect:/admin/exam"; result.put("success", true);
result.put("message", "添加成功");
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok(result);
} }
@PostMapping("/admin/add/essay") @PostMapping("/admin/add/essay")
public String addEssayQuestion(@RequestParam String title, @ResponseBody
public ResponseEntity<Map<String, Object>> addEssayQuestion(@RequestParam String title,
@RequestParam Integer type, @RequestParam Integer type,
@RequestParam(required = false) String answer, @RequestParam(required = false) String answer,
@RequestParam(required = false) String r_answer, @RequestParam(required = false) String r_answer,
@RequestParam Integer score) { @RequestParam Integer score) {
Map<String, Object> result = new HashMap<>();
try {
adminService.addEssayQuestion(title, type, answer, r_answer, score); adminService.addEssayQuestion(title, type, answer, r_answer, score);
return "redirect:/admin/exam"; result.put("success", true);
result.put("message", "添加成功");
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
} }
return ResponseEntity.ok(result);
@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") @PostMapping("/admin/delete")
public String deleteByTitle(@RequestParam String title) { @ResponseBody
public ResponseEntity<Map<String, Object>> deleteByTitle(@RequestParam String title) {
Map<String, Object> result = new HashMap<>();
try {
adminService.deleteQuestionByTitle(title); adminService.deleteQuestionByTitle(title);
return "redirect:/admin/exam"; result.put("success", true);
result.put("message", "删除成功");
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok(result);
} }
@GetMapping("/admin/mkexam") @GetMapping("/admin/mkexam")
@@ -96,10 +129,85 @@ public class AdminController {
} }
@GetMapping("/admin/listexam") @GetMapping("/admin/listexam")
public String listExamPage(@RequestParam(required = false) String title, Model model) { public String listExamPage() {
List<examPaper> examPapers = makeExamService.getExamList(title);
model.addAttribute("examPapers", examPapers);
model.addAttribute("searchTitle", title);
return "listexam"; return "listexam";
} }
// API: 获取试卷列表
@GetMapping("/admin/api/exams")
@ResponseBody
public ResponseEntity<Map<String, Object>> getExams(@RequestParam(required = false) String title) {
Map<String, Object> result = new HashMap<>();
List<examPaper> examPapers = makeExamService.getExamList(title);
result.put("success", true);
result.put("data", examPapers);
return ResponseEntity.ok(result);
}
// API: 根据ID获取题目详情
@GetMapping("/admin/api/question/{id}")
@ResponseBody
public ResponseEntity<Map<String, Object>> getQuestionById(@PathVariable Integer id) {
Map<String, Object> result = new HashMap<>();
question q = adminService.getQuestionById(id);
if (q != null) {
result.put("success", true);
result.put("data", q);
} else {
result.put("success", false);
result.put("message", "题目不存在");
}
return ResponseEntity.ok(result);
}
// API: 更新题目
@PostMapping("/admin/api/question/update")
@ResponseBody
public ResponseEntity<Map<String, Object>> updateQuestion(
@RequestParam Integer id,
@RequestParam Integer type,
@RequestParam String title,
@RequestParam(required = false) String answerA,
@RequestParam(required = false) String answerB,
@RequestParam(required = false) String answerC,
@RequestParam(required = false) String answerD,
@RequestParam(required = false) String answer,
@RequestParam(required = false) String rAnswer,
@RequestParam Integer score) {
Map<String, Object> result = new HashMap<>();
try {
question existing = adminService.getQuestionById(id);
if (existing == null) {
result.put("success", false);
result.put("message", "题目不存在");
return ResponseEntity.ok(result);
}
question.questionBuilder builder = question.builder()
.id(id)
.type(type)
.title(title)
.score(score)
.rAnswer(rAnswer);
if (type == 1) {
// 选择题
builder.answerA(answerA)
.answerB(answerB)
.answerC(answerC)
.answerD(answerD);
} else {
// 简答题
builder.answer(answer);
}
adminService.updateQuestion(builder.build());
result.put("success", true);
result.put("message", "修改成功");
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok(result);
}
} }

View File

@@ -32,4 +32,14 @@ public interface adminService extends IService<question> {
* 根据标题删除题目 * 根据标题删除题目
*/ */
boolean deleteQuestionByTitle(String title); boolean deleteQuestionByTitle(String title);
/**
* 根据ID查询题目
*/
question getQuestionById(Integer id);
/**
* 更新题目
*/
boolean updateQuestion(question q);
} }

View File

@@ -126,6 +126,10 @@ public class CorrectServiceImpl implements CorrectService {
.autoCorrect(true) .autoCorrect(true)
.autoScore(score) .autoScore(score)
.isCorrect(isCorrect) .isCorrect(isCorrect)
.answerA(q.getAnswerA())
.answerB(q.getAnswerB())
.answerC(q.getAnswerC())
.answerD(q.getAnswerD())
.build(); .build();
detailItems.add(item); detailItems.add(item);
} }

View File

@@ -60,4 +60,14 @@ public class adminServiceImpl extends ServiceImpl<questionMapper, question> impl
wrapper.eq(question::getTitle, title); wrapper.eq(question::getTitle, title);
return remove(wrapper); return remove(wrapper);
} }
@Override
public question getQuestionById(Integer id) {
return getById(id);
}
@Override
public boolean updateQuestion(question q) {
return updateById(q);
}
} }

View File

@@ -63,4 +63,24 @@ public class CorrectDetailItem {
* 是否正确(选择题) * 是否正确(选择题)
*/ */
private Boolean isCorrect; private Boolean isCorrect;
/**
* 选项A选择题
*/
private String answerA;
/**
* 选项B选择题
*/
private String answerB;
/**
* 选项C选择题
*/
private String answerC;
/**
* 选项D选择题
*/
private String answerD;
} }

View File

@@ -2,8 +2,8 @@ spring:
application: application:
name: exam name: exam
datasource: datasource:
url: jdbc:mysql://localhost:3306/exam # url: jdbc:mysql://localhost:3306/exam
# url: jdbc:mysql://192.168.1.56:3306/exam?useSSL=true&requireSSL=true&serverTimezone=UTC url: jdbc:mysql://192.168.1.56:3306/exam?useSSL=true&requireSSL=true&serverTimezone=UTC
username: root username: root
password: 521707 password: 521707
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver

View File

@@ -207,39 +207,39 @@
<div class="card"> <div class="card">
<h2>添加题目</h2> <h2>添加题目</h2>
<div class="tabs"> <div class="tabs">
<button class="tab-btn active" onclick="switchTab('choice')">选择题 (Type 1)</button> <button class="tab-btn active" onclick="switchTab('choice')">选择题</button>
<button class="tab-btn" onclick="switchTab('essay')">简答题 (Type 2)</button> <button class="tab-btn" onclick="switchTab('essay')">简答题</button>
</div> </div>
<!-- 选择题表单 --> <!-- 选择题表单 -->
<div id="choice-tab" class="tab-content active"> <div id="choice-tab" class="tab-content active">
<form th:action="@{/admin/add/choice}" method="post"> <form id="choiceForm">
<input type="hidden" name="type" value="1"> <input type="hidden" name="type" value="1">
<div class="form-group"> <div class="form-group">
<label>题目</label> <label>题目</label>
<textarea name="title" placeholder="请输入题目内容" required></textarea> <textarea name="title" id="choiceTitle" placeholder="请输入题目内容" required></textarea>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label>选项 A</label> <label>选项 A</label>
<input type="text" name="answer_a" placeholder="选填"> <input type="text" name="answer_a" id="choiceA" placeholder="选填">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>选项 B</label> <label>选项 B</label>
<input type="text" name="answer_b" placeholder="选填"> <input type="text" name="answer_b" id="choiceB" placeholder="选填">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>选项 C</label> <label>选项 C</label>
<input type="text" name="answer_c" placeholder="选填"> <input type="text" name="answer_c" id="choiceC" placeholder="选填">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>选项 D</label> <label>选项 D</label>
<input type="text" name="answer_d" placeholder="选填"> <input type="text" name="answer_d" id="choiceD" placeholder="选填">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>正确答案<span class="required">*</span></label> <label>正确答案<span class="required">*</span></label>
<select name="r_answer" required> <select name="r_answer" id="choiceRAnswer" required>
<option value="">请选择正确答案</option> <option value="">请选择正确答案</option>
<option value="A">A</option> <option value="A">A</option>
<option value="B">B</option> <option value="B">B</option>
@@ -249,7 +249,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>分值</label> <label>分值</label>
<input type="number" name="score" placeholder="请输入分值" required min="0"> <input type="number" name="score" id="choiceScore" placeholder="请输入分值" required min="0">
</div> </div>
<button type="submit" class="btn btn-primary">添加选择题</button> <button type="submit" class="btn btn-primary">添加选择题</button>
</form> </form>
@@ -257,23 +257,19 @@
<!-- 简答题表单 --> <!-- 简答题表单 -->
<div id="essay-tab" class="tab-content"> <div id="essay-tab" class="tab-content">
<form th:action="@{/admin/add/essay}" method="post"> <form id="essayForm">
<input type="hidden" name="type" value="2"> <input type="hidden" name="type" value="2">
<div class="form-group"> <div class="form-group">
<label>题目</label> <label>题目</label>
<textarea name="title" placeholder="请输入题目内容" required></textarea> <textarea name="title" id="essayTitle" placeholder="请输入题目内容" required></textarea>
</div>
<div class="form-group">
<label>学生答案参考</label>
<textarea name="answer" placeholder="选填"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>参考答案</label> <label>参考答案</label>
<textarea name="r_answer" placeholder="选填"></textarea> <textarea name="r_answer" id="essayRAnswer" placeholder="选填"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>分值</label> <label>分值</label>
<input type="number" name="score" placeholder="请输入分值" required min="0"> <input type="number" name="score" id="essayScore" placeholder="请输入分值" required min="0">
</div> </div>
<button type="submit" class="btn btn-primary">添加简答题</button> <button type="submit" class="btn btn-primary">添加简答题</button>
</form> </form>
@@ -285,29 +281,29 @@
<h2>查询题目</h2> <h2>查询题目</h2>
<div class="grid-2"> <div class="grid-2">
<div> <div>
<form th:action="@{/admin/query/type}" method="get" class="query-section"> <div class="query-section">
<div class="form-group"> <div class="form-group">
<label>按类型查询</label> <label>按类型查询</label>
<select name="type" required> <select id="queryType" required>
<option value="1">选择题</option> <option value="1">选择题</option>
<option value="2">简答题</option> <option value="2">简答题</option>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-success">查询</button> <button type="button" class="btn btn-success" onclick="queryByType()">查询</button>
</form> </div>
</div> </div>
<div> <div>
<form th:action="@{/admin/query/score}" method="get" class="query-section"> <div class="query-section">
<div class="form-group"> <div class="form-group">
<label>按分值查询</label> <label>按分值查询</label>
<input type="number" name="score" placeholder="请输入分值" required min="0"> <input type="number" id="queryScore" placeholder="请输入分值" required min="0">
</div>
<button type="button" class="btn btn-success" onclick="queryByScore()">查询</button>
</div> </div>
<button type="submit" class="btn btn-success">查询</button>
</form>
</div> </div>
</div> </div>
<div style="margin-top: 16px; display: flex; gap: 12px;"> <div style="margin-top: 16px; display: flex; gap: 12px;">
<a th:href="@{/admin/exam}" class="btn btn-primary">显示全部</a> <button type="button" class="btn btn-primary" onclick="loadAllQuestions()">显示全部</button>
<a th:href="@{/admin/mkexam}" class="btn btn-success">创建试卷</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/listexam}" class="btn btn-success">查看试卷</a>
<a th:href="@{/admin/correct}" class="btn btn-success" style="background: #f59e0b;">批改试卷</a> <a th:href="@{/admin/correct}" class="btn btn-success" style="background: #f59e0b;">批改试卷</a>
@@ -318,64 +314,29 @@
<div class="card"> <div class="card">
<h2> <h2>
题目列表 题目列表
<span th:if="${queryType != null}" class="tag tag-choice" th:text="${queryType}" style="margin-left: 10px;"></span> <span id="queryTypeTag" class="tag tag-choice" style="margin-left: 10px; display: none;"></span>
<span th:if="${queryScore != null}" class="tag tag-essay" th:text="${queryScore}" style="margin-left: 10px;"></span> <span id="queryScoreTag" class="tag tag-essay" style="margin-left: 10px; display: none;"></span>
<span id="questionCount" class="tag" style="margin-left: 10px; background: #e3f2fd; color: #1976d2; display: none;">共 0 题</span>
</h2> </h2>
<div th:if="${#lists.isEmpty(questions)}" class="empty-state"> <div id="questionsContainer">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> <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> </svg>
<p>暂无题目数据</p> <p>加载中...</p>
</div> </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>
<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>
</div> </div>
<script> <script>
// 页面加载时加载全部题目
document.addEventListener('DOMContentLoaded', function() {
loadAllQuestions();
});
// 切换标签页
function switchTab(type) { function switchTab(type) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
@@ -383,6 +344,496 @@
event.target.classList.add('active'); event.target.classList.add('active');
document.getElementById(type + '-tab').classList.add('active'); document.getElementById(type + '-tab').classList.add('active');
} }
// 添加选择题
document.getElementById('choiceForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/admin/add/choice', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
showAlert('选择题添加成功!', 'success');
this.reset();
loadAllQuestions();
} else {
showAlert(data.message || '添加失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('添加失败,请检查网络', 'error');
});
});
// 添加简答题
document.getElementById('essayForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/admin/add/essay', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
showAlert('简答题添加成功!', 'success');
this.reset();
loadAllQuestions();
} else {
showAlert(data.message || '添加失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('添加失败,请检查网络', 'error');
});
});
// 加载全部题目
function loadAllQuestions() {
document.getElementById('queryTypeTag').style.display = 'none';
document.getElementById('queryScoreTag').style.display = 'none';
fetchQuestions('/admin/api/questions');
}
// 按类型查询
function queryByType() {
const type = document.getElementById('queryType').value;
const typeTag = document.getElementById('queryTypeTag');
typeTag.textContent = type === '1' ? '选择题' : '简答题';
typeTag.style.display = 'inline';
document.getElementById('queryScoreTag').style.display = 'none';
fetchQuestions('/admin/api/questions?type=' + type);
}
// 按分值查询
function queryByScore() {
const score = document.getElementById('queryScore').value;
if (!score && score !== '0') {
showAlert('请输入分值', 'error');
return;
}
const scoreTag = document.getElementById('queryScoreTag');
scoreTag.textContent = score + '分';
scoreTag.style.display = 'inline';
document.getElementById('queryTypeTag').style.display = 'none';
fetchQuestions('/admin/api/questions?score=' + score);
}
// 获取题目列表
function fetchQuestions(url) {
const container = document.getElementById('questionsContainer');
container.innerHTML = `
<div 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>
`;
fetch(url)
.then(res => res.json())
.then(data => {
if (data.success) {
renderQuestions(data.data);
} else {
showAlert(data.message || '加载失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('加载失败,请检查网络', 'error');
});
}
// 渲染题目列表
function renderQuestions(questions) {
const container = document.getElementById('questionsContainer');
const countTag = document.getElementById('questionCount');
// 更新计数显示
if (countTag) {
countTag.textContent = `${questions ? questions.length : 0}`;
countTag.style.display = 'inline';
}
if (!questions || questions.length === 0) {
container.innerHTML = `
<div 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>
`;
return;
}
let html = `
<table>
<thead>
<tr>
<th width="60">ID</th>
<th>题目</th>
<th width="100">类型</th>
<th>选项/学生答案</th>
<th>正确答案</th>
<th width="80">分值</th>
<th width="150">操作</th>
</tr>
</thead>
<tbody>
`;
questions.forEach(q => {
const typeTag = q.type == 1 ?
'<span class="tag tag-choice">选择题</span>' :
'<span class="tag tag-essay">简答题</span>';
let optionsHtml = '';
if (q.type == 1) {
const parts = [];
if (q.answerA) parts.push(`A: ${escapeHtml(q.answerA)}`);
if (q.answerB) parts.push(`B: ${escapeHtml(q.answerB)}`);
if (q.answerC) parts.push(`C: ${escapeHtml(q.answerC)}`);
if (q.answerD) parts.push(`D: ${escapeHtml(q.answerD)}`);
optionsHtml = parts.join('<br>');
} else {
optionsHtml = q.answer ? escapeHtml(q.answer) : '无答案';
}
const rAnswerHtml = q.rAnswer ?
`<span class="tag tag-answer">${escapeHtml(q.rAnswer)}</span>` :
'<span style="color: #999;">-</span>';
html += `
<tr>
<td>${q.id}</td>
<td style="max-width: 300px; word-wrap: break-word;">${escapeHtml(q.title)}</td>
<td>${typeTag}</td>
<td>${optionsHtml}</td>
<td>${rAnswerHtml}</td>
<td><span class="score-badge">${q.score}</span></td>
<td>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn btn-primary" style="padding: 6px 12px; font-size: 12px;" onclick="editQuestion(${q.id}, ${q.type})">修改</button>
<button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="deleteQuestion('${escapeHtml(q.title)}')">删除</button>
</div>
</td>
</tr>
`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// 删除题目
function deleteQuestion(title) {
if (!confirm('确定要删除这道题吗?')) return;
const formData = new FormData();
formData.append('title', title);
fetch('/admin/delete', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
showAlert('删除成功!', 'success');
loadAllQuestions();
} else {
showAlert(data.message || '删除失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('删除失败,请检查网络', 'error');
});
}
// 转义HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示提示
function showAlert(message, type) {
// 移除已有的提示
const existingAlert = document.querySelector('.dynamic-alert');
if (existingAlert) existingAlert.remove();
const alert = document.createElement('div');
alert.className = `alert alert-${type} dynamic-alert`;
alert.textContent = message;
alert.style.position = 'fixed';
alert.style.top = '20px';
alert.style.left = '50%';
alert.style.transform = 'translateX(-50%)';
alert.style.zIndex = '1000';
alert.style.minWidth = '200px';
alert.style.textAlign = 'center';
document.body.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 3000);
}
// 当前编辑的题目ID和类型
let currentEditId = null;
let currentEditType = null;
// 打开编辑弹窗
function editQuestion(id, type) {
currentEditId = id;
currentEditType = type;
// 获取题目详情
fetch('/admin/api/question/' + id)
.then(res => res.json())
.then(data => {
if (data.success) {
fillEditForm(data.data);
document.getElementById('editModal').style.display = 'block';
} else {
showAlert(data.message || '加载题目失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('加载题目失败,请检查网络', 'error');
});
}
// 填充编辑表单
function fillEditForm(q) {
document.getElementById('editId').value = q.id;
document.getElementById('editType').value = q.type;
document.getElementById('editTitle').value = q.title || '';
document.getElementById('editScore').value = q.score || '';
if (q.type == 1) {
// 选择题表单
document.getElementById('editChoiceForm').style.display = 'block';
document.getElementById('editEssayForm').style.display = 'none';
document.getElementById('editTypeLabel').textContent = '选择题';
document.getElementById('editAnswerA').value = q.answerA || '';
document.getElementById('editAnswerB').value = q.answerB || '';
document.getElementById('editAnswerC').value = q.answerC || '';
document.getElementById('editAnswerD').value = q.answerD || '';
document.getElementById('editRAnswer').value = q.rAnswer || '';
} else {
// 简答题表单
document.getElementById('editChoiceForm').style.display = 'none';
document.getElementById('editEssayForm').style.display = 'block';
document.getElementById('editTypeLabel').textContent = '简答题';
document.getElementById('editAnswer').value = q.answer || '';
document.getElementById('editRAnswerEssay').value = q.rAnswer || '';
}
}
// 关闭编辑弹窗
function closeEditModal() {
document.getElementById('editModal').style.display = 'none';
currentEditId = null;
currentEditType = null;
}
// 保存修改
function saveEdit() {
if (!currentEditId) return;
const formData = new FormData();
formData.append('id', currentEditId);
formData.append('type', currentEditType);
formData.append('title', document.getElementById('editTitle').value);
formData.append('score', document.getElementById('editScore').value);
if (currentEditType == 1) {
formData.append('answerA', document.getElementById('editAnswerA').value);
formData.append('answerB', document.getElementById('editAnswerB').value);
formData.append('answerC', document.getElementById('editAnswerC').value);
formData.append('answerD', document.getElementById('editAnswerD').value);
formData.append('rAnswer', document.getElementById('editRAnswer').value);
} else {
formData.append('answer', document.getElementById('editAnswer').value);
formData.append('rAnswer', document.getElementById('editRAnswerEssay').value);
}
fetch('/admin/api/question/update', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
showAlert('修改成功!', 'success');
closeEditModal();
loadAllQuestions();
} else {
showAlert(data.message || '修改失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('修改失败,请检查网络', 'error');
});
}
// 点击弹窗外部关闭
window.onclick = function(event) {
const modal = document.getElementById('editModal');
if (event.target == modal) {
closeEditModal();
}
}
</script> </script>
<!-- 编辑弹窗 -->
<div id="editModal" class="modal">
<div class="modal-content" style="max-width: 600px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h2>修改题目 <span id="editTypeLabel" class="tag" style="margin-left: 10px;"></span></h2>
<span class="close-btn" onclick="closeEditModal()">&times;</span>
</div>
<input type="hidden" id="editId">
<input type="hidden" id="editType">
<div class="form-group">
<label>题目内容 <span class="required">*</span></label>
<textarea id="editTitle" rows="3" required></textarea>
</div>
<!-- 选择题表单 -->
<div id="editChoiceForm">
<div class="grid-2">
<div class="form-group">
<label>选项 A</label>
<input type="text" id="editAnswerA">
</div>
<div class="form-group">
<label>选项 B</label>
<input type="text" id="editAnswerB">
</div>
<div class="form-group">
<label>选项 C</label>
<input type="text" id="editAnswerC">
</div>
<div class="form-group">
<label>选项 D</label>
<input type="text" id="editAnswerD">
</div>
</div>
<div class="form-group">
<label>正确答案 <span class="required">*</span></label>
<select id="editRAnswer" 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>
<!-- 简答题表单 -->
<div id="editEssayForm" style="display: none;">
<div class="form-group">
<label>学生答案参考</label>
<textarea id="editAnswer" rows="2"></textarea>
</div>
<div class="form-group">
<label>参考答案</label>
<textarea id="editRAnswerEssay" rows="2"></textarea>
</div>
</div>
<div class="form-group">
<label>分值 <span class="required">*</span></label>
<input type="number" id="editScore" required min="0">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="saveEdit()">保存修改</button>
</div>
</div>
</div>
<style>
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2000;
justify-content: center;
align-items: flex-start;
padding-top: 50px;
padding-bottom: 50px;
overflow-y: auto;
}
.modal-content {
background: white;
border-radius: 16px;
padding: 30px;
width: 90%;
max-width: 600px;
position: relative;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
margin: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 2px solid #f59e0b;
}
.modal-header h2 {
margin: 0;
color: #333;
}
.close-btn {
font-size: 28px;
cursor: pointer;
color: #999;
line-height: 1;
}
.close-btn:hover {
color: #f59e0b;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.required {
color: #e74c3c;
}
</style>
</body> </body>
</html> </html>

View File

@@ -239,42 +239,20 @@
<h2>试卷列表</h2> <h2>试卷列表</h2>
<!-- 搜索框 --> <!-- 搜索框 -->
<form class="search-section" action="/admin/listexam" method="get"> <div class="search-section">
<div class="form-group"> <div class="form-group">
<label>试卷标题</label> <label>试卷标题</label>
<input type="text" name="title" th:value="${searchTitle}" placeholder="输入试卷标题进行模糊搜索..."> <input type="text" id="searchTitle" placeholder="输入试卷标题进行模糊搜索...">
</div> </div>
<button type="submit" class="btn btn-primary">搜索</button> <button type="button" class="btn btn-primary" onclick="searchExams()">搜索</button>
<a href="/admin/listexam" class="btn btn-secondary">重置</a> <button type="button" class="btn btn-secondary" onclick="resetSearch()">重置</button>
</form>
<div th:if="${#lists.isEmpty(examPapers)}" class="empty-state">
暂无试卷记录,点击上方"创建试卷"生成一份吧
</div> </div>
<table th:unless="${#lists.isEmpty(examPapers)}"> <div id="examListContainer">
<thead> <div class="empty-state">
<tr> 加载中...
<th width="60">ID</th> </div>
<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> </div>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
@@ -307,8 +285,124 @@
</div> </div>
<script> <script>
// 页面加载时加载全部试卷
document.addEventListener('DOMContentLoaded', function() {
loadExams();
});
// 加载试卷列表
function loadExams(title = '') {
const container = document.getElementById('examListContainer');
container.innerHTML = '<div class="empty-state">加载中...</div>';
let url = '/admin/api/exams';
if (title) {
url += '?title=' + encodeURIComponent(title);
}
fetch(url)
.then(res => res.json())
.then(data => {
if (data.success) {
renderExams(data.data);
} else {
showAlert(data.message || '加载失败', 'error');
}
})
.catch(err => {
console.error(err);
showAlert('加载失败,请检查网络', 'error');
});
}
// 渲染试卷列表
function renderExams(examPapers) {
const container = document.getElementById('examListContainer');
if (!examPapers || examPapers.length === 0) {
container.innerHTML = '<div class="empty-state">暂无试卷记录,点击上方"创建试卷"生成一份吧</div>';
return;
}
let html = `
<table>
<thead>
<tr>
<th width="60">ID</th>
<th width="280">UID</th>
<th>试卷标题</th>
<th width="220">操作</th>
</tr>
</thead>
<tbody>
`;
examPapers.forEach(paper => {
const title = paper.title || '未命名试卷';
html += `
<tr>
<td>${paper.id}</td>
<td style="font-family: monospace; color: #666; font-size: 13px;">${paper.uid}</td>
<td>${escapeHtml(title)}</td>
<td>
<div class="action-btns">
<button class="btn btn-primary btn-sm" onclick="viewExamDetail('${paper.uid}')">查看内容</button>
<a href="/${paper.uid}" target="_blank" class="btn btn-success btn-sm">前往答题</a>
</div>
</td>
</tr>
`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// 搜索试卷
function searchExams() {
const title = document.getElementById('searchTitle').value.trim();
loadExams(title);
}
// 重置搜索
function resetSearch() {
document.getElementById('searchTitle').value = '';
loadExams();
}
// 转义HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示提示
function showAlert(message, type) {
const existingAlert = document.querySelector('.dynamic-alert');
if (existingAlert) existingAlert.remove();
const alert = document.createElement('div');
alert.className = `alert alert-${type} dynamic-alert`;
alert.textContent = message;
alert.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; min-width: 200px; text-align: center; padding: 12px 16px; border-radius: 8px;';
if (type === 'success') {
alert.style.background = '#d4edda';
alert.style.color = '#155724';
} else {
alert.style.background = '#f8d7da';
alert.style.color = '#721c24';
}
document.body.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 3000);
}
// 查看试卷详情(原有函数)
function viewExamDetail(uid) { function viewExamDetail(uid) {
// 打开弹窗
document.getElementById('detailModal').style.display = 'block'; document.getElementById('detailModal').style.display = 'block';
document.getElementById('modalTitle').textContent = '试卷加载中...'; document.getElementById('modalTitle').textContent = '试卷加载中...';
@@ -321,22 +415,21 @@
document.getElementById('modalChoiceCount').textContent = data.choiceCount; document.getElementById('modalChoiceCount').textContent = data.choiceCount;
document.getElementById('modalEssayCount').textContent = data.essayCount; document.getElementById('modalEssayCount').textContent = data.essayCount;
// 渲染选择题
let choiceHtml = ''; let choiceHtml = '';
if (data.choiceQuestions && data.choiceQuestions.length > 0) { if (data.choiceQuestions && data.choiceQuestions.length > 0) {
data.choiceQuestions.forEach((q, index) => { data.choiceQuestions.forEach((q, index) => {
choiceHtml += ` choiceHtml += `
<div class="question-item"> <div class="question-item">
<div style="margin-bottom: 12px;"> <div style="margin-bottom: 12px;">
<strong>${index + 1}. ${q.title}</strong> <strong>${index + 1}. ${escapeHtml(q.title)}</strong>
<span class="question-type">选择题</span> <span class="question-type">选择题</span>
<span class="question-score">${q.score}分</span> <span class="question-score">${q.score}分</span>
</div> </div>
<div class="answer-content"> <div class="answer-content">
${q.answerA ? `<div>A. ${q.answerA}</div>` : ''} ${q.answerA ? `<div>A. ${escapeHtml(q.answerA)}</div>` : ''}
${q.answerB ? `<div>B. ${q.answerB}</div>` : ''} ${q.answerB ? `<div>B. ${escapeHtml(q.answerB)}</div>` : ''}
${q.answerC ? `<div>C. ${q.answerC}</div>` : ''} ${q.answerC ? `<div>C. ${escapeHtml(q.answerC)}</div>` : ''}
${q.answerD ? `<div>D. ${q.answerD}</div>` : ''} ${q.answerD ? `<div>D. ${escapeHtml(q.answerD)}</div>` : ''}
</div> </div>
<div class="correct-answer">正确答案:${q.ranswer || q.rAnswer || '-'}</div> <div class="correct-answer">正确答案:${q.ranswer || q.rAnswer || '-'}</div>
</div> </div>
@@ -347,18 +440,17 @@
} }
document.getElementById('choiceContainer').innerHTML = choiceHtml; document.getElementById('choiceContainer').innerHTML = choiceHtml;
// 渲染简答题
let essayHtml = ''; let essayHtml = '';
if (data.essayQuestions && data.essayQuestions.length > 0) { if (data.essayQuestions && data.essayQuestions.length > 0) {
data.essayQuestions.forEach((q, index) => { data.essayQuestions.forEach((q, index) => {
essayHtml += ` essayHtml += `
<div class="question-item"> <div class="question-item">
<div style="margin-bottom: 12px;"> <div style="margin-bottom: 12px;">
<strong>${index + 1}. ${q.title}</strong> <strong>${index + 1}. ${escapeHtml(q.title)}</strong>
<span class="question-type">简答题</span> <span class="question-type">简答题</span>
<span class="question-score">${q.score}分</span> <span class="question-score">${q.score}分</span>
</div> </div>
<div class="correct-answer">参考答案:${q.ranswer || q.rAnswer || q.answer || '无'}</div> <div class="correct-answer">参考答案:${escapeHtml(q.ranswer || q.rAnswer || q.answer || '无')}</div>
</div> </div>
`; `;
}); });
@@ -377,13 +469,17 @@
document.getElementById('detailModal').style.display = 'none'; document.getElementById('detailModal').style.display = 'none';
} }
// 点击遮罩层关闭弹窗
window.onclick = function(event) { window.onclick = function(event) {
let modal = document.getElementById('detailModal'); let modal = document.getElementById('detailModal');
if (event.target == modal) { if (event.target == modal) {
modal.style.display = "none"; modal.style.display = "none";
} }
} }
// 回车键搜索
document.getElementById('searchTitle')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchExams();
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -260,6 +260,25 @@
<div class="answer-section"> <div class="answer-section">
<!-- 选择题 --> <!-- 选择题 -->
<div th:if="${q.type == 1}"> <div th:if="${q.type == 1}">
<!-- 选项列表 -->
<div class="options-list" style="margin-bottom: 10px; padding-left: 10px;">
<div class="option-item" th:if="${q.answerA != null}" style="margin: 5px 0;">
<span style="font-weight: bold;">A.</span>
<span th:text="${q.answerA}"></span>
</div>
<div class="option-item" th:if="${q.answerB != null}" style="margin: 5px 0;">
<span style="font-weight: bold;">B.</span>
<span th:text="${q.answerB}"></span>
</div>
<div class="option-item" th:if="${q.answerC != null}" style="margin: 5px 0;">
<span style="font-weight: bold;">C.</span>
<span th:text="${q.answerC}"></span>
</div>
<div class="option-item" th:if="${q.answerD != null}" style="margin: 5px 0;">
<span style="font-weight: bold;">D.</span>
<span th:text="${q.answerD}"></span>
</div>
</div>
<div class="answer-row"> <div class="answer-row">
<span class="answer-label">答案:</span> <span class="answer-label">答案:</span>
<span class="answer-content" <span class="answer-content"