<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>玻璃计算器 – 流式草稿计算</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"/>
<style>
:root {
--bg-gradient: linear-gradient(135deg, #4e54c8, #8f94fb);
--primary: #ffffff;
--accent: #7f8ff4;
--header-h: 60px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Poppins', sans-serif;
background: var(--bg-gradient);
color: var(--primary);
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.logo {
font-weight: 600;
font-size: 1.4rem;
letter-spacing: 1px;
}
.hero {
padding-top: calc(var(--header-h) + 80px);
padding-bottom: 40px;
text-align: center;
}
.hero h1 { margin: 0; font-size: 2.6rem; font-weight: 600; }
.hero p { margin-top: 12px; font-size: 1.2rem; opacity: 0.9; max-width: 680px; margin-left:auto; margin-right:auto; }
#canvas {
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 0 16px;
padding-bottom: 60px;
}
.calc-row {
position: relative;
z-index: 0;
display: flex;
align-items: center;
margin-bottom: 24px;
padding: 20px 24px;
background: rgba(255, 255, 255, 0.28);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 28px;
backdrop-filter: blur(16px) saturate(220%);
-webkit-backdrop-filter: blur(16px) saturate(220%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25), 0 6px 20px rgba(0, 0, 0, 0.2);
}
.calc-row::before,
.calc-row::after {
content: "";
position: absolute;
border-radius: inherit;
pointer-events: none;
}
.calc-row::before {
inset: 0;
padding: 2px;
background:
linear-gradient(145deg, rgba(255,255,255,0.6), rgba(255,255,255,0.1))
border-box;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.calc-row::after {
inset: 4px;
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.35);
}
.expression {
flex: 1;
min-height: 32px;
padding: 6px;
border: none;
outline: none;
font-size: 1.2rem;
color: var(--primary);
position: relative;
z-index: 1;
}
.expression:focus {
outline: none;
}
.result {
margin-left: 20px;
font-weight: 600;
font-size: 1.2rem;
cursor: grab;
padding: 4px 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px) saturate(180%);
-webkit-backdrop-filter: blur(10px) saturate(180%);
white-space: nowrap;
min-width: 48px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
color: #1d2275;
}
.result.error {
color: #ff6b6b;
}
.ref {
display: inline-block;
padding: 2px 4px;
margin: 0 2px;
border-radius: 8px;
background: rgba(255,255,255,0.25);
border: 1px solid rgba(255,255,255,0.4);
cursor: pointer;
user-select: none;
}
#addRowBtn {
display: block;
margin: 20px auto;
padding: 10px 26px;
font-size: 1.1rem;
font-weight: 600;
color: var(--primary);
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255,255,255,0.45);
border-radius: 20px;
cursor: pointer;
transition: background 0.3s, box-shadow 0.3s;
backdrop-filter: blur(8px) saturate(180%);
-webkit-backdrop-filter: blur(8px) saturate(180%);
}
#addRowBtn:hover {
background: rgba(255, 255, 255, 0.35);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<header>
<div class="logo">Glass Calc</div>
</header>
<section class="hero">
<h1>流式草稿计算</h1>
<p>像写草稿一样进行数学计算,拖拽任意结果参与下一步运算,实时更新关联数字的所有结果。这是向 Tydlig/有数
致敬的网页版计算体验。</p>
</section>
<div id="canvas"></div>
<button id="addRowBtn">添加表达式</button>
<script>
(function() {
const rows = [];
let nextId = 1;
const canvas = document.getElementById('canvas');
const addBtn = document.getElementById('addRowBtn');
function getRowById(id) {
return rows.find(r => r.id === Number(id));
}
function updateAllRefSpans(refId) {
const refRows = document.querySelectorAll(`span.ref[data-ref-id="${refId}"]`);
const row = getRowById(refId);
if (!row) return;
refRows.forEach(span => {
span.textContent = row.value;
});
}
function evaluateRow(row, visited = new Set()) {
if (visited.has(row.id)) return; visited.add(row.id);
const exprNodes = Array.from(row.exprEl.childNodes);
let exprStr = '';
const newRefs = new Set();
exprNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
exprStr += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('ref')) {
const rid = parseInt(node.getAttribute('data-ref-id'));
newRefs.add(rid);
const refRow = getRowById(rid);
const val = refRow ? refRow.value : 0;
exprStr += '(' + val + ')';
}
});
row.references.forEach(rid => {
if (!newRefs.has(rid)) {
const refRow = getRowById(rid);
if (refRow) refRow.dependents.delete(row.id);
}
});
newRefs.forEach(rid => {
if (!row.references.has(rid)) {
const refRow = getRowById(rid);
if (refRow) refRow.dependents.add(row.id);
}
});
row.references = newRefs;
try {
let result;
if (exprStr.trim() === '') {
result = 0;
} else {
const scope = Object.create(Math);
const fn = new Function('with(this){ return ' + exprStr + '}');
result = fn.call(scope);
}
row.value = result;
if (exprStr.trim() === '') {
row.resultEl.textContent = '';
} else {
row.resultEl.textContent = String(result);
}
row.resultEl.classList.remove('error');
} catch (e) {
row.value = NaN;
row.resultEl.textContent = '错误';
row.resultEl.classList.add('error');
}
updateAllRefSpans(row.id);
row.dependents.forEach(did => {
const depRow = getRowById(did);
if (depRow) evaluateRow(depRow, visited);
});
}
function insertRefSpan(exprEl, refId, value) {
const sel = window.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
range.deleteContents();
const span = document.createElement('span');
span.className = 'ref';
span.setAttribute('data-ref-id', refId);
span.contentEditable = false;
span.textContent = value;
range.insertNode(span);
const after = document.createTextNode('');
span.after(after);
sel.removeAllRanges();
const newRange = document.createRange();
newRange.setStart(after, 0);
newRange.setEnd(after, 0);
sel.addRange(newRange);
}
function setupRowEvents(row) {
const evaluate = () => evaluateRow(row);
row.exprEl.addEventListener('input', evaluate);
row.exprEl.addEventListener('keyup', evaluate);
row.exprEl.addEventListener('keydown', evaluate);
row.exprEl.addEventListener('dragover', ev => {
ev.preventDefault();
});
row.exprEl.addEventListener('drop', ev => {
ev.preventDefault();
const refId = ev.dataTransfer.getData('text/ref');
if (!refId) return;
const refRow = getRowById(refId);
if (!refRow) return;
insertRefSpan(row.exprEl, refId, refRow.value);
evaluateRow(row);
});
row.resultEl.addEventListener('dragstart', ev => {
ev.dataTransfer.setData('text/ref', row.id);
ev.dataTransfer.effectAllowed = 'copy';
});
}
function createRow() {
const id = nextId++;
const rowEl = document.createElement('div');
rowEl.className = 'calc-row';
const expr = document.createElement('div');
expr.className = 'expression';
expr.contentEditable = true;
expr.spellcheck = false;
expr.setAttribute('placeholder', '输入表达式…');
const result = document.createElement('div');
result.className = 'result';
result.draggable = true;
rowEl.appendChild(expr);
rowEl.appendChild(result);
canvas.appendChild(rowEl);
const row = { id, el: rowEl, exprEl: expr, resultEl: result, references: new Set(), dependents: new Set(), value: 0 };
rows.push(row);
setupRowEvents(row);
evaluateRow(row);
setTimeout(() => {
expr.focus();
}, 0);
return row;
}
addBtn.addEventListener('click', () => {
createRow();
});
createRow();
})();
</script>
</body>
</html>