console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IAT 内隐联想测验</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-user-select:none; user-select:none; }
html,body { height:100%; }
body { font-family:"Microsoft YaHei",sans-serif; background:#f0f0f0; }
#screen { width:100%; height:100%; display:flex; flex-direction:column;
align-items:center; justify-content:center; font-size:20px; }
.cat { font-size:34px; font-weight:bold; color:#333; padding:20px;
background:#fff; border:3px solid #ccc; border-radius:12px; min-width:200px; text-align:center; }
.feedback { font-size:24px; font-weight:bold; margin-top:20px; }
.ok { color:#2ecc71; } .err { color:#e74c3c; }
.ins { text-align:left; line-height:1.8; padding:20px; max-width:500px; }
.ins h1 { text-align:center; margin-bottom:16px; }
.input-box { width:85%; margin:0 auto; padding:14px; font-size:22px; text-align:center;
border:2px solid #ccc; border-radius:8px; }
#btns { position:fixed; left:0; right:0; bottom:0; height:22%; display:none; z-index:10; }
.btn { flex:1; display:flex; align-items:center; justify-content:center;
font-size:24px; font-weight:bold; color:#fff; }
.btnL { background:#3498db; border-right:3px solid #fff; }
.btnR { background:#e67e22; }
.btn:active{ filter:brightness(0.8); }
.start-btn { display:block; margin:24px auto 0; width:80%; padding:16px;
background:#2ecc71; color:#fff; font-size:22px; font-weight:bold;
border:none; border-radius:10px; }
</style>
</head>
<body>
<div id="screen"></div>
<div id="btns">
<div class="btn btnL" id="btnL"></div>
<div class="btn btnR" id="btnR"></div>
</div>
<script>
var SELF_WORDS=['我','自己','本人','咱','俺','我们'];
var OTHER_WORDS=['他','别人','他人','他们','外人','人家'];
var THIN_WORDS=['苗条','纤细','瘦身','轻盈','清瘦','骨感'];
var FAT_WORDS=['肥胖','臃肿','粗壮','敦实','肥硕','丰满'];
var ITI_MS=300, ERROR_MS=400;
var PARTICIPANT_ID='';
var phaseData=[];
var rawData=[];
var scr=document.getElementById('screen');
var btns=document.getElementById('btns');
var btnL=document.getElementById('btnL'), btnR=document.getElementById('btnR');
var respCb=null;
function sleep(ms){ return new Promise(function(r){ setTimeout(r,ms); }); }
function showHTML(h){ scr.innerHTML=h; }
function setBtns(lblL,lblR){ btnL.innerHTML=lblL; btnR.innerHTML=lblR; }
function showBtns(){ btns.style.display='flex'; }
function hideBtns(){ btns.style.display='none'; }
btnL.addEventListener('touchstart',function(e){ e.preventDefault(); if(respCb){ respCb('left'); } },{passive:false});
btnR.addEventListener('touchstart',function(e){ e.preventDefault(); if(respCb){ respCb('right'); } },{passive:false});
function pressOnce(){
return new Promise(function(resolve){ respCb=function(side){ respCb=null; resolve(side); }; });
}
function waitTap(){
return new Promise(function(resolve){
var b=document.getElementById('startBtn');
b.addEventListener('touchstart',function(e){ e.preventDefault(); resolve(); },{passive:false});
});
}
function waitStartWithID(){
return new Promise(function(resolve){
var input=document.getElementById('pid');
var b=document.getElementById('startBtn');
b.addEventListener('touchstart',function(e){ e.preventDefault();
PARTICIPANT_ID=(input.value||'').trim(); resolve();
},{passive:false});
});
}
// 单试次:correctSide 用 'L'/'R',按钮返回 'left'/'right',统一转成 left/right 比较
async function iatTrial(word, correctSide, record){
showHTML('<div class="cat">'+word+'</div>');
showBtns();
var t0=performance.now();
var side=await pressOnce(); // 'left' 或 'right'
hideBtns();
var rt=performance.now()-t0;
var correct= (side===(correctSide==='L'?'left':'right')); // 修复:转换后再比
if(record){ rawData.push({word:word,correctSide:correctSide,resp:side,correct:correct?1:0,rt:Math.round(rt)}); }
if(!correct){
showHTML('<div class="cat">'+word+'</div><div class="feedback err">✗ 错误</div>');
await sleep(ERROR_MS);
}
return {correct:correct, rt:rt};
}
async function runBlock(block, recordData){
for(var i=0;i<block.trials.length;i++){
var word=block.trials[i][0], side=block.trials[i][1];
var res=await iatTrial(word, side, recordData);
if(recordData){ phaseData.push({block:block.name, word:word, correct:res.correct?1:0, rt:Math.round(res.rt)}); }
await sleep(ITI_MS);
}
}
function makeTrials(leftWords,rightWords,n){
var pool=[];
for(var i=0;i<leftWords.length;i++){ pool.push([leftWords[i],'L']); }
for(var j=0;j<rightWords.length;j++){ pool.push([rightWords[j],'R']); }
var out=[];
while(out.length<n){
for(var k=pool.length-1;k>0;k--){ var m=Math.floor(Math.random()*(k+1)); var tmp=pool[k]; pool[k]=pool[m]; pool[m]=tmp; }
for(var s=0;s<pool.length && out.length<n;s++){
if(out.length>0 && out[out.length-1][0]===pool[s][0]) continue;
out.push(pool[s]);
}
}
return out;
}
async function blockIntro(phase,text){
showHTML('<div class="ins"><h1>阶段 '+phase+'</h1><p>'+text+'</p>'+
'<button class="start-btn" id="startBtn">准备好了,开始</button></div>');
await waitTap();
}
async function runIAT(){
// 阶段1:概念词分类(自我 vs 他人)
await blockIntro(1,'请把词语归类。<br>点左边蓝色按钮 = <b style="color:#3498db">自我</b>类<br>点右边橙色按钮 = <b style="color:#e67e22">他人</b>类<br><br>尽量快,同时减少错误。');
setBtns('自我','他人');
await runBlock({name:'b1_concept_practice',trials:makeTrials(SELF_WORDS,OTHER_WORDS,10)},false);
// 阶段2:属性词分类(瘦 vs 胖)
await blockIntro(2,'请把词语归类。<br>点左边蓝色按钮 = <b style="color:#3498db">瘦</b>相关词<br>点右边橙色按钮 = <b style="color:#e67e22">胖</b>相关词');
setBtns('瘦','胖');
await runBlock({name:'b2_attr_practice',trials:makeTrials(THIN_WORDS,FAT_WORDS,10)},false);
// 阶段3:联合1练习
await blockIntro(3,'现在把两类合在一起。<br>点左边蓝色按钮 = <b style="color:#3498db">自我 或 瘦</b><br>点右边橙色按钮 = <b style="color:#e67e22">他人 或 胖</b><br><br>这是练习。');
setBtns('自我 或 瘦','他人 或 胖');
await runBlock({name:'b3_combined_prac',trials:makeTrials(SELF_WORDS.concat(THIN_WORDS),OTHER_WORDS.concat(FAT_WORDS),20)},false);
// 阶段4:联合1正式
await blockIntro(4,'同样的任务,正式记录。<br>点左边蓝色按钮 = <b style="color:#3498db">自我 或 瘦</b><br>点右边橙色按钮 = <b style="color:#e67e22">他人 或 胖</b>');
setBtns('自我 或 瘦','他人 或 胖');
await runBlock({name:'b4_combined_test',trials:makeTrials(SELF_WORDS.concat(THIN_WORDS),OTHER_WORDS.concat(FAT_WORDS),40)},true);
// 阶段5:概念反转
await blockIntro(5,'下面任务改变了,请留意!<br>点左边蓝色按钮 = <b style="color:#3498db">他人</b><br>点右边橙色按钮 = <b style="color:#e67e22">自我</b>');
setBtns('他人','自我');
await runBlock({name:'b5_reversal',trials:makeTrials(OTHER_WORDS,SELF_WORDS,10)},false);
// 阶段6:联合2练习
await blockIntro(6,'现在两类合在一起。<br>点左边蓝色按钮 = <b style="color:#3498db">他人 或 瘦</b><br>点右边橙色按钮 = <b style="color:#e67e22">自我 或 胖</b><br><br>这是练习。');
setBtns('他人 或 瘦','自我 或 胖');
await runBlock({name:'b6_combined2_prac',trials:makeTrials(OTHER_WORDS.concat(THIN_WORDS),SELF_WORDS.concat(FAT_WORDS),20)},false);
// 阶段7:联合2正式
await blockIntro(7,'同样的任务,正式记录。<br>点左边蓝色按钮 = <b style="color:#3498db">他人 或 瘦</b><br>点右边橙色按钮 = <b style="color:#e67e22">自我 或 胖</b>');
setBtns('他人 或 瘦','自我 或 胖');
await runBlock({name:'b7_combined2_test',trials:makeTrials(OTHER_WORDS.concat(THIN_WORDS),SELF_WORDS.concat(FAT_WORDS),40)},true);
}
function computeD(){
var block4=phaseData.filter(function(x){return x.block==='b4_combined_test';});
var block7=phaseData.filter(function(x){return x.block==='b7_combined2_test';});
function clean(block){
return block.filter(function(x){return x.correct===1 && x.rt>300 && x.rt<10000;}).map(function(x){return x.rt;});
}
var rt4=clean(block4), rt7=clean(block7);
if(rt4.length<2 || rt7.length<2){ return {d:null,msg:'数据不足'}; }
function mean(a){ return a.reduce(function(s,v){return s+v;},0)/a.length; }
function sd(a,m){ return Math.sqrt(a.reduce(function(s,v){return s+(v-m)*(v-m);},0)/(a.length-1)); }
var m4=mean(rt4), m7=mean(rt7);
var pooled=Math.sqrt((sd(rt4,m4)*sd(rt4,m4)+sd(rt7,m7)*sd(rt7,m7))/2);
if(pooled<1){ return {d:null,msg:'标准差过小'}; }
return {d:(m4-m7)/pooled, m4:m4, m7:m7};
}
async function main(){
showHTML(
'<div class="ins"><h1>内隐联想测验(IAT)</h1>'+
'<p><b>请先填写您的编号:</b></p>'+
'<input class="input-box" id="pid" type="text" placeholder="请输入编号">'+
'<p style="margin-top:16px;">这是一个词语分类任务。屏幕中间会依次出现一个词,'+
'请判断它属于哪一类,点击屏幕底部对应的按钮(左=蓝色,右=橙色)。</p>'+
'<p>全程共7个阶段,大约需要10分钟。请尽量快,同时保持准确。</p>'+
'<button class="start-btn" id="startBtn">填好编号,开始实验</button></div>');
await waitStartWithID();
await runIAT();
var res=computeD();
var d=res.d;
var interpretation=(d===null)?res.msg:(d<0?'内隐关联偏向「自我=瘦」':'内隐关联偏向「自我=胖」');
showHTML('<div class="ins"><h1>实验完成</h1>'+
'<p>编号:'+PARTICIPANT_ID+'</p>'+
'<p>D 分数:'+(d===null?'无法计算':d.toFixed(3))+'</p>'+
'<p>解读:'+interpretation+'</p>'+
'<p style="color:#888;font-size:14px;">完整逐试次数据已输出到控制台(F12→Console)</p></div>');
console.log(JSON.stringify(rawData));
}
main();
</script>
</body>
</html>