[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>多功能题库转换工具(增强版)</title>
<!-- Word 解析 -->
<script src="https://unpkg.com/mammoth@1.5.1/mammoth.browser.min.js"></script>
<!-- Excel 解析与导出 -->
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<!-- PDF.js 用于PDF解析 -->
<script src="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Microsoft YaHei",Segoe UI,Arial,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;padding:18px}
.container{max-width:1400px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 20px 40px rgba(0,0,0,.12)}
.header{background:linear-gradient(135deg,#2c3e50,#3498db);color:#fff;padding:18px;text-align:center}
.header h1{font-size:22px;margin-bottom:6px}
.header p{opacity:.95;font-size:14px}
.content{display:grid;grid-template-columns:450px 1fr;gap:20px;padding:20px}
.card{background:#f8f9fa;padding:16px;border-radius:10px}
.upload-label{display:block;user-select:none;border:3px dashed #3498db;border-radius:10px;padding:20px;text-align:center;cursor:pointer;background:#ecf0f1}
.upload-label:hover{background:#d6eaf8;border-color:#2980b9}
.upload-label i{font-size:36px;color:#3498db;margin-bottom:8px}
.file-type-tabs{display:flex;gap:6px;margin-top:10px}
.tab-btn{padding:8px 14px;border-radius:6px;background:#ecf0f1;border:none;cursor:pointer;flex:1;font-size:13px}
.tab-btn.active{background:#3498db;color:white}
.btn{display:inline-block;background:linear-gradient(135deg,#3498db,#2980b9);color:#fff;border:none;padding:8px 14px;border-radius:18px;font-size:13px;cursor:pointer;margin:6px}
.btn:disabled{background:#bdc3c7;cursor:not-allowed}
.btn-download{background:linear-gradient(135deg,#27ae60,#229954)}
.btn-json{background:linear-gradient(135deg,#f39c12,#e67e22)}
.btn-ghost{background:#fff;border:1px solid #ccc;color:#333}
.btn-pdf{background:linear-gradient(135deg,#e74c3c,#c0392b)}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
.stat-card{background:#fff;padding:10px;border-radius:8px;text-align:center;box-shadow:0 6px 18px rgba(0,0,0,.04)}
.stat-number{font-size:18px;color:#3498db;font-weight:700}
.preview{max-height:640px;overflow:auto;background:#fff;padding:12px;border-radius:8px;margin-top:10px}
.q-item{padding:10px;border-radius:8px;margin-bottom:10px;border-left:4px solid #3498db;background:#fff;box-shadow:0 2px 6px rgba(0,0,0,.04)}
.q-meta{display:flex;align-items:center;gap:8px}
.q-type{color:#fff;padding:3px 8px;border-radius:12px;font-size:12px}
.type-judgment{background:#e74c3c}
.type-single{background:#3498db}
.type-multi{background:#9b59b6}
.q-actions{margin-left:auto;display:flex;gap:8px}
.btn-edit{background:#fff;color:#2980b9;border:1px solid #2980b9;padding:6px;border-radius:6px;cursor:pointer}
.btn-mark{background:#fff;color:#27ae60;border:1px solid #27ae60;padding:6px;border-radius:6px;cursor:pointer}
.uncertain{border-left-color:#e74c3c;background:#fff6f6}
.uncertain-badge{display:inline-block;background:#e74c3c;color:#fff;padding:2px 6px;border-radius:10px;font-size:12px;margin-left:8px}
.hint{color:#f39c12;font-size:13px}
.log{margin-top:12px;padding:12px;background:#2c3e50;color:#ecf0f1;border-radius:8px;font-family:monospace;font-size:13px;max-height:240px;overflow:auto;display:none}
.log .warn{color:#f1c40f}
.log .error{color:#e74c3c}
.log .success{color:#2ecc71}
.log .info{color:#3498db}
.modal-backdrop{position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,.45);display:none;align-items:center;justify-content:center;z-index:9999}
.modal{background:#fff;border-radius:10px;padding:16px;min-width:320px;max-width:920px;max-height:90vh;overflow:auto}
.modal h3{margin-bottom:10px}
.form-row{display:flex;gap:8px;margin-bottom:8px}
.form-row .col{flex:1}
input[type="text"], textarea, select{width:100%;padding:8px;border-radius:6px;border:1px solid #ccc;font-size:13px}
textarea{min-height:80px;resize:vertical}
.batch-list{max-height:420px;overflow:auto;padding:8px;border:1px dashed #ddd;border-radius:6px;background:#fff}
.batch-item{display:flex;align-items:center;gap:8px;padding:6px;border-bottom:1px solid #f1f1f1}
.fix-preview{max-height:420px;overflow:auto;padding:8px;border:1px dashed #bcd;border-radius:6px;background:#fff}
.fix-row{padding:6px;border-bottom:1px solid #eee}
.small{font-size:12px;color:#666}
.controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:8px}
.controls .col{flex:1;min-width:160px}
.footer{padding:12px;text-align:center;background:#f3f6f9;color:#7f8c8d}
.template-selector{margin:12px 0}
.template-item{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px 0;cursor:pointer;background:#fff}
.template-item:hover{background:#f0f7ff}
.template-item.active{border-color:#3498db;background:#e3f2fd}
.cloud-section{margin-top:12px;padding:12px;background:#fff;border-radius:8px;border:1px dashed #3498db}
.cloud-actions{display:flex;gap:8px;margin-top:8px}
.progress-bar{width:100%;height:6px;background:#ecf0f1;border-radius:3px;overflow:hidden;margin-top:8px}
.progress-fill{height:100%;background:#3498db;transition:width 0.3s}
.api-section{margin-top:12px;padding:12px;background:#f8f9fa;border-radius:8px}
.api-key-input{display:flex;gap:8px;margin-top:8px}
.api-key-input input{flex:1}
.export-format-selector{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.export-format-selector label{display:flex;align-items:center;gap:4px}
@media(max-width:1100px){.content{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 智能题库转换工具(增强版)</h1>
<p>支持 .docx .xlsx .xls .pdf .txt · 智能清洗 · 模板匹配 · 云存储 · API接口</p>
</div>
<div class="content">
<!-- 左侧 -->
<div class="card">
<h2>📤 上传与处理</h2>
<label id="uploadLabel" class="upload-label" for="fileInput" tabindex="0" role="button" aria-label="点击或拖拽上传文件">
<i class="fas fa-cloud-upload-alt"></i>
<div><strong>点击或拖拽文件到此区域</strong></div>
<div id="uploadStatus" class="small">支持 .docx .xlsx .xls .pdf .txt</div>
</label>
<input type="file" id="fileInput" accept=".docx,.xlsx,.xls,.pdf,.txt" style="display:none">
<div class="file-type-tabs" id="fileTypeTabs">
<button class="tab-btn active" data-type="auto">智能检测</button>
<button class="tab-btn" data-type="word">Word模式</button>
<button class="tab-btn" data-type="excel">Excel模式</button>
<button class="tab-btn" data-type="pdf">PDF模式</button>
</div>
<!-- 模板选择器 -->
<div class="template-selector" id="templateSelector" style="display:none">
<label class="small">选择题库模板:</label>
<div id="templateList">
<div class="template-item active" data-template="standard">标准模板 (题型-题目-选项-答案)</div>
<div class="template-item" data-template="simple">简易模板 (题目[选项]答案)</div>
<div class="template-item" data-template="exam">考试模板 (题号.题目 选项 答案)</div>
<div class="template-item" data-template="custom">自定义模板...</div>
</div>
</div>
<!-- PDF 页面范围选择 -->
<div id="pdfPageContainer" style="margin-top:8px;display:none">
<label class="small">PDF页面范围:</label>
<div style="display:flex;gap:8px;margin-top:6px">
<input type="number" id="pdfStartPage" placeholder="起始页" min="1" style="flex:1;padding:8px" value="1">
<input type="number" id="pdfEndPage" placeholder="结束页" style="flex:1;padding:8px">
</div>
</div>
<div style="margin-top:12px;text-align:center">
<button class="btn" id="processBtn" disabled>🔄 智能转换</button>
<button class="btn btn-download" id="downloadBtn" disabled>💾 下载 JS</button>
<button class="btn btn-json" id="downloadJsonBtn" disabled>📄 导出 JSON</button>
<button class="btn btn-pdf" id="downloadPdfBtn" disabled>📘 生成 PDF</button>
</div>
<!-- 进度条 -->
<div class="progress-bar" id="progressBar" style="display:none">
<div class="progress-fill" id="progressFill" style="width:0%"></div>
</div>
<!-- Excel 工作表选择 -->
<div id="excelSheetContainer" style="margin-top:8px;display:none">
<label class="small">选择工作表:</label>
<select id="sheetSelect" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc;margin-top:6px"></select>
</div>
<!-- Word 解析模式 -->
<div id="wordParseContainer" style="margin-top:8px;display:none">
<label class="small">解析模式:</label>
<select id="parseMode" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc;margin-top:6px">
<option value="auto">智能识别(表格/段落)</option>
<option value="table">表格优先</option>
<option value="paragraph">段落识别</option>
</select>
</div>
<div class="stats" id="stats" style="margin-top:12px">
<div class="stat-card"><div class="stat-number" id="stat-jud">0</div><div class="small">判断题</div></div>
<div class="stat-card"><div class="stat-number" id="stat-single">0</div><div class="small">单选题</div></div>
<div class="stat-card"><div class="stat-number" id="stat-multi">0</div><div class="small">多选题</div></div>
</div>
<!-- API接口设置 -->
<div class="api-section" id="apiSection">
<label class="small"><i class="fas fa-plug"></i> API接口设置</label>
<div class="api-key-input">
<input type="password" id="apiKeyInput" placeholder="输入API密钥(可选)">
<button class="btn-ghost">保存</button>
</div>
<div style="margin-top:8px">
<button class="btn-ghost" id="exportApiBtn" disabled>🌐 发布到API</button>
<button class="btn-ghost" id="importApiBtn">📥 从API导入</button>
</div>
</div>
<!-- 云存储 -->
<div class="cloud-section" id="cloudSection">
<label class="small"><i class="fas fa-cloud"></i> 云存储</label>
<div class="cloud-actions">
<button class="btn-ghost" id="saveCloudBtn" disabled>💾 保存到云端</button>
<button class="btn-ghost" id="loadCloudBtn">📂 从云端加载</button>
<button class="btn-ghost" id="syncCloudBtn" disabled>🔄 同步云端</button>
</div>
</div>
<div style="margin-top:12px">
<button class="btn-ghost" id="batchBtn" disabled>🧰 批量修正</button>
<button class="btn-ghost" id="exportLogBtn" disabled>📥 导出日志</button>
<button class="btn-ghost" id="collabBtn">👥 协作模式</button>
</div>
<div class="log" id="logContainer"><strong>处理日志:</strong><div id="logContent"></div></div>
</div>
<!-- 右侧 -->
<div class="card">
<h2>👁️ 预览与手动修正</h2>
<div class="controls" style="margin-bottom:10px">
<div class="col"><input type="text" id="searchInput" placeholder="搜索题目 / 选项 / 答案 / ID" /></div>
<div style="min-width:120px">
<select id="filterType" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc">
<option value="all">所有题型</option><option value="判断">判断</option><option value="单选">单选</option><option value="多选">多选</option>
</select>
</div>
<div style="min-width:140px">
<select id="filterConf" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc">
<option value="all">所有置信度</option><option value="high">high</option><option value="medium">medium</option><option value="low">low</option>
</select>
</div>
<div style="min-width:120px">
<select id="pageSize" style="width:100%;padding:8px;border-radius:6px;border:1px solid #ccc">
<option value="20">每页 20</option><option value="50">50</option><option value="100">100</option><option value="500">500</option>
</select>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div>
<button class="btn-ghost" id="exportFilteredJsonBtn" disabled>导出筛选 JSON</button>
<button class="btn-ghost" id="exportFilteredXlsxBtn" disabled>导出筛选 XLSX</button>
<button class="btn-ghost" id="exportFilteredCsvBtn" disabled>导出筛选 CSV</button>
</div>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn-ghost" id="prevPageBtn">上一页</button>
<span id="pageInfo" class="small">第 1 / 1 页</span>
<button class="btn-ghost" id="nextPageBtn">下一页</button>
</div>
</div>
<div class="preview" id="previewContent">
<p class="small" style="padding:14px;text-align:center;color:#888">转换结果将显示在这里...</p>
</div>
</div>
</div>
<div class="footer">
<p>© 2025 智能题库转换工具(增强版)</p>
<p class="small" style="margin-top:4px">版本 2.0 | 支持离线使用 | 数据安全加密</p>
</div>
</div>
<!-- 编辑模态 -->
<div class="modal-backdrop" id="editModalBackdrop">
<div class="modal" role="dialog" aria-modal="true">
<h3>编辑题目 <span id="editTitleId"></span></h3>
<div class="form-row">
<div class="col"><label>题型</label><select id="editType"><option>判断</option><option>单选</option><option>多选</option></select></div>
<div class="col"><label>题目</label><input type="text" id="editQuestion"></div>
</div>
<div class="form-row">
<div class="col"><label>选项(用 | 分隔)</label><textarea id="editOptions" placeholder="例如:北京 | 上海 | 广州 | 深圳"></textarea></div>
</div>
<div class="form-row">
<div class="col"><label>答案(字母或文本)</label><input type="text" id="editAnswer"></div>
<div class="col"><label>提示</label><input type="text" id="editHint"></div>
</div>
<div class="form-row">
<div class="col"><label>置信度</label><select id="editConfidence"><option>high</option><option>medium</option><option>low</option></select></div>
<div class="col"><label>原因(可选)</label><input type="text" id="editReason"></div>
</div>
<div style="text-align:right;margin-top:8px">
<button class="btn-ghost">取消</button>
<button class="btn">保存</button>
</div>
</div>
</div>
<!-- 批量修正模态 -->
<div class="modal-backdrop" id="batchModalBackdrop">
<div class="modal">
<h3>批量修正工具(含自动修复预览)</h3>
<div style="margin:8px 0">
<button class="btn">全选</button>
<button class="btn">全不选</button>
<button class="btn btn-ghost">标为已确认(高置信度)</button>
<button class="btn btn-ghost">生成自动修复建议</button>
<button class="btn btn-ghost">应用所选修复</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:8px">
<div style="flex:1">
<div class="small" style="margin-bottom:6px">待处理题目(可单条编辑)</div>
<div class="batch-list" id="batchList"></div>
</div>
<div style="width:48%;min-width:300px">
<div class="small" style="margin-bottom:6px">自动修复预览(生成后显示)</div>
<div class="fix-preview" id="fixPreview">
<div class="small" style="color:#999">请先点击 "生成自动修复建议" 查看修复预览</div>
</div>
</div>
</div>
<div style="text-align:right;margin-top:8px">
<button class="btn-ghost">关闭</button>
</div>
</div>
</div>
<!-- 协作模式模态 -->
<div class="modal-backdrop" id="collabModalBackdrop">
<div class="modal">
<h3>👥 协作模式</h3>
<div style="margin-bottom:12px">
<p class="small">生成协作链接,邀请他人共同编辑题库:</p>
<div style="display:flex;gap:8px">
<input type="text" id="collabLink" readonly style="flex:1;padding:8px;background:#f8f9fa">
<button class="btn">复制链接</button>
</div>
<div style="margin-top:8px">
<label><input type="checkbox" id="allowEdit"> 允许他人编辑</label>
<label style="margin-left:12px"><input type="checkbox" id="allowDownload"> 允许他人下载</label>
</div>
</div>
<div style="margin-top:12px">
<p class="small">当前协作者:</p>
<div id="collaboratorList" style="max-height:200px;overflow:auto;padding:8px;border:1px solid #ddd;border-radius:6px">
<div class="small" style="color:#999">无协作者</div>
</div>
</div>
<div style="text-align:right;margin-top:12px">
<button class="btn-ghost">关闭</button>
<button class="btn">生成新链接</button>
</div>
</div>
</div>
<!-- 自定义模板模态 -->
<div class="modal-backdrop" id="templateModalBackdrop">
<div class="modal">
<h3>自定义模板设置</h3>
<div style="margin-bottom:12px">
<label>模板名称:</label>
<input type="text" id="templateName" placeholder="输入模板名称" style="width:100%;margin-top:4px">
</div>
<div style="margin-bottom:12px">
<label>正则表达式匹配规则:</label>
<textarea id="templateRegex" placeholder="例如:^(\d+)\.\s*(.*?)\s*A[\.:]\s*(.*?)\s*B[\.:]\s*(.*?)\s*C[\.:]\s*(.*?)\s*答案[::]\s*([A-D]+)" rows="4"></textarea>
<p class="small" style="color:#666;margin-top:4px">使用正则表达式匹配题目格式,使用 $1, $2 等捕获组</p>
</div>
<div style="margin-bottom:12px">
<label>字段映射:</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:4px">
<div><label class="small">题型(可选)</label><input type="text" id="mapType" placeholder="$1"></div>
<div><label class="small">题目</label><input type="text" id="mapQuestion" placeholder="$2"></div>
<div><label class="small">选项A</label><input type="text" id="mapOptionA" placeholder="$3"></div>
<div><label class="small">选项B</label><input type="text" id="mapOptionB" placeholder="$4"></div>
<div><label class="small">选项C</label><input type="text" id="mapOptionC" placeholder="$5"></div>
<div><label class="small">选项D</label><input type="text" id="mapOptionD" placeholder="$6"></div>
<div><label class="small">答案</label><input type="text" id="mapAnswer" placeholder="$7"></div>
</div>
</div>
<div style="margin-bottom:12px">
<label>测试文本:</label>
<textarea id="testText" placeholder="粘贴一段题目文本来测试模板" rows="3"></textarea>
<button class="btn-ghost" style="margin-top:4px">测试模板</button>
<div id="testResult" class="small" style="margin-top:4px;padding:4px;background:#f8f9fa;border-radius:4px;display:none"></div>
</div>
<div style="text-align:right;margin-top:12px">
<button class="btn-ghost">取消</button>
<button class="btn">保存模板</button>
</div>
</div>
</div>
</body>
</html>
<script>
/* ==================== 全局数据 ==================== */
let questionBank = [];
let currentId = 1;
let pageSize = 20;
let currentPage = 1;
// Excel 相关
let excelWorkbook = null;
let excelSheetDataMap = {};
let excelSheetName = '';
// PDF 相关
let pdfDocument = null;
// 文件类型
let currentFileType = 'auto';
let currentFileName = '';
let currentTemplate = 'standard';
// 云存储相关
let cloudStorage = {
apiKey: localStorage.getItem('question_tool_api_key') || '',
cloudData: JSON.parse(localStorage.getItem('question_cloud_data') || '{}'),
lastSync: localStorage.getItem('question_last_sync') || ''
};
// 模板库
const TEMPLATES = {
standard: {
name: '标准模板',
regex: /题型[::]\s*(.*?)\s*题目[::]\s*(.*?)\s*(?:选项A[::]\s*(.*?)\s*)?(?:选项B[::]\s*(.*?)\s*)?(?:选项C[::]\s*(.*?)\s*)?(?:选项D[::]\s*(.*?)\s*)?答案[::]\s*(.*)/i,
mapping: { type: '$1', question: '$2', options: ['$3','$4','$5','$6'], answer: '$7' }
},
simple: {
name: '简易模板',
regex: /(.*?)\s*\[(.*?)\]\s*答案[::]\s*(.*)/i,
mapping: { question: '$1', options: '$2'.split(/[;,]/), answer: '$3' }
},
exam: {
name: '考试模板',
regex: /^(\d+)\.\s*(.*?)\s*A[\.:]\s*(.*?)\s*B[\.:]\s*(.*?)\s*C[\.:]\s*(.*?)\s*D[\.:]\s*(.*?)\s*答案[::]\s*([A-D]+)/i,
mapping: { id: '$1', question: '$2', options: ['$3','$4','$5','$6'], answer: '$7' }
}
};
/* ==================== 工具函数 ==================== */
function addLog(msg, level='info'){
const lc = document.getElementById('logContainer');
const content = document.getElementById('logContent');
if(!lc || !content) return console.log(msg);
lc.style.display='block';
const d = document.createElement('div');
d.className = level;
d.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
content.appendChild(d);
content.scrollTop = content.scrollHeight;
console.log(msg);
}
function escapeHtml(str){
if(str===undefined||str===null) return '';
return String(str)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"')
.replace(/'/g,''')
.replace(/\n/g,'<br/>');
}
function updateProgress(percent, message = ''){
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
if(progressBar && progressFill){
progressBar.style.display = 'block';
progressFill.style.width = percent + '%';
if(message) addLog(message, 'info');
}
}
/* ==================== 模板匹配系统 ==================== */
function applyTemplate(text, templateName){
const template = TEMPLATES[templateName];
if(!template) return null;
const lines = text.split('\n').filter(line => line.trim());
const questions = [];
for(let line of lines){
const match = line.match(template.regex);
if(match){
const q = {};
for(const [key, value] of Object.entries(template.mapping)){
if(Array.isArray(value)){
q[key] = value.map(v => {
if(v && v.startsWith('$')){
const idx = parseInt(v.substring(1));
return match[idx] || '';
}
return v;
}).filter(opt => opt.trim());
} else if(value && value.startsWith('$')){
const idx = parseInt(value.substring(1));
q[key] = match[idx] || '';
} else {
q[key] = value;
}
}
// 自动推断题型
if(!q.type){
if(q.options && q.options.length >= 2){
if(q.answer && q.answer.length > 1){
q.type = '多选';
} else {
q.type = '单选';
}
} else {
q.type = '判断';
q.options = ['正确', '错误'];
}
}
questions.push(q);
}
}
return questions;
}
/* ==================== 自定义模板功能 ==================== */
function openTemplateModal(){
// 清空表单
document.getElementById('templateName').value = '';
document.getElementById('templateRegex').value = '^(\d+)\.\s*(.*?)\s*A[\.:]\s*(.*?)\s*B[\.:]\s*(.*?)\s*C[\.:]\s*(.*?)\s*D[\.:]\s*(.*?)\s*答案[::]\s*([A-D]+)';
document.getElementById('mapType').value = '';
document.getElementById('mapQuestion').value = '$2';
document.getElementById('mapOptionA').value = '$3';
document.getElementById('mapOptionB').value = '$4';
document.getElementById('mapOptionC').value = '$5';
document.getElementById('mapOptionD').value = '$6';
document.getElementById('mapAnswer').value = '$7';
document.getElementById('testText').value = '';
document.getElementById('testResult').style.display = 'none';
document.getElementById('templateModalBackdrop').style.display = 'flex';
}
function closeTemplateModal(){
document.getElementById('templateModalBackdrop').style.display = 'none';
}
function testTemplate(){
const regexStr = document.getElementById('templateRegex').value;
const testText = document.getElementById('testText').value;
const resultDiv = document.getElementById('testResult');
if(!regexStr || !testText){
resultDiv.innerHTML = '<span style="color:#e74c3c">请输入正则表达式和测试文本</span>';
resultDiv.style.display = 'block';
return;
}
try {
const regex = new RegExp(regexStr, 'i');
const match = testText.match(regex);
if(match){
let resultHtml = '<span style="color:#27ae60">✅ 匹配成功!</span><br>';
resultHtml += '<div style="margin-top:8px;font-size:11px">';
for(let i = 0; i < match.length; i++){
resultHtml += `<strong>$${i}</strong>: ${escapeHtml(match[i] || '')}<br>`;
}
resultHtml += '</div>';
// 预览提取的字段
const type = document.getElementById('mapType').value || '';
const question = document.getElementById('mapQuestion').value || '';
const optionA = document.getElementById('mapOptionA').value || '';
const optionB = document.getElementById('mapOptionB').value || '';
const optionC = document.getElementById('mapOptionC').value || '';
const optionD = document.getElementById('mapOptionD').value || '';
const answer = document.getElementById('mapAnswer').value || '';
resultHtml += '<div style="margin-top:8px;border-top:1px solid #ddd;padding-top:8px">';
resultHtml += '<strong>字段映射预览:</strong><br>';
const getValue = (placeholder) => {
if(!placeholder || !placeholder.startsWith('$')) return placeholder;
const idx = parseInt(placeholder.substring(1));
return match[idx] || '';
};
if(type) resultHtml += `题型: ${getValue(type)}<br>`;
if(question) resultHtml += `题目: ${getValue(question)}<br>`;
if(optionA) resultHtml += `选项A: ${getValue(optionA)}<br>`;
if(optionB) resultHtml += `选项B: ${getValue(optionB)}<br>`;
if(optionC) resultHtml += `选项C: ${getValue(optionC)}<br>`;
if(optionD) resultHtml += `选项D: ${getValue(optionD)}<br>`;
if(answer) resultHtml += `答案: ${getValue(answer)}<br>`;
resultHtml += '</div>';
resultDiv.innerHTML = resultHtml;
} else {
resultDiv.innerHTML = '<span style="color:#e74c3c">❌ 匹配失败,请检查正则表达式</span>';
}
} catch(err){
resultDiv.innerHTML = `<span style="color:#e74c3c">❌ 正则表达式错误: ${escapeHtml(err.message)}</span>`;
}
resultDiv.style.display = 'block';
}
function saveCustomTemplate(){
const name = document.getElementById('templateName').value.trim();
const regexStr = document.getElementById('templateRegex').value.trim();
if(!name){
alert('请输入模板名称');
return;
}
if(!regexStr){
alert('请输入正则表达式');
return;
}
try {
// 测试正则表达式是否有效
new RegExp(regexStr, 'i');
} catch(err){
alert(`正则表达式无效: ${err.message}`);
return;
}
// 构建映射对象
const mapping = {};
const type = document.getElementById('mapType').value.trim();
const question = document.getElementById('mapQuestion').value.trim();
const optionA = document.getElementById('mapOptionA').value.trim();
const optionB = document.getElementById('mapOptionB').value.trim();
const optionC = document.getElementById('mapOptionC').value.trim();
const optionD = document.getElementById('mapOptionD').value.trim();
const answer = document.getElementById('mapAnswer').value.trim();
if(type) mapping.type = type;
if(question) mapping.question = question;
const options = [];
if(optionA) options.push(optionA);
if(optionB) options.push(optionB);
if(optionC) options.push(optionC);
if(optionD) options.push(optionD);
if(options.length > 0) mapping.options = options;
if(answer) mapping.answer = answer;
// 保存模板
TEMPLATES[name] = {
name: name,
regex: new RegExp(regexStr, 'i'),
mapping: mapping
};
// 更新模板选择器
updateTemplateList();
// 选中新创建的模板
currentTemplate = name;
document.querySelectorAll('.template-item').forEach(item => {
item.classList.remove('active');
if(item.dataset.template === name){
item.classList.add('active');
}
});
addLog(`自定义模板 "${name}" 已保存`, 'success');
closeTemplateModal();
}
function updateTemplateList(){
const templateList = document.getElementById('templateList');
const customTemplates = Object.keys(TEMPLATES).filter(key =>
!['standard', 'simple', 'exam'].includes(key)
);
// 移除现有的自定义模板项(除了标准的那三个)
document.querySelectorAll('.template-item[data-template^="custom_"]').forEach(item => {
item.remove();
});
// 添加新的自定义模板
customTemplates.forEach(key => {
const template = TEMPLATES[key];
const item = document.createElement('div');
item.className = 'template-item';
item.dataset.template = key;
item.textContent = `${template.name} (自定义)`;
item.addEventListener('click', () => {
document.querySelectorAll('.template-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
currentTemplate = key;
});
templateList.appendChild(item);
});
}
/* ==================== PDF解析功能 ==================== */
async function extractTextFromPDF(file, startPage = 1, endPage = null){
updateProgress(0, '正在加载PDF文档...');
try {
const arrayBuffer = await file.arrayBuffer();
pdfDocument = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const numPages = pdfDocument.numPages;
if(!endPage || endPage > numPages) endPage = numPages;
let fullText = '';
for(let i = startPage; i <= endPage; i++){
updateProgress(((i - startPage) / (endPage - startPage + 1)) * 100, `正在解析第 ${i}/${endPage} 页...`);
const page = await pdfDocument.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items.map(item => item.str).join(' ');
fullText += pageText + '\n';
}
updateProgress(100, 'PDF解析完成');
return fullText;
} catch(err){
addLog(`PDF解析失败: ${err.message}`, 'error');
throw err;
}
}
/* ==================== TXT文件解析 ==================== */
function parseTextFile(text, templateName = 'auto'){
if(templateName === 'auto'){
// 尝试自动检测模板
for(const [name, template] of Object.entries(TEMPLATES)){
const sampleLines = text.split('\n').slice(0, 5).join('\n');
if(template.regex.test(sampleLines)){
currentTemplate = name;
break;
}
}
}
let questions;
if(currentTemplate in TEMPLATES){
questions = applyTemplate(text, currentTemplate);
} else {
// 使用默认段落解析
questions = parseWordTextToQuestions(text, 'paragraph');
}
return questions.map((q, idx) => {
const parsed = parseAnswer(q.answer || '', q.options || [], q.type || '未知');
let hint = '请查看教材';
if(q.type==='判断') hint = parsed.answer==='A'?'正确':'错误';
else if(q.options && q.options.length>0){
const hintParts = [];
for(const ch of String(parsed.answer)){
if(/[A-Z]/.test(ch)){
const idx = ch.charCodeAt(0)-65;
if(q.options[idx]) hintParts.push(q.options[idx]);
}
}
if(hintParts.length>0) hint = hintParts.join(', ');
}
return {
id: currentId++,
type: q.type || '未知',
question: q.question || '',
options: q.options || [],
answer: parsed.answer,
hint,
confidence: parsed.confidence,
reason: parsed.reason,
sourceRow: idx+1,
sourceFile: currentFileName,
rawRow: [q.question, ...(q.options || []), q.answer]
};
});
}
/* ==================== 答案解析核心 ==================== */
function mapChineseLetterToLatin(s){
if(!s) return s;
const MAP = {
'甲':'A','乙':'B','丙':'C','丁':'D','甲)':'A','乙)':'B','丙)':'C','丁)':'D',
'一':'A','二':'B','三':'C','四':'D','①':'A','②':'B','③':'C','④':'D',
'1':'A','2':'B','3':'C','4':'D','Ⅰ':'A','Ⅱ':'B','Ⅲ':'C','Ⅳ':'D'
};
return s.split('').map(ch=>MAP[ch]||ch).join('');
}
function parseAnswer(rawAnswer, options, type){
if(rawAnswer===undefined||rawAnswer===null) return {answer:'', confidence:'low', reason:'空答案'};
let s = String(rawAnswer).trim();
s = s.replace(/[\s ]+/g,' ').replace(/[,、;;]/g,',').trim();
const mapped = mapChineseLetterToLatin(s);
const normalized = String(mapped).replace(/[,,\s\/\\]+/g,',').trim();
if(/判断|对错|是非/i.test(type) || /^[对错是否√×ABab]$/.test(s) || /^[ABab]$/.test(mapped)){
if(/^(对|正确|T|True|是|√|yes|A)$/i.test(s) || /^[Aa]$/.test(mapped)) return {answer:'A', confidence:'high', reason:'识别为正确'};
if(/^(错|错误|F|False|否|×|no|B)$/i.test(s) || /^[Bb]$/.test(mapped)) return {answer:'B', confidence:'high', reason:'识别为错误'};
return {answer:s, confidence:'low', reason:'无法识别判断题答案'};
}
if(/^[A-Za-z]+([,,\s\/\\][A-Za-z]+)*$/.test(normalized)){
const letters = (normalized.match(/[A-Za-z]/g)||[]).map(l=>l.toUpperCase());
const uniq = [...new Set(letters)].sort().join('');
return {answer:uniq, confidence:'high', reason:'字母形式答案'};
}
const parts = normalized.split(/[,]+/).filter(Boolean);
if(parts.length===1 && options && options.length>0){
const part = parts[0].toLowerCase();
for(let i=0;i<options.length;i++){
const opt = String(options[i]||'').trim().toLowerCase();
if(opt===part) return {answer:String.fromCharCode(65+i), confidence:'high', reason:'选项文本精确匹配'};
}
}
if(options && options.length>0){
const matched = [];
for(let i=0;i<options.length;i++){
const opt = String(options[i]||'').trim();
if(!opt) continue;
const loOpt = opt.toLowerCase(), loS = s.toLowerCase();
if(loOpt.includes(loS) || loS.includes(loOpt)) matched.push(i);
}
if(matched.length>0){
const ans = matched.map(i=>String.fromCharCode(65+i)).sort().join('');
const conf = (matched.length===1 || /多选|multiple/i.test(type)) ? 'high' : 'medium';
return {answer:ans, confidence:conf, reason:`模糊匹配到选项 (${matched.map(i=>String.fromCharCode(65+i)).join(',')})`};
}
}
const letters = mapped.match(/[A-Za-z]/g);
if(letters){
const uniq = [...new Set(letters.map(l=>l.toUpperCase()))].sort().join('');
return {answer:uniq, confidence:'medium', reason:'从文本中提取到字母'};
}
return {answer:s, confidence:'low', reason:'无法匹配答案'};
}
/* ==================== 文件处理 ==================== */
function handleFile(file){
if(!file) return;
currentFileName = file.name;
currentId = 1;
const ext = file.name.split('.').pop().toLowerCase();
const tabs = document.querySelectorAll('.tab-btn');
// 显示模板选择器
document.getElementById('templateSelector').style.display = 'block';
let detectedType = 'auto';
if(ext.match(/xlsx?/)) detectedType = 'excel';
else if(ext === 'docx') detectedType = 'word';
else if(ext === 'pdf') detectedType = 'pdf';
else if(ext === 'txt') detectedType = 'txt';
tabs.forEach(tab => {
if(tab.dataset.type === detectedType){
tab.classList.add('active');
currentFileType = detectedType;
} else {
tab.classList.remove('active');
}
});
updateUIByFileType();
if(ext.match(/xlsx?/)){
handleExcelFile(file);
} else if(ext === 'docx'){
handleWordFile(file);
} else if(ext === 'pdf'){
handlePDFFile(file);
} else if(ext === 'txt'){
handleTextFile(file);
} else {
alert('不支持的文件类型,请上传 .docx .xlsx .xls .pdf .txt 文件');
return;
}
}
async function handlePDFFile(file){
try {
window.pdfFile = file;
const uploadStatus = document.getElementById('uploadStatus');
if(uploadStatus) uploadStatus.textContent = `PDF文件: ${file.name}`;
// 显示PDF页面范围选择
document.getElementById('pdfPageContainer').style.display = 'block';
document.getElementById('processBtn').disabled = false;
document.getElementById('exportLogBtn').disabled = false;
addLog(`PDF文件 "${file.name}" 已加载,请设置页面范围后点击转换`, 'success');
} catch(err){
addLog(`PDF文件处理失败: ${err.message}`, 'error');
alert('PDF文件处理失败: ' + err.message);
}
}
async function handleTextFile(file){
const reader = new FileReader();
reader.onload = async e=>{
try{
const text = e.target.result;
window.txtRawText = text;
const uploadStatus = document.getElementById('uploadStatus');
if(uploadStatus) uploadStatus.textContent = `文本文件: ${file.name} (${text.length}字符)`;
document.getElementById('processBtn').disabled = false;
document.getElementById('exportLogBtn').disabled = false;
addLog(`文本文件 "${file.name}" 加载成功`, 'success');
} catch(err){
addLog(`文本文件解析失败: ${err.message}`, 'error');
alert('文本文件解析失败: ' + err.message);
}
};
reader.readAsText(file);
}
/* ==================== 更新UI ==================== */
function updateUIByFileType(){
const excelContainer = document.getElementById('excelSheetContainer');
const wordContainer = document.getElementById('wordParseContainer');
const pdfContainer = document.getElementById('pdfPageContainer');
if(currentFileType === 'excel'){
excelContainer.style.display = 'block';
wordContainer.style.display = 'none';
pdfContainer.style.display = 'none';
} else if(currentFileType === 'word'){
excelContainer.style.display = 'none';
wordContainer.style.display = 'block';
pdfContainer.style.display = 'none';
} else if(currentFileType === 'pdf'){
excelContainer.style.display = 'none';
wordContainer.style.display = 'none';
pdfContainer.style.display = 'block';
} else {
// auto模式根据文件名判断
if(currentFileName.match(/\.xlsx?$/i)){
excelContainer.style.display = 'block';
wordContainer.style.display = 'none';
pdfContainer.style.display = 'none';
} else if(currentFileName.match(/\.docx$/i)){
excelContainer.style.display = 'none';
wordContainer.style.display = 'block';
pdfContainer.style.display = 'none';
} else if(currentFileName.match(/\.pdf$/i)){
excelContainer.style.display = 'none';
wordContainer.style.display = 'none';
pdfContainer.style.display = 'block';
} else {
excelContainer.style.display = 'none';
wordContainer.style.display = 'none';
pdfContainer.style.display = 'none';
}
}
}
/* ==================== 处理文件 ==================== */
async function processFile(){
document.getElementById('processBtn').disabled = true;
const logEl = document.getElementById('logContainer');
if(logEl) logEl.style.display='block';
addLog('开始智能转换...', 'info');
questionBank = [];
currentId = 1;
updateProgress(0);
try {
if(currentFileType === 'excel' || currentFileName.match(/\.xlsx?$/i)){
if(!excelWorkbook || !excelSheetName || !excelSheetDataMap[excelSheetName]){
alert('请先加载Excel文件并选择工作表');
document.getElementById('processBtn').disabled = false;
return;
}
const data = excelSheetDataMap[excelSheetName];
questionBank = convertExcelToQuestionBank(data, excelSheetName);
addLog(`Excel转换完成,共 ${questionBank.length} 题。`, 'success');
} else if(currentFileType === 'word' || currentFileName.match(/\.docx$/i)){
if(!window.wordRawText){
alert('请先加载Word文件');
document.getElementById('processBtn').disabled = false;
return;
}
const parseMode = document.getElementById('parseMode').value;
questionBank = parseWordTextToQuestions(window.wordRawText, parseMode);
addLog(`Word转换完成,共 ${questionBank.length} 题。`, 'success');
} else if(currentFileType === 'pdf' || currentFileName.match(/\.pdf$/i)){
if(!window.pdfFile){
alert('请重新上传PDF文件');
document.getElementById('processBtn').disabled = false;
return;
}
const startPage = parseInt(document.getElementById('pdfStartPage').value) || 1;
const endPage = parseInt(document.getElementById('pdfEndPage').value) || null;
const text = await extractTextFromPDF(window.pdfFile, startPage, endPage);
questionBank = parseTextFile(text, currentTemplate);
addLog(`PDF转换完成,共 ${questionBank.length} 题。`, 'success');
} else if(currentFileType === 'txt' || currentFileName.match(/\.txt$/i)){
if(!window.txtRawText){
alert('请先加载文本文件');
document.getElementById('processBtn').disabled = false;
return;
}
questionBank = parseTextFile(window.txtRawText, currentTemplate);
addLog(`文本文件转换完成,共 ${questionBank.length} 题。`, 'success');
}
updateProgress(100);
updateStats(questionBank);
pageSize = Number(document.getElementById('pageSize').value || 20);
currentPage = 1;
updatePreview();
document.getElementById('downloadBtn').disabled = questionBank.length===0;
document.getElementById('downloadJsonBtn').disabled = questionBank.length===0;
document.getElementById('batchBtn').disabled = questionBank.length===0;
document.getElementById('saveCloudBtn').disabled = questionBank.length===0;
document.getElementById('syncCloudBtn').disabled = questionBank.length===0;
document.getElementById('exportApiBtn').disabled = questionBank.length===0;
document.getElementById('downloadPdfBtn').disabled = questionBank.length===0;
} catch(err){
addLog(`转换过程中出错: ${err.message}`, 'error');
document.getElementById('processBtn').disabled = false;
}
}
/* ==================== Excel处理函数 ==================== */
function handleExcelFile(file){
const reader = new FileReader();
reader.onload = e=>{
try{
const data = new Uint8Array(e.target.result);
const wb = XLSX.read(data, {type:'array'});
excelWorkbook = wb;
excelSheetDataMap = {};
const sheetSelect = document.getElementById('sheetSelect');
sheetSelect.innerHTML = '';
wb.SheetNames.forEach((name)=>{
const ws = wb.Sheets[name];
const arr = XLSX.utils.sheet_to_json(ws, {header:1});
excelSheetDataMap[name] = arr;
const opt = document.createElement('option');
opt.value = name;
opt.textContent = `${name} (${Math.max(0, arr.length-1)}行)`;
sheetSelect.appendChild(opt);
});
excelSheetName = wb.SheetNames[0];
sheetSelect.value = excelSheetName;
const uploadStatus = document.getElementById('uploadStatus');
if(uploadStatus) uploadStatus.textContent = `Excel文件: ${file.name} (${wb.SheetNames.length}个工作表)`;
document.getElementById('processBtn').disabled=false;
document.getElementById('exportLogBtn').disabled=false;
addLog(`Excel文件 "${file.name}" 加载成功,${wb.SheetNames.length}个工作表`, 'success');
sheetSelect.onchange = function(){
excelSheetName = this.value;
const s = excelSheetDataMap[excelSheetName]||[];
if(uploadStatus) uploadStatus.textContent = `已选择工作表: "${excelSheetName}", 共 ${Math.max(0, s.length-1)} 行`;
};
}catch(err){
addLog(`Excel文件读取失败: ${err.message}`, 'error');
alert('Excel文件读取失败: ' + err.message);
}
};
reader.readAsArrayBuffer(file);
}
function convertExcelToQuestionBank(data, sheetName='Sheet1'){
const all = [];
const header = (data[0]||[]).map(h=>String(h||'').trim());
function findHeaderIndex(regex, defIndex){ const idx = header.findIndex(h=>regex.test(String(h))); return idx>=0?idx:defIndex; }
const colMap = {
type: findHeaderIndex(/类型|题型|category/i, 0),
question: findHeaderIndex(/题目|问题|题干|question/i, 1),
answer: findHeaderIndex(/答案|正确答案|answer/i, header.length-1)
};
let optionCols = [];
for(let i=0;i<header.length;i++){
if(i===colMap.type||i===colMap.question||i===colMap.answer) continue;
if(/^[A-D]$|选项|选项\d|A项|B项|C项|D项|option/i.test(header[i]||'')) optionCols.push(i);
}
if(optionCols.length===0){
const start = Math.max(0, colMap.question+1);
const end = Math.max(colMap.answer, header.length);
for(let j=start;j<end;j++) optionCols.push(j);
}
for(let i=1;i<data.length;i++){
const row = data[i]||[];
if(row.filter(Boolean).length===0) continue;
const rawType = String(row[colMap.type]||'').trim();
const question = String(row[colMap.question]||'').trim();
const rawAnswer = row[colMap.answer]===undefined?'':String(row[colMap.answer]).trim();
if(!question){ addLog(`第 ${i+1} 行:题目为空,已跳过。`, 'warn'); continue; }
const opts = [];
for(const ci of optionCols){
const cell = row[ci];
if(cell!==undefined && String(cell).trim()!=='') opts.push(String(cell).trim());
}
let type = '未知';
if(/判断|对错|是非/.test(rawType)) type='判断';
else if(/单选/.test(rawType)) type='单选';
else if(/多选/.test(rawType)) type='多选';
else {
if(rawAnswer && /^[A-Za-z]$/.test(String(rawAnswer).trim())) type='单选';
else if(rawAnswer && /[A-Za-z].*[A-Za-z]/.test(String(rawAnswer).trim())) type='多选';
}
if(type==='未知'){ addLog(`第 ${i+1} 行:无法识别题型 "${rawType}",已跳过。`, 'warn'); continue; }
if(type==='判断' && opts.length===0) opts.push('正确','错误');
const parsed = parseAnswer(rawAnswer, opts, type);
const answer = parsed.answer;
const confidence = parsed.confidence;
const reason = parsed.reason || '';
let hint = '请查看教材';
if(type==='判断'){ hint = answer==='A'?'正确':answer==='B'?'错误':answer; }
else {
const hintParts = [];
for(const ch of String(answer)){
if(/[A-Z]/.test(ch)){
const idx = ch.charCodeAt(0)-65;
if(opts[idx]) hintParts.push(opts[idx]);
}
}
if(hintParts.length>0) hint = hintParts.join(', ');
}
const q = {
id: currentId++,
type,
question,
options: opts,
answer,
hint,
confidence,
reason,
sourceSheet: sheetName,
sourceRow: i+1,
rawRow: row.map(c=>c===undefined?'':String(c))
};
all.push(q);
if(confidence==='low') addLog(`第 ${i+1} 行 题目 #${q.id} 答案无法识别: "${rawAnswer}" (原因: ${reason})`, 'warn');
else if(confidence==='medium') addLog(`第 ${i+1} 行 题目 #${q.id} 答案模糊匹配: "${rawAnswer}" (原因: ${reason})`, 'warn');
}
return all;
}
/* ==================== Word处理函数 ==================== */
async function handleWordFile(file){
const reader = new FileReader();
reader.onload = async e=>{
try{
const arrayBuffer = e.target.result;
const result = await mammoth.extractRawText({ arrayBuffer });
const text = result.value;
window.wordRawText = text;
const uploadStatus = document.getElementById('uploadStatus');
if(uploadStatus) uploadStatus.textContent = `Word文件: ${file.name} (${text.length}字符)`;
document.getElementById('processBtn').disabled=false;
document.getElementById('exportLogBtn').disabled=false;
addLog(`Word文件 "${file.name}" 加载成功,${text.length}字符`, 'success');
}catch(err){
addLog(`Word文件解析失败: ${err.message}`, 'error');
alert('Word文件解析失败: ' + err.message);
}
};
reader.readAsArrayBuffer(file);
}
function parseWordTextToQuestions(text, parseMode='auto'){
const lines = text.split(/\n/).map(l=>l.trim()).filter(l=>l);
const questions = [];
let current = null;
let optionPattern = /^[A-Da-d][\..、::)\)]\s*(.+)$/;
let answerPattern = /答案[::]\s*(.+)/i;
for(let i=0; i<lines.length; i++){
const line = lines[i];
if(/^\d+[\..]/.test(line) || /^[一二三四五六七八九十]+、/.test(line) || /^第.+题/.test(line)){
if(current) questions.push(current);
current = { type: '未知', question: line.replace(/^\d+[\..]|^[一二三四五六七八九十]+、|^第.+题/, '').trim(), options: [], answer: '' };
continue;
}
if(!current) continue;
const optMatch = line.match(optionPattern);
if(optMatch){
current.options.push(optMatch[1].trim());
continue;
}
const ansMatch = line.match(answerPattern);
if(ansMatch){
current.answer = ansMatch[1].trim();
continue;
}
if(/判断|单选|多选/.test(line)){
current.type = line.includes('判断') ? '判断' : line.includes('多选') ? '多选' : '单选';
}
}
if(current) questions.push(current);
return questions.map((q, idx)=>{
if(q.type==='未知'){
if(q.answer && q.answer.length===1) q.type='单选';
else if(q.answer && q.answer.length>1) q.type='多选';
else if(q.options.length===0) q.type='判断';
}
if(q.type==='判断' && q.options.length===0) q.options=['正确','错误'];
const parsed = parseAnswer(q.answer, q.options, q.type);
let hint = '请查看教材';
if(q.type==='判断') hint = parsed.answer==='A'?'正确':'错误';
else {
const hintParts = [];
for(const ch of String(parsed.answer)){
if(/[A-Z]/.test(ch)){
const idx = ch.charCodeAt(0)-65;
if(q.options[idx]) hintParts.push(q.options[idx]);
}
}
if(hintParts.length>0) hint = hintParts.join(', ');
}
return {
id: currentId++,
type: q.type,
question: q.question,
options: q.options,
answer: parsed.answer,
hint,
confidence: parsed.confidence,
reason: parsed.reason,
sourceRow: idx+1,
rawRow: [q.question, ...q.options, q.answer]
};
});
}
/* ==================== 预览和分页功能 ==================== */
function updateStats(all){
const s = {jud:0,sin:0,mul:0};
(all||questionBank).forEach(q=>{
if(q.type==='判断') s.jud++;
else if(q.type==='单选') s.sin++;
else if(q.type==='多选') s.mul++;
});
document.getElementById('stat-jud').textContent = s.jud;
document.getElementById('stat-single').textContent = s.sin;
document.getElementById('stat-multi').textContent = s.mul;
}
function getFilteredQuestions(){
const txt = (document.getElementById('searchInput').value||'').trim().toLowerCase();
const type = document.getElementById('filterType').value;
const conf = document.getElementById('filterConf').value;
return questionBank.filter(q=>{
if(type!=='all' && q.type!==type) return false;
if(conf!=='all' && q.confidence!==conf) return false;
if(!txt) return true;
if(String(q.id).includes(txt)) return true;
if((q.question||'').toLowerCase().includes(txt)) return true;
if((q.answer||'').toLowerCase().includes(txt)) return true;
if((q.options||[]).some(o=>o.toLowerCase().includes(txt))) return true;
return (q.reason||'').toLowerCase().includes(txt) || (q.hint||'').toLowerCase().includes(txt);
});
}
function updatePreview(all){
const root = document.getElementById('previewContent');
const filtered = all || getFilteredQuestions();
if(!filtered || filtered.length===0){
root.innerHTML = '<p class="small" style="padding:14px;text-align:center;color:#888">未找到有效题目。</p>';
updatePageInfo(0,1);
return;
}
const total = filtered.length;
const pSize = pageSize;
const totalPages = Math.max(1, Math.ceil(total/pSize));
if(currentPage > totalPages) currentPage = totalPages;
const start = (currentPage-1)*pSize, end = Math.min(total, start+pSize);
const pageItems = filtered.slice(start, end);
let html = '';
for(const q of pageItems){
const typeClass = q.type==='判断'?'type-judgment':q.type==='单选'?'type-single':'type-multi';
const uncertain = q.confidence!=='high' ? 'uncertain' : '';
const badge = q.confidence!=='high' ? `<span class="uncertain-badge">${q.confidence==='low'?'无法识别':'模糊匹配'}</span>` : '';
const optsTxt = q.options && q.options.length>0 ? `<div class="small" style="margin-top:6px;color:#666">选项: ${escapeHtml(q.options.join(' | '))}</div>` : '';
const reasonTxt = q.reason ? `<div class="small" style="margin-top:6px;color:#b03">原因: ${escapeHtml(q.reason)}</div>` : '';
const sourceTxt = q.sourceSheet ?
`<div class="small" style="margin-top:6px;color:#999">来源: ${escapeHtml(q.sourceSheet||'')} 行 ${q.sourceRow||''}</div>` :
`<div class="small" style="margin-top:6px;color:#999">来源: 行 ${q.sourceRow||''}</div>`;
html += `<div class="q-item ${uncertain}" id="q-${q.id}">
<div class="q-meta">
<span class="q-type ${typeClass}">${q.type}</span>
<strong>#${q.id}</strong>
<div style="margin-left:10px;color:#333;font-weight:600">${escapeHtml(q.question)}</div>
<div class="q-actions">
<button class="btn-edit"><i class="fa fa-edit"></i> 编辑</button>
<button class="btn-mark"><i class="fa fa-check"></i> 标为已确认</button>
</div>
${badge}
</div>
${optsTxt}
<div style="margin-top:8px;color:#27ae60">答案: <span id="ans-${q.id}">${escapeHtml(q.answer)}</span></div>
<div class="hint">提示: ${escapeHtml(q.hint||'')}</div>
${reasonTxt}
${sourceTxt}
</div>`;
}
html += `<div style="text-align:center;margin-top:8px;color:#888">共 ${total} 道题(第 ${currentPage} 页,显示 ${start+1} - ${end})</div>`;
root.innerHTML = html;
updatePageInfo(currentPage, Math.max(1, Math.ceil(total/pSize)));
document.getElementById('exportFilteredJsonBtn').disabled = total===0;
document.getElementById('exportFilteredXlsxBtn').disabled = total===0;
document.getElementById('exportFilteredCsvBtn').disabled = total===0;
}
function updatePageInfo(page, totalPages){
document.getElementById('pageInfo').textContent = `第 ${page} / ${totalPages} 页`;
document.getElementById('prevPageBtn').disabled = page<=1;
document.getElementById('nextPageBtn').disabled = page>=totalPages;
}
/* ==================== 编辑功能 ==================== */
let editingId = null;
function openEditModal(id){
const q = questionBank.find(x=>x.id===id);
if(!q) return alert('题目未找到');
editingId = id;
document.getElementById('editTitleId').textContent = `#${id}`;
document.getElementById('editType').value = q.type;
document.getElementById('editQuestion').value = q.question;
document.getElementById('editOptions').value = q.options.join(' | ');
document.getElementById('editAnswer').value = q.answer;
document.getElementById('editHint').value = q.hint || '';
document.getElementById('editConfidence').value = q.confidence || 'high';
document.getElementById('editReason').value = q.reason || '';
document.getElementById('editModalBackdrop').style.display = 'flex';
}
function closeEditModal(){ document.getElementById('editModalBackdrop').style.display = 'none'; editingId = null; }
function saveEditModal(){
if(editingId===null) return;
const q = questionBank.find(x=>x.id===editingId);
if(!q) return alert('题目未找到');
q.type = document.getElementById('editType').value;
q.question = String(document.getElementById('editQuestion').value||'').trim();
q.options = String(document.getElementById('editOptions').value||'').split('|').map(s=>s.trim()).filter(Boolean);
const rawAns = String(document.getElementById('editAnswer').value||'').trim();
const parsed = parseAnswer(rawAns, q.options, q.type);
q.answer = parsed.answer;
q.confidence = document.getElementById('editConfidence').value || 'high';
q.reason = String(document.getElementById('editReason').value||'').trim() || '手动修正';
q.hint = String(document.getElementById('editHint').value||'').trim() || q.hint;
addLog(`题目 #${q.id} 已保存。`, 'success');
closeEditModal();
updatePreview();
updateStats(questionBank);
}
function markConfirmed(id){
const q = questionBank.find(x=>x.id===id);
if(!q) return alert('题目未找到');
q.confidence = 'high';
q.reason = (q.reason? q.reason + ' | ':'') + '手动确认';
addLog(`题目 #${q.id} 标记为已确认。`, 'success');
updatePreview();
}
/* ==================== 批量功能 ==================== */
function populateBatchList(){
const list = document.getElementById('batchList');
list.innerHTML = '';
const items = questionBank;
if(items.length===0){ list.innerHTML = '<div class="small" style="padding:8px">当前没有题目。</div>'; return; }
for(const q of items){
const div = document.createElement('div');
div.className = 'batch-item';
div.innerHTML = `<input type="checkbox" class="batch-chk" data-id="${q.id}">
<div style="min-width:36px;"><strong>#${q.id}</strong></div>
<div style="flex:1"><div style="font-weight:600">${escapeHtml(q.question)}</div><div class="small">答案: ${escapeHtml(q.answer)} | 置信度: ${q.confidence}</div></div>
<div><button class="btn-edit">编辑</button></div>`;
list.appendChild(div);
}
}
function openBatchModal(){
populateBatchList();
document.getElementById('fixPreview').innerHTML = '<div class="small" style="color:#999">请点击"生成自动修复建议"以查看清洗结果。</div>';
document.getElementById('batchModalBackdrop').style.display = 'flex';
}
function closeBatchModal(){ document.getElementById('batchModalBackdrop').style.display = 'none'; }
function selectAllBatch(flag){ document.querySelectorAll('#batchList .batch-chk').forEach(cb=>cb.checked = !!flag); }
function batchMarkConfirmed(){
const chks = Array.from(document.querySelectorAll('#batchList .batch-chk')).filter(c=>c.checked);
if(chks.length===0) return alert('请先选择要标记的题目。');
chks.forEach(cb=>{
const id = Number(cb.getAttribute('data-id'));
const q = questionBank.find(x=>x.id===id);
if(q){ q.confidence='high'; q.reason=(q.reason? q.reason+' | ':'')+'批量手动确认'; }
});
addLog(`批量标记 ${chks.length} 道题为已确认。`, 'success');
updatePreview();
}
function batchGenerateFixPreview(){
const suggestions = generateFixSuggestions();
const previewEl = document.getElementById('fixPreview');
previewEl.innerHTML = '';
if(suggestions.length===0){
previewEl.innerHTML = '<div class="small" style="color:#999;padding:8px">未生成任何修复建议。</div>';
return;
}
const container = document.createElement('div');
suggestions.forEach(sug=>{
const row = document.createElement('div');
row.className = 'fix-row';
row.innerHTML = `<div style="display:flex;align-items:flex-start;gap:8px">
<input type="checkbox" class="fix-chk" data-id="${sug.id}" checked />
<div style="flex:1">
<div style="font-weight:700">#${sug.id}</div>
<div class="small" style="margin-top:6px">原选项: ${escapeHtml(sug.originalOptions.join(' | '))}</div>
<div class="small" style="margin-top:6px;color:#2d3748">建议选项: ${escapeHtml(sug.cleanedOptions.join(' | '))}</div>
<div class="small" style="margin-top:6px;color:#27ae60">原答案: ${escapeHtml(sug.originalAnswer)} → 建议答案: ${escapeHtml(sug.suggestedAnswer)}(置信度 ${sug.origConfidence} → ${sug.suggestedConfidence})</div>
<div class="small" style="margin-top:4px;color:#999">原因: ${escapeHtml(sug.reason)}</div>
</div>
</div>`;
container.appendChild(row);
});
previewEl.appendChild(container);
previewEl._suggestions = suggestions;
addLog(`生成 ${suggestions.length} 条自动修复建议。`, 'info');
}
function batchApplySelectedFixes(){
const previewEl = document.getElementById('fixPreview');
if(!previewEl || !previewEl._suggestions) return alert('请先生成修复建议');
const suggestions = previewEl._suggestions;
const checks = Array.from(previewEl.querySelectorAll('.fix-chk')).filter(c=>c.checked).map(c=>Number(c.getAttribute('data-id')));
if(checks.length===0) return alert('请先选择要应用的修复建议');
let applied = 0;
suggestions.forEach(sug=>{
if(checks.includes(sug.id)){
const ok = applySuggestion(sug);
if(ok) applied++;
}
});
addLog(`批量应用修复完成:已应用 ${applied} 条建议。`, 'success');
updatePreview();
populateBatchList();
}
function generateFixSuggestions(){
const suggestions = [];
for(const q of questionBank){
if(!q || q.confidence==='high') continue;
const cleaned = normalizeOptionsForQuestion(q);
const parsedAfter = parseAnswer(q.answer || '', cleaned, q.type);
let fallbackParsed = null;
if((parsedAfter.confidence==='low' || parsedAfter.confidence==='medium') && q.rawRow){
const combined = q.rawRow.join(' ');
fallbackParsed = parseAnswer(combined, cleaned, q.type);
}
const finalParsed = (fallbackParsed && fallbackParsed.confidence>'low') ? fallbackParsed : parsedAfter;
const optsChanged = JSON.stringify(cleaned) !== JSON.stringify(q.options.map(o=>String(o)));
const answerChanged = finalParsed.answer !== q.answer;
const confImproved = (finalParsed.confidence==='high' && q.confidence!=='high') || (finalParsed.confidence==='medium' && q.confidence==='low');
if(optsChanged || answerChanged || confImproved){
suggestions.push({
id: q.id,
originalOptions: q.options.slice(),
cleanedOptions: cleaned,
originalAnswer: q.answer,
suggestedAnswer: finalParsed.answer,
origConfidence: q.confidence,
suggestedConfidence: finalParsed.confidence,
reason: finalParsed.reason || '清洗后建议'
});
}
}
return suggestions;
}
function applySuggestion(sug){
const q = questionBank.find(x=>x.id===sug.id);
if(!q) return false;
q.options = sug.cleanedOptions.slice();
q.answer = sug.suggestedAnswer;
q.confidence = sug.suggestedConfidence || 'medium';
q.reason = (q.reason? q.reason + ' | ':'') + `自动修复(${sug.reason||'清洗'})`;
addLog(`题目 #${q.id} 应用自动修复: 答案 ${sug.originalAnswer} -> ${sug.suggestedAnswer}, 置信度 ${sug.origConfidence} -> ${sug.suggestedConfidence}`, 'success');
return true;
}
function cleanOptionText(text){
if(text===undefined || text===null) return '';
let s = String(text).trim();
s = s.replace(/^[\s"']*(?:$|()?[A-Za-z甲乙丙丁一二三四①②③④ⅠⅡⅢⅣ0-90-9]+(?:$|))?[\..、::)\)]*\s*/i, '');
s = s.replace(/^[\s\-\–\—\u2014\.\:\:\、\)]+/, '').replace(/[\s\.\,\:\:\、\)]+$/,'').trim();
s = s.replace(/[\s ]+/g, ' ').trim();
return s;
}
function normalizeOptionsForQuestion(q){
if(!q || !Array.isArray(q.options)) return [];
return q.options.map(o=>cleanOptionText(o));
}
/* ==================== 导出功能 ==================== */
function downloadJSON(){
if(!questionBank.length) return alert('没有可导出的题库');
const json = JSON.stringify(questionBank, null, 2);
const blob = new Blob([json], {type:'application/json;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download='question_bank.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
addLog('JSON 文件已开始下载。', 'success');
}
function generateJSFile(questions){
const jsonText = JSON.stringify(questions, null, 2);
const safe = jsonText.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\r/g,'').replace(/\n/g,'\\n').replace(/<\/script>/gi,'<\\/script>');
const header = `// 题库数据 - 自动生成\n// 生成时间: ${new Date().toLocaleString()}\n// 题目总数: ${questions.length}\n\n`;
const restore = `var questionBank = JSON.parse('${safe}');\n\n`;
const helpers = `questionBank.getStats = function(){ return { judgment: this.filter(q=>q.type==="判断").length, singleChoice: this.filter(q=>q.type==="单选").length, multiChoice: this.filter(q=>q.type==="多选").length, total: this.length }; };\nfunction getRandomQuestion(type=null){ let pool = questionBank; if(type) pool = questionBank.filter(q=>q.type===type); if(pool.length===0) return null; return pool[Math.floor(Math.random()*pool.length)]; }\nfunction findQuestionById(id){ return questionBank.find(q=>q.id===id)||null; }\nfunction checkAnswer(question, userAnswer){ if(!question || userAnswer===undefined || userAnswer===null) return false; const stdUser = String(userAnswer).replace(/[,,\\s]+/g,'').toUpperCase(); const stdCorrect = String(question.answer).replace(/[,,\\s]+/g,'').toUpperCase(); return stdUser===stdCorrect; }\n(function(){ if(typeof global!=='undefined'){ global.questionBank = questionBank; } if(typeof window!=='undefined'){ window.questionBank = questionBank; } if(typeof self!=='undefined'){ self.questionBank = questionBank; } })();\nconsole.log('题库加载完成!');\n`;
return header + restore + helpers;
}
function downloadJS(){
if(!questionBank.length) return alert('没有可导出的题库');
const content = generateJSFile(questionBank);
const blob = new Blob([content], {type:'application/javascript'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download='question_bank.js'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
addLog('JS 文件已开始下载。', 'success');
}
function exportFilteredJSON(){
const arr = getFilteredQuestions();
const blob = new Blob([JSON.stringify(arr, null, 2)], {type:'application/json;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download='filtered_questions.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
addLog(`导出筛选后的 ${arr.length} 道题(JSON)。`, 'success');
}
function exportFilteredXLSX(){
const arr = getFilteredQuestions().map(q=>({id:q.id, type:q.type, question:q.question, options:q.options.join(' | '), answer:q.answer, confidence:q.confidence, reason:q.reason, sourceSheet:q.sourceSheet, sourceRow:q.sourceRow}));
const ws = XLSX.utils.json_to_sheet(arr);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'questions');
XLSX.writeFile(wb, 'filtered_questions.xlsx');
addLog(`导出筛选后的 ${arr.length} 道题(XLSX)。`, 'success');
}
function exportFilteredCSV(){
const arr = getFilteredQuestions().map(q=>({id:q.id, type:q.type, question:q.question, options:q.options.join(' | '), answer:q.answer, confidence:q.confidence, reason:q.reason}));
const ws = XLSX.utils.json_to_sheet(arr);
const csv = XLSX.utils.sheet_to_csv(ws);
const blob = new Blob([csv], {type:'text/csv;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download='filtered_questions.csv'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
addLog(`导出筛选后的 ${arr.length} 道题(CSV)。`, 'success');
}
function exportAuditLog(){
const items = Array.from(document.getElementById('logContent').children).map(n=>n.textContent).join('\n');
const blob = new Blob([items], {type:'text/plain;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = 'audit_log.txt'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
addLog('审计日志已导出。', 'success');
}
/* ==================== 事件处理 ==================== */
function onFilterChange(){ currentPage = 1; updatePreview(); }
function onPageSizeChange(){ pageSize = Number(document.getElementById('pageSize').value); currentPage = 1; updatePreview(); }
function prevPage(){ if(currentPage>1){ currentPage--; updatePreview(); } }
function nextPage(){ const filtered = getFilteredQuestions(); const totalPages = Math.max(1, Math.ceil(filtered.length/pageSize)); if(currentPage<totalPages){ currentPage++; updatePreview(); } }
/* ==================== 初始化 ==================== */
document.addEventListener('DOMContentLoaded', ()=>{
const uploadLabel = document.getElementById('uploadLabel');
const fileInput = document.getElementById('fileInput');
const tabBtns = document.querySelectorAll('.tab-btn');
addLog('智能题库转换工具已就绪(增强版)', 'info');
// 文件上传
fileInput.addEventListener('change', (e)=>{
if(e.target.files && e.target.files.length>0){
const file = e.target.files[0];
handleFile(file);
if(file.name.match(/\.pdf$/i)){
window.pdfFile = file;
}
fileInput.value = '';
}
});
// 标签切换
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFileType = btn.dataset.type;
updateUIByFileType();
});
});
// 模板选择
const templateItems = document.querySelectorAll('.template-item');
templateItems.forEach(item => {
item.addEventListener('click', () => {
templateItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
currentTemplate = item.dataset.template;
if(currentTemplate === 'custom'){
openTemplateModal();
}
});
});
// 拖拽上传
uploadLabel.addEventListener('dragover', (e)=>{ e.preventDefault(); uploadLabel.style.background='#d6eaf8'; });
uploadLabel.addEventListener('dragleave', ()=>{ uploadLabel.style.background=''; });
uploadLabel.addEventListener('drop', (e)=>{
e.preventDefault();
uploadLabel.style.background='';
const files = e.dataTransfer && e.dataTransfer.files;
if(files && files.length>0){
const file = files[0];
handleFile(file);
if(file.name.match(/\.pdf$/i)){
window.pdfFile = file;
}
}
});
// 加载已保存的API密钥
document.getElementById('apiKeyInput').value = cloudStorage.apiKey;
// 检查URL中的协作参数
const urlParams = new URLSearchParams(window.location.search);
const collabData = urlParams.get('collab');
if(collabData){
try {
const data = JSON.parse(atob(collabData));
questionBank = data.questions;
addLog(`从协作链接加载了 ${questionBank.length} 道题`, 'success');
updateStats(questionBank);
updatePreview();
} catch(err){
addLog('协作链接解析失败', 'error');
}
}
// 暴露全局函数
window.processFile = processFile;
window.downloadJS = downloadJS;
window.downloadJSON = downloadJSON;
window.openEditModal = openEditModal;
window.openBatchModal = openBatchModal;
window.closeBatchModal = closeBatchModal;
window.closeEditModal = closeEditModal;
window.selectAllBatch = selectAllBatch;
window.batchMarkConfirmed = batchMarkConfirmed;
window.batchGenerateFixPreview = batchGenerateFixPreview;
window.batchApplySelectedFixes = batchApplySelectedFixes;
window.exportFilteredJSON = exportFilteredJSON;
window.exportFilteredXLSX = exportFilteredXLSX;
window.exportFilteredCSV = exportFilteredCSV;
window.prevPage = prevPage;
window.nextPage = nextPage;
window.onFilterChange = onFilterChange;
window.onPageSizeChange = onPageSizeChange;
window.exportAuditLog = exportAuditLog;
window.downloadTemplatePDF = downloadTemplatePDF;
window.saveApiKey = saveApiKey;
window.exportToAPI = exportToAPI;
window.importFromAPI = importFromAPI;
window.openCollaborationModal = openCollaborationModal;
window.closeCollaborationModal = closeCollaborationModal;
window.generateCollaborationLink = generateCollaborationLink;
window.copyCollaborationLink = copyCollaborationLink;
window.saveToCloud = saveToCloud;
window.loadFromCloud = loadFromCloud;
window.syncWithCloud = syncWithCloud;
window.markConfirmed = markConfirmed;
window.openTemplateModal = openTemplateModal;
window.closeTemplateModal = closeTemplateModal;
window.testTemplate = testTemplate;
window.saveCustomTemplate = saveCustomTemplate;
});
</script>