让你的Protocol“动”起来
引导ChatGPT做了一个网页模板,感觉这样做实验的时候,能一直翻页看到实验进度在往前走,有那么一点炫酷。用电脑或者平板打开,手机页面适配没做,会字很小很丑。
不同的实验可以套用相同代码,在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": ""
}
]
}
]
}
原文地址:https://blog.fanyiming.life/posts/712b2e5e.html
好酷的模板,感觉用来记录带流程的工作会很有意思
谢谢夸奖
有意思诶~
自己找乐子