吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3901|回复: 111
收起左侧

[其他原创] 刷题题库系统 V1.1

  [复制链接]
部落稻草人 发表于 2026-7-27 12:41
本帖最后由 部落稻草人 于 2026-8-2 17:45 编辑

【分享】自用题库刷题系统,免安装浏览器即用,支持 Excel/Word 导入

做这个工具的初衷是自己偶尔要背题考试,一直找不到一个符合自己一些要求且能跨设备的刷题工具。最近突然发现AI制作软件,自己纯小白就参考吧内各位大佬模板,自己搞了一个,分享给大家。


屏幕截图 2026-07-24 210107.png

软件特点:

✅ 免安装,一个 HTML 文件浏览器打开就用
✅ 数据存本地浏览器,不上传任何服务器
✅ Windows 电脑和安卓手机都能用
✅ 支持单选、多选、判断、填空、主观题五种题型
✅ 顺序练习、随机练习、模拟考试、错题专项四种模式
✅ Excel 题库一键导入/导出,自动识别题型
✅ Word 题库(.docx)直接导入,规则引擎自动解析
✅ 多题库管理,自由选择题库练习
✅ 错题自动收集,按错误次数排序
✅ 模拟考试支持自定义组卷、导出空白试卷打印
✅ 试题搜索、收藏夹、个人笔记、图片附件
✅ 深色模式、字体大小调节
✅ 答题进度自动保存,关闭再打开可继续

使用方式:
Windows 双击 index.html,安卓用 Chrome 打开,就这么简单。

题库格式:
下载演示模板 → 按模板填入题目 → 导入 → 开始刷题。
列:题干、答案、选项A~H、解析、类型、领域

v1.1 更新说明
本次更新:界面重构 + 错题增强 + 手机适配 + Bug修复


界面重构
移除左侧导航栏,全部功能入口合并到顶部,不再遮挡题目
主题切换和字体调节从底部移到顶部右侧
首页移除占位文字
顶部新增收藏入口
手机端适配
答题进度改为顶部紧凑色点条(14px)+ 题号文字,单行横向滚动
上一题/下一题按钮紧凑排列
题目区域自适应高度,不被遮挡
错题专项同步适配
错题集增强
批量选择删除(全选 / 按答错次数筛选 / 删除选中)
答对自动移出错题集(可开关,默认关闭)
手动显示答案模式(选择不立即出答案,确认后才显示)
错题导出 Excel
筛选后全选只选可见项
Bug 修复
多选题选项乱序后对错判断错误
选项乱序回看上一题勾选位置不对
主观题回看显示原始 JSON 代码
错题集多选题手动模式无效
错题导出无反应
其他
填空题支持多答案(用 / 或 ; 分隔,填任一个即对)
模拟考试加入自动下一题选项

下载地址:

通过网盘分享的文件:刷题题库
链接: https://pan.baidu.com/s/1c5DUaVmLiz5_8y4ya3q44w?pwd=kdw6 提取码: kdw6
把 dist/index.html 发给朋友,或者放 NAS 共享文件夹里,
电脑手机都能访问,题库通过 Excel 互传即可。

纯本地纯免费,没有任何广告和收费,放心用。第一次制作,也非常不成熟
有问题欢迎回帖交流。

屏幕截图 2026-07-24 203050.png
屏幕截图 2026-07-24 203107.png
屏幕截图 2026-07-24 203125.png
屏幕截图 2026-07-24 210612.png
屏幕截图 2026-07-24 210637.png
屏幕截图 2026-07-24 210644.png





/**

  • Excel 读写服务(基于 SheetJS)
  • 功能:
    • 读取 .xlsx 文件 → 题目数组
    • 题型自动识别
    • 生成演示模板
    • 导出题库为 .xlsx
      */

import * as XLSX from 'xlsx'

// ============================================================
//  常量定义
// ============================================================

