修正了修改或删除题目后全局刷新的问题,添加了联合搜索题目
This commit is contained in:
@@ -33,17 +33,19 @@ public class AdminController {
|
|||||||
return "admin";
|
return "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: 获取题目列表(支持全部、按类型、按分值)
|
// API: 获取题目列表(支持全部、按类型、按分值、按标题模糊查询、联合查询)
|
||||||
@GetMapping("/admin/api/questions")
|
@GetMapping("/admin/api/questions")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<Map<String, Object>> getQuestions(
|
public ResponseEntity<Map<String, Object>> getQuestions(
|
||||||
@RequestParam(required = false) Integer type,
|
@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<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
List<question> questions;
|
List<question> questions;
|
||||||
|
|
||||||
if (type != null) {
|
if (type != null || title != null) {
|
||||||
questions = adminService.getQuestionsByType(type);
|
// 联合查询:按类型和/或标题
|
||||||
|
questions = adminService.getQuestionsByTypeAndTitle(type, title);
|
||||||
} else if (score != null) {
|
} else if (score != null) {
|
||||||
questions = adminService.getQuestionsByScore(score);
|
questions = adminService.getQuestionsByScore(score);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ public interface adminService extends IService<question> {
|
|||||||
*/
|
*/
|
||||||
List<question> getQuestionsByType(Integer type);
|
List<question> getQuestionsByType(Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型和/或标题模糊查询题目(联合查询)
|
||||||
|
* @param type 题目类型,可为null
|
||||||
|
* @param title 标题关键词,可为null,支持模糊匹配
|
||||||
|
* @return 符合条件的题目列表
|
||||||
|
*/
|
||||||
|
List<question> getQuestionsByTypeAndTitle(Integer type, String title);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据分数查询所有题目
|
* 根据分数查询所有题目
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ public class adminServiceImpl extends ServiceImpl<questionMapper, question> impl
|
|||||||
return list(wrapper);
|
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
|
@Override
|
||||||
public List<question> getQuestionsByScore(Integer score) {
|
public List<question> getQuestionsByScore(Integer score) {
|
||||||
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<question> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -283,26 +283,26 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="query-section">
|
<div class="query-section">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>按类型查询</label>
|
<label>题目类型</label>
|
||||||
<select id="queryType" required>
|
<select id="queryType">
|
||||||
|
<option value="">全部类型</option>
|
||||||
<option value="1">选择题</option>
|
<option value="1">选择题</option>
|
||||||
<option value="2">简答题</option>
|
<option value="2">简答题</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-success" onclick="queryByType()">查询</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="query-section">
|
<div class="query-section">
|
||||||
<div class="form-group">
|
<div class="form-group" style="flex: 1;">
|
||||||
<label>按分值查询</label>
|
<label>标题关键词</label>
|
||||||
<input type="number" id="queryScore" placeholder="请输入分值" required min="0">
|
<input type="text" id="queryTitle" placeholder="请输入标题关键词(模糊查询)">
|
||||||
</div>
|
</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>
|
||||||
</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>
|
<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>
|
||||||
@@ -316,6 +316,7 @@
|
|||||||
题目列表
|
题目列表
|
||||||
<span id="queryTypeTag" class="tag tag-choice" style="margin-left: 10px; display: none;"></span>
|
<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="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>
|
<span id="questionCount" class="tag" style="margin-left: 10px; background: #e3f2fd; color: #1976d2; display: none;">共 0 题</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -399,31 +400,49 @@
|
|||||||
function loadAllQuestions() {
|
function loadAllQuestions() {
|
||||||
document.getElementById('queryTypeTag').style.display = 'none';
|
document.getElementById('queryTypeTag').style.display = 'none';
|
||||||
document.getElementById('queryScoreTag').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');
|
fetchQuestions('/admin/api/questions');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按类型查询
|
// 联合查询:按类型和/或标题
|
||||||
function queryByType() {
|
function queryQuestions() {
|
||||||
const type = document.getElementById('queryType').value;
|
const type = document.getElementById('queryType').value;
|
||||||
const typeTag = document.getElementById('queryTypeTag');
|
const title = document.getElementById('queryTitle').value.trim();
|
||||||
typeTag.textContent = type === '1' ? '选择题' : '简答题';
|
|
||||||
typeTag.style.display = 'inline';
|
|
||||||
document.getElementById('queryScoreTag').style.display = 'none';
|
|
||||||
fetchQuestions('/admin/api/questions?type=' + type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按分值查询
|
// 如果类型和标题都为空,查询全部
|
||||||
function queryByScore() {
|
if (!type && !title) {
|
||||||
const score = document.getElementById('queryScore').value;
|
loadAllQuestions();
|
||||||
if (!score && score !== '0') {
|
|
||||||
showAlert('请输入分值', 'error');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scoreTag = document.getElementById('queryScoreTag');
|
|
||||||
scoreTag.textContent = score + '分';
|
const typeTag = document.getElementById('queryTypeTag');
|
||||||
scoreTag.style.display = 'inline';
|
const titleTag = document.getElementById('queryTitleTag');
|
||||||
document.getElementById('queryTypeTag').style.display = 'none';
|
|
||||||
fetchQuestions('/admin/api/questions?score=' + score);
|
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>';
|
'<span style="color: #999;">-</span>';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr id="question-row-${q.id}" data-id="${q.id}" data-type="${q.type}">
|
||||||
<td>${q.id}</td>
|
<td>${q.id}</td>
|
||||||
<td style="max-width: 300px; word-wrap: break-word;">${escapeHtml(q.title)}</td>
|
<td style="max-width: 300px; word-wrap: break-word;">${escapeHtml(q.title)}</td>
|
||||||
<td>${typeTag}</td>
|
<td>${typeTag}</td>
|
||||||
@@ -524,7 +543,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
<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-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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -536,7 +555,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除题目
|
// 删除题目
|
||||||
function deleteQuestion(title) {
|
function deleteQuestion(id, title) {
|
||||||
if (!confirm('确定要删除这道题吗?')) return;
|
if (!confirm('确定要删除这道题吗?')) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -550,7 +569,13 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert('删除成功!', 'success');
|
showAlert('删除成功!', 'success');
|
||||||
loadAllQuestions();
|
// 局部删除:从DOM中移除该行,不刷新整个列表
|
||||||
|
const row = document.getElementById('question-row-' + id);
|
||||||
|
if (row) {
|
||||||
|
row.remove();
|
||||||
|
// 更新计数
|
||||||
|
updateQuestionCount();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showAlert(data.message || '删除失败', 'error');
|
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
|
// 转义HTML
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
@@ -683,8 +717,9 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert('修改成功!', 'success');
|
showAlert('修改成功!', 'success');
|
||||||
|
// 局部更新:只刷新修改的那一行(必须在 closeEditModal 之前调用)
|
||||||
|
updateQuestionRow(currentEditId, currentEditType, formData);
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
loadAllQuestions();
|
|
||||||
} else {
|
} else {
|
||||||
showAlert(data.message || '修改失败', 'error');
|
showAlert(data.message || '修改失败', 'error');
|
||||||
}
|
}
|
||||||
@@ -695,13 +730,58 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击弹窗外部关闭
|
// 局部更新题目行
|
||||||
window.onclick = function(event) {
|
function updateQuestionRow(id, type, formData) {
|
||||||
const modal = document.getElementById('editModal');
|
const row = document.getElementById('question-row-' + id);
|
||||||
if (event.target == modal) {
|
if (!row) return;
|
||||||
closeEditModal();
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<!-- 编辑弹窗 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user