让你的Protocol“动”起来

引导ChatGPT做了一个网页模板,感觉这样做实验的时候,能一直翻页看到实验进度在往前走,有那么一点炫酷。用电脑或者平板打开,手机页面适配没做,会字很小很丑。

效果:RIP-seq Protocol

不同的实验可以套用相同代码,在Json文件中改实验步骤就可以了。

以下为源码:

index.html

<!DOCTYPE html>
<html>
<head>
 <title></title>
 <link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1></h1>
 <div id="progress-bar">
   <div id="progress"></div>
   <span id="progress-label"></span>
 </div>

<div class="steps-wrapper">
 <div id="steps-container"></div>
 <div id="steps-content">
   <h2></h2>
 </div>
</div>

<div class="substeps-wrapper">
 <div id="substeps-container"></div>
 <div id="substeps-content">
   <h3></h3>
   <p></p>
 </div>
</div>

 <button id="prev-btn">上一步</button>
 <button id="next-btn">下一步</button>

 <script src="script.js"></script>
</body>
</html>

script.js

document.addEventListener("DOMContentLoaded", function(event) {
   var stepsContainer = document.getElementById("steps-container");
   var stepsContent = document.getElementById("steps-content");
   var currentStep = 0;

   var substepsContainer = document.getElementById("substeps-container");
   var substepsContent = document.getElementById("substeps-content");
   var currentSubstep = 0;

   var prevButton = document.getElementById("prev-btn");
   var nextButton = document.getElementById("next-btn");

   var steps = [];
   var title = '';

   function loadInputData() {
     var xhr = new XMLHttpRequest();
     xhr.overrideMimeType("application/json");
     xhr.open("GET", "input.json", true);
     xhr.onreadystatechange = function() {
       if (xhr.readyState === 4 && xhr.status === 200) {
         var data = JSON.parse(xhr.responseText);
         steps = data.steps;
         title = data.title;
         document.getElementsByTagName("title")[0].innerHTML = title;   //打印标题
         document.getElementsByTagName("h1")[0].innerHTML = title;   //打印标题
         renderSteps();
         renderSubsteps();
         updateButtons();
         updateProgressBar();
      }
    };
     document.addEventListener("keydown", function(event) {
       if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
         prevButton.click(); // 模拟点击“上一步”按钮
      } else if (event.key === "ArrowDown" || event.key === "ArrowRight") {
         nextButton.click(); // 模拟点击“下一步”按钮
      }
    });
     xhr.send();
  }

   function renderSteps() {
     var stepsHTML = "";
     for (var i = 0; i < steps.length; i++) {
       stepsHTML += '<button class="step-btn" data-step="' + i + '">' + steps[i].title + '</button>';
    }
     stepsContainer.innerHTML = stepsHTML;
     var stepButtons = document.getElementsByClassName("step-btn");
     for (var j = 0; j < stepButtons.length; j++) {
       stepButtons[j].addEventListener("click", function() {
         currentStep = parseInt(this.getAttribute("data-step"));
         currentSubstep = 0;
       //输入Steps内容
        var currentStepObj = steps[currentStep];
        if (currentStepObj) {
       stepsContent.getElementsByTagName("h2")[0].innerHTML = steps[currentStep].title;
      } else {
       stepsContent.getElementsByTagName("h2")[0].innerHTML = "";
      }
         renderSubsteps();
         updateButtons();
         updateProgressBar();
      });
    }
           //输入Steps内容
           var currentStepObj = steps[currentStep];
           if (currentStepObj) {
             stepsContent.getElementsByTagName("h2")[0].innerHTML = steps[currentStep].title;
          } else {
             stepsContent.getElementsByTagName("h2")[0].innerHTML = "";
          }
  }

   function renderSubsteps() {
     var currentStepObj = steps[currentStep];
     var substepsHTML = "";

     if (currentStepObj.substeps) {
       for (var k = 0; k < currentStepObj.substeps.length; k++) {
         var substep = currentStepObj.substeps[k];
         var substepClass = k === currentSubstep ? "substep-btn current-substep" : "substep-btn";
         substepsHTML += '<button class="' + substepClass + '" data-substep="' + k + '">' + substep.title + '</button>';
      }
    }

     substepsContainer.innerHTML = substepsHTML;

     var substepButtons = document.getElementsByClassName("substep-btn");
     for (var l = 0; l < substepButtons.length; l++) {
       substepButtons[l].addEventListener("click", handleSubstepButtonClick);
    }

     if (currentStepObj.substeps) {
       substepsContent.style.display = "block";
    } else {
       substepsContent.style.display = "none";
    }

     // 移除之前插入的图片元素
     var oldImage = substepsContent.querySelector("img");
     if (oldImage) {
      oldImage.remove();
    }
     if (currentStepObj.substeps && currentStepObj.substeps[currentSubstep]) {
       substepsContent.getElementsByTagName("h3")[0].innerHTML = currentStepObj.substeps[currentSubstep].title;
       substepsContent.getElementsByTagName("p")[0].innerHTML = currentStepObj.substeps[currentSubstep].content;
    } else {
       substepsContent.getElementsByTagName("h3")[0].innerHTML = "";
       substepsContent.getElementsByTagName("p")[0].innerHTML = "";
    }

     // 移除之前插入的图片元素
     var oldImage = substepsContent.querySelector("img");
     if (oldImage) {
       oldImage.parentNode.removeChild(oldImage);
    }

     // 插入新的图片元素
     if (currentStepObj.substeps && currentStepObj.substeps[currentSubstep] && currentStepObj.substeps[currentSubstep].image) {
       var image = document.createElement("img");
       image.src = currentStepObj.substeps[currentSubstep].image;
       substepsContent.appendChild(image);
    }
  }

   function handleSubstepButtonClick() {
     var substepIndex = parseInt(this.getAttribute("data-substep"));
     if (substepIndex !== currentSubstep) {
       currentSubstep = substepIndex;
       renderSubsteps();
       updateButtons();
       updateProgressBar();
    }
  }



   function updateButtons() {
     var currentStepObj = steps[currentStep];
     prevButton.disabled = currentStep === 0 && currentSubstep === 0;
     nextButton.disabled = currentStep === steps.length - 1 && currentSubstep === currentStepObj.substeps.length - 1;

  }

   function updateProgressBar() {
     var totalSteps = steps.length;
     var totalSubsteps = steps[currentStep].substeps ? steps[currentStep].substeps.length : 0;

     var progress = totalSubsteps ? ((currentStep + ((currentSubstep + 1) / (totalSubsteps + 0))) / totalSteps) * 100 : currentStep * 100;
     document.getElementById("progress").style.width = progress + "%";
     var progressLabel = document.getElementById("progress-label");
     var totalProgress = 0;
     for (var k = 0; k < steps.length; k++) {
       totalProgress += steps[k].substeps.length;
    }
     var currentProgress = 0;
     for (var i = 0; i < currentStep; i++) {
       currentProgress += steps[i].substeps.length;
    }
     currentProgress += currentSubstep;
       progressLabel.textContent = "Progress: " + (currentProgress + 1) + "/" + totalProgress; // 更新进度标签的文本内容

     var stepButtons = document.getElementsByClassName("step-btn");
     for (var i = 0; i < stepButtons.length; i++) {
       if (i < currentStep) {
         stepButtons[i].classList.remove("current-step");
         stepButtons[i].classList.add("completed-step");
      } else if (i === currentStep) {
         stepButtons[i].classList.add("current-step");
         stepButtons[i].classList.remove("completed-step");
      } else {
         stepButtons[i].classList.remove("current-step");
         stepButtons[i].classList.remove("completed-step");
      }
    }
     stepButtons[currentStep].scrollIntoView({ block: "nearest", inline: "center" }); //画面外的按钮自动滚动至画面中央

     var substepButtons = document.getElementsByClassName("substep-btn");
     for (var j = 0; j < substepButtons.length; j++) {
       if (j < currentSubstep) {
         substepButtons[j].classList.remove("current-substep");
         substepButtons[j].classList.add("completed-substep");
      } else if (j === currentSubstep) {
         substepButtons[j].classList.add("current-substep");
         substepButtons[j].classList.remove("completed-substep");
      } else {
         substepButtons[j].classList.remove("current-substep");
         substepButtons[j].classList.remove("completed-substep");
      }
    }
     substepButtons[currentSubstep].scrollIntoView({ block: "nearest", inline: "center" }); //画面外的按钮自动滚动至画面中央
  }

   prevButton.addEventListener("click", function() {
     if (currentSubstep > 0) {
       currentSubstep--;
    } else if (currentStep > 0) {
       currentStep--;
       currentSubstep = steps[currentStep].substeps.length - 1;
    }
     renderSteps();
     renderSubsteps();
     updateButtons();
     updateProgressBar();

  });

   nextButton.addEventListener("click", function() {
     var currentStepObj = steps[currentStep];

     if (currentSubstep < currentStepObj.substeps.length - 1) {
       currentSubstep++;
    } else if (currentStep < steps.length - 1) {
       currentStep++;
       currentSubstep = 0;
    }
     renderSteps();
     renderSubsteps();
     updateButtons();
     updateProgressBar();
  });


   loadInputData();
});