/* Excel 列名映射 /
const COL = {
stem: '题干',
answer: '答案',
optionA: '选项 A',
optionB: '选项 B',
optionC: '选项 C',
optionD: '选项 D',
optionE: '选项 E',
optionF: '选项 F',
optionG: '选项 G',
optionH: '选项 H',
explanation: '解析',
type: '类型',
domain: '领域',
}

const ALL_COLS = [
COL.stem, COL.answer,
COL.optionA, COL.optionB, COL.optionC, COL.optionD,
COL.optionE, COL.optionF, COL.optionG, COL.optionH,
COL.explanation, COL.type, COL.domain,
]

/* 题型枚举映射:Excel 中文 → 内部 ID /
const TYPE_MAP = {
'单选题': 'single',
'单选': 'single',
'多选题': 'multi',
'多选': 'multi',
'判断题': 'judge',
'判断': 'judge',
'填空题': 'fill',
'填空': 'fill',
'主观题': 'essay',
'简答题': 'essay',
'简答': 'essay',
'分析题': 'essay',
'论述题': 'essay',
'论述': 'essay',
}

/* 内部 ID → 中文名称 /
const TYPE_LABEL = {
single: '单选题',
multi: '多选题',
judge: '判断题',
fill: '填空题',
essay: '主观题',
}

/* 对错关键词 /
const JUDGE_KEYWORDS = [
'对', '错', '正确', '错误', '✓', '✗',
'true', 'false', 'TRUE', 'FALSE', 'True', 'False',
'是', '否', 'yes', 'no', 'YES', 'NO', '1', '0',
]

// ============================================================
//  题型自动识别
// ============================================================

/**

  • 自动识别单道题目的题型
  • @Param {Object} q - { stem, answer, options: string[] }
  • @returns {string} - 'single' | 'multi' | 'judge' | 'fill' | 'essay'
    */
    function autoDetectType(q) {
    const answer = (q.answer || '').trim()
    const options = (q.options || []).filter(o => o && o.trim() !== '')
    const stem = q.stem || ''
    const hasOptions = options.length > 0
    const optionCount = options.length

    // 获取选项中有内容的个数
    const filledOptions = options.filter(o => o.trim() !== '')

    // 条件①:答案是对错关键词,且无选项 → 判断题
    if (!hasOptions && JUDGE_KEYWORDS.includes(answer)) {
    return 'judge'
    }

    // 条件②:题干含空白括号或下划线,且无选项 → 填空题
    if (!hasOptions && /(\s)|(\s)|_{3,}/.test(stem)) {
    return 'fill'
    }

    // 条件③:仅 A、B 有内容,C~H 全空,答案为单字母
    if (optionCount === 2 && filledOptions.length === 2 && /^[A-Ha-h]$/.test(answer)) {
    // 检查 A、B 内容是否为对错类词汇
    const aContent = options[0].trim()
    const bContent = options[1].trim()
    if (JUDGE_KEYWORDS.includes(aContent) || JUDGE_KEYWORDS.includes(bContent)) {
    return 'judge'
    }
    return 'single'
    }

    // 条件④:答案含 ≥2 字母,且有选项 → 多选题
    if (hasOptions && /[A-Ha-h]{2,}/.test(answer.replace(/[,,、\s]+/g, ''))) {
    return 'multi'
    }

    // 条件⑤:答案为单字母,且有选项 → 单选题
    if (hasOptions && /^[A-Ha-h]$/.test(answer)) {
    return 'single'
    }

    // 条件⑥:无选项,答案为较长自然文本 → 主观题
    if (!hasOptions && answer.length > 2) {
    return 'essay'
    }

    // 条件⑦:兜底 → 填空题
    return 'fill'
    }

/* 提取多选题答案中的多个字母 /
function extractMultiAnswer(raw) {
return raw.replace(/[,,、\s]+/g, '').toUpperCase().split('').sort().join(',')
}

/* 规范化答案文本 /
function normalizeAnswer(raw, type) {
const text = (raw || '').trim()
if (type === 'multi') {
return extractMultiAnswer(text)
}
if (type === 'single') {
return text.toUpperCase().replace(/[,,、\s]+/g, '')
}
return text
}

// ============================================================
//  选项自动提取(从题干中解析 A/B/C/D 选项)
// ============================================================

/**

  • 从题干文字中提取选项内容
  • 支持格式:A. xxx  B. xxx  /  A、xxx  B、xxx  /  A.xxx  B.xxx
  • 同行并列或换行分隔均可
  • @param {string} stem - 题干原文
  • @returns {string[]} - 提取的选项数组,如 ['上海', '北京', '广州', '深圳']
    */
    function extractOptionsFromStem(stem) {
    if (!stem) return []

    // 匹配 A~H 后跟分隔符 + 内容的模式
    // 分隔符:. . 、 。) ) 空格
    const letterLabels = 'ABCDEFGH'
    const patterns = []

    for (const letter of letterLabels) {
    // 匹配选项标签:A. 或 A、或 A.或 A) 或 A) 后跟内容
    const re = new RegExp(
    ${letter}[..、。\\))]\\s*([^A-H\\n]*),
    'g'
    )
    const matches = [...stem.matchAll(re)]
    for (const m of matches) {
    const content = m[1].trim()
    if (content) {
    patterns.push({ letter, content, index: m.index })
    }
    }
    }

    // 至少找到 A 和 B 两个选项才算有效
    const foundLetters = [...new Set(patterns.map(p => p.letter))]
    if (foundLetters.length < 2) return []

    // 按出现顺序排序,每个字母取第一次出现
    const result = []
    const seen = new Set()
    patterns.sort((a, b) => a.index - b.index)
    for (const p of patterns) {
    if (!seen.has(p.letter)) {
    seen.add(p.letter)
    result.push(p.content)
    }
    }

    // 净化:去掉选项内容末尾可能残留的其他选项标签
    const cleaned = result.map(content => {
    // 去掉末尾被误包含的下一个选项标签
    return content.replace(/\s[A-H][.、.)\)]\s$/, '').trim()
    })

    return cleaned
    }

