Files
gangyan/scripts/latex-editor/index.html
liuguancen 6337af9481 feat: 新增4个工具(imgcompress/LamaCleaner/webp2jpg/Overleaf/LaTeX公式编辑器) + 应用广场布局优化
新增工具部署:
- imgcompress (端口18087) - 图片压缩、格式转换、AI抠图
- Lama Cleaner (端口18088) - AI图像擦除/去水印
- webp2jpg-online (端口18089) - 图片格式批量互转
- Overleaf (端口18090) - 在线LaTeX论文编辑器(docker-compose + MongoDB 8.0)
- LaTeX公式编辑器 (端口18091) - 纯前端KaTeX公式编辑

应用广场优化:
- 去掉tab切换,所有分类平铺展示
- CSS Grid自适应布局,一行可排3-4个卡片
- 重新分为4个分类:文档处理、图片处理、创作绘图、科研写作

其他:
- 更新 CLAUDE.md 项目配置文档
- PPTist AI后端优化prompt和流式输出格式

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:23:06 +08:00

151 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LaTeX 公式编辑器</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background: #f0f2f5; color: #333; }
.header { background: #004EA0; color: #fff; padding: 16px 32px; font-size: 18px; font-weight: bold; }
.container { max-width: 1200px; margin: 24px auto; padding: 0 24px; display: flex; gap: 20px; }
.panel { background: #fff; border-radius: 10px; padding: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); flex: 1; }
.panel h3 { font-size: 15px; color: #333; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
textarea { width: 100%; height: 200px; border: 1px solid #ddd; border-radius: 8px; padding: 14px; font-family: 'Courier New', monospace; font-size: 15px; resize: vertical; outline: none; }
textarea:focus { border-color: #004EA0; }
.preview { min-height: 200px; border: 1px solid #eee; border-radius: 8px; padding: 20px; display: flex; align-items: center; justify-content: center; background: #fafbfc; overflow: auto; }
.preview .katex { font-size: 24px; }
.error { color: #dc2626; font-size: 13px; text-align: center; }
.toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.toolbar button { padding: 4px 10px; border: 1px solid #ddd; border-radius: 6px; background: #f5f7fa; cursor: pointer; font-size: 13px; transition: all 0.15s; }
.toolbar button:hover { border-color: #004EA0; color: #004EA0; background: #e8f0fe; }
.symbols { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 16px; }
.symbols button { width: 42px; height: 36px; border: 1px solid #eee; border-radius: 6px; background: #fafbfc; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
.symbols button:hover { background: #e8f0fe; border-color: #004EA0; }
.actions { margin-top: 14px; display: flex; gap: 10px; }
.actions button { padding: 8px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; }
.btn-primary { background: #004EA0; color: #fff; }
.btn-primary:hover { background: #003a7a; }
.btn-secondary { background: #f0f2f5; color: #333; border: 1px solid #ddd !important; }
.btn-secondary:hover { background: #e5e7eb; }
.templates { margin-top: 20px; }
.templates h4 { font-size: 14px; color: #666; margin-bottom: 8px; }
.tpl-list { display: flex; flex-wrap: wrap; gap: 6px; }
.tpl-list button { padding: 4px 12px; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; color: #555; }
.tpl-list button:hover { border-color: #004EA0; color: #004EA0; }
.copied { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(0,0,0,0.7); color: #fff; padding: 10px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 999; }
</style>
</head>
<body>
<div class="header">LaTeX 公式编辑器</div>
<div class="container">
<div class="panel" style="flex:1.2">
<h3>输入公式</h3>
<div class="toolbar">
<button onclick="ins('\\frac{a}{b}')">分数</button>
<button onclick="ins('\\sqrt{x}')">根号</button>
<button onclick="ins('\\sum_{i=1}^{n}')">求和</button>
<button onclick="ins('\\int_{a}^{b}')">积分</button>
<button onclick="ins('\\lim_{x \\to \\infty}')">极限</button>
<button onclick="ins('\\prod_{i=1}^{n}')">连乘</button>
<button onclick="ins('x^{2}')">上标</button>
<button onclick="ins('x_{i}')">下标</button>
<button onclick="ins('\\binom{n}{k}')">组合</button>
<button onclick="ins('\\vec{a}')">向量</button>
<button onclick="ins('\\hat{x}')"></button>
<button onclick="ins('\\bar{x}')">横线</button>
<button onclick="ins('\\dot{x}')"></button>
<button onclick="ins('\\overline{AB}')">上划线</button>
<button onclick="ins('\\begin{matrix} a & b \\\\ c & d \\end{matrix}')">矩阵</button>
<button onclick="ins('\\begin{cases} x & y \\\\ a & b \\end{cases}')">分段</button>
<button onclick="ins('\\log')">log</button>
<button onclick="ins('\\ln')">ln</button>
</div>
<textarea id="input" placeholder="在此输入 LaTeX 公式E = mc^{2}" oninput="render()">E = mc^{2}</textarea>
<div class="symbols" id="symbolBar"></div>
<div class="actions">
<button class="btn-primary" onclick="copyLatex()">复制公式</button>
<button class="btn-secondary" onclick="copyImg()">复制为图片</button>
<button class="btn-secondary" onclick="document.getElementById('input').value='';render()">清空</button>
</div>
<div class="templates">
<h4>常用公式模板</h4>
<div class="tpl-list">
<button onclick="setTpl('E = mc^{2}')">质能方程</button>
<button onclick="setTpl('e^{i\\pi} + 1 = 0')">欧拉公式</button>
<button onclick="setTpl('a^2 + b^2 = c^2')">勾股定理</button>
<button onclick="setTpl('x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}')">求根公式</button>
<button onclick="setTpl('f\'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}')">导数定义</button>
<button onclick="setTpl('\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}')">高斯积分</button>
<button onclick="setTpl('\\nabla \\times \\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t}')">麦克斯韦</button>
<button onclick="setTpl('\\sigma = \\sqrt{\\frac{1}{N}\\sum_{i=1}^{N}(x_i - \\mu)^2}')">标准差</button>
<button onclick="setTpl('P(A|B) = \\frac{P(B|A) \\cdot P(A)}{P(B)}')">贝叶斯</button>
<button onclick="setTpl('\\mathbf{A} = \\begin{pmatrix} a_{11} & a_{12} \\\\ a_{21} & a_{22} \\end{pmatrix}')">矩阵</button>
</div>
</div>
</div>
<div class="panel">
<h3>实时预览 <span style="font-weight:normal;font-size:12px;color:#999">公式渲染由 KaTeX 提供</span></h3>
<div class="preview" id="preview"></div>
</div>
</div>
<div class="copied" id="toast">已复制!</div>
<script>
const symbols = ['\\alpha','\\beta','\\gamma','\\delta','\\epsilon','\\zeta','\\eta','\\theta','\\lambda','\\mu','\\nu','\\xi','\\pi','\\rho','\\sigma','\\tau','\\phi','\\psi','\\omega','\\Gamma','\\Delta','\\Theta','\\Lambda','\\Pi','\\Sigma','\\Phi','\\Psi','\\Omega','\\infty','\\partial','\\nabla','\\forall','\\exists','\\in','\\notin','\\subset','\\cup','\\cap','\\times','\\cdot','\\div','\\pm','\\mp','\\leq','\\geq','\\neq','\\approx','\\equiv','\\sim','\\rightarrow','\\leftarrow','\\Rightarrow','\\Leftarrow'];
const bar = document.getElementById('symbolBar');
symbols.forEach(s => {
const b = document.createElement('button');
try { katex.render(s, b, {throwOnError:false}); } catch(e) { b.textContent = s; }
b.onclick = () => ins(s);
bar.appendChild(b);
});
function render() {
const input = document.getElementById('input').value.trim();
const el = document.getElementById('preview');
if (!input) { el.innerHTML = '<span style="color:#bbb">在左侧输入公式</span>'; return; }
try { katex.render(input, el, { displayMode: true, throwOnError: true }); }
catch(e) { el.innerHTML = '<span class="error">' + e.message + '</span>'; }
}
function ins(s) {
const t = document.getElementById('input');
const start = t.selectionStart, end = t.selectionEnd;
t.value = t.value.slice(0, start) + s + t.value.slice(end);
t.focus();
t.selectionStart = t.selectionEnd = start + s.length;
render();
}
function setTpl(s) { document.getElementById('input').value = s; render(); }
function copyLatex() {
navigator.clipboard.writeText(document.getElementById('input').value);
showToast('公式已复制!');
}
function copyImg() {
const el = document.getElementById('preview');
const svg = el.querySelector('svg') || el.querySelector('.katex');
if (!svg) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const data = new XMLSerializer().serializeToString(el);
const img = new Image();
const blob = new Blob(['<svg xmlns="http://www.w3.org/2000/svg" width="'+el.scrollWidth+'" height="'+el.scrollHeight+'"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="background:#fff;padding:20px">'+el.innerHTML+'</div></foreignObject></svg>'], {type:'image/svg+xml'});
const url = URL.createObjectURL(blob);
img.onload = () => {
canvas.width = img.width * 2; canvas.height = img.height * 2;
ctx.scale(2,2); ctx.drawImage(img, 0, 0);
canvas.toBlob(b => { navigator.clipboard.write([new ClipboardItem({'image/png': b})]); showToast('图片已复制!'); });
URL.revokeObjectURL(url);
};
img.src = url;
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 1500);
}
render();
</script>
</body>
</html>