mobile app chat contest_detail exam_detail exam_list themes

This commit is contained in:
2026-03-09 18:50:22 +08:00
parent 7dec929e29
commit 6ccfe9b107
5 changed files with 174 additions and 87 deletions

View File

@@ -80,7 +80,7 @@
</div>
</div>
<!-- 聊天视图 -->
<div id="chatView" class="flex-1 hidden z-10 relative h-full">
<div id="chatView" class="flex-1 hidden flex-col z-10 relative h-full">
<!-- 顶栏 -->
<div id="chatHeader" class="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-slate-800/60 backdrop-blur-xl flex-shrink-0 z-20">
<div class="flex items-center gap-3">
@@ -413,6 +413,7 @@ function renderNotifications() {
const isResult = n.type === 'contest_result' || n.type === 'teacher_result';
const isNewExam = n.type === 'contest_new_exam';
const isGraded = n.type === 'exam_graded';
const isSubmission = n.type === 'exam_submission';
const isSystem = n.type === 'system_announcement';
const isPendingContest = isContestApp && n.application_status === 'pending';
const isPendingTeacher = isTeacherApp && n.application_status === 'pending';
@@ -421,8 +422,8 @@ function renderNotifications() {
if ((isContestApp || isTeacherApp) && n.application_status === 'approved') statusHtml = '<span class="text-xs text-green-600 font-medium">已批准</span>';
else if ((isContestApp || isTeacherApp) && n.application_status === 'rejected') statusHtml = '<span class="text-xs text-red-600 font-medium">已拒绝</span>';
else if (isFriendReq && n.application_status === 'accepted') statusHtml = '<span class="text-xs text-green-600 font-medium">已同意</span>';
const icon = isContestApp ? '📋' : isTeacherApp ? '👨‍🏫' : isFriendReq ? '👤' : isResult ? '📢' : isNewExam ? '📝' : isGraded ? '✅' : isSystem ? '📢' : '🔔';
const iconBg = isContestApp ? 'bg-orange-100' : isTeacherApp ? 'bg-purple-100' : isFriendReq ? 'bg-blue-100' : isResult ? 'bg-green-100' : isNewExam ? 'bg-indigo-100' : isGraded ? 'bg-emerald-100' : isSystem ? 'bg-amber-100' : 'bg-blue-100';
const icon = isContestApp ? '📋' : isTeacherApp ? '👨‍🏫' : isFriendReq ? '👤' : isResult ? '📢' : isNewExam ? '📝' : isSubmission ? '📝' : isGraded ? '✅' : isSystem ? '📢' : '🔔';
const iconBg = isContestApp ? 'bg-orange-100' : isTeacherApp ? 'bg-purple-100' : isFriendReq ? 'bg-blue-100' : isResult ? 'bg-green-100' : isNewExam ? 'bg-indigo-100' : isSubmission ? 'bg-amber-100' : isGraded ? 'bg-emerald-100' : isSystem ? 'bg-amber-100' : 'bg-blue-100';
const clickAction = `showNotifDetail(${n.id})`;
let actionsHtml = '';
if (isPendingContest && currentUser.role === 'admin') {
@@ -584,13 +585,15 @@ function showNotifDetail(nid) {
'teacher_application': '教师申请', 'teacher_result': '教师审核结果',
'contest_application': '杯赛申请', 'contest_result': '杯赛通知',
'contest_new_exam': '新考试', 'exam_graded': '成绩通知',
'system_announcement': '系统通知', 'friend_request': '好友申请'
'exam_submission': '待批改', 'system_announcement': '系统通知',
'friend_request': '好友申请'
};
const typeIcons = {
'teacher_application': '👨‍🏫', 'teacher_result': '🎓',
'contest_application': '📋', 'contest_result': '🏅',
'contest_new_exam': '📝', 'exam_graded': '✅',
'system_announcement': '📢', 'friend_request': '👤'
'exam_submission': '📝', 'system_announcement': '📢',
'friend_request': '👤'
};
document.getElementById('notifDetailIcon').textContent = typeIcons[n.type] || '🔔';
@@ -611,8 +614,13 @@ function showNotifDetail(nid) {
<button onclick="approveTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">批准</button>
<button onclick="rejectTeacher(${n.application_id})" class="px-4 py-2 text-sm bg-red-500 text-white rounded-md hover:bg-red-600">拒绝</button>
</div>`;
} else if (n.type === 'exam_submission' && n.exam_id && n.submission_id) {
actHtml = `<a href="/exams/${n.exam_id}/grade/${n.submission_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
去批改
</a>`;
} else if (n.type === 'contest_new_exam' || n.type === 'exam_graded') {
actHtml = `<a href="/exams/${n.post_id}" class="inline-block px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-blue-700">查看考试</a>`;
actHtml = `<a href="/exams/${n.post_id}" class="btn-futuristic inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium">查看考试</a>`;
} else if (n.type === 'friend_request' && n.application_status === 'pending' && n.friend_request_id) {
actHtml = `<div class="flex gap-3">
<button onclick="acceptFriendReq(${n.friend_request_id},${n.id})" class="px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600">同意</button>

View File

@@ -379,6 +379,7 @@ async function loadExams() {
const statusText = e.status === 'available' ? '进行中' : '已关闭';
const subCount = e.submission_count !== null ? `<span class="text-xs text-slate-400 ml-2">${e.submission_count}人提交</span>` : '';
const removeBtn = isOwner ? `<button onclick="removeExam(${e.id}, '${escapeHtml(e.title)}')" class="ml-2 px-2 py-1 text-xs text-red-600 border border-red-300 rounded hover:bg-red-50">移除</button>` : '';
const toggleBtn = isOwner ? (e.status === 'available' ? `<button onclick="toggleExamStatus(${e.id}, 'closed')" class="ml-2 px-2 py-1 text-xs text-orange-600 border border-orange-300 rounded hover:bg-orange-50">关闭考试</button>` : `<button onclick="toggleExamStatus(${e.id}, 'available')" class="ml-2 px-2 py-1 text-xs text-green-600 border border-green-300 rounded hover:bg-green-50">开放考试</button>`) : '';
html += `<div class="flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
@@ -391,7 +392,7 @@ async function loadExams() {
<div class="flex items-center gap-2 ml-3 shrink-0">
<a href="/exams/${e.id}" class="px-3 py-1 text-xs font-medium text-cyan-400 border border-primary rounded hover:bg-blue-50">进入考试</a>
${isMember ? `<a href="/exams/${e.id}/submissions" class="px-3 py-1 text-xs font-medium text-amber-500 border border-amber-400 rounded hover:bg-amber-50">批改</a>` : ''}
${removeBtn}
${toggleBtn}${removeBtn}
</div>
</div>`;
});
@@ -415,6 +416,20 @@ async function removeExam(examId, title) {
} catch(e) { alert('网络错误'); }
}
// 切换考试状态(关闭/开放)
function toggleExamStatus(examId, status) {
const label = status === 'closed' ? '关闭' : '开放';
if (!confirm(`确定${label}该考试?`)) return;
fetch(`/api/exams/${examId}/status`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ status: status })
}).then(r => r.json()).then(data => {
if (data.success) { loadExams(); }
else { alert(data.message); }
}).catch(() => alert('操作失败'));
}
// 导入考试弹窗
function showImportModal() {
document.getElementById('import-exam-modal').classList.remove('hidden');

View File

@@ -288,7 +288,7 @@ async function examUploadFiles(files, qid) {
function initAnswers() {
// 优先从服务器草稿恢复
{% if draft %}
const serverDraft = {{ draft.answers | tojson }};
const serverDraft = {{ draft.get_answers() | tojson }};
if (serverDraft && typeof serverDraft === 'object' && Object.keys(serverDraft).length > 0) {
answers = serverDraft;
restoreAnswersToForm();

View File

@@ -110,6 +110,8 @@
{% if exam.status == 'closed' %}
<span class="badge-futuristic absolute top-4 right-4 bg-slate-900/50 backdrop-blur-md text-slate-300 border-slate-700/50">已关闭</span>
{% elif exam.scheduled_end and now and now > exam.scheduled_end %}
<span class="badge-futuristic absolute top-4 right-4 bg-slate-900/50 backdrop-blur-md text-slate-300 border-slate-700/50">已结束</span>
{% else %}
<span class="badge-futuristic absolute top-4 right-4 bg-white/90 backdrop-blur-md text-cyan-600 border-cyan-500/30">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-500 mr-1.5 animate-pulse"></span>进行中
@@ -198,7 +200,7 @@
<a href="/exams/{{ exam.id }}/submissions" class="px-3 py-1.5 bg-slate-800/50 text-slate-300 hover:bg-slate-700/50 hover:text-slate-200 rounded-lg text-xs font-medium transition-colors border border-cyan-500/30">提交情况</a>
<a href="/exams/{{ exam.id }}/print" class="px-3 py-1.5 bg-slate-800/50 text-slate-300 hover:bg-slate-700/50 hover:text-slate-200 rounded-lg text-xs font-medium transition-colors border border-cyan-500/30">打印试卷</a>
{% if user.role == 'admin' or exam.creator_id == user.id %}
{% if user.role == 'admin' or exam.creator_id == user.id or (exam.contest_id and exam.contest_id in cup_owner_contest_ids) %}
{% if exam.status == 'available' %}
<button onclick="toggleExamStatus({{ exam.id }}, 'closed')" class="px-3 py-1.5 bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 rounded-lg text-xs font-medium transition-colors border border-orange-500/30">关闭考试</button>
{% else %}