styles.css

body {
   font-family: 'Roboto', Arial, sans-serif;
   background-color: #f1f1f1;
   color: #333;
   margin: 0;
   padding: 20px;
   border: 1px solid #ccc;
   border-radius: 8px;
   font-size: 16px;
}

 h1 {
   text-align: center;
}

 #progress-bar {
   width: 100%;
   height: 20px;
   background-image: linear-gradient(to right, #ffffff, #a9a8a8);
   margin-bottom: 20px;
   position: relative;
   border: 1px solid #ccc;
   border-radius: 4px;
}

 #progress {
   height: 100%;
   background-color: #0077b6;
   width: 0;
   transition: width 0.3s ease-in-out;
   border: 1px solid #ccc;
   border-radius: 4px;
}

 #steps-container {
   justify-content: center;
   margin-bottom: 20px;
   overflow-x: scroll;
   white-space: nowrap;
   background-color: #fff;
   border: 1px solid #ccc;
   border-radius: 8px;
   padding: 10px;
}

 .step-btn {
   font-size: 16px;
   padding: 0.5rem;
   margin-right: 10px;
   background-color: #f1f1f1;
   border-radius: 5px;
   border-width: 0;
   cursor: pointer;
   min-width: 8rem;
   height: 4rem;
   overflow: hidden;
   text-overflow: ellipsis;
   box-shadow: 0 2px 4px rgba(0, 180, 216, 0.3);
   color: #000000;
}

 .step-btn.active {
   background-color: #0077b6;
}

 #substeps-container {
   overflow-x: scroll;
   white-space: nowrap;
   justify-content: center;
   margin-bottom: 20px;
   background-color: #fff;
   border: 1px solid #ccc;
   border-radius: 8px;
   padding: 10px;
}

 .substep-btn {
   font-size: 1rem;
   padding: 0.3rem;
   margin-right: 10px;
   background-color: #f1f1f1;
   border-radius: 5px;
   border-width: 0;
   cursor: pointer;
   width: 6rem;
   height: 2rem;
   overflow: hidden;
   text-overflow: ellipsis;
   color: #333;
}

 .substep-btn.active {
   background-color: #ccc;
}

 #prev-btn,
 #next-btn {
   font-size: 16px;
   padding: 10px 20px;
   background-color: #5085df;
   color: #fff;
   border: none;
   border-radius: 10px;
   margin: 10px;
   cursor: pointer;
}

 #prev-btn:hover,
 #next-btn:hover {
   background-color: #00a0d8;
}

 #prev-btn:disabled,
 #next-btn:disabled {
   background-color: #ccc;
   cursor: not-allowed;
}

 .current-step {
   background-color: #fff200f5;
}

 .completed-step {
   background-color: #9ccaeb;
}

 .current-substep {
   background-color: #fff200f5;
}

 .completed-substep {
   background-color: #9ccaeb;
}

 #steps-content {
   height: 2rem;
}

 #substeps-content {
   height: 18rem;
   overflow-y: scroll;
   background-color: #fff;
   color: #333;
   border: 1px solid #ccc;
   border-radius: 8px;
   padding: 10px;
}

 html {
   scroll-behavior: smooth;
}