/**

  • 从题干中移除已提取的选项文字,使题干更干净
  • @param {string} stem - 原始题干
  • @returns {string} - 移除选项后的题干
    /
    function cleanStemFromOptions(stem) {
    // 去掉题干中从第一个选项标签开始的所有内容
    // 匹配第一个 A. 或 A、 出现的位置
    const re = /\s
    [Aa][.、.\))]\s*/
    const match = stem.match(re)
    if (match) {
    return stem.substring(0, match.index).trim()
    }
    // 另一个常见模式:题干结尾有括号 (A) (B) 这种,不处理
    return stem
    }

// ============================================================
//  表头兼容读取
// ============================================================

/**

  • 从 Excel 行中读取单元格值,兼容多种表头写法
  • 例如 '选项 A' 和 '选项A' 都能匹配
    */
    function getCell(row, primaryKey, altKeys = []) {
    const keys = [primaryKey, ...altKeys]
    for (const key of keys) {
    if (row[key] !== undefined && row[key] !== null && String(row[key]).trim() !== '') {
    return String(row[key]).trim()
    }
    }
    // 模糊匹配:遍历 row 的 key,找包含主键关键词的
    for (const rowKey of Object.keys(row)) {
    // '选项 A' 匹配 '选项A'、'选项  A' 等
    const normalized = rowKey.replace(/\s+/g, '')
    const primaryNormalized = primaryKey.replace(/\s+/g, '')
    if (normalized === primaryNormalized) {
    const val = String(row[rowKey] || '').trim()
    if (val) return val
    }
    }
    return ''
    }

// ============================================================
//  Excel 读取
// ============================================================

/**

  • 从 Excel 文件读取题目数组
  • @param {File} file - 用户选择的 .xlsx 文件
  • @returns {Promise<{questions: Array, errors: Array}>}
    */
    export async function importFromExcel(file) {
    const buffer = await file.arrayBuffer()
    const workbook = XLSX.read(buffer, { type: 'array' })
    const sheetName = workbook.SheetNames[0]
    const sheet = workbook.Sheets[sheetName]

    // 转为 JSON 数组(每行为对象,key 为列标题)
    const rows = XLSX.utils.sheet_to_json(sheet, { defval: '' })

    if (rows.length === 0) {
    throw new Error('Excel 文件中没有数据')
    }

    const questions = []
    const errors = []

    for (let i = 0; i < rows.length; i++) {
    const row = rows[i]
    const rowNum = i + 2 // Excel 行号(第 1 行是表头)

    const stem = getCell(row, COL.stem, ['题目', '问题'])
    const rawAnswer = getCell(row, COL.answer, ['参考答案'])

    // 必填校验
    if (!stem && !rawAnswer) {
    continue // 完全空行跳过
    }
    if (!stem) {
    errors.push({ row: rowNum, reason: '题干为空' })
    continue
    }
    if (!rawAnswer) {
    errors.push({ row: rowNum, reason: '答案为空' })
    continue
    }

    // 提取选项 — 先从 Excel 列读取(兼容 '选项 A' / '选项A' / 'A' 等表头)
    let options = [
    getCell(row, COL.optionA, ['选项A', 'A']),
    getCell(row, COL.optionB, ['选项B', 'B']),
    getCell(row, COL.optionC, ['选项C', 'C']),
    getCell(row, COL.optionD, ['选项D', 'D']),
    getCell(row, COL.optionE, ['选项E', 'E']),
    getCell(row, COL.optionF, ['选项F', 'F']),
    getCell(row, COL.optionG, ['选项G', 'G']),
    getCell(row, COL.optionH, ['选项H', 'H']),
    ].filter(o => o !== '')

    // 选项列为空 → 从题干中自动提取
    let finalStem = stem
    if (options.length === 0) {
    const extracted = extractOptionsFromStem(stem)
    if (extracted.length >= 2) {
    options = extracted
    finalStem = cleanStemFromOptions(stem)
    }
    }

    // 题型判定:优先读取类型列,为空则自动识别
    let type
    const typeCell = getCell(row, COL.type, ['题型'])
    if (typeCell && TYPE_MAP[typeCell]) {
    type = TYPE_MAP[typeCell]
    } else {
    type = autoDetectType({ stem: finalStem, answer: rawAnswer, options })
    }

    const question = {
    stem: finalStem,
    answer: normalizeAnswer(rawAnswer, type),
    options,
    explanation: getCell(row, COL.explanation, ['解析内容']),
    type,
    domain: getCell(row, COL.domain, ['分类', '类别']),
    orderIndex: i,  // 保留 Excel 行顺序
    }

    questions.push(question)
    }

    return { questions, errors }
    }

