survey/public/survey.html
2026-01-01 15:58:33 +08:00

3075 lines
123 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>尚逸基石学习能力测评</title>
<script src="https://cdn.bootcdn.net/ajax/libs/qrcode/1.5.1/qrcode.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
padding: 20px;
line-height: 1.6;
}
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 30px;
position: relative;
overflow: hidden;
box-shadow: 0 4px 20px rgba(255, 126, 95, 0.3);
text-align: center;
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.header p {
position: relative;
z-index: 2;
opacity: 0.9;
}
.config-section {
padding: 30px;
background: #ffffff;
}
.form-container {
padding: 0;
}
.form-section {
margin-bottom: 30px;
}
.form-section:last-child {
margin-bottom: 0;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 24px;
}
.section-icon {
font-size: 24px;
margin-right: 12px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px;
}
.section-header h3 {
font-size: 20px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.section-divider {
flex: 1;
height: 1px;
background: linear-gradient(90deg, #e2e8f0, transparent);
margin-left: 20px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.form-field {
position: relative;
}
.form-field.full-width {
grid-column: 1 / -1;
}
.field-label {
display: flex;
align-items: center;
margin-bottom: 8px;
font-weight: 500;
}
.label-text {
color: #4a5568;
font-size: 14px;
}
.required {
color: #e53e3e;
margin-left: 4px;
font-size: 12px;
}
.optional {
color: #a0aec0;
margin-left: 8px;
font-size: 12px;
background: #f7fafc;
padding: 2px 8px;
border-radius: 12px;
}
.modern-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.modern-input:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
transform: translateY(-1px);
}
.modern-input::placeholder {
color: #a0aec0;
}
.select-wrapper {
position: relative;
}
.modern-select {
width: 100%;
padding: 12px 40px 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 16px;
background: white;
appearance: none;
cursor: pointer;
transition: all 0.3s ease;
}
.modern-select:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
transform: translateY(-1px);
}
.select-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #a0aec0;
font-size: 12px;
pointer-events: none;
transition: transform 0.3s ease;
}
.modern-select:focus + .select-arrow {
transform: translateY(-50%) rotate(180deg);
}
.field-hint {
margin-top: 6px;
font-size: 13px;
color: #718096;
}
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
}
.rule-card {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 24px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.rule-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #ff7e5f, #feb47b);
}
.rule-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #cbd5e0;
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.rule-type {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.rule-type.basic {
background: #c6f6d5;
color: #22543d;
}
.rule-type.advanced {
background: #fed7aa;
color: #7c2d12;
}
.rule-type.competition {
background: #fed7e2;
color: #702459;
}
.rule-points {
font-size: 12px;
color: #718096;
background: white;
padding: 4px 8px;
border-radius: 8px;
}
.rule-input-area {
margin-bottom: 12px;
}
.rule-number-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 18px;
font-weight: 600;
text-align: center;
transition: all 0.3s ease;
background: white;
color: #2d3748;
user-select: none;
}
.rule-number-input:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
transform: scale(1.02);
}
.rule-calculation {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #4a5568;
font-weight: 500;
}
.rule-status {
text-align: center;
font-size: 12px;
}
.status-text {
color: #718096;
}
.status-text.success {
color: #38a169;
}
.status-text.error {
color: #e53e3e;
}
.config-summary {
background: #f9f9f9;
padding: 20px;
border-radius: 10px;
border-top: 2px solid #ff7e5f;
margin-top: 20px;
}
.summary-content {
display: flex;
justify-content: space-between;
align-items: center;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.summary-stats {
display: flex;
align-items: center;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1;
margin-bottom: 8px;
color: #ff7e5f;
}
.stat-label {
font-size: 14px;
color: #718096;
font-weight: 500;
}
.stat-divider {
width: 1px;
height: 40px;
background: #e0e0e0;
}
.start-button {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(255, 126, 95, 0.3);
}
.start-button:hover:not(:disabled) {
background: linear-gradient(135deg, #ff6b52, #fe9b5b);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 126, 95, 0.4);
}
.start-button:disabled {
background: #ccc;
color: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.generate-link-button {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.generate-link-button:hover:not(:disabled) {
background: linear-gradient(135deg, #45a049, #5cbf60);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}
.generate-link-button:disabled {
background: #ccc;
color: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.button-container {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.button-arrow {
transition: transform 0.3s ease;
}
.start-button:hover:not(:disabled) .button-arrow {
transform: translateX(4px);
}
.rule-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #ff7e5f;
}
.rule-title {
font-weight: 600;
color: #2d5a3d;
margin-bottom: 5px;
}
.rule-details {
font-size: 14px;
color: #666;
}
.rules-config {
display: flex;
flex-direction: column;
gap: 20px;
}
.rule-input-group {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
}
.rule-input-group:hover {
border-color: #ff7e5f;
box-shadow: 0 4px 15px rgba(255, 126, 95, 0.1);
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.rule-label {
font-weight: 600;
color: #2d5a3d;
font-size: 16px;
}
.rule-score {
background: linear-gradient(135deg, #fff8f3, #ffffff);
color: #ff7e5f;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
border: 1px solid #ff7e5f;
}
.rule-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.rule-input {
flex: 1;
padding: 12px 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
background: white;
}
.rule-input:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
}
.input-unit {
color: #666;
font-weight: 500;
font-size: 14px;
}
.rule-calculation {
color: #666;
font-size: 14px;
padding: 8px 12px;
background: white;
border-radius: 6px;
border-left: 4px solid #4CAF50;
}
.rule-summary {
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg, #e3f2fd, #f8f9fa);
border-radius: 12px;
border: 2px solid #2196F3;
display: flex;
justify-content: space-between;
align-items: center;
}
.summary-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.summary-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.summary-value {
font-size: 20px;
font-weight: 700;
color: #2196F3;
}
.tag-filter {
margin: 15px 0;
}
.tag-filter label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.tag-input {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.tag-input:focus {
outline: none;
border-color: #ff7e5f;
}
.tag-select {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
background: white;
transition: all 0.3s;
min-height: 50px;
margin-bottom: 15px;
}
.tag-select:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
}
.tag-select option {
padding: 8px 12px;
}
.student-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
color: #2d5a3d;
margin-bottom: 8px;
font-size: 14px;
}
.form-input {
padding: 12px 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
background: white;
}
.form-input:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
}
.form-input:valid {
border-color: #4CAF50;
}
.form-input:invalid:not(:placeholder-shown) {
border-color: #f44336;
}
.selected-tags {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
border-left: 4px solid #ff7e5f;
margin-top: 10px;
}
.selected-tags #selectedTagsList {
color: #2d5a3d;
font-weight: 500;
}
.start-btn {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: block;
margin: 20px auto;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 126, 95, 0.4);
}
.start-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.survey-content {
padding: 30px;
display: none;
}
.progress-bar {
background: #e9ecef;
height: 8px;
border-radius: 4px;
margin-bottom: 30px;
overflow: hidden;
}
.progress-fill {
background: linear-gradient(90deg, #ff7e5f, #feb47b);
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
.question-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
.question-card:hover {
border-color: #ff7e5f;
box-shadow: 0 4px 20px rgba(255, 126, 95, 0.15);
}
.question-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.question-number {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 15px;
}
.question-type {
background: linear-gradient(135deg, #fff8f3, #ffffff);
color: #ff7e5f;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin-left: auto;
border: 1px solid #ff7e5f;
}
.question-type.基础题 {
background: #e8f5e8;
color: #2e7d32;
}
.question-type.进阶题 {
background: #fff3e0;
color: #f57c00;
}
.question-type.竞赛题 {
background: #fce4ec;
color: #c2185b;
}
.question-text {
font-size: 18px;
color: #333;
line-height: 1.6;
margin-bottom: 20px;
font-weight: 500;
}
.options-list {
list-style: none;
}
.option-item {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
}
.option-item:hover {
background: linear-gradient(135deg, #fff8f3, #ffffff);
border-color: #ff7e5f;
transform: translateY(-1px);
}
.option-item.selected {
background: linear-gradient(135deg, #fff8f3, #ffffff);
border-color: #ff7e5f;
}
.option-item input[type="radio"] {
margin-right: 12px;
transform: scale(1.2);
}
.option-text {
flex: 1;
font-size: 16px;
color: #333;
}
.submit-btn {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: block;
margin: 30px auto;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.results-section {
padding: 30px;
display: none;
}
.score-card {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 30px;
border-radius: 12px;
text-align: center;
margin-bottom: 30px;
}
.score-value {
font-size: 48px;
font-weight: 700;
margin-bottom: 10px;
}
.score-label {
font-size: 18px;
opacity: 0.9;
}
.results-details {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
}
.result-item {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #ff7e5f;
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
.error {
background: #ffebee;
color: #c62828;
padding: 20px;
border-radius: 8px;
margin: 20px;
border-left: 4px solid #f44336;
}
/* 对话框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
}
.dialog-overlay.show {
display: flex;
opacity: 1;
}
.dialog {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: scale(0.8);
transition: transform 0.3s ease;
}
.dialog-overlay.show .dialog {
transform: scale(1);
}
.dialog-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
}
.dialog-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.dialog-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.dialog-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.dialog-body {
padding: 20px;
max-height: 50vh;
overflow-y: auto;
}
.dialog-footer {
padding: 20px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.dialog-btn {
padding: 10px 20px;
border-radius: 6px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.dialog-btn-primary {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
}
.dialog-btn-primary:hover {
background: linear-gradient(135deg, #ff6b52, #fe9b5b);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 126, 95, 0.3);
}
.dialog-btn-secondary {
background: #f5f5f5;
color: #666;
}
.dialog-btn-secondary:hover {
background: #e0e0e0;
}
.view-report-btn {
background: linear-gradient(135deg, #FF6B6B, #4ECDC4);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.view-report-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.4);
}
.report-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 900px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.close-modal {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.close-modal:hover {
background: rgba(255, 255, 255, 0.2);
}
.modal-body {
padding: 20px;
max-height: calc(85vh - 80px);
overflow-y: auto;
}
.report-actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.copy-btn, .download-btn {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.copy-btn:hover, .download-btn:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(76, 175, 80, 0.3);
}
.download-btn {
background: linear-gradient(135deg, #2196F3, #42A5F5);
}
.download-btn:hover {
box-shadow: 0 3px 10px rgba(33, 150, 243, 0.3);
}
.report-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
overflow-x: auto;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.report-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 12px;
}
.report-table th,
.report-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
vertical-align: top;
}
.report-table th {
background: #f2f2f2;
font-weight: bold;
}
.report-table tr:nth-child(even) {
background: #f9f9f9;
}
@media (max-width: 768px) {
body {
padding: 0;
}
.container {
margin: 0;
border-radius: 0;
}
.header {
padding: 20px;
}
.header h1 {
font-size: 24px;
}
.config-section {
padding: 15px;
}
.form-section {
margin-bottom: 25px;
}
.section-header {
flex-direction: column;
align-items: flex-start;
margin-bottom: 16px;
}
.section-icon {
margin-bottom: 8px;
}
.section-divider {
display: none;
}
.form-row {
grid-template-columns: 1fr;
gap: 12px;
}
.rules-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.rule-card {
padding: 16px;
}
.rule-number-input {
font-size: 16px;
}
.config-summary {
padding: 15px;
}
.summary-content {
flex-direction: column;
gap: 20px;
padding: 15px;
}
.button-container {
flex-direction: column;
width: 100%;
gap: 10px;
}
.summary-stats {
justify-content: center;
}
.stat-value {
font-size: 24px;
}
.start-button, .generate-link-button {
width: 100%;
justify-content: center;
}
}
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 10px;
}
.header, .survey-content, .results-section {
padding: 20px;
}
.question-text {
font-size: 16px;
}
.option-text {
font-size: 14px;
}
.score-value {
font-size: 36px;
}
.modal-content {
width: 95%;
max-height: 90vh;
}
.modal-header {
padding: 15px;
}
.modal-header h3 {
font-size: 16px;
}
.modal-body {
padding: 15px;
max-height: calc(90vh - 60px);
}
.report-actions {
flex-direction: column;
}
.copy-btn, .download-btn {
width: 100%;
}
.report-content {
font-size: 12px;
padding: 15px;
}
}
.footer {
padding: 20px;
text-align: center;
color: #8b5a2b;
font-size: 14px;
background: #fef6f0;
border-top: 1px solid #ffe4d6;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 尚逸基石学习能力测评</h1>
<p>基于科学的题目分类和标签筛选系统,精准评估您的学科能力</p>
</div>
<div class="config-section" id="configSection">
<!-- 学员信息区域 -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">👤</div>
<h3>学员信息</h3>
<div class="section-divider"></div>
</div>
<div class="form-row">
<div class="form-field">
<label for="studentName" class="field-label">
<span class="label-text">姓名</span>
<span class="required">*</span>
</label>
<input type="text" id="studentName" class="modern-input" placeholder="请输入您的姓名" required>
</div>
<div class="form-field">
<label for="studentSchool" class="field-label">
<span class="label-text">学校</span>
<span class="required">*</span>
</label>
<input type="text" id="studentSchool" class="modern-input" placeholder="请输入您的学校" required>
</div>
<div class="form-field">
<label for="studentGrade" class="field-label">
<span class="label-text">年级</span>
<span class="required">*</span>
</label>
<div class="select-wrapper">
<select id="studentGrade" class="modern-select" required>
<option value="">选择年级...</option>
<option value="一年级">一年级</option>
<option value="二年级">二年级</option>
<option value="三年级">三年级</option>
<option value="四年级">四年级</option>
<option value="五年级">五年级</option>
<option value="六年级">六年级</option>
<option value="七年级">七年级</option>
<option value="八年级">八年级</option>
</select>
<div class="select-arrow"></div>
</div>
</div>
<div class="form-field">
<label for="studentPhone" class="field-label">
<span class="label-text">手机号</span>
<span class="required">*</span>
</label>
<input type="tel" id="studentPhone" class="modern-input" placeholder="请输入您的手机号" required>
</div>
</div>
</div>
<!-- 题目筛选区域 -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">🏷️</div>
<h3>题目筛选</h3>
<div class="section-divider"></div>
</div>
<div class="form-row">
<div class="form-field">
<label for="subjectFilterSelect" class="field-label">
<span class="label-text">选择学科</span>
<span class="optional">可选</span>
</label>
<div class="select-wrapper">
<select id="subjectFilterSelect" class="modern-select">
<option value="">选择学科...</option>
</select>
<div class="select-arrow"></div>
</div>
</div>
<div class="form-field">
<label for="gradeFilterSelect" class="field-label">
<span class="label-text">选择年级册次</span>
<span class="optional">可选</span>
</label>
<div class="select-wrapper">
<select id="gradeFilterSelect" class="modern-select">
<option value="">选择年级册次...</option>
</select>
<div class="select-arrow"></div>
</div>
</div>
<div class="form-field">
<label for="unitFilterSelect" class="field-label">
<span class="label-text">选择单元</span>
<span class="optional">可选</span>
</label>
<div class="select-wrapper">
<select id="unitFilterSelect" class="modern-select" disabled>
<option value="">先选择年级册次...</option>
</select>
<div class="select-arrow"></div>
</div>
</div>
</div>
<div class="field-hint">
<span id="selectedTagsList">未选择筛选条件,将使用全部题目</span>
</div>
</div>
<!-- 日常成绩区间选择 -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">📊</div>
<h3>日常成绩区间</h3>
<div class="section-divider"></div>
</div>
<div class="form-row">
<div class="form-field full-width">
<label for="scoreRangeSelect" class="field-label">
<span class="label-text">选择您的日常成绩区间</span>
<span class="required">*</span>
</label>
<div class="select-wrapper">
<select id="scoreRangeSelect" class="modern-select" required>
<option value="">请选择日常成绩区间...</option>
<option value="below70">70分以下</option>
<option value="71to85">71-85分</option>
<option value="86to92">86-92分</option>
<option value="above93">93分以上</option>
</select>
<div class="select-arrow"></div>
</div>
<div class="field-hint">
根据您的日常成绩水平,系统将自动为您配置合适的题目难度组合
</div>
</div>
</div>
<!-- 自动配置结果展示 -->
<div class="rules-grid" id="autoRulesDisplay" style="display: none;">
<div class="rule-card">
<div class="rule-header">
<div class="rule-type basic">基础题</div>
<div class="rule-points">5分/题</div>
</div>
<div class="rule-input-area">
<div class="rule-number-input" id="autoBasicCount">0</div>
<div class="rule-calculation">
<span id="autoBasicScore">0</span>
</div>
</div>
<div class="rule-status" id="autoBasicStatus">
<span class="status-text">题目配置中...</span>
</div>
</div>
<div class="rule-card">
<div class="rule-header">
<div class="rule-type advanced">进阶题</div>
<div class="rule-points">10分/题</div>
</div>
<div class="rule-input-area">
<div class="rule-number-input" id="autoAdvancedCount">0</div>
<div class="rule-calculation">
<span id="autoAdvancedScore">0</span>
</div>
</div>
<div class="rule-status" id="autoAdvancedStatus">
<span class="status-text">题目配置中...</span>
</div>
</div>
<div class="rule-card">
<div class="rule-header">
<div class="rule-type competition">竞赛题</div>
<div class="rule-points">15分/题</div>
</div>
<div class="rule-input-area">
<div class="rule-number-input" id="autoCompetitionCount">0</div>
<div class="rule-calculation">
<span id="autoCompetitionScore">0</span>
</div>
</div>
<div class="rule-status" id="autoCompetitionStatus">
<span class="status-text">题目配置中...</span>
</div>
</div>
</div>
</div>
<!-- 配置总结 -->
<div class="config-summary">
<div class="summary-content">
<div class="summary-stats">
<div class="stat-item">
<div class="stat-value" id="summaryTotalQuestions">14</div>
<div class="stat-label">总题数</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-value" id="summaryTotalScore">100</div>
<div class="stat-label">总分数</div>
</div>
</div>
<div class="button-container">
<button class="start-button" id="startSurvey">
<span class="button-text">开始答题</span>
<span class="button-arrow"></span>
</button>
<button class="generate-link-button" id="generateLinkBtn">
<span class="button-text">📱 生成测评二维码</span>
</button>
</div>
</div>
</div>
<div class="survey-content" id="surveyContent">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div id="questionsContainer"></div>
<button class="submit-btn" id="submitBtn" disabled>提交问卷</button>
</div>
<div class="results-section" id="resultsSection">
<div class="score-card">
<div class="score-value" id="scoreValue">0</div>
<div class="score-label">总分 (100分制)</div>
<button class="view-report-btn" id="viewReportBtn" style="margin-top: 20px;">📊 查看测评报告</button>
</div>
<div class="results-details" id="resultsDetails"></div>
<div class="report-modal" id="reportModal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>📊 考试答题情况分析报告</h3>
<button class="close-modal" id="closeModal">×</button>
</div>
<div class="modal-body">
<div class="report-actions">
<button class="copy-btn" id="copyReportBtn">📋 复制报告</button>
<button class="download-btn" id="downloadReportBtn">💾 下载报告</button>
</div>
<div class="report-content" id="reportContent"></div>
</div>
</div>
</div>
</div>
<div class="footer">
<p>© 2024 尚逸基石学科能力测评系统</p>
</div>
</div>
<!-- 对话框组件 -->
<div class="dialog-overlay" id="dialogOverlay">
<div class="dialog">
<div class="dialog-header">
<h3 class="dialog-title" id="dialogTitle">提示</h3>
<button class="dialog-close" id="dialogClose">×</button>
</div>
<div class="dialog-body" id="dialogBody">
对话框内容
</div>
<div class="dialog-footer" id="dialogFooter">
<button class="dialog-btn dialog-btn-secondary" id="dialogCancel">取消</button>
<button class="dialog-btn dialog-btn-primary" id="dialogConfirm">确定</button>
</div>
</div>
</div>
<script>
class SurveySystem {
constructor() {
this.questions = {};
this.filteredQuestions = {};
this.currentQuestions = [];
this.answers = {};
this.currentRules = {
"基础题": 0,
"进阶题": 0,
"竞赛题": 0
};
this.scoreRangeRules = {
"below70": { "基础题": 20, "进阶题": 0, "竞赛题": 0 },
"71to85": { "基础题": 15, "进阶题": 3, "竞赛题": 2 },
"86to92": { "基础题": 10, "进阶题": 5, "竞赛题": 5 },
"above93": { "基础题": 5, "进阶题": 10, "竞赛题": 5 }
};
this.selectedTag = '';
this.availableFilters = {
subjects: [],
grades: [],
units: []
};
this.currentFilters = {
subject: '',
grade: '', // 完整的年级信息(如:一年级上册)
unit: ''
};
this.selectedScoreRange = '';
// 检查是否为管理员模式
this.isAdminMode = this.checkAdminMode();
this.init();
}
// 检查是否为管理员模式
checkAdminMode() {
// 检查URL参数
const urlParams = new URLSearchParams(window.location.search);
const adminParam = urlParams.get('admin');
// 如果URL有admin=true参数则为管理员模式
return adminParam === 'true';
}
// 根据模式切换按钮可见性
toggleButtonsVisibility() {
const startBtn = document.getElementById('startSurvey');
const generateLinkBtn = document.getElementById('generateLinkBtn');
if (this.isAdminMode) {
// 管理员模式:只显示生成测评链接按钮
startBtn.style.display = 'none';
generateLinkBtn.style.display = 'flex';
} else {
// 普通用户模式:只显示开始答题按钮
startBtn.style.display = 'flex';
generateLinkBtn.style.display = 'none';
}
}
// 对话框显示方法
showDialog(options) {
const { title = '提示', content, onConfirm = null, onCancel = null, showCancel = false } = options;
const overlay = document.getElementById('dialogOverlay');
const dialogTitle = document.getElementById('dialogTitle');
const dialogBody = document.getElementById('dialogBody');
const dialogFooter = document.getElementById('dialogFooter');
const dialogClose = document.getElementById('dialogClose');
const dialogCancel = document.getElementById('dialogCancel');
const dialogConfirm = document.getElementById('dialogConfirm');
// 设置内容
dialogTitle.textContent = title;
dialogBody.innerHTML = typeof content === 'string' ? `<p>${content}</p>` : content;
// 设置按钮
dialogCancel.style.display = showCancel ? 'block' : 'none';
// 绑定事件
const closeDialog = () => {
overlay.classList.remove('show');
setTimeout(() => {
overlay.style.display = 'none';
if (onCancel) onCancel();
}, 300);
};
dialogClose.onclick = closeDialog;
dialogCancel.onclick = closeDialog;
dialogConfirm.onclick = () => {
overlay.classList.remove('show');
setTimeout(() => {
overlay.style.display = 'none';
if (onConfirm) onConfirm();
}, 300);
};
// 显示对话框
overlay.style.display = 'flex';
setTimeout(() => {
overlay.classList.add('show');
}, 10);
}
// 显示错误对话框
showErrorDialog(title, message) {
this.showDialog({
title: title,
content: `<div style="color: #c62828; display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">⚠️</span>
<div>${message}</div>
</div>`,
onConfirm: null,
showCancel: false
});
}
// 显示二维码对话框
showQRCodeDialog(options) {
const { title = '二维码生成', assessmentLink, studentName, studentSchool, studentGrade } = options;
// 创建二维码对话框HTML
const qrDialogHTML = `
<div id="qrCodeDialogOverlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; opacity: 0; transition: opacity 0.3s ease; padding: 20px; box-sizing: border-box;">
<div style="background: white; border-radius: 12px; width: 90%; max-width: 500px; max-height: 95vh; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); transform: scale(0.8); transition: transform 0.3s ease; display: flex; flex-direction: column;">
<div style="padding: 20px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%); color: white; flex-shrink: 0;">
<h3 style="font-size: 18px; font-weight: 600; margin: 0;">${title}</h3>
<button id="closeQrDialog" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.3s;">×</button>
</div>
<div style="padding: 20px; flex-grow: 1; overflow-y: auto;">
<div style="text-align: center; padding: 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">🎉</div>
<div style="color: #4CAF50; font-size: 18px; font-weight: bold; margin-bottom: 15px;">
测评二维码已生成!
</div>
<div style="color: #666; margin-bottom: 20px;">
学生信息:${studentName} | ${studentSchool} | ${studentGrade}
</div>
<div id="qrcode" style="display: flex; justify-content: center; margin-bottom: 20px;"></div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; word-break: break-all; font-size: 12px; color: #333; text-align: left; margin-bottom: 20px;">
<strong>答题链接:</strong><br>
${assessmentLink}
</div>
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
<button id="downloadQRCode" style="background: linear-gradient(135deg, #4CAF50, #66BB6A); color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;">📱 下载二维码</button>
<button id="copyLink" style="background: linear-gradient(135deg, #2196F3, #42A5F5); color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;">🔗 复制链接</button>
</div>
<div style="color: #999; font-size: 12px; margin-top: 15px;">
请学生扫描二维码或点击链接开始答题
</div>
</div>
</div>
</div>
</div>
`;
// 移除之前的对话框(如果存在)
const existingDialog = document.getElementById('qrCodeDialogOverlay');
if (existingDialog) {
existingDialog.remove();
}
// 添加新对话框到页面
document.body.insertAdjacentHTML('beforeend', qrDialogHTML);
// 显示对话框
setTimeout(() => {
const overlay = document.getElementById('qrCodeDialogOverlay');
overlay.style.opacity = '1';
overlay.querySelector('div').style.transform = 'scale(1)';
}, 10);
// 生成二维码
const qrCodeContainer = document.getElementById('qrcode');
qrCodeContainer.innerHTML = ''; // 清空容器
QRCode.toCanvas(assessmentLink, {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
}, function(error, canvas) {
if (error) {
console.error('生成二维码失败:', error);
qrCodeContainer.innerHTML = '<div style="color: red;">二维码生成失败</div>';
} else {
qrCodeContainer.appendChild(canvas);
}
});
// 绑定事件
document.getElementById('closeQrDialog').onclick = () => {
const overlay = document.getElementById('qrCodeDialogOverlay');
overlay.style.opacity = '0';
overlay.querySelector('div').style.transform = 'scale(0.8)';
setTimeout(() => {
overlay.remove();
}, 300);
};
document.getElementById('downloadQRCode').onclick = () => {
const canvas = document.querySelector('#qrcode canvas');
if (canvas) {
const link = document.createElement('a');
link.download = `测评二维码_${studentName}_${new Date().toISOString().slice(0, 10)}.png`;
link.href = canvas.toDataURL();
link.click();
} else {
this.showNotification('二维码还未生成完成,请稍后再试', 'error');
}
};
document.getElementById('copyLink').onclick = async () => {
try {
// 先尝试使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(assessmentLink);
this.showNotification('链接已复制到剪贴板!');
} else {
// 降级到传统方法
const textArea = document.createElement('textarea');
textArea.value = assessmentLink;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
this.showNotification('链接已复制到剪贴板!');
} else {
throw new Error('复制命令执行失败');
}
} catch (execError) {
document.body.removeChild(textArea);
throw execError;
}
}
} catch (err) {
console.error('复制失败:', err);
// 提供手动复制方案
const tempInput = document.createElement('input');
tempInput.value = assessmentLink;
tempInput.style.position = 'fixed';
tempInput.style.left = '0';
tempInput.style.top = '0';
tempInput.style.width = '200px';
tempInput.style.fontSize = '12px';
tempInput.style.padding = '5px';
tempInput.style.border = '1px solid #ccc';
tempInput.style.borderRadius = '3px';
tempInput.style.zIndex = '10001';
tempInput.readOnly = true;
document.body.appendChild(tempInput);
tempInput.focus();
tempInput.select();
// 添加提示
this.showNotification('请手动复制选中的链接Ctrl+C 或 Command+C', 'error');
// 3秒后移除输入框
setTimeout(() => {
if (document.body.contains(tempInput)) {
document.body.removeChild(tempInput);
}
}, 3000);
}
};
// 点击遮罩层关闭
document.getElementById('qrCodeDialogOverlay').onclick = (e) => {
if (e.target.id === 'qrCodeDialogOverlay') {
document.getElementById('closeQrDialog').click();
}
};
}
async init() {
try {
await this.loadQuestions();
await this.loadFilters();
this.initializeFilterSelectors();
this.bindEvents();
// 根据管理员模式显示不同的按钮
this.toggleButtonsVisibility();
// 检查URL参数如果有assessment参数则自动配置
this.checkForAssessmentParams();
this.updateConfigDisplay();
} catch (error) {
this.showError('系统初始化失败: ' + error.message);
}
}
checkForAssessmentParams() {
const urlParams = new URLSearchParams(window.location.search);
const assessmentParam = urlParams.get('assessment');
// 注意现在不再使用assessment参数因为测评链接直接使用会话ID
// 保留此函数是为了向后兼容但不再处理assessment参数
}
async loadQuestions() {
try {
const response = await fetch('/api/questions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.questions = await response.json();
console.log('题库加载成功:', {
基础题: this.questions['基础题']?.length || 0,
进阶题: this.questions['进阶题']?.length || 0,
竞赛题: this.questions['竞赛题']?.length || 0
});
} catch (error) {
throw new Error('题库加载失败: ' + error.message);
}
}
async loadFilters() {
try {
const response = await fetch('/api/filters');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
this.availableFilters = {
subjects: result.data.subjects || [],
grades: result.data.grades || [],
units: result.data.units || []
};
console.log('筛选条件加载成功:', this.availableFilters);
} else {
throw new Error('获取筛选条件失败');
}
} catch (error) {
console.warn('筛选条件加载失败,将使用备用方案:', error.message);
this.generateFiltersFromQuestions();
}
}
generateFiltersFromQuestions() {
const subjects = new Set();
const grades = new Set();
const units = new Set();
Object.values(this.questions).flat().forEach(question => {
// 收集学科
const subject = question['学科'] || '';
if (subject) subjects.add(subject);
// 收集年级
const grade = question['年级'] || '';
if (grade) grades.add(grade);
// 收集单元
const unit = question['单元'] || '';
if (unit) units.add(unit);
});
this.availableFilters = {
subjects: Array.from(subjects).sort(),
grades: Array.from(grades).sort(),
units: Array.from(units).sort()
};
console.log('从题目生成筛选条件:', this.availableFilters);
}
initializeFilterSelectors() {
// 初始化学科和年级选择器
this.populateSubjectSelector();
this.populateGradeSelector();
// 初始化单元选择器
this.updateUnitSelector();
}
populateSubjectSelector() {
const subjectSelect = document.getElementById('subjectFilterSelect');
// 清空现有选项
subjectSelect.innerHTML = '<option value="">选择学科...</option>';
// 添加可用学科
this.availableFilters.subjects.forEach(subject => {
const option = document.createElement('option');
option.value = subject;
option.textContent = subject;
subjectSelect.appendChild(option);
});
}
populateGradeSelector() {
const gradeSelect = document.getElementById('gradeFilterSelect');
// 清空现有选项
gradeSelect.innerHTML = '<option value="">选择年级册次...</option>';
// 按中文数字顺序排序年级
const sortedGrades = this.sortGradesByChineseNumbers(this.availableFilters.grades);
// 添加可用年级(已经是完整的年级册次信息)
sortedGrades.forEach(grade => {
const option = document.createElement('option');
option.value = grade;
option.textContent = grade;
gradeSelect.appendChild(option);
});
}
sortGradesByChineseNumbers(grades) {
// 中文数字映射表
const chineseNumberMap = {
'一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8,
'九': 9, '十': 10, '十一': 11, '十二': 12
};
return grades.sort((a, b) => {
// 提取年级数字(如从"一年级上册"中提取"一"
const aMatch = a.match(/^([一二三四五六七八九十]+)/);
const bMatch = b.match(/^([一二三四五六七八九十]+)/);
if (aMatch && bMatch) {
const aNumber = chineseNumberMap[aMatch[1]] || 999;
const bNumber = chineseNumberMap[bMatch[1]] || 999;
return aNumber - bNumber;
}
// 如果无法提取数字,按字母顺序排序
return a.localeCompare(b);
});
}
updateUnitSelector() {
const unitSelect = document.getElementById('unitFilterSelect');
const gradeSelect = document.getElementById('gradeFilterSelect');
const selectedSubject = document.getElementById('subjectFilterSelect').value;
// 启用/禁用单元选择
unitSelect.disabled = !selectedSubject || !gradeSelect.value;
if (selectedSubject && gradeSelect.value) {
// 根据年级册次筛选单元
const filteredUnits = this.availableFilters.units.filter(unit => {
// 通过在题目数据中查找来筛选单元
return Object.values(this.questions).flat().some(question =>
question['学科'] === selectedSubject &&
question['年级'] === gradeSelect.value &&
question['单元'] === unit
);
});
// 按单元数字排序
filteredUnits.sort((a, b) => {
const aNum = parseInt(a.split('-')[0]) || 0;
const bNum = parseInt(b.split('-')[0]) || 0;
return aNum - bNum;
});
// 添加期中、期末选项
const hasMidTerm = filteredUnits.length > 1;
const hasFinalTerm = filteredUnits.length > 0;
unitSelect.innerHTML = '<option value="">选择单元...</option>';
// 添加期中选项如果有2个或更多单元
if (hasMidTerm) {
const midOption = document.createElement('option');
midOption.value = '期中';
midOption.textContent = '期中考试';
unitSelect.appendChild(midOption);
}
// 添加期末选项(如果有单元)
if (hasFinalTerm) {
const finalOption = document.createElement('option');
finalOption.value = '期末';
finalOption.textContent = '期末考试';
unitSelect.appendChild(finalOption);
}
// 添加分隔线和具体单元选项
if (hasMidTerm || hasFinalTerm) {
const separator = document.createElement('option');
separator.disabled = true;
separator.textContent = '─────────────';
unitSelect.appendChild(separator);
}
// 添加具体的单元选项
filteredUnits.forEach(unit => {
const option = document.createElement('option');
option.value = unit;
option.textContent = unit;
unitSelect.appendChild(option);
});
} else {
unitSelect.innerHTML = '<option value="">先选择年级册次...</option>';
}
}
onSubjectFilterChange() {
this.currentFilters.subject = document.getElementById('subjectFilterSelect').value;
this.updateUnitSelector();
this.updateSelectedFilters();
}
onGradeFilterChange() {
this.currentFilters.grade = document.getElementById('gradeFilterSelect').value;
this.updateUnitSelector();
this.updateSelectedFilters();
}
onUnitFilterChange() {
this.currentFilters.unit = document.getElementById('unitFilterSelect').value;
this.updateSelectedFilters();
}
onScoreRangeChange() {
const scoreRangeSelect = document.getElementById('scoreRangeSelect');
this.selectedScoreRange = scoreRangeSelect.value;
if (this.selectedScoreRange) {
// 根据选择的成绩区间设置题目配置
this.currentRules = { ...this.scoreRangeRules[this.selectedScoreRange] };
// 显示自动配置结果
this.displayAutoRules();
// 更新配置显示
this.updateConfigDisplay();
} else {
// 清空配置
this.currentRules = { "基础题": 0, "进阶题": 0, "竞赛题": 0 };
document.getElementById('autoRulesDisplay').style.display = 'none';
}
}
displayAutoRules() {
// 不显示自动配置结果区域
document.getElementById('autoRulesDisplay').style.display = 'none';
// 计算题目总数和总分数
const basicScore = this.currentRules['基础题'] * 5;
const advancedScore = this.currentRules['进阶题'] * 10;
const competitionScore = this.currentRules['竞赛题'] * 15;
// 更新总计
const totalQuestions = this.currentRules['基础题'] + this.currentRules['进阶题'] + this.currentRules['竞赛题'];
const totalScore = basicScore + advancedScore + competitionScore;
document.getElementById('summaryTotalQuestions').textContent = totalQuestions;
document.getElementById('summaryTotalScore').textContent = totalScore;
}
bindEvents() {
document.getElementById('startSurvey').addEventListener('click', () => this.startSurvey());
document.getElementById('submitBtn').addEventListener('click', () => this.submitSurvey());
document.getElementById('generateLinkBtn').addEventListener('click', () => this.generateAssessmentLink());
// 新的筛选条件级联选择事件
document.getElementById('subjectFilterSelect').addEventListener('change', () => this.onSubjectFilterChange());
document.getElementById('gradeFilterSelect').addEventListener('change', () => this.onGradeFilterChange());
document.getElementById('unitFilterSelect').addEventListener('change', () => this.onUnitFilterChange());
// 成绩区间选择事件
document.getElementById('scoreRangeSelect').addEventListener('change', () => this.onScoreRangeChange());
// 测评报告相关事件
document.getElementById('viewReportBtn').addEventListener('click', () => this.showReportModal());
document.getElementById('closeModal').addEventListener('click', () => this.hideReportModal());
document.getElementById('copyReportBtn').addEventListener('click', () => this.copyReport());
document.getElementById('downloadReportBtn').addEventListener('click', () => this.downloadReport());
// 点击模态框外部关闭
document.getElementById('reportModal').addEventListener('click', (e) => {
if (e.target.id === 'reportModal') {
this.hideReportModal();
}
});
}
updateSelectedFilters() {
// 更新当前筛选条件
this.currentFilters.subject = document.getElementById('subjectFilterSelect').value || '';
this.currentFilters.grade = document.getElementById('gradeFilterSelect').value || '';
this.currentFilters.unit = document.getElementById('unitFilterSelect').value || '';
// 构建用于显示的描述性标签
const filters = [];
if (this.currentFilters.subject) filters.push(`学科:${this.currentFilters.subject}`);
if (this.currentFilters.grade) filters.push(`年级:${this.currentFilters.grade}`);
if (this.currentFilters.unit) filters.push(`单元:${this.currentFilters.unit}`);
this.selectedTag = filters.join(' | ') || '全部题目';
this.updateSelectedFiltersDisplay();
this.updateConfigDisplay();
}
updateSelectedFiltersDisplay() {
const container = document.getElementById('selectedTagsList');
const subjectSelect = document.getElementById('subjectFilterSelect');
const gradeSelect = document.getElementById('gradeFilterSelect');
const unitSelect = document.getElementById('unitFilterSelect');
const subject = subjectSelect.value;
const grade = gradeSelect.value;
const unit = unitSelect.value;
if (subject || grade || unit) {
// 构建筛选条件描述
const filters = [];
if (subject) filters.push(`学科:${subject}`);
if (grade) filters.push(`年级:${grade}`);
if (unit) filters.push(`单元:${unit}`);
container.innerHTML = `筛选条件: ${filters.join(' | ')}`;
} else {
container.innerHTML = '未选择筛选条件,将使用全部题目';
}
}
filterQuestionsByTags() {
// 如果没有选择任何筛选条件,返回全部题目
if (!this.currentFilters.subject && !this.currentFilters.grade && !this.currentFilters.unit) {
return this.questions;
}
const filtered = {
"基础题": [],
"进阶题": [],
"竞赛题": []
};
Object.keys(this.questions).forEach(type => {
filtered[type] = this.questions[type].filter(question => {
// 学科筛选
if (this.currentFilters.subject && question['学科'] !== this.currentFilters.subject) {
return false;
}
// 年级册次筛选
if (this.currentFilters.grade && question['年级'] !== this.currentFilters.grade) {
return false;
}
// 单元筛选(包括期中、期末的特殊处理)
if (this.currentFilters.unit) {
if (this.currentFilters.unit === '期中') {
// 期中:选择前一半的单元
return this.isInMidTermUnits(question);
} else if (this.currentFilters.unit === '期末') {
// 期末:选择所有单元
return true; // 因为已经通过了年级册次筛选,所以所有题目都符合期末要求
} else if (question['单元'] && question['单元'] !== this.currentFilters.unit) {
// 具体单元筛选
return false;
}
}
return true;
});
});
return filtered;
}
isInMidTermUnits(question) {
// 判断题目是否属于期中考试范围(前一半单元)
if (!question['单元']) return false;
// 获取当前年级册次的所有单元
const gradeUnits = this.availableFilters.units.filter(unit => {
return Object.values(this.questions).flat().some(q =>
q['学科'] === this.currentFilters.subject &&
q['年级'] === this.currentFilters.grade &&
q['单元'] === unit
);
});
// 按单元数字排序
gradeUnits.sort((a, b) => {
const aNum = parseInt(a.split('-')[0]) || 0;
const bNum = parseInt(b.split('-')[0]) || 0;
return aNum - bNum;
});
// 确定期中包含的单元(前一半)
const midTermCount = Math.ceil(gradeUnits.length / 2);
const midTermUnits = gradeUnits.slice(0, midTermCount);
// 检查当前题目的单元是否在期中范围内
return midTermUnits.includes(question['单元']);
}
selectRandomQuestions(questionsByType, rules) {
const selected = [];
Object.keys(rules).forEach(type => {
const count = rules[type];
const availableQuestions = questionsByType[type] || [];
if (availableQuestions.length < count) {
console.warn(`${type}可用题目不足 (${availableQuestions.length}/${count})`);
}
// 随机选择题目
const shuffled = [...availableQuestions].sort(() => 0.5 - Math.random());
const selectedForType = shuffled.slice(0, Math.min(count, availableQuestions.length));
selectedForType.forEach(question => {
selected.push({
...question,
questionType: type,
questionId: `${type}_${question['序号']}`
});
});
});
// 随机打乱题目顺序
return selected.sort(() => 0.5 - Math.random());
}
updateConfigDisplay() {
this.filteredQuestions = this.filterQuestionsByTags();
// 更新自动规则状态显示
this.updateAutoRuleStatus();
// 检查是否有足够的题目开始答题
const basicAvailable = this.filteredQuestions['基础题']?.length || 0;
const advancedAvailable = this.filteredQuestions['进阶题']?.length || 0;
const competitionAvailable = this.filteredQuestions['竞赛题']?.length || 0;
const basicQuestions = this.currentRules['基础题'] || 0;
const advancedQuestions = this.currentRules['进阶题'] || 0;
const competitionQuestions = this.currentRules['竞赛题'] || 0;
const canStart = basicQuestions <= basicAvailable &&
advancedQuestions <= advancedAvailable &&
competitionQuestions <= competitionAvailable &&
(basicQuestions + advancedQuestions + competitionQuestions) > 0 &&
this.selectedScoreRange !== '';
const startBtn = document.getElementById('startSurvey');
const generateLinkBtn = document.getElementById('generateLinkBtn');
startBtn.disabled = !canStart;
generateLinkBtn.disabled = !canStart;
if (!canStart) {
if (!this.selectedScoreRange) {
startBtn.querySelector('.button-text').textContent = '请选择日常成绩区间';
generateLinkBtn.querySelector('.button-text').textContent = '请先配置测评';
} else if ((basicQuestions + advancedQuestions + competitionQuestions) === 0) {
startBtn.querySelector('.button-text').textContent = '请设置题目数量';
generateLinkBtn.querySelector('.button-text').textContent = '请先配置测评';
} else {
startBtn.querySelector('.button-text').textContent = '题目不足,无法开始';
generateLinkBtn.querySelector('.button-text').textContent = '题目不足,无法生成二维码';
}
startBtn.disabled = true;
generateLinkBtn.disabled = true;
} else {
startBtn.querySelector('.button-text').textContent = '开始答题';
generateLinkBtn.querySelector('.button-text').textContent = '📱 生成测评二维码';
startBtn.disabled = false;
generateLinkBtn.disabled = false;
}
}
updateAutoRuleStatus() {
if (!this.selectedScoreRange) return;
const basicAvailable = this.filteredQuestions['基础题']?.length || 0;
const advancedAvailable = this.filteredQuestions['进阶题']?.length || 0;
const competitionAvailable = this.filteredQuestions['竞赛题']?.length || 0;
const basicNeeded = this.currentRules['基础题'] || 0;
const advancedNeeded = this.currentRules['进阶题'] || 0;
const competitionNeeded = this.currentRules['竞赛题'] || 0;
// 更新基础题状态
const basicStatusElement = document.querySelector('#autoBasicStatus .status-text');
if (basicNeeded > basicAvailable) {
basicStatusElement.className = 'status-text error';
basicStatusElement.textContent = `题目不足:仅${basicAvailable}题可用`;
} else if (basicNeeded === 0) {
basicStatusElement.className = 'status-text';
basicStatusElement.textContent = `${basicAvailable}题可用`;
} else {
basicStatusElement.className = 'status-text success';
basicStatusElement.textContent = `✓ 已配置${basicNeeded}题,共${basicAvailable}题可用`;
}
// 更新进阶题状态
const advancedStatusElement = document.querySelector('#autoAdvancedStatus .status-text');
if (advancedNeeded > advancedAvailable) {
advancedStatusElement.className = 'status-text error';
advancedStatusElement.textContent = `题目不足:仅${advancedAvailable}题可用`;
} else if (advancedNeeded === 0) {
advancedStatusElement.className = 'status-text';
advancedStatusElement.textContent = `${advancedAvailable}题可用`;
} else {
advancedStatusElement.className = 'status-text success';
advancedStatusElement.textContent = `✓ 已配置${advancedNeeded}题,共${advancedAvailable}题可用`;
}
// 更新竞赛题状态
const competitionStatusElement = document.querySelector('#autoCompetitionStatus .status-text');
if (competitionNeeded > competitionAvailable) {
competitionStatusElement.className = 'status-text error';
competitionStatusElement.textContent = `题目不足:仅${competitionAvailable}题可用`;
} else if (competitionNeeded === 0) {
competitionStatusElement.className = 'status-text';
competitionStatusElement.textContent = `${competitionAvailable}题可用`;
} else {
competitionStatusElement.className = 'status-text success';
competitionStatusElement.textContent = `✓ 已配置${competitionNeeded}题,共${competitionAvailable}题可用`;
}
}
updateInputAvailability(input, requested, available) {
const ruleCard = input.closest('.rule-card');
const statusElement = ruleCard.querySelector('.status-text');
if (requested > available) {
input.style.borderColor = '#e53e3e';
statusElement.className = 'status-text error';
statusElement.textContent = `题目不足:仅${available}题可用`;
} else if (requested === 0) {
input.style.borderColor = '#e2e8f0';
statusElement.className = 'status-text';
statusElement.textContent = `${available}题可用`;
} else {
input.style.borderColor = '#48bb78';
statusElement.className = 'status-text success';
statusElement.textContent = `✓ 已选择${requested}题,共${available}题可用`;
}
}
async generateAssessmentLink() {
try {
// 验证学员信息(和开始答题一样的验证逻辑)
const studentName = document.getElementById('studentName').value.trim();
const studentSchool = document.getElementById('studentSchool').value.trim();
const studentGrade = document.getElementById('studentGrade').value.trim();
const studentPhone = document.getElementById('studentPhone').value.trim();
const scoreRange = document.getElementById('scoreRangeSelect').value;
if (!studentName || !studentSchool || !studentGrade || !studentPhone) {
this.showErrorDialog('学员信息不完整',
`请填写完整的学员信息后才能生成测评二维码:<br><br>
${!studentName ? '✗ 姓名<br>' : '✓ 姓名<br>'}
${!studentSchool ? '✗ 学校<br>' : '✓ 学校<br>'}
${!studentGrade ? '✗ 年级<br>' : '✓ 年级<br>'}
${!studentPhone ? '✗ 手机号<br>' : '✓ 手机号'}`
);
return;
}
if (!scoreRange) {
this.showErrorDialog('配置不完整', '请选择您的日常成绩区间,这将用于自动配置合适的题目难度组合。');
return;
}
// 检查题目是否充足
const canStart = Object.keys(this.currentRules).every(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return available >= needed;
});
if (!canStart) {
// 获取具体的题目不足信息
const shortageInfo = Object.keys(this.currentRules).map(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return `${type}: 需要${needed}题,可用${available}${needed > available ? ' (不足)' : ' (充足)'}`;
}).join('<br>');
this.showErrorDialog('题目不足',
`当前筛选条件下的题目数量不足以满足测评要求:<br><br>
${shortageInfo}<br><br>
建议调整筛选条件或降低题目要求。`
);
return;
}
// 禁用按钮并显示加载状态
const generateBtn = document.getElementById('generateLinkBtn');
generateBtn.querySelector('.button-text').textContent = '正在生成二维码...';
generateBtn.disabled = true;
// iOS兼容性使用setTimeout确保请求是用户直接触发的
const response = await new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const fetchResponse = await fetch('/api/create-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: studentName,
school: studentSchool,
grade: studentGrade,
phone: studentPhone,
selectedSubject: document.getElementById('subjectFilterSelect').value || '',
selectedSemester: document.getElementById('gradeFilterSelect').value || '',
selectedExamType: '',
selectedUnit: document.getElementById('unitFilterSelect').value || '',
selectedCategory: '',
selectedQuestionType: '',
selectedTag: this.selectedTag,
selectedTagsList: [],
questionsConfig: this.currentRules,
scoreRange: scoreRange
})
});
resolve(fetchResponse);
} catch (error) {
reject(error);
}
}, 0);
});
if (!response.ok) {
throw new Error(`创建会话失败: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// 生成答题页面链接
const assessmentLink = `${window.location.origin}/quiz/${result.sessionId}`;
// 创建二维码对话框
this.showQRCodeDialog({
title: '测评二维码生成成功!',
assessmentLink: assessmentLink,
studentName: studentName,
studentSchool: studentSchool,
studentGrade: studentGrade
});
// 可选:在控制台输出链接,方便调试
console.log('生成的答题链接:', assessmentLink);
} else {
throw new Error('创建会话失败');
}
} catch (error) {
console.error('生成测评二维码失败:', error);
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const errorMessage = error.message.includes('not allowed') && isIOS
? `iOS设备权限限制请确保<br><br>
• 在Safari设置中允许网页访问网络<br>
• 尝试在Safari中"重新加载页面"<br>
• 检查是否启用了"限制网页跟踪"<br>
• 如仍失败请尝试使用其他浏览器如Chrome<br><br>
技术细节:${error.message}`
: `创建测评二维码时发生错误:<br><br>
错误信息:${error.message}<br><br>
请检查网络连接或联系技术支持。`;
this.showErrorDialog('生成测评二维码失败', errorMessage);
} finally {
// 恢复按钮状态
const generateBtn = document.getElementById('generateLinkBtn');
if (generateBtn) {
generateBtn.querySelector('.button-text').textContent = '📱 生成测评二维码';
generateBtn.disabled = false;
}
}
}
async startSurvey() {
try {
// 验证学员信息
const studentName = document.getElementById('studentName').value.trim();
const studentSchool = document.getElementById('studentSchool').value.trim();
const studentGrade = document.getElementById('studentGrade').value.trim();
const studentPhone = document.getElementById('studentPhone').value.trim();
const scoreRange = document.getElementById('scoreRangeSelect').value;
if (!studentName || !studentSchool || !studentGrade || !studentPhone) {
this.showErrorDialog('学员信息不完整',
`请填写完整的学员信息后才能开始答题:<br><br>
${!studentName ? '✗ 姓名<br>' : '✓ 姓名<br>'}
${!studentSchool ? '✗ 学校<br>' : '✓ 学校<br>'}
${!studentGrade ? '✗ 年级<br>' : '✓ 年级<br>'}
${!studentPhone ? '✗ 手机号<br>' : '✓ 手机号'}`
);
return;
}
if (!scoreRange) {
this.showErrorDialog('配置不完整', '请选择您的日常成绩区间,这将用于自动配置合适的题目难度组合。');
return;
}
// 检查题目是否充足
const canStart = Object.keys(this.currentRules).every(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return available >= needed;
});
if (!canStart) {
// 获取具体的题目不足信息
const shortageInfo = Object.keys(this.currentRules).map(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return `${type}: 需要${needed}题,可用${available}${needed > available ? ' (不足)' : ' (充足)'}`;
}).join('<br>');
this.showErrorDialog('题目不足',
`当前筛选条件下的题目数量不足以满足测评要求:<br><br>
${shortageInfo}<br><br>
建议调整筛选条件或降低题目要求。`
);
return;
}
// 禁用按钮并显示加载状态
const startBtn = document.getElementById('startSurvey');
const originalText = startBtn.querySelector('.button-text').textContent;
startBtn.querySelector('.button-text').textContent = '正在创建答题会话...';
startBtn.disabled = true;
// iOS兼容性使用setTimeout确保请求是用户直接触发的
const response = await new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const fetchResponse = await fetch('/api/create-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: studentName,
school: studentSchool,
grade: studentGrade,
phone: studentPhone,
selectedSubject: document.getElementById('subjectFilterSelect').value || '',
selectedSemester: document.getElementById('gradeFilterSelect').value || '',
selectedExamType: '',
selectedUnit: document.getElementById('unitFilterSelect').value || '',
selectedCategory: '',
selectedQuestionType: '',
selectedTag: this.selectedTag,
selectedTagsList: [],
questionsConfig: this.currentRules,
scoreRange: scoreRange
})
});
resolve(fetchResponse);
} catch (error) {
reject(error);
}
}, 0);
});
if (!response.ok) {
throw new Error(`创建会话失败: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// 保存会话信息
this.sessionId = result.sessionId;
this.studentId = result.studentId;
// 跳转到答题页面
window.location.href = `/quiz/${result.sessionId}`;
} else {
throw new Error('创建会话失败');
}
} catch (error) {
console.error('开始答题失败:', error);
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const errorMessage = error.message.includes('not allowed') && isIOS
? `iOS设备权限限制请确保<br><br>
• 在Safari设置中允许网页访问网络<br>
• 尝试在Safari中"重新加载页面"<br>
• 检查是否启用了"限制网页跟踪"<br>
• 如仍失败请尝试使用其他浏览器如Chrome<br><br>
技术细节:${error.message}`
: `创建答题会话时发生错误:<br><br>
错误信息:${error.message}<br><br>
请检查网络连接或联系技术支持。`;
this.showErrorDialog('开始答题失败', errorMessage);
// 恢复按钮状态
const startBtn = document.getElementById('startSurvey');
startBtn.querySelector('.button-text').textContent = '开始答题';
startBtn.disabled = false;
}
}
renderQuestions() {
const container = document.getElementById('questionsContainer');
container.innerHTML = '';
this.currentQuestions.forEach((question, index) => {
const questionCard = document.createElement('div');
questionCard.className = 'question-card';
questionCard.innerHTML = `
<div class="question-header">
<div class="question-number">${index + 1}</div>
<div class="question-type ${question.questionType}">${question.questionType}</div>
</div>
<div class="question-text">${question['题干']}</div>
<ul class="options-list">
${this.renderOptions(question, index)}
</ul>
`;
container.appendChild(questionCard);
});
// 绑定选项事件
this.bindOptionEvents();
}
renderOptions(question, questionIndex) {
const options = [];
const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
// 通过遍历所有键来找到选项
for (let i = 0; i < labels.length; i++) {
let optionText = null;
// 遍历问题的所有键,寻找匹配的选项
for (const key in question) {
// 检查键是否包含当前选项字母(忽略空格数量)
if (key.replace(/\s+/g, '').includes(`选项${labels[i]}`)) {
optionText = question[key];
break;
}
}
if (optionText && optionText.trim()) {
options.push(`
<li class="option-item" data-question="${questionIndex}" data-option="${labels[i]}">
<input type="radio" name="question_${questionIndex}" value="${labels[i]}" id="option_${questionIndex}_${i}">
<label for="option_${questionIndex}_${i}" class="option-text">${labels[i]}. ${optionText}</label>
</li>
`);
}
}
return options.join('');
}
bindOptionEvents() {
document.querySelectorAll('.option-item').forEach(item => {
item.addEventListener('click', function() {
const questionIndex = this.dataset.question;
const option = this.dataset.option;
const radio = this.querySelector('input[type="radio"]');
// 清除同一题目的其他选中状态
document.querySelectorAll(`.option-item[data-question="${questionIndex}"]`).forEach(otherItem => {
otherItem.classList.remove('selected');
});
// 设置当前选中状态
this.classList.add('selected');
radio.checked = true;
// 记录答案
window.survey.answers[questionIndex] = option;
window.survey.checkCompletion();
});
});
}
updateProgress() {
const total = this.currentQuestions.length;
const answered = Object.keys(this.answers).length;
const percentage = total > 0 ? (answered / total) * 100 : 0;
document.getElementById('progressFill').style.width = percentage + '%';
}
checkCompletion() {
this.updateProgress();
const total = this.currentQuestions.length;
const answered = Object.keys(this.answers).length;
document.getElementById('submitBtn').disabled = answered !== total;
}
submitSurvey() {
try {
const results = this.calculateResults();
this.displayResults(results);
document.getElementById('surveyContent').style.display = 'none';
document.getElementById('resultsSection').style.display = 'block';
} catch (error) {
this.showError('提交失败: ' + error.message);
}
}
calculateResults() {
let totalScore = 0;
let correctCount = 0;
const results = [];
this.currentQuestions.forEach((question, index) => {
const userAnswer = this.answers[index];
const correctAnswer = question['答案'];
const score = parseInt(question['分数']) || 0;
const isCorrect = userAnswer === correctAnswer;
if (isCorrect) {
totalScore += score;
correctCount++;
}
results.push({
questionNumber: index + 1,
questionText: question['题干'],
userAnswer: userAnswer || '未作答',
correctAnswer: correctAnswer,
isCorrect: isCorrect,
score: score,
questionType: question.questionType
});
});
return {
totalScore: totalScore,
correctCount: correctCount,
totalQuestions: this.currentQuestions.length,
percentage: Math.round((totalScore / 100) * 100),
results: results
};
}
displayResults(results) {
document.getElementById('scoreValue').textContent = results.totalScore + '分';
let detailsHTML = `
<h3>答题详情</h3>
<p>正确: ${results.correctCount}/${results.totalQuestions} 题</p>
<hr style="margin: 15px 0;">
`;
results.results.forEach(result => {
const statusClass = result.isCorrect ? '✅ 正确' : '❌ 错误';
detailsHTML += `
<div class="result-item">
<strong>第${result.questionNumber}题 (${result.questionType})</strong> - ${statusClass}<br>
<small>题目: ${result.questionText}</small><br>
<small>你的答案: ${result.userAnswer} | 正确答案: ${result.correctAnswer}</small><br>
<small>得分: ${result.isCorrect ? result.score : 0}分</small>
</div>
`;
});
document.getElementById('resultsDetails').innerHTML = detailsHTML;
this.currentResults = results;
}
showReportModal() {
const reportText = this.generateReportText();
document.getElementById('reportContent').innerHTML = this.formatReportAsTable(reportText);
document.getElementById('reportModal').style.display = 'flex';
}
hideReportModal() {
document.getElementById('reportModal').style.display = 'none';
}
generateReportText() {
if (!this.currentResults) return '';
const timestamp = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
let report = `# 考试答题情况分析\n\n`;
report += `测评时间: ${timestamp}\n`;
report += `总分: ${this.currentResults.totalScore}\n`;
report += `正确率: ${this.currentResults.correctCount}/${this.currentResults.totalQuestions} (${Math.round((this.currentResults.correctCount/this.currentResults.totalQuestions)*100)}%)\n\n`;
// 添加一些基本信息(可以后续扩展为实际的用户信息表单)
report += `| 题目 | 题型 | 用户答案 | 正确答案 | 是否正确 |\n`;
report += `|------|------|----------|----------|----------|\n`;
report += `| 姓名 | 填空题 | 学员 | 无标准答案 | 无法判断 |\n`;
report += `| 学校 | 填空题 | 学校名称 | 无标准答案 | 无法判断 |\n`;
report += `| 年级 | 填空题 | ${this.selectedTag || '未选择'} | 无标准答案 | 无法判断 |\n`;
// 添加答题情况
this.currentResults.results.forEach(result => {
const status = result.isCorrect ? '正确' : '错误';
report += `| ${result.questionText} | ${result.questionType} | ${result.userAnswer || '未作答'} | ${result.correctAnswer} | ${status} |\n`;
});
return report;
}
formatReportAsTable(reportText) {
const lines = reportText.split('\n');
let html = '';
let inTable = false;
lines.forEach(line => {
if (line.startsWith('# ')) {
html += `<h2>${line.substring(2)}</h2>\n`;
} else if (line.startsWith('## ')) {
html += `<h3>${line.substring(3)}</h3>\n`;
} else if (line.startsWith('|') && line.includes('---')) {
// 表格头
html += `<table class="report-table"><thead><tr>\n`;
const headers = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
headers.forEach(header => {
html += `<th>${header}</th>\n`;
});
html += `</tr></thead><tbody>\n`;
inTable = true;
} else if (line.startsWith('|') && inTable) {
// 表格行
html += `<tr>\n`;
const cells = line.split('|').filter(cell => cell !== '').map(cell => cell.trim());
cells.forEach(cell => {
html += `<td>${cell}</td>\n`;
});
html += `</tr>\n`;
} else if (line.trim() === '' && inTable) {
html += `</tbody></table>\n`;
inTable = false;
} else if (line.trim() !== '') {
html += `<p>${line}</p>\n`;
}
});
if (inTable) {
html += `</tbody></table>\n`;
}
return html;
}
copyReport() {
const reportText = this.generateReportText();
navigator.clipboard.writeText(reportText).then(() => {
this.showNotification('报告已复制到剪贴板!');
}).catch(err => {
console.error('复制失败:', err);
this.showNotification('复制失败,请手动复制', 'error');
});
}
downloadReport() {
const reportText = this.generateReportText();
const now = new Date();
const timestamp = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Shanghai"}))
.toISOString().slice(0, 19).replace(/[:-]/g, '');
const filename = `测评报告_${timestamp}.txt`;
const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification(`报告已下载: ${filename}`);
}
showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? 'linear-gradient(135deg, #4CAF50, #66BB6A)' : 'linear-gradient(135deg, #f44336, #e57373)'};
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-size: 14px;
font-weight: 500;
animation: slideIn 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
document.querySelector('.container').insertBefore(errorDiv, document.querySelector('.config-section'));
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
}
// 初始化系统
window.survey = new SurveySystem();
</script>
</body>
</html>