/*限制图片宽度*/
 #substeps-content img{
max-width: 100%;
}
​
/*下面这个适配手机屏幕好像有点问题,不过懒得修了,反正也不怎么用手机*/
@media screen and (max-width: 480px) {
 #steps-content {
   height: 5rem;
}
 #substeps-content {
   height: 60rem;
}
 .substep-btn {
   width: 20rem;
   height: 5rem;
}  
 .step-btn {
   width: 25rem;
   height: 10rem;
}
}

input.json

{
 "title": "RIP-seq Protocol",
 "steps": [
  {
     "title": "1. cell cuture",
     "substeps": [
      {
         "title": "1.1",
         "content": "NCCIT细胞每个样品养到6孔板的两个孔长满,大概5~6百万细胞",
         "image": "https://fanyiming.life/images/avatar.jpg"
      }
    ]
  },
  {
     "title": "2. Beads preparation",
     "substeps": [
      {
         "title": "2.1",
         "content": "每个样品用40μl ProteinG Dyna beads,取样品数 * 40μL",
         "image": ""
      },
      {
         "title": "2.2",
         "content": "加0.9 mL Lysis Buffer (加1% protease inhibitor cocktail III)洗四次",
         "image": ""
      },
      {
         "title": "2.3",
         "content": "用200μL Lysis Buffer (加1% protease inhibitor cocktail III)重悬,每40 μL beads添加5 μg抗体",
         "image": ""
      }
    ]
  },
  {
     "title": "3. UV cross-linking",
     "substeps": [
      {
         "title": "3.1",
         "content": "从培养箱中取出细胞,弃去培养基,加0.5 mL 预冷的PBS (这里的PBS可以不加protease inhibitor)",
         "image": ""
      },
      {
         "title": "3.2",
         "content": "吸弃PBS,敞开盖子,细胞裸露在空气中,放入UV交联仪",
         "image": ""
      },
      {
         "title": "3.3",
         "content": "400 mJ/cm 交联。(对于本实验室仪器,点energy,输入4000,然后Start)",
         "image": ""
      },
      {
         "title": "3.4",
         "content": "每个孔加入1mL PBS(+protease inhibitor),对于NCCIT来说,直接用枪头吹打,收集到2mL EP管中。",
         "image": ""
      },
      {
         "title": "3.5",
         "content": "台盼蓝法对细胞进行计数,记细胞总数,死活都要。",
         "image": ""
      },
      {
         "title": "3.6",
         "content": "每个样品取用5M细胞,4℃,5000rpm离心5min收集",
         "image": ""
      }
    ]
  },
  {
​
     "title": "4 细胞裂解和免疫沉淀",
     "substeps": [
      {
         "title": "4.1",
         "content": "离心结束后,弃上清,每个管子加入150 μL lysis buffer (+cocktail),转移到1.5 mL lowBind EP管中。(因为下一步要震荡,我们实验室的那个机器只能用1.5mL的管子),冰上放置5min。",
         "image": ""
      },
      {
         "title": "4.2",
         "content": "每管加入14 μL Turbo DNase (Ambion, AM2238)和7.5 μL RNase Inhibitor,37℃下1100rpm震荡5min.",
         "image": ""
      },
      {
         "title": "4.3",
         "content": "4℃, 14000g离心10min,小心取出,保持沉淀在底部。",
         "image": ""
      },
      {
         "title": "4.4",
         "content": "准备新的EP管,取出2~3μL作为Input,放到-20℃暂存。",
         "image": ""
      },
      {
         "title": "4.5",
         "content": "把包好抗体的beads分成每20μL最初的beads体积一个1.5mL EP管,磁力架放置1min,弃上清。",
         "image": ""
      },
      {
         "title": "4.6",
         "content": "每个含有beads的EP管中加入75μL lysates (每个样品2个replicates),4℃慢速旋转孵育2h~4h。",
         "image": ""
      },
      {
         "title": "4.7",
         "content": "用High Stringent Buffer(+cocktail)洗beads,一次,4℃慢速旋转10min。",
         "image": ""
      },
      {
         "title": "4.8",
         "content": "用High Salt Buffer(+cocktail)洗beads,一次,4℃慢速旋转10min。",
         "image": ""
      },
      {
         "title": "4.9",
         "content": "用Low Salt Buffer(+cocktail)洗beads,一次,4℃慢速旋转10min。",
         "image": ""
      },
      {
         "title": "4.10",
         "content": "把Input从冰箱中取出,跟刚才洗好的replicate1和replicate2一起,用50 μL Proteinase K digestion Buffer (+ 2.5μL RNase Inhibitor和5μL proteinase K)重悬beads,50℃, 1100rpm振荡 2h。\n (beads就呆在管子里,之后RNAC抽提的时候会跑到phase-lock tubes的底部。)",
         "image": ""
      }
    ]
  },
  {
     "title": "5 RNA抽提",
     "substeps": [
      {
         "title": "5.1",
         "content": "准备Phase-Lock tubes, 室温16000g离心30s。",
         "image": ""
      },
      {
         "title": "5.2",
         "content": "把样品取出,加150μl lysis Buffer(+cocktail)补全体积到200μL。",
         "image": ""
      },
      {
         "title": "5.3",
         "content": "加入200 μL RNA抽提液(25:24:1),剧烈震荡50次,室温13200rpm离心5min。",
         "image": ""
      },
      {
         "title": "5.4",
         "content": "加入200μL氯仿,剧烈震荡50次,室温13200rpm离心5min。",
         "image": ""
      },
      {
         "title": "5.5",
         "content": "上清液转移到新的1.5mL LowBind EP管中。",
         "image": ""
      }
    ]
  },
  {
     "title": "6 DNA消化后再次抽提",
     "substeps": [
      {
         "title": "6.1",
         "content": "每管加入18μL Turbo DNase,37℃, 1100rpm振荡15min。",
         "image": ""
      },
      {
         "title": "6.2",
         "content": "准备Phase-Lock tubes, 室温16000g离心30s。",
         "image": ""
      },
      {
         "title": "6.3",
         "content": "反应结束后,转移全部溶液(大约200μL)至Phase-Lock tubes中。",
         "image": ""
      },
      {
         "title": "6.4",
         "content": "加入200 μL RNA抽提液(25:24:1),剧烈震荡50次,室温13200rpm离心5min。",
         "image": ""
      },
      {
         "title": "6.5",
         "content": "加入200μL氯仿,剧烈震荡50次,室温13200rpm离心5min。",
         "image": ""
      },
      {
         "title": "6.6",
         "content": "上清液转移到新的1.5mL LowBind EP管中。",
         "image": ""
      }
    ]
  },
  {
     "title": "7 核酸醇沉",
     "substeps": [
      {
         "title": "7.1",
         "content": "加入20μL 3M NaAc和1μL Glycogen (20μg/μL),混匀。",
         "image": ""
      },
      {
         "title": "7.2",
         "content": "加入600 μL预冷的无水乙醇。",
         "image": ""
      },
      {
         "title": "7.3",
         "content": "混匀后,放-80℃醇沉(大于2h,长则一星期)。",
         "image": ""
      },
      {
         "title": "7.4",
         "content": "4℃, 14000g离心20min。",
         "image": ""
      },
      {
         "title": "7.5",
         "content": "弃上清,加500μl预冷的75%乙醇。4摄氏度,14000rpm,离心5min。",
         "image": ""
      },
      {
         "title": "7.6",
         "content": "弃上清,4摄氏度,14000rpm,离心1min,用10μl枪头吸干上清。",
         "image": ""
      },
      {
         "title": "7.7",
         "content": "风干3min,加适量体积的Nuclease-Free Water,溶解。(对于本RIP-seq来说加12μL洗脱)",
         "image": ""
      },
      {
         "title": "7.8",
         "content": "用NanoDrop测浓度",
         "image": ""
      }
    ]
  },
  {
     "title": "8 二代测序建库",
     "substeps": [    
      {
         "title": "8.1打断",
         "content": "按浓度最低的取3μL至八联排,其他取跟它相同的ng数,用RNase-Free Water补全至3μL。每管加入3μL Frag/Elute Buffer。94℃打断8min,4℃ hold",
         "image": ""
      },
      {
         "title": "8.2一链合成",
         "content": "每管加入4.8μL RT strand specificity Reagent + 1.2μL First Strand synthesis enzyme mix,可先预混。\n 25℃,10min → 42℃,15min → 70℃,15min → 4℃,hold",
         "image": ""
      },
      {
         "title": "8.3二链合成",
         "content": "每管加入22μL RNase-Free Water + 4μL second strand synthesis reaction buffer with dUTP + 2μL Second strand synthesis enzyme mix,可先预混,总体积40μL。16℃孵育1h",
         "image": ""
      },
      {
         "title": "8.4用beads纯化",
         "content": "每管加入72μL NFTmag NGS DNA clean Beads,混匀,室温静置5min,磁力架静置5min,弃上清。\n将tube保持在磁力架上,加入100μL 80%乙醇,放30s,弃上清。再加入100μL 80%乙醇,放30s,弃上清。室温干燥3min,加入19.5μL low-EDTA TE,吹打混匀,室温静置2min,磁力架1min,吸取18.5μL上清至另一八联排中",
         "image": ""
      },
      {
         "title": "8.5末端修复",
         "content": "上一步的Tube中,每管加入5μL End-Prep Buffer和1.5μL End-Prep enzymes(总体积25μL)。20℃,30min → 65℃,30min → 4℃,hold",
         "image": ""
      },
      {
         "title": "8.6接头连接",
         "content": "上一步的Tube中,每管加入8.25μL Ligation buffer,1.5μL Ligase Mix和1.25μL Truncated Adapter。其中,Truncated Adapter不要跟另外二者预混,最后单独加,以免形成接头二聚体。22℃反应15min(总体积36μL,热盖关闭)",
         "image": ""
      },
      {
         "title": "8.7片段分选",
         "content": "上一步的Tube中,加入14μL Nuclease-Free Water,成50μL体系。加15μL beads,混匀,室温静置5min,磁力架5min。转移上清至另一八联排中(!!不要丢掉上清!!),加10μL beads混匀,室温静置5min,磁力架5min,弃上清。100μL 80%乙醇洗两次。风干3min,加10.5 μL low-EDTA TE,吹打混匀,室温静置2min,磁力架1min,吸取10 μL上清至另一八联排中",
         "image": ""
      },
      {
         "title": "8.8PCR文库扩增",
         "content": "取新的八联排,每管加入6.25μL 2XPCR mix和0.125μL UDG enzyme(可以看出后者用量非常小,二者先预混)。然后分别加入4.875μL上一步得到的“片段分选产物”,再各自加入不同的Dual-index primer 1.25 μL,在实验记录本上记清楚加了哪几个primer,分别对应哪个样品。37℃,10min → 98℃,1min → 98℃,10s → 60℃,15s → 72℃,30s (goto step3, 8X) → 72℃,1min → 4℃,hold。返回去8次,即9个cycles",
         "image": ""
      },
      {
         "title": "8.9跑胶检测扩增效果",
         "content": "提前配置1%琼脂糖凝胶,取2μL PCR产物跑胶。根据结果,看是否有个别样品需要多加1~2个循环。加完心里有数的话就不用再跑胶了。",
         "image": ""
      },
      {
         "title": "8.10 PCR产物回收",
         "content": "按体积加1:1的beads,混匀,室温5min,磁力架5min,弃上清,100μL 80%乙醇洗两次,风干2min,加15μL low-EDTA TE混匀,室温2min,磁力架1min,吸取上清至新的八联排。",
         "image": ""
      },
      {
         "title": "8.11 Qubit核酸定量",
         "content": "每个Qubit管中加入199μL Qubit反应液体,加入1μL样品,混匀。放入仪器检测,读数,记录",
         "image": ""
      },
      {
         "title": "8.12混样",
         "content": "最低浓度的样品取一半(保证有一次失误补救的机会),其他取相同ng数(至少大于5ng)。可以几个一组混样(而不是全部混成1个样)方便加测。要注意记录每个样品取了几微升,然后按照总体积的80%加入beads,混匀,室温5min,磁力架5min,弃上清,100μL 80%乙醇洗两次,风干2min,加20μL Low-EDTA TE洗脱(如果样品数少,减少洗脱体积)",
         "image": ""
      },
      {
         "title": "8.13再次Qubit定量",
         "content": "因为填测序订单需要写浓度,所以需要再测一遍Qubit,199μL Qubit反应液体,加入1μL样品,混匀。放入仪器检测,读数,记录",
         "image": ""
      },
      {
         "title": "8.14填写订单,送测",
         "content": "麻烦易出错的地方在于填好每个子样品对应的Dual-Index Primer,错了不只耽误自己同时还耽误一起上机的其他人。",
         "image": ""
      }
    ]
  }
]
}