// ============================================================
//  Excel 导出
// ============================================================

/**

  • 导出题目数组为 .xlsx 并触发下载
  • @param {Array} questions - 题目数组
  • @param {string} filename - 文件名
    */
    export function exportToExcel(questions, filename = '题库导出') {
    const rows = questions.map(q => ({
    [COL.optionA]: q.options[0] || '',
    [COL.optionB]: q.options[1] || '',
    [COL.optionC]: q.options[2] || '',
    [COL.optionD]: q.options[3] || '',
    [COL.optionE]: q.options[4] || '',
    [COL.optionF]: q.options[5] || '',
    [COL.optionG]: q.options[6] || '',
    [COL.optionH]: q.options[7] || '',
    [COL.explanation]: q.explanation || '',
    [COL.type]: TYPE_LABEL[q.type] || '',
    [COL.domain]: q.domain || '',
    }))

    const worksheet = XLSX.utils.json_to_sheet(rows, { header: ALL_COLS })
    // 设置列宽
    worksheet['!cols'] = [
    { wch: 40 }, // 题干
    { wch: 20 }, // 答案
    ...Array(8).fill({ wch: 15 }), // 选项 A~H
    { wch: 30 }, // 解析
    { wch: 10 }, // 类型
    { wch: 15 }, // 领域
    ]

    const workbook = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(workbook, worksheet, '题库')

    // 触发浏览器下载
    XLSX.writeFile(workbook, ${filename}.xlsx)
    }

// ============================================================
//  演示模板
// ============================================================

