兼容各操作系统平台的Anki选择题库模板
注:此模板已经更新。功能更强的模板请参阅:对兼容各操作系统的Anki选择题模板的更新——提供更方便的笔记修改功能_兼容各操作系统平台的anki选择题卡片-CSDN博客
〇、在Anki中创建一个空的选择题模板:
如上图, 通过工具菜单中的“管理笔记类型”命令打开笔记类型对话框,点击“添加”按钮添加一个“基础”类型类型的笔记,默认的名称类似于“基础-####”这样,点击重命名,将笔记类型的名称改为“选择题”或其他看着顺眼的名称,当然不改名也行。在“笔记类型”对话框左侧选择刚添加的笔记类型的名称,然后点击右边的“字段”按钮,即可添加所需的字段,添加完后可以将原来的字段删除;点击右边的“卡片”按钮,即可修改卡片模板的正面、背面和样式的模板。
一、字段设计
1、Question:选择题题干;
2、Options:选择题选项;
3、Answer:正确答案;
4、Extra:题目解析等附加信息。
二、目标
选择题计分规则为:单选正确得1分,错误得0分;多选如选择错误选项得0分,完全正确得2分,没有错误选项但是正确选项选择不完整(部分正确),所选的每个选项得0.5分。卡片正面显示题干和选项,选择选项后,点击显示答案进入背面,背面显示正确答案,累计得分,单多选题正确率等情况及解析信息。其中,正面显示的选项顺序随机排列,背面显示的选项顺序与正面要一致。正面和背面显示内容见以下图示。
正面:
背面
背面做错时:
三、实现分析
有个Monokai风格的选择题模板在windows系统里基本实现了上述功能,但不能跨平台,另外我原来下载的那个版本js代码也写得稀乱的(特别是选项组合部分,居然用字符串拼接而不用DOM操作),不过只要解决跨平台问题以及重新改写下js代码就可以达到目的了。实现以上功能的关键在于将做练习时在正面所选择的选项以及正面显示的选项顺序传递到背面,此外还需要在整个一次练习中共享已做练习数量、已做练习正误情况及得分等信息。要在记忆卡正面和背面以及不同练习题之间共享信息,就需要将这些信息用js脚本持久化。而不同平台(windows、mac、android、ios、linux等)对js对象持久化的支持并不统一,anki本身在不同平台上的实现在对卡片的解析方式也不统一,Anki的android客户端没有持久化的窗口属性,而桌面客户端不允许使用sessionStorage属性,对于linux和mac 2.1客户端,还需要额外的考虑:window属性,在审查模式下是持久的,在预览模式下是存在的--但不是持久的。github上有一个项目anki-persistence,提供了兼容各种平台实现anki中js对象持久化的功能,作者为Simon Lammer。该脚本内容如下:
// v0.5.3 - https://github.com/SimonLammer/anki-persistence/blob/7107e73086189c190c4d326ef11ebbcded9a08c6/script.js
if (void 0 === window.Persistence) {
var _persistenceKey = "github.com/SimonLammer/anki-persistence/",
_defaultKey = "_default";
if (window.Persistence_sessionStorage = function() {
var e = !1;
try {
"object" == typeof window.sessionStorage && (e = !0, this.clear = function() {
for (var e = 0; e < sessionStorage.length; e++) {
var t = sessionStorage.key(e);
0 == t.indexOf(_persistenceKey) && (sessionStorage.removeItem(t), e--)
}
}, this.setItem = function(e, t) {
void 0 == t && (t = e, e = _defaultKey), sessionStorage.setItem(_persistenceKey + e, JSON.stringify(t))
}, this.getItem = function(e) {
return void 0 == e && (e = _defaultKey), JSON.parse(sessionStorage.getItem(_persistenceKey + e))
}, this.removeItem = function(e) {
void 0 == e && (e = _defaultKey), sessionStorage.removeItem(_persistenceKey + e)
})
} catch (e) {}
this.isAvailable = function() {
return e
}
}, window.Persistence_windowKey = function(e) {
var t = window[e],
i = !1;
"object" == typeof t && (i = !0, this.clear = function() {
t[_persistenceKey] = {}
}, this.setItem = function(e, i) {
void 0 == i && (i = e, e = _defaultKey), t[_persistenceKey][e] = i
}, this.getItem = function(e) {
return void 0 == e && (e = _defaultKey), void 0 == t[_persistenceKey][e] ? null : t[_persistenceKey][e]
}, this.removeItem = function(e) {
void 0 == e && (e = _defaultKey), delete t[_persistenceKey][e]
}, void 0 == t[_persistenceKey] && this.clear()), this.isAvailable = function() {
return i
}
}, window.Persistence = new Persistence_sessionStorage, Persistence.isAvailable() || (window.Persistence = new Persistence_windowKey("py")), !Persistence.isAvailable()) {
var titleStartIndex = window.location.toString().indexOf("title"),
titleContentIndex = window.location.toString().indexOf("main", titleStartIndex);
titleStartIndex > 0 && titleContentIndex > 0 && titleContentIndex - titleStartIndex < 10 && (window.Persistence = new Persistence_windowKey("qt"))
}
}
此脚本有两个独立的持久性的内部实现:Persistence_sessionStorage和Persistence_windowKey。前者使用sessionStorage属性,而后者将一个新的属性附加到窗口对象现有的持久化(不会在anki记忆卡的正面和背面之间改变)属性上,并使用该附加属性来持久化和检索数据。使用方法是在正面模板和背面模板最开头用<script>标签将上述脚本包裹起来,在需要持久化js对象时调用Persistence.setItem(value)方法(当然还有Persistence.setItem(key, value)方法,具体见上述脚本代码),在需要读出持久化的对象时调用Persistence.getItem()方法。利用Simon Lammer的anki-persistence脚本,实现如前所述的选择题模板如下:
(20230714修订:使单选题的选项显示为单选框,且做单选题时不能选择多个选项。)
1、正面模板
<script>
// v0.5.3 - https://github.com/SimonLammer/anki-persistence/blob/7107e73086189c190c4d326ef11ebbcded9a08c6/script.js
if(void 0===window.Persistence){var _persistenceKey="github.com/SimonLammer/anki-persistence/",_defaultKey="_default";if(window.Persistence_sessionStorage=function(){var e=!1;try{"object"==typeof window.sessionStorage&&(e=!0,this.clear=function(){for(var e=0;e<sessionStorage.length;e++){var t=sessionStorage.key(e);0==t.indexOf(_persistenceKey)&&(sessionStorage.removeItem(t),e--)}},this.setItem=function(e,t){void 0==t&&(t=e,e=_defaultKey),sessionStorage.setItem(_persistenceKey+e,JSON.stringify(t))},this.getItem=function(e){return void 0==e&&(e=_defaultKey),JSON.parse(sessionStorage.getItem(_persistenceKey+e))},this.removeItem=function(e){void 0==e&&(e=_defaultKey),sessionStorage.removeItem(_persistenceKey+e)})}catch(e){}this.isAvailable=function(){return e}},window.Persistence_windowKey=function(e){var t=window[e],i=!1;"object"==typeof t&&(i=!0,this.clear=function(){t[_persistenceKey]={}},this.setItem=function(e,i){void 0==i&&(i=e,e=_defaultKey),t[_persistenceKey][e]=i},this.getItem=function(e){return void 0==e&&(e=_defaultKey),void 0==t[_persistenceKey][e]?null:t[_persistenceKey][e]},this.removeItem=function(e){void 0==e&&(e=_defaultKey),delete t[_persistenceKey][e]},void 0==t[_persistenceKey]&&this.clear()),this.isAvailable=function(){return i}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var titleStartIndex=window.location.toString().indexOf("title"),titleContentIndex=window.location.toString().indexOf("main",titleStartIndex);titleStartIndex>0&&titleContentIndex>0&&titleContentIndex-titleStartIndex<10&&(window.Persistence=new Persistence_windowKey("qt"))}}</script>
<!--正面模板-->
<div class="text" id="question">{{edit:Question}}</div>
<ol class="options" id="optionList"></ol>
<div id="options" style="display:none">{{Options}}</div>
<div id="answer" style="display:none">{{text:Answer}}</div>
<script>
var myinfo;
//if (Persistence.isAvailable()) {
myinfo = Persistence.getItem();
if (myinfo == null) {
myinfo = {
single: 0, //本次已做全部练习题中单选题数量
singleCorrect: 0, //本次已做全部练习题中单选题正确数量
multi: 0, //本次已做全部练习题中多选题数量
multiCorrect: 0, //本次已做全部练习题中多选题完全正确数量
partCorrect: 0, //本次已做全部练习题中多选部分正确数量
multiScore: 0, //本次已做全部练习题中多选题得分
score: 0, //当前所作练习题得分
sum: 0, //本次已做全部练习题累计得分
total: 0, //本次已做练习总数量
totalScore: 0, //本次已做练习满分
newOrderOps: [], //当前所作练习题打乱顺序后的选项
newOrderAnswer: '', //当前所作练习题打乱选项顺序后新的正确答案编号
choiced: '', //当前所作练习题选中的选项
ifright: '' //当前所作练习题选中的选项是否正确
};
}
myinfo.total++;
myinfo.choiced = '';
myinfo.newOrderAnswer = '';
myinfo.newOrderOps = [];
var question = document.getElementById("question");
//读入答案,去掉多余字符和空格
var correctAnswer = document.getElementById('answer').innerHTML
.toUpperCase().replace(/[^A-Z]+/, "");
if (correctAnswer.length > 1) { //正确答案大于一个为多选题
myinfo.totalScore += 2;
myinfo.multi++;
question.innerHTML = "<span class='imp'>【多选题】</span>" + question.innerHTML;
} else { //单选题
myinfo.totalScore++;
myinfo.single++;
question.innerHTML = "<span class='imp'>【单选题】</span>" + question.innerHTML;
}
var options = document.getElementById("options"),
optionList = document.getElementById("optionList");
var s = 0;
var indexs = [];
//处理原始顺序的选项,将div标签和br标签以及多余的换行替换掉
var options = options.innerHTML;
options = options.replace(/<\/?div>/g, "\n");
options = options.replace(/\n+/g, "\n");
options = options.replace(/<br.*?>/g, "\n");
options = options.replace(/^\n/, "");
options = options.replace(/\n$/, "");
//以换行符分隔选项为数组
options = options.split("\n");
//随机组合选项
for (var op in options) {
//随机产生一个索引,如果产生的索引已处理过,继续产生下一个索引,没处理过就中断循环开始处理
do {
s = Math.random() * (options.length);
s = Math.floor(s);
if (indexs.join().indexOf(s.toString()) == -1) {
indexs.push(s);
myinfo.newOrderOps.push(options[s]);
break;
}
} while (true);
//将随机产生的选项组合成li包着的input和label
list = document.createElement("li");
label = document.createElement("label");
label.innerHTML = options[s];
var input = document.createElement("input");
//根据答案字符长短判定应该用多选框还是单选框
input.type = correctAnswer.length > 1?"checkbox":"radio";
input.value = s;
input.name = "opts";//将选项成组,以防单选题可选择多个选项
input.id = "opts_" + s;
label.for = "opts_" + s;
list.addEventListener("click", clickOption);
list.appendChild(input);
list.appendChild(label);
optionList.appendChild(list);
}
for (i = 0; i < options.length; i++) {
//将正确答案的字母序号转换成打乱顺序后的字母编号,并记录到myinfo.newOrderAnswer中
if (correctAnswer.indexOf(String.fromCharCode(65 + indexs[i])) >= 0) {
myinfo.newOrderAnswer += String.fromCharCode(65 + i);
}
}
Persistence.setItem(myinfo);
//在选项li标签所在区域点击时,实际触发事件的可能是li、label或者input组件,无论是那个组件,都定位到checkbox
function clickOption(ev) {
var checkbox = ev.target;
var tagName = checkbox.tagName;
if (tagName == 'LI') {
checkbox = checkbox.children[0];
} else if (tagName == 'LABEL') {
checkbox = checkbox.parentNode.children[0];
}
checkbox.checked = 'checked';
var s = checkbox.value;
//在打乱顺序后的索引数组中找到选项的新数字序号,再转换成对应的字母编号
var ch = String.fromCharCode(65 + indexs.join('').indexOf(s.toString()));
if (myinfo.choiced.indexOf(ch) == -1) {
if(correctAnswer.length > 1){//多选题,在已选择项上加上一个新选项
myinfo.choiced += ch;
}else{//单选题,将已选择项变更为刚选的选项
myinfo.choiced = ch;
}
} else { //点击已选中的选项则取消该选项的选中状态
myinfo.choiced = myinfo.choiced.replace(ch, '');
checkbox.checked = null;
}
//if (Persistence.isAvailable()) {
Persistence.setItem(myinfo);
/*} else {
window.myinfo = myinfo;
}*/
//根据选项是否被选择赋予不同的显示样式
for (var j=0;j<optionList.children.length;j++) {
var ch = String.fromCharCode(65 + j)
if (myinfo.choiced.indexOf(ch) == -1) {
optionList.children[j].className = "unchoiced";
} else {
optionList.children[j].className = "choiced";
}
}
}
/*} else {//无法持久化js对象,只能针对单面单题练习
}*/
</script>
2、背面模板
<!--背面模板-->
<script>
// v0.5.3 - https://github.com/SimonLammer/anki-persistence/blob/7107e73086189c190c4d326ef11ebbcded9a08c6/script.js
if(void 0===window.Persistence){var _persistenceKey="github.com/SimonLammer/anki-persistence/",_defaultKey="_default";if(window.Persistence_sessionStorage=function(){var e=!1;try{"object"==typeof window.sessionStorage&&(e=!0,this.clear=function(){for(var e=0;e<sessionStorage.length;e++){var t=sessionStorage.key(e);0==t.indexOf(_persistenceKey)&&(sessionStorage.removeItem(t),e--)}},this.setItem=function(e,t){void 0==t&&(t=e,e=_defaultKey),sessionStorage.setItem(_persistenceKey+e,JSON.stringify(t))},this.getItem=function(e){return void 0==e&&(e=_defaultKey),JSON.parse(sessionStorage.getItem(_persistenceKey+e))},this.removeItem=function(e){void 0==e&&(e=_defaultKey),sessionStorage.removeItem(_persistenceKey+e)})}catch(e){}this.isAvailable=function(){return e}},window.Persistence_windowKey=function(e){var t=window[e],i=!1;"object"==typeof t&&(i=!0,this.clear=function(){t[_persistenceKey]={}},this.setItem=function(e,i){void 0==i&&(i=e,e=_defaultKey),t[_persistenceKey][e]=i},this.getItem=function(e){return void 0==e&&(e=_defaultKey),void 0==t[_persistenceKey][e]?null:t[_persistenceKey][e]},this.removeItem=function(e){void 0==e&&(e=_defaultKey),delete t[_persistenceKey][e]},void 0==t[_persistenceKey]&&this.clear()),this.isAvailable=function(){return i}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var titleStartIndex=window.location.toString().indexOf("title"),titleContentIndex=window.location.toString().indexOf("main",titleStartIndex);titleStartIndex>0&&titleContentIndex>0&&titleContentIndex-titleStartIndex<10&&(window.Persistence=new Persistence_windowKey("qt"))}}</script>
<div id="performance">正确率:100%</div>
<hr />
<div class="text">{{edit:Question}}</div>
<ol class="options" id="optionList"></ol>
<hr />
<div id="key" class="green"><span class="small_text">上面的选项以<span class="blue">此种形式</span>显示的为被你选中的正确选项,以<span class="green">此种形式</span>显示的为未被你选中的正确选项,以<span class="wrong">此种形式</span>显示的不是正确选项却被你选中了。本题结果如下:</sapn><br/></div>
<hr>
<div class="extra">
<sapn class="blue">【解析】</sapn><br>{{edit:Extra}}</div>
<script>
var myinfo;
myinfo = Persistence.getItem();
//计算成绩的函数
function calcScore() {
if (myinfo.choiced.length == 0) {
myinfo.ifright = "为什么一个都不选?"
myinfo.score = 0;
} else {
myinfo.score = 1;
for (var i = 0; i < myinfo.choiced.length; i++) {
if (myinfo.newOrderAnswer.indexOf(myinfo.choiced.charAt(i)) == -1) {
myinfo.score = 0;
myinfo.ifright = "错误";
break;
}
}
if (myinfo.score != 0) {
if (myinfo.newOrderAnswer.length == 1) {
myinfo.singleCorrect++;
myinfo.score = 1;
myinfo.ifright = "完全正确";
} else {
if (myinfo.choiced.length == myinfo.newOrderAnswer.length) {
myinfo.multiCorrect++;
myinfo.multiScore += 2;
myinfo.score = 2;
myinfo.choiced = myinfo.newOrderAnswer;
myinfo.ifright = "完全正确";
} else {
myinfo.partCorrect++;
myinfo.score = myinfo.choiced.length * 0.5;
myinfo.multiScore += myinfo.score;
myinfo.ifright = "不完全正确";
}
}
}
}
myinfo.sum += myinfo.score;
Persistence.setItem(myinfo);
}
//显示选项
var optionOl = document.getElementById("optionList");
ops = myinfo.newOrderOps;
for (var i = 0; i < ops.length; i++) {
var ch = String.fromCharCode(65 + i);
list = document.createElement("li");
label = document.createElement("label");
label.innerHTML = ops[i];
var input = document.createElement("input");
//根据选择的答案是否有多个字符判断应选用多选框还是单选框
input.type = myinfo.choiced.length > 1?"checkbox":"radio";
list.appendChild(input);
list.appendChild(label);
optionOl.appendChild(list);
if (myinfo.newOrderAnswer.indexOf(ch) >= 0) {
if (myinfo.choiced.indexOf(ch) >= 0) {
list.className = 'blue';
input.checked = 'checked';
} else {
list.className = 'green';
}
} else {
if (myinfo.choiced.indexOf(ch) >= 0) {
list.className = 'wrong';
input.checked = 'checked';
} else {
list.className = 'unchoiced'
}
}
}
//显示成绩
calcScore();
var performance = document.getElementById("performance");
var key = document.getElementById("key");
var total = myinfo.single + myinfo.multi;
if (typeof(myinfo) != "undefined") {
var singlePer = myinfo.single == 0 ? "100.00" :
((myinfo.singleCorrect / myinfo.single) * 100).toFixed(2);
var multiErr = myinfo.multi - myinfo.multiCorrect - myinfo.partCorrect
var multiPer = myinfo.multi == 0 ? "100.00" :
((myinfo.multiScore / (myinfo.multi * 2)) * 100).toFixed(2);
var scorePer = ((myinfo.sum / myinfo.totalScore) * 100).toFixed(2)
performance.innerHTML = "本次练习<span class='imp'>" + total +
"</span>题---单选题<span class='imp'>" + myinfo.single +
"</span>题---多选题<span class='imp'>" + myinfo.multi +
"</span>题;<br>单选正确<span class='imp'>" + myinfo.singleCorrect +
"</span>题---单选正确率<span class='imp'>" + singlePer +
"%</span>;<br>多选正确<span class='imp'>" + myinfo.multiCorrect +
"</span>题---多选部分正确<span class='imp'>" + myinfo.partCorrect +
"</span>题---多选错误<span class='imp'>" + multiErr +
"</span>题---多选得分<span class='imp'>" + myinfo.multiScore +
"</span>分---多选得分率<span class='imp'>" + multiPer +
"%</span>;<br>累计得分:<span class='imp'>" + myinfo.sum +
"</span>分---已做题目满分<span class='imp'>" + myinfo.totalScore +
"</span>分---得分率<span class='imp'>" + scorePer + "%</span>";
key.innerHTML += "<div>正确答案:<span class='imp'>" + myinfo.newOrderAnswer +
";</span>你的答案:<span class='imp'>" + myinfo.choiced +
";</span>结果判定:<span class='imp'>" +
myinfo.ifright + "</span>;本题得分:<span class='imp'>" +
myinfo.score + "</span>。</div>";
}
//} else {}
</script>
3、样式
.card { font-family: 微软雅黑; font-size:1.2em; text-align:left;
color: white; background-color:#000;letter-spacing:2px;}
table{border-collapse:collapse; }
td{padding:5px;text-align:center;border:2px solid green;vertical-align: middle;}
td.left{text-align:left;}
td.red{
border-right: solid thick red;
}
hr{border: none;
height: 5px;
background-color:yellow;}
p{text-indent:2em;}
div{margin:5px auto }
.text{color:#ff0;font-weight:bold;font-size:1.2em;}
.small_text{color:#ff0;font-weight:bold;font-size:0.9em;}
.imp,a,a:visited,a:hover,a:link,a:active{color:#F90;font:normal bold 微软雅黑,sans-serif;}
.hint{color:#a6e22e;}
.unchoiced{ color: white;}
.choiced{font-weight: bold; color: #0f3;background-color:#a0a;}
.extra{ margin-top:15px; font-size:1.2em; line-height:1.5em;color: #eeeebb; text-align:left;}
.green,i{ font-weight: bold; font-style:normal;color: #0f0;}
.blue,b{ font-weight: bold; font-style:normal;color: #3ff;}
.wrong{ font-weight: bold; color: red;text-decoration:line-through;}
.options{ list-style:upper-latin;font-size:1.2em;}
.options *{ cursor:pointer;}
/*.options *:hover{ font-weight:bold;color: #f90;}*/
.options li{ margin-top:0.8em;}
/*下面两行样式定义决定是否显示选项前面的圆形或方形框,注释掉就会显示*/
.options input[type="radio"]{display:none;}
.options input[type="checkbox"]{display:none;}
#performance{ text-align:left; font-size:16px;}
</style>
对css作了进一步修改:考虑到a、b、i标签比带class属性的span标签更容易输入,所以将这几个标签都加入了样式表。a标签等价于class属性为imp的span标签,b标签等价于class属性为blue的span标签,i标签等价于class属性为green的span标签。这样,在anki中修改文字样式时只需要选中文字后点击编辑界面上的B和I按钮就相当于输入了对应的span标签,利用Edit Field During Review插件修改卡片时,只需输入a、b、i标签,而无需输入<span class="###">这样一长串字符。
上述模板中js代码逻辑比较简单,并且有不少注释,不再细述。代码有些是拷贝的以前的,所以风格不完全一致,也没有做模块化的函数包装,各位可以自行优化。css样式最小的需要只有代码里出现过的类名需要定义,上面的样式可以简化以及重新编写,仅供参考。此前在csdn中看到一篇帖子问pc可以正常运行的选择题模板为什么在手机上不能正确运行,当时我回了个帖子说有空了直接贴代码,希望那一位真的能看到这个代码。顺便说下,我用这个模板做了全套一建二建的题库,使用效果良好。
更多推荐
所有评论(0)