修正了修改或删除题目后全局刷新的问题,添加了联合搜索题目

This commit is contained in:
Yakumo Hokori
2026-03-06 18:56:45 +08:00
parent 350ebcef11
commit b71a358ed2
5 changed files with 147 additions and 43 deletions

View File

@@ -33,17 +33,19 @@ public class AdminController {
return "admin";
}
// API: 获取题目列表(支持全部、按类型、按分值)
// API: 获取题目列表(支持全部、按类型、按分值、按标题模糊查询、联合查询
@GetMapping("/admin/api/questions")
@ResponseBody
public ResponseEntity<Map<String, Object>> getQuestions(
@RequestParam(required = false) Integer type,
@RequestParam(required = false) Integer score) {
@RequestParam(required = false) Integer score,
@RequestParam(required = false) String title) {
Map<String, Object> result = new HashMap<>();
List<question> questions;
if (type != null) {
questions = adminService.getQuestionsByType(type);
if (type != null || title != null) {
// 联合查询:按类型和/或标题
questions = adminService.getQuestionsByTypeAndTitle(type, title);
} else if (score != null) {
questions = adminService.getQuestionsByScore(score);
} else {

View File

@@ -23,6 +23,14 @@ public interface adminService extends IService<question> {
*/
List<question> getQuestionsByType(Integer type);
/**
* 根据类型和/或标题模糊查询题目(联合查询)
* @param type 题目类型可为null
* @param title 标题关键词可为null支持模糊匹配
* @return 符合条件的题目列表
*/
List<question> getQuestionsByTypeAndTitle(Integer type, String title);
/**
* 根据分数查询所有题目
*/

View File

@@ -47,6 +47,20 @@ public class adminServiceImpl extends ServiceImpl<questionMapper, question> impl
return list(wrapper);
}
@Override
public List<question> getQuestionsByTypeAndTitle(Integer type, String title) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
if (type != null) {
wrapper.eq(question::getType, type);
}
if (title != null && !title.trim().isEmpty()) {
wrapper.like(question::getTitle, title.trim());
}
return list(wrapper);
}
@Override
public List<question> getQuestionsByScore(Integer score) {
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();

View File

@@ -2,8 +2,8 @@ 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
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

View File

@@ -283,26 +283,26 @@
<div>
<div class="query-section">
<div class="form-group">
<label>按类型查询</label>
<select id="queryType" required>
<label>题目类型</label>
<select id="queryType">
<option value="">全部类型</option>
<option value="1">选择题</option>
<option value="2">简答题</option>
</select>
</div>
<button type="button" class="btn btn-success" onclick="queryByType()">查询</button>
</div>
</div>
<div>
<div class="query-section">
<div class="form-group">
<label>按分值查询</label>
<input type="number" id="queryScore" placeholder="请输入分值" required min="0">
<div class="form-group" style="flex: 1;">
<label>标题关键词</label>
<input type="text" id="queryTitle" placeholder="请输入标题关键词(模糊查询)">
</div>
<button type="button" class="btn btn-success" onclick="queryByScore()">查询</button>
<button type="button" class="btn btn-success" onclick="queryQuestions()" style="margin-bottom: 0; height: fit-content; align-self: flex-end;">查询</button>
</div>
</div>
</div>
<div style="margin-top: 16px; display: flex; gap: 12px;">
<div style="margin-top: 16px; display: flex; gap: 12px; flex-wrap: wrap;">
<button type="button" class="btn btn-primary" onclick="loadAllQuestions()">显示全部</button>
<a th:href="@{/admin/mkexam}" class="btn btn-success">创建试卷</a>
<a th:href="@{/admin/listexam}" class="btn btn-success">查看试卷</a>
@@ -316,6 +316,7 @@
题目列表
<span id="queryTypeTag" class="tag tag-choice" style="margin-left: 10px; display: none;"></span>
<span id="queryScoreTag" class="tag tag-essay" style="margin-left: 10px; display: none;"></span>
<span id="queryTitleTag" class="tag" style="margin-left: 10px; display: none; background: #e8f5e9; color: #2e7d32;"></span>
<span id="questionCount" class="tag" style="margin-left: 10px; background: #e3f2fd; color: #1976d2; display: none;">共 0 题</span>
</h2>
@@ -399,31 +400,49 @@
function loadAllQuestions() {
document.getElementById('queryTypeTag').style.display = 'none';
document.getElementById('queryScoreTag').style.display = 'none';
document.getElementById('queryTitleTag').style.display = 'none';
document.getElementById('queryType').value = '';
document.getElementById('queryTitle').value = '';
fetchQuestions('/admin/api/questions');
}
// 按类型查询
function queryByType() {
// 联合查询:按类型和/或标题
function queryQuestions() {
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');
const title = document.getElementById('queryTitle').value.trim();
// 如果类型和标题都为空,查询全部
if (!type && !title) {
loadAllQuestions();
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);
const typeTag = document.getElementById('queryTypeTag');
const titleTag = document.getElementById('queryTitleTag');
if (type) {
typeTag.textContent = type === '1' ? '选择题' : '简答题';
typeTag.style.display = 'inline';
} else {
typeTag.style.display = 'none';
}
if (title) {
titleTag.textContent = '关键词: ' + title;
titleTag.style.display = 'inline';
} else {
titleTag.style.display = 'none';
}
document.getElementById('queryScoreTag').style.display = 'none';
let url = '/admin/api/questions?';
const params = [];
if (type) params.push('type=' + encodeURIComponent(type));
if (title) params.push('title=' + encodeURIComponent(title));
url += params.join('&');
fetchQuestions(url);
}
// 获取题目列表
@@ -514,7 +533,7 @@
'<span style="color: #999;">-</span>';
html += `
<tr>
<tr id="question-row-${q.id}" data-id="${q.id}" data-type="${q.type}">
<td>${q.id}</td>
<td style="max-width: 300px; word-wrap: break-word;">${escapeHtml(q.title)}</td>
<td>${typeTag}</td>
@@ -524,7 +543,7 @@
<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>
<button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="deleteQuestion(${q.id}, '${escapeHtml(q.title)}')">删除</button>
</div>
</td>
</tr>
@@ -536,7 +555,7 @@
}
// 删除题目
function deleteQuestion(title) {
function deleteQuestion(id, title) {
if (!confirm('确定要删除这道题吗?')) return;
const formData = new FormData();
@@ -550,7 +569,13 @@
.then(data => {
if (data.success) {
showAlert('删除成功!', 'success');
loadAllQuestions();
// 局部删除从DOM中移除该行不刷新整个列表
const row = document.getElementById('question-row-' + id);
if (row) {
row.remove();
// 更新计数
updateQuestionCount();
}
} else {
showAlert(data.message || '删除失败', 'error');
}
@@ -561,6 +586,15 @@
});
}
// 更新题目计数
function updateQuestionCount() {
const countTag = document.getElementById('questionCount');
if (countTag) {
const rows = document.querySelectorAll('#questionsContainer tbody tr');
countTag.textContent = `${rows.length}`;
}
}
// 转义HTML
function escapeHtml(text) {
if (!text) return '';
@@ -683,8 +717,9 @@
.then(data => {
if (data.success) {
showAlert('修改成功!', 'success');
// 局部更新:只刷新修改的那一行(必须在 closeEditModal 之前调用)
updateQuestionRow(currentEditId, currentEditType, formData);
closeEditModal();
loadAllQuestions();
} else {
showAlert(data.message || '修改失败', 'error');
}
@@ -695,13 +730,58 @@
});
}
// 点击弹窗外部关闭
window.onclick = function(event) {
const modal = document.getElementById('editModal');
if (event.target == modal) {
closeEditModal();
// 局部更新题目行
function updateQuestionRow(id, type, formData) {
const row = document.getElementById('question-row-' + id);
if (!row) return;
const title = formData.get('title');
const score = formData.get('score');
const rAnswer = formData.get('rAnswer');
const typeTag = type == 1 ?
'<span class="tag tag-choice">选择题</span>' :
'<span class="tag tag-essay">简答题</span>';
let optionsHtml = '';
if (type == 1) {
const answerA = formData.get('answerA');
const answerB = formData.get('answerB');
const answerC = formData.get('answerC');
const answerD = formData.get('answerD');
const parts = [];
if (answerA) parts.push(`A: ${escapeHtml(answerA)}`);
if (answerB) parts.push(`B: ${escapeHtml(answerB)}`);
if (answerC) parts.push(`C: ${escapeHtml(answerC)}`);
if (answerD) parts.push(`D: ${escapeHtml(answerD)}`);
optionsHtml = parts.join('<br>');
} else {
const answer = formData.get('answer');
optionsHtml = answer ? escapeHtml(answer) : '无答案';
}
const rAnswerHtml = rAnswer ?
`<span class="tag tag-answer">${escapeHtml(rAnswer)}</span>` :
'<span style="color: #999;">-</span>';
// 更新行内容
row.innerHTML = `
<td>${id}</td>
<td style="max-width: 300px; word-wrap: break-word;">${escapeHtml(title)}</td>
<td>${typeTag}</td>
<td>${optionsHtml}</td>
<td>${rAnswerHtml}</td>
<td><span class="score-badge">${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(${id}, ${type})">修改</button>
<button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="deleteQuestion(${id}, '${escapeHtml(title)}')">删除</button>
</div>
</td>
`;
}
</script>
<!-- 编辑弹窗 -->