/* 演示模板的示例数据 /
const DEMO_QUESTIONS = [
{

[COL.answer]: 'B',
[COL.optionA]: '上海',
[COL.optionB]: '北京',
[COL.optionC]: '广州',
[COL.optionD]: '深圳',
[COL.optionE]: '', [COL.optionF]: '', [COL.optionG]: '', [COL.optionH]: '',
[COL.explanation]: '北京是中华人民共和国的首都。',
[COL.type]: '单选题',
[COL.domain]: '常识',

},
{

[COL.answer]: '对',
[COL.optionA]: '', [COL.optionB]: '', [COL.optionC]: '', [COL.optionD]: '',
[COL.optionE]: '', [COL.optionF]: '', [COL.optionG]: '', [COL.optionH]: '',
[COL.explanation]: '金星自转方向与公转方向相反,是太阳系中唯一逆向自转的行星。',
[COL.type]: '判断题',
[COL.domain]: '天文',

},
{

[COL.answer]: 'A,B,D',
[COL.optionA]: '太阳能',
[COL.optionB]: '风能',
[COL.optionC]: '煤炭',
[COL.optionD]: '水能',
[COL.optionE]: '', [COL.optionF]: '', [COL.optionG]: '', [COL.optionH]: '',
[COL.explanation]: '太阳能、风能、水能属于可再生能源;煤炭属于不可再生化石能源。',
[COL.type]: '多选题',
[COL.domain]: '能源',

},
{

[COL.answer]: '叶绿体',
[COL.optionA]: '', [COL.optionB]: '', [COL.optionC]: '', [COL.optionD]: '',
[COL.optionE]: '', [COL.optionF]: '', [COL.optionG]: '', [COL.optionH]: '',
[COL.explanation]: '光合作用在叶绿体中进行,线粒体是呼吸作用的场所。',
[COL.type]: '填空题',
[COL.domain]: '生物',

},
{

[COL.answer]: '四大发明是指南针、造纸术、火药、印刷术。它们对中国古代的政治、经济、文化发展产生了巨大推动作用,并经丝绸之路传播至西方,对世界文明进程产生了深远影响。',
[COL.optionA]: '', [COL.optionB]: '', [COL.optionC]: '', [COL.optionD]: '',
[COL.optionE]: '', [COL.optionF]: '', [COL.optionG]: '', [COL.optionH]: '',
[COL.explanation]: '答题要点:需完整列举四大发明名称,并阐述其对中国及世界的影响。',
[COL.type]: '主观题',
[COL.domain]: '历史',

},
]

/* 下载演示模板 Excel 文件 /
export function downloadDemoTemplate() {
const worksheet = XLSX.utils.json_to_sheet(DEMO_QUESTIONS, { header: ALL_COLS })
worksheet['!cols'] = [
{ wch: 45 }, // 题干
{ wch: 20 }, // 答案
...Array(8).fill({ wch: 15 }), // 选项
{ wch: 35 }, // 解析
{ wch: 10 }, // 类型
{ wch: 10 }, // 领域
]

const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, '题库模板')

XLSX.writeFile(workbook, '题库演示模板.xlsx')
}

// ============================================================
//  工具导出
// ============================================================

export { TYPE_MAP, TYPE_LABEL, autoDetectType }

免费评分

参与人数 21吾爱币 +17 热心值 +18 收起 理由
mywuwu + 1 + 1 我很赞同!
renxing + 1 + 1 谢谢@Thanks!
wyangdh + 1 + 1 谢谢@Thanks!
black911 + 1 我很赞同!
无极166 + 1 + 1 谢谢@Thanks!
火林xyh + 1 + 1 谢谢@Thanks!
gztf + 1 + 1 谢谢@Thanks!
tzxinqing + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
hanxuyao1959 + 1 用心讨论,共获提升!
yuppy34 + 1 + 1 谢谢@Thanks!
h07799486 + 1 + 1 谢谢@Thanks!
hy511 + 1 用心讨论,共获提升!
mryouan + 1 + 1 我很赞同!
zr2019 + 1 + 1 谢谢@Thanks!
cux666 + 1 热心回复!
youngnku + 1 + 1 热心回复!
sunhechao + 1 + 1 我很赞同!
阿巴阿巴阿巴 + 1 继续努力
yanglinman + 1 谢谢@Thanks!
wanderrr + 1 + 1 我很赞同!
jjkkii + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

erchuxin 发表于 2026-7-27 15:02
感谢大佬蓝奏分流:https://shichuang.lanzouv.com/i2KWC3z0e1yd

免费评分

参与人数 1热心值 +1 收起 理由
golency521 + 1 我很赞同!

查看全部评分

 楼主| 部落稻草人 发表于 2026-8-9 14:58
树下彩虹025 发表于 2026-8-8 22:42
题库是word或者pdf格式的,是否先要转换成excel,然后再导入?

需要,但是word转excel做的不是很完善,可以尝试给ai,让ai转,豆包什么的都可以完成
 楼主| 部落稻草人 发表于 2026-7-27 21:52
youngnku 发表于 2026-7-27 20:46
最后让他做成个能分享的格式,他又变成了一个html这个有关系,正常应该是js、css、html分离的,变量名也 ...

通过网盘分享的文件:分享
链接: https://pan.baidu.com/s/1kXw_R9cYPP40YRENAVtbqg?pwd=knn7 提取码: knn7
wintqin 发表于 2026-7-27 14:26
不错哦,再优化一下
lxw3254 发表于 2026-7-27 14:27
看似不错,试用一下
jjkkii 发表于 2026-7-27 14:28
可以 可以  用的着
GCYOPO 发表于 2026-7-27 14:29
nice!!!
u2000 发表于 2026-7-27 14:32
矮油不错哦~真是什么考试的题库
youngnku 发表于 2026-7-27 14:42
有没有好心人转个蓝盘,百度网盘没法用啊
e8vom 发表于 2026-7-27 14:53
可以可以,下个来试试
s466692282 发表于 2026-7-27 15:02
支持数学,物理公式不?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-12 17:41

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表