SOURCE

console 命令行工具 X clear

                    
>
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">&#10007; 错误</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>