update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View File

@@ -0,0 +1,136 @@
(function () {
const $ = (id) => document.getElementById(id);
const codeEl = $("code");
const runBtn = $("runBtn");
const copyBtn = $("copyBtn");
const pasteBtn = $("pasteBtn");
const clearBtn = $("clearBtn");
const outputEl = $("output");
const sandboxEl = $("sandbox");
// 以JS方式设置带换行的占位符避免HTML属性中的 \n 无效
codeEl.placeholder = "在此编写或粘贴 JavaScript 代码…\n例如\nconsole.log('Hello, InfoGenie!');";
let sandboxReady = false;
// 沙箱页面srcdoc内容拦截 console、收集错误、支持 async/await
const sandboxHtml = `<!doctype html><html><head><meta charset=\"utf-8\"></head><body>
<script>
(function(){
function serialize(value){
try {
if (typeof value === 'string') return value;
if (typeof value === 'function') return value.toString();
if (value === undefined) return 'undefined';
if (value === null) return 'null';
if (typeof value === 'object') return JSON.stringify(value, null, 2);
return String(value);
} catch (e) {
try { return String(value); } catch(_){ return Object.prototype.toString.call(value); }
}
}
['log','info','warn','error'].forEach(level => {
const orig = console[level];
console[level] = (...args) => {
try { parent.postMessage({type:'console', level, args: args.map(serialize)}, '*'); } catch(_){ }
try { orig && orig.apply(console, args); } catch(_){ }
};
});
window.onerror = function(message, source, lineno, colno, error){
var stack = error && error.stack ? error.stack : (source + ':' + lineno + ':' + colno);
parent.postMessage({type:'error', message: String(message), stack}, '*');
};
parent.postMessage({type:'ready'}, '*');
window.addEventListener('message', async (e) => {
const data = e.data;
if (!data || data.type !== 'code') return;
try {
// 用 async IIFE 包裹,支持顶层 await
await (async () => { eval(data.code); })();
parent.postMessage({type:'done'}, '*');
} catch (err) {
parent.postMessage({type:'error', message: (err && err.message) || String(err), stack: err && err.stack}, '*');
}
}, false);
})();
<\/script>
</body></html>`;
// 初始化沙箱
sandboxEl.srcdoc = sandboxHtml;
sandboxEl.addEventListener('load', () => {
// 等待 ready 消息
});
window.addEventListener('message', (e) => {
const data = e.data;
if (!data) return;
switch (data.type) {
case 'ready':
sandboxReady = true;
break;
case 'console':
(data.args || []).forEach((text) => appendLine(text, data.level));
break;
case 'error':
appendLine('[错误] ' + data.message, 'error');
if (data.stack) appendLine(String(data.stack), 'error');
break;
case 'done':
tip('执行完成。');
break;
default:
break;
}
});
runBtn.addEventListener('click', () => {
const code = codeEl.value || '';
if (!code.trim()) { tip('请先输入要执行的代码。'); return; }
outputEl.textContent = '';
if (!sandboxReady) tip('沙箱初始化中,稍候执行…');
// 发送代码到沙箱执行
try {
sandboxEl.contentWindow.postMessage({ type: 'code', code }, '*');
} catch (err) {
appendLine('[错误] ' + ((err && err.message) || String(err)), 'error');
}
});
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(codeEl.value);
tip('已复制到剪贴板。');
} catch (err) {
tip('复制失败,请检查剪贴板权限。');
}
});
pasteBtn.addEventListener('click', async () => {
try {
const txt = await navigator.clipboard.readText();
if (txt) codeEl.value = txt;
tip('已粘贴剪贴板内容。');
} catch (err) {
tip('粘贴失败,请允许访问剪贴板。');
}
});
clearBtn.addEventListener('click', () => {
outputEl.textContent = '';
});
function appendLine(text, level) {
const span = document.createElement('span');
span.className = 'line ' + (level || 'log');
span.textContent = String(text);
outputEl.appendChild(span);
outputEl.appendChild(document.createTextNode('\n'));
outputEl.scrollTop = outputEl.scrollHeight;
}
function tip(text){ appendLine('[提示] ' + text, 'info'); }
})();

View File

@@ -0,0 +1,42 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#e8f8e4" />
<meta name="color-scheme" content="light" />
<title>JavaScript在线执行器</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="page" role="main">
<header class="header">
<h1 class="title">JavaScript在线执行器</h1>
</header>
<section class="editor-section">
<label class="label" for="code">代码编辑区</label>
<textarea id="code" class="editor" placeholder="在此编写或粘贴 JavaScript 代码… \n 例如:\n console.log('Hello, InfoGenie!');" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"></textarea>
<div class="toolbar" role="group" aria-label="编辑操作">
<button id="pasteBtn" class="btn" type="button" title="从剪贴板粘贴">粘贴代码</button>
<button id="copyBtn" class="btn" type="button" title="复制到剪贴板">复制代码</button>
<button id="runBtn" class="btn primary" type="button" title="执行当前代码">执行代码</button>
</div>
</section>
<section class="output-section">
<div class="output-header">
<span class="label">结果显示区</span>
<button id="clearBtn" class="btn ghost" type="button" title="清空结果">清空结果</button>
</div>
<pre id="output" class="output" aria-live="polite" aria-atomic="false"></pre>
</section>
<!-- 隐藏的沙箱 iframe用于安全执行 JS 代码 -->
<iframe id="sandbox" class="sandbox" sandbox="allow-scripts" title="执行沙箱" aria-hidden="true"></iframe>
</main>
<script src="./app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,176 @@
/* 全局与主题 */
:root {
--bg-1: #eaf9e8; /* 淡绿色 */
--bg-2: #f4ffd9; /* 淡黄绿色 */
--panel: rgba(255, 255, 255, 0.78);
--text: #1d2a1d;
--muted: #486a48;
--accent: #5bb271;
--accent-2: #93d18f;
--border: rgba(93, 160, 93, 0.25);
--code-bg: rgba(255, 255, 255, 0.88);
--error: #b00020;
--warn: #8a6d3b;
--info: #2f6f3a;
}
/* 隐藏滚动条但保留滚动 */
html, body {
height: 100%;
overflow: auto;
-ms-overflow-style: none; /* IE 10+ */
scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; }
/* 背景与排版 */
html, body {
margin: 0;
padding: 0;
background: linear-gradient(180deg, var(--bg-1), var(--bg-2));
color: var(--text);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
}
.page {
box-sizing: border-box;
max-width: 760px;
margin: 0 auto;
padding: calc(env(safe-area-inset-top, 12px) + 8px) 14px calc(env(safe-area-inset-bottom, 12px) + 14px);
display: flex;
flex-direction: column;
gap: 16px;
}
.header {
display: flex;
flex-direction: column;
gap: 6px;
}
.title {
margin: 0;
font-size: 22px;
line-height: 1.2;
letter-spacing: 0.2px;
}
.subtitle {
margin: 0;
font-size: 13px;
color: var(--muted);
}
.editor-section, .output-section {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 10px 20px rgba(64, 129, 64, 0.06);
backdrop-filter: saturate(1.2) blur(8px);
padding: 12px;
}
.label {
display: block;
font-size: 12px;
color: var(--muted);
margin-bottom: 8px;
}
.editor {
box-sizing: border-box;
width: 100%;
min-height: 36vh;
max-height: 48vh;
resize: vertical;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
outline: none;
background: var(--code-bg);
color: #192519;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre;
overflow: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.editor::-webkit-scrollbar { width: 0; height: 0; }
.toolbar {
display: flex;
gap: 8px;
margin-top: 10px;
}
.btn {
-webkit-tap-highlight-color: transparent;
appearance: none;
border: 1px solid var(--border);
background: #ffffffd6;
color: #204220;
padding: 10px 14px;
border-radius: 10px;
font-size: 14px;
line-height: 1;
cursor: pointer;
}
.btn:hover { filter: brightness(1.02) saturate(1.02); }
.btn:active { transform: translateY(1px); }
.btn.primary {
background: linear-gradient(180deg, var(--accent-2), var(--accent));
color: #fff;
border-color: rgba(0,0,0,0.06);
}
.btn.ghost {
background: transparent;
}
.output-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.output {
box-sizing: border-box;
width: 100%;
min-height: 28vh;
max-height: 40vh;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--code-bg);
color: #192519;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
overflow: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.output::-webkit-scrollbar { width: 0; height: 0; }
.sandbox { display: none; width: 0; height: 0; border: 0; }
/* 控制不同日志级别颜色 */
.line.log { color: #1f2a1f; }
.line.info { color: var(--info); }
.line.warn { color: var(--warn); }
.line.error { color: var(--error); }
.line.tip { color: #507a58; font-style: italic; }
/* 竖屏优化 */
@media (orientation: portrait) {
.page { max-width: 640px; }
.editor { min-height: 40vh; }
.output { min-height: 30vh; }
}
/* 小屏进一步优化 */
@media (max-width: 380px) {
.btn { padding: 9px 12px; font-size: 13px; }
.title { font-size: 20px; }
}

View File

@@ -0,0 +1,339 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>📒JSON编辑器</title>
<!-- CodeMirror 样式与 Lint 样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5/theme/neo.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5/addon/lint/lint.css" />
<style>
:root {
--toolbar-h: 64px;
--radius: 14px;
--bg-start: #dff7e0; /* 淡绿色 */
--bg-end: #f2ffd2; /* 淡黄绿色 */
--brand: #4caf50; /* 绿色主题色 */
--brand-weak: #7bd07f;
--text: #103510;
--muted: #366b36;
--danger: #e53935;
--shadow: 0 10px 25px rgba(16, 53, 16, 0.08);
}
html, body {
height: 100%;
background: linear-gradient(180deg, var(--bg-start) 0%, var(--bg-end) 100%);
color: var(--text);
font-family: system-ui, -apple-system, Segoe UI, Roboto, PingFang SC, Microsoft YaHei, "Noto Sans", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: auto; /* 保留滚动 */
}
/* 隐藏滚动条同时保留滚动效果 */
html, body { scrollbar-width: none; }
html::-webkit-scrollbar, body::-webkit-scrollbar { width: 0; height: 0; }
.page {
min-height: calc(var(--vh, 1vh) * 100);
display: flex;
flex-direction: column;
padding: 14px;
gap: 12px;
}
.toolbar {
height: var(--toolbar-h);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 18px;
color: var(--muted);
letter-spacing: .2px;
}
.title .dot {
width: 8px; height: 8px; border-radius: 99px; background: var(--brand);
box-shadow: 0 0 0 3px rgba(76, 175, 80, .18);
}
.actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.btn {
-webkit-tap-highlight-color: transparent;
user-select: none;
outline: none;
border: none;
padding: 10px 14px;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
color: #0f3210;
background: linear-gradient(180deg, #e6ffe6, #d8ffd8);
box-shadow: var(--shadow);
transition: transform .06s ease, box-shadow .2s ease;
}
.btn:active { transform: scale(.98); }
.btn.primary { background: linear-gradient(180deg, #d7ffda, #caffcf); color: #0c2b0d; }
.btn.danger { background: linear-gradient(180deg, #ffe6e6, #ffdcdc); color: #5f1b1b; }
.editor-wrap {
position: relative;
flex: 1;
background: rgba(255,255,255,.55);
backdrop-filter: saturate(150%) blur(6px);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
/* CodeMirror 基本样式与滚动条隐藏 */
.CodeMirror {
height: calc(var(--vh, 1vh) * 100 - var(--toolbar-h) - 40px);
font-size: 15px;
line-height: 1.6;
padding: 12px 0;
background: transparent;
}
.CodeMirror-scroll { scrollbar-width: none; }
.CodeMirror-scroll::-webkit-scrollbar { width: 0; height: 0; }
.CodeMirror-gutters { background: transparent; border-right: none; }
.cm-error-line { background: rgba(229, 57, 53, .08); }
.statusbar {
position: absolute; right: 12px; bottom: 12px;
display: inline-flex; align-items: center; gap: 8px;
background: rgba(255,255,255,.75);
border-radius: 999px; padding: 8px 12px; box-shadow: var(--shadow);
font-size: 13px; color: var(--muted);
}
.status-dot { width: 8px; height: 8px; border-radius: 99px; background: #a0a0a0; box-shadow: 0 0 0 3px rgba(0,0,0,.08); }
.statusbar.good .status-dot { background: #27ae60; box-shadow: 0 0 0 3px rgba(39,174,96,.18); }
.statusbar.bad .status-dot { background: var(--danger); box-shadow: 0 0 0 3px rgba(229,57,53,.18); }
@media (max-width: 640px) {
:root { --toolbar-h: 76px; }
.title { font-size: 16px; }
.btn { padding: 11px 14px; font-size: 15px; }
.actions { gap: 6px; }
}
</style>
<!-- CodeMirror 与 JSONLint 脚本 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/addon/lint/lint.js"></script>
<!-- JSONLint 提供更完善的错误定位(行/列) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsonlint/1.6.0/jsonlint.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5/addon/lint/json-lint.js"></script>
</head>
<body>
<div class="page">
<header class="toolbar">
<div class="title">
<span class="dot"></span>
<span>📒JSON 编辑器</span>
</div>
<div class="actions">
<button class="btn" id="pasteBtn" title="从剪贴板粘贴">粘贴</button>
<button class="btn" id="copyBtn" title="复制到剪贴板">复制</button>
<button class="btn primary" id="formatBtn" title="格式化为缩进">格式化</button>
<button class="btn" id="minifyBtn" title="压缩为单行">压缩</button>
<button class="btn danger" id="clearBtn" title="清空内容">清空</button>
</div>
</header>
<section class="editor-wrap">
<textarea id="jsonInput" spellcheck="false">{
"hello": "world",
"list": [1, 2, 3],
"nested": { "a": true, "b": null }
}</textarea>
<div id="statusbar" class="statusbar" aria-live="polite">
<span class="status-dot"></span>
<span id="statusText">就绪</span>
</div>
</section>
</div>
<script>
// 解决移动端 100vh 问题,保证竖屏高度正确
function setVH() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
setVH();
window.addEventListener('resize', setVH);
window.addEventListener('orientationchange', setVH);
// 初始化 CodeMirror
const cm = CodeMirror.fromTextArea(document.getElementById('jsonInput'), {
mode: { name: 'javascript', json: true },
theme: 'neo',
lineNumbers: true,
lint: true,
gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers'],
tabSize: 2,
indentUnit: 2,
lineWrapping: false,
autofocus: true,
});
const statusbar = document.getElementById('statusbar');
const statusText = document.getElementById('statusText');
let lastErrorLine = null;
function updateStatusOK() {
statusbar.classList.add('good');
statusbar.classList.remove('bad');
statusText.textContent = 'JSON 有效';
}
function updateStatusError(msg, line, col) {
statusbar.classList.remove('good');
statusbar.classList.add('bad');
statusText.textContent = msg;
if (typeof line === 'number') {
if (lastErrorLine != null) {
cm.removeLineClass(lastErrorLine - 1, 'background', 'cm-error-line');
}
lastErrorLine = line;
cm.addLineClass(line - 1, 'background', 'cm-error-line');
}
}
function clearErrorHighlight() {
if (lastErrorLine != null) {
cm.removeLineClass(lastErrorLine - 1, 'background', 'cm-error-line');
lastErrorLine = null;
}
}
const debounce = (fn, wait = 300) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};
const validate = () => {
const txt = cm.getValue();
if (!txt.trim()) {
clearErrorHighlight();
statusbar.classList.remove('good','bad');
statusText.textContent = '空内容';
return;
}
try {
// 使用 jsonlint 以获得行列信息
window.jsonlint.parse(txt);
clearErrorHighlight();
updateStatusOK();
} catch (e) {
// e.message 形如:"Parse error on line 3:\n...\nExpected 'STRING', got 'undefined'"
let line, col;
const lineMatch = /line\s+(\d+)/i.exec(e.message || '');
if (lineMatch) line = parseInt(lineMatch[1], 10);
const colMatch = /column\s+(\d+)/i.exec(e.message || '');
if (colMatch) col = parseInt(colMatch[1], 10);
const msg = (e.message || 'JSON 解析错误').split('\n')[0];
updateStatusError(msg, line, col);
}
};
cm.on('change', debounce(validate, 250));
// 首次校验
validate();
// 按钮逻辑
const copyBtn = document.getElementById('copyBtn');
const pasteBtn = document.getElementById('pasteBtn');
const formatBtn = document.getElementById('formatBtn');
const minifyBtn = document.getElementById('minifyBtn');
const clearBtn = document.getElementById('clearBtn');
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
statusText.textContent = '已复制到剪贴板';
statusbar.classList.add('good'); statusbar.classList.remove('bad');
} catch (err) {
// 作为回退方案
const ta = document.createElement('textarea');
ta.value = text; document.body.appendChild(ta);
ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
statusText.textContent = '复制完成(回退方案)';
statusbar.classList.add('good'); statusbar.classList.remove('bad');
}
}
async function readFromClipboard() {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
const text = window.prompt('浏览器限制了读取剪贴板,请粘贴到此:');
return text || '';
}
}
copyBtn.addEventListener('click', () => copyToClipboard(cm.getValue()));
pasteBtn.addEventListener('click', async () => {
const text = await readFromClipboard();
if (text) {
cm.setValue(text);
cm.focus();
}
});
formatBtn.addEventListener('click', () => {
const txt = cm.getValue();
try {
const obj = JSON.parse(txt);
cm.setValue(JSON.stringify(obj, null, 2));
cm.focus();
updateStatusOK();
} catch (e) {
updateStatusError('格式化失败:内容不是有效 JSON');
}
});
minifyBtn.addEventListener('click', () => {
const txt = cm.getValue();
try {
const obj = JSON.parse(txt);
cm.setValue(JSON.stringify(obj));
cm.focus();
updateStatusOK();
} catch (e) {
updateStatusError('压缩失败:内容不是有效 JSON');
}
});
clearBtn.addEventListener('click', () => {
cm.setValue('');
statusbar.classList.remove('good','bad');
statusText.textContent = '已清空';
clearErrorHighlight();
cm.focus();
});
// 触控体验优化:增大点击区域与取消按钮文本选择
document.querySelectorAll('.btn').forEach(btn => {
btn.style.touchAction = 'manipulation';
});
</script>
</body>
</html>

View File

@@ -0,0 +1,337 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#dff5d0" />
<title>Markdown解析器</title>
<!-- Markdown 解析与安全清洗 -->
<script src="./marked.min.js"></script>
<script src="./purify.min.js"></script>
<style>
:root {
--bg-start: #e9f9e4; /* 淡绿色 */
--bg-end: #f3ffdf; /* 淡黄绿色 */
--card: rgba(255, 255, 255, 0.66);
--card-border: rgba(108, 170, 92, 0.25);
--text: #2b3a2e;
--muted: #5c745a;
--accent: #69b36d;
--accent-2: #9adf76;
--shadow: 0 10px 30px rgba(67, 125, 67, 0.15);
--radius: 18px;
--radius-sm: 12px;
--maxw: min(96vw, 1600px);
}* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", Arial, sans-serif;
color: var(--text);
background: linear-gradient(160deg, var(--bg-start) 0%, var(--bg-end) 100%);
background-attachment: fixed;
}
header {
position: sticky;
top: 0;
z-index: 5;
backdrop-filter: saturate(120%) blur(8px);
background: linear-gradient(160deg, rgba(233,249,228,0.75) 0%, rgba(243,255,223,0.75) 100%);
border-bottom: 1px solid var(--card-border);
}
.wrap {
max-width: var(--maxw);
margin: 0 auto;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 36px; height: 36px; border-radius: 50%;
background: radial-gradient(circle at 30% 30%, var(--accent-2), var(--accent));
box-shadow: var(--shadow);
border: 1px solid var(--card-border);
flex: 0 0 auto;
}
h1 { font-size: 18px; margin: 0; font-weight: 700; letter-spacing: .4px; }
.sub { color: var(--muted); font-size: 12px; }
main { max-width: var(--maxw); margin: 20px auto; padding: 0 16px 36px; }
.panel {
background: var(--card);
border: 1px solid var(--card-border);
box-shadow: var(--shadow);
border-radius: var(--radius);
overflow: hidden;
}
.editor, .preview-box { padding: 14px; }
.label {
font-size: 13px; font-weight: 600; color: var(--muted);
display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
}
textarea {
width: 100%;
min-height: 38vh; /* 适配手机竖屏 */
resize: vertical;
padding: 14px 12px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--card-border);
border-radius: var(--radius-sm);
outline: none;
font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
color: #1f3024;
transition: box-shadow .2s ease, border-color .2s ease;
}
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(105, 179, 109, 0.2);
background: #fff;
}
.toolbar {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
button {
appearance: none; border: none; cursor: pointer;
padding: 10px 14px; font-weight: 600; font-size: 14px;
border-radius: 999px;
background: linear-gradient(135deg, var(--accent-2), var(--accent));
color: #083610; box-shadow: var(--shadow);
transition: transform .04s ease, filter .2s ease;
}
button:active { transform: translateY(1px) scale(0.99); }
button.secondary { background: #ffffffb3; color: #2b3a2e; border: 1px solid var(--card-border); }
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.markdown-body {
padding: 12px 10px;
background: rgba(255,255,255,0.6);
border: 1px solid var(--card-border);
border-radius: var(--radius-sm);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 基础 Markdown 样式(简化版) */
.markdown-body h1, .markdown-body h2, .markdown-body h3,
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin: 14px 0 8px; font-weight: 700; line-height: 1.25; color: #17361f;
}
.markdown-body h1 { font-size: 22px; }
.markdown-body h2 { font-size: 20px; }
.markdown-body h3 { font-size: 18px; }
.markdown-body p, .markdown-body ul, .markdown-body ol { margin: 10px 0; }
.markdown-body a { color: #0d6f3a; text-decoration: underline; }
.markdown-body blockquote { border-left: 4px solid var(--accent); padding: 8px 10px; margin: 10px 0; background: #f6fff0; }
.markdown-body code { background: #f1f7ea; padding: 2px 6px; border-radius: 6px; }
.markdown-body pre { background: #f1f7ea; padding: 10px; border-radius: 10px; overflow: auto; }
.markdown-body table { border-collapse: collapse; width: 100%; }
.markdown-body th, .markdown-body td { border: 1px solid #cfe6c8; padding: 8px; }
.markdown-body th { background: #e8f6df; }
/* 全屏预览覆盖层 */
.overlay {
position: fixed; inset: 0; z-index: 50;
background: linear-gradient(160deg, rgba(233,249,228,0.96), rgba(243,255,223,0.96));
display: none; flex-direction: column;
}
.overlay[aria-hidden="false"] { display: flex; }
.overlay-toolbar {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 12px; gap: 10px;
border-bottom: 1px solid var(--card-border);
background: rgba(255,255,255,0.55);
backdrop-filter: blur(8px) saturate(120%);
}
.overlay-title { font-weight: 700; color: #164926; font-size: 15px; }
.overlay-content {
padding: 14px; overflow: auto; height: 100%;
}
.tip { font-size: 12px; color: var(--muted); margin-top: 6px; }
/* 适配更大屏幕时的布局 */
@media (min-width: 840px) {
.grid { grid-template-columns: 1fr 1fr; }
textarea { min-height: 55vh; }
}
</style>
</head>
<body>
<header>
<div class="wrap">
<div class="logo" aria-hidden="true"></div>
<div>
<h1>Markdown解析器</h1>
</div>
</div>
</header> <main>
<div class="grid">
<section class="panel editor" aria-label="编辑器">
<div class="label">Markdown 输入</div>
<textarea id="md-input" placeholder="在此输入 Markdown 文本"></textarea>
<div class="toolbar">
<button id="btn-preview">预览</button>
<button class="secondary" id="btn-clear" title="清空输入">清空</button>
</div>
</section><section class="panel preview-box" aria-label="预览">
<div class="label">实时预览</div>
<article id="preview" class="markdown-body" aria-live="polite"></article>
</section>
</div>
</main> <!-- 全屏预览覆盖层 --> <div id="overlay" class="overlay" aria-hidden="true" role="dialog" aria-modal="true">
<div class="overlay-toolbar">
<div class="overlay-title">预览</div>
<div style="display:flex; gap:10px;">
<button class="secondary" id="btn-exit">退出预览</button>
</div>
</div>
<div class="overlay-content">
<article id="overlay-preview" class="markdown-body"></article>
</div>
</div> <script>
// Marked 基础配置
marked.setOptions({
gfm: true,
breaks: true,
headerIds: true,
mangle: false
});
const $ = (sel) => document.querySelector(sel);
const input = $('#md-input');
const preview = $('#preview');
const overlay = $('#overlay');
const overlayPreview = $('#overlay-preview');
const btnPreview = $('#btn-preview');
const btnExit = $('#btn-exit');
const btnClear = $('#btn-clear');
const STORAGE_KEY = 'md-editor-content-v1';
// 桌面端自动扩展输入框高度
const MQ_DESKTOP = window.matchMedia('(min-width: 840px)');
function autoResizeTextarea(el) {
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}
function applyTextareaMode() {
if (MQ_DESKTOP.matches) {
input.style.overflowY = 'hidden';
input.style.resize = 'none';
autoResizeTextarea(input);
} else {
input.style.overflowY = '';
input.style.resize = '';
input.style.height = '';
}
}
MQ_DESKTOP.addEventListener('change', applyTextareaMode);
function renderMarkdown(targetEl, srcText) {
try {
const html = marked.parse(srcText ?? '');
// 使用 DOMPurify 进行安全清洗
targetEl.innerHTML = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
} catch (e) {
targetEl.textContent = '解析出错:' + e.message;
}
}
function syncRender() {
const text = input.value;
// 保存到本地
try { localStorage.setItem(STORAGE_KEY, text); } catch (e) {}
renderMarkdown(preview, text);
if (MQ_DESKTOP.matches) { autoResizeTextarea(input); }
}
// 初始载入:从本地存储恢复
(function init() {
let initial = '';
try { initial = localStorage.getItem(STORAGE_KEY) || ''; } catch (e) {}
if (!initial) {
initial = `# 🌿 欢迎使用Markdown解析器\n\n在左侧/上方输入 Markdown右侧/下方会 **实时预览**。\n\n- 支持 GFM、自动换行、代码块\n- 点击右上方的 **全屏预览** 按钮\n- 内容会保存在本地浏览器中\n\n> 小提示:支持表格、引用、链接等常见语法~\n\n| 功能 | 状态 |\n| ---- | ---- |\n| 实时预览 | ✅ |\n| 全屏预览 | ✅ |\n| 本地保存 | ✅ |\n`;
}
input.value = initial;
syncRender();
applyTextareaMode();
})();
// 输入实时渲染
input.addEventListener('input', syncRender);
// 清空
btnClear.addEventListener('click', () => {
input.value = '';
syncRender();
input.focus();
});
// 打开全屏预览
btnPreview.addEventListener('click', async () => {
overlay.setAttribute('aria-hidden', 'false');
overlayPreview.innerHTML = preview.innerHTML;
// 尝试调用原生全屏 API兼容性降级到覆盖层
try {
if (!document.fullscreenElement && overlay.requestFullscreen) {
await overlay.requestFullscreen();
}
} catch (e) {
// 忽略错误,覆盖层已显示
}
});
// 退出全屏预览
function exitOverlay() {
overlay.setAttribute('aria-hidden', 'true');
if (document.fullscreenElement && document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
}
}
btnExit.addEventListener('click', exitOverlay);
// Esc 键退出
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.getAttribute('aria-hidden') === 'false') {
exitOverlay();
}
});
// 当内容变化时,若处于全屏预览,则同步内容
const obs = new MutationObserver(() => {
if (overlay.getAttribute('aria-hidden') === 'false') {
overlayPreview.innerHTML = preview.innerHTML;
}
});
obs.observe(preview, { childList: true, subtree: true });
</script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,262 @@
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>做决定转盘</title>
<style>
:root{
--bg1:#e9f7e9; /* 淡绿 */
--bg2:#f7f9e3; /* 淡黄绿 */
--card:#ffffffcc;
--text:#1b3a2a;
--muted:#3a6b4a;
--accent:#6ccf6e;
--accent-2:#f1f7cf;
--shadow:0 10px 30px rgba(43,96,73,.12);
--radius:18px;
}
html,body{height:100%;}
body{
margin:0; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Inter, 'Helvetica Neue', Arial, 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', "Microsoft YaHei", sans-serif;
color:var(--text);
background: linear-gradient(160deg, var(--bg1), var(--bg2));
display:flex; align-items:center; justify-content:center;
}
.app{ width:min(720px,100%); padding:14px; }
.title{ text-align:center; margin:6px 0 12px; font-weight:800; letter-spacing:.5px; }
.badge{ font-size:12px; background: var(--accent-2); padding: 4px 8px; border-radius:999px; }/* 单列移动端优先 */
.grid{ display:grid; gap:12px; grid-template-columns: 1fr; }
.card{ background:var(--card); backdrop-filter: blur(6px); border-radius:var(--radius); box-shadow:var(--shadow); padding:14px; }
/* 轮盘区域 */
.wheel-wrap{ display:grid; place-items:center; padding:8px 0 2px; }
.wheel{ width:min(92vw, 440px); height:min(92vw, 440px); max-width:480px; max-height:480px; position:relative; }
canvas{ width:100%; height:100%; display:block; }
.pointer{ position:absolute; left:50%; top:-8px; transform:translateX(-50%); width:0; height:0; border-left:12px solid transparent; border-right:12px solid transparent; border-bottom:18px solid var(--accent); filter: drop-shadow(0 2px 3px rgba(0,0,0,.15)); }
/* 行为区:大按钮,便于拇指操作 */
.actions{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px; }
button{ width:100%; box-sizing:border-box; border-radius:14px; border:1px solid #d8ebd8; padding: 12px 14px; min-height:48px; font-size:16px; outline:none; background:#ffffffdd; }
button:active{ transform: translateY(1px); }
button.primary{ background: linear-gradient(180deg, #bff0c4, #98e09d); border:none; color:#0f3b23; font-weight:800; letter-spacing:.3px; box-shadow:0 8px 18px rgba(88,167,110,.25); }
.result{ text-align:center; font-weight:800; font-size:20px; margin:10px 0 4px; }
.hint{ text-align:center; font-size:12px; color:#2f6a45; opacity:.85; }
/* 选项编辑:折叠,默认收起,减少滚动 */
details.options{ margin-top:8px; }
details.options[open]{ background:#ffffffb8; border-radius:14px; padding:8px; }
details.options > summary{ list-style:none; cursor:pointer; padding:10px 12px; border-radius:12px; background:#ffffff; border:1px solid #d8ebd8; font-weight:700; }
details.options > summary::-webkit-details-marker { display:none; }
.mini{ font-size:12px; opacity:.9; margin:6px 2px; }
.controls{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; align-items:end; margin-top:6px; }
label{ font-size:13px; opacity:.9; }
input[type="number"], input[type="text"]{ width:100%; box-sizing:border-box; border-radius:12px; border:1px solid #d8ebd8; padding:12px 12px; font-size:16px; background:#ffffffdd; }
.opt-row{ display:grid; grid-template-columns: 1fr 44px; gap:8px; }
.opt-list{ display:grid; gap:8px; max-height: 38vh; overflow:auto; padding-right:4px; }
.row{ display:flex; gap:8px; align-items:center; }
@media (min-width: 900px){
/* 桌面端给一点并排,但默认单列 */
.grid{ grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="app">
<h2 class="title">做决定转盘 </h2><div class="grid">
<!-- 先展示转盘,便于手机端操作 -->
<div class="card">
<div class="wheel-wrap">
<div class="wheel">
<div class="pointer" aria-hidden="true"></div>
<canvas id="wheel" width="600" height="600" aria-label="做决定转盘画布"></canvas>
</div>
</div>
<div class="actions">
<button id="decide" class="primary" type="button">🎲 做决定</button>
<button id="saveImg" type="button">🖼️ 保存为图片</button>
</div>
<div class="result" id="result"></div>
<div class="hint">提示:点击“做决定”抽取后,可“保存结果为图片”分享到聊天/相册。</div>
</div>
<!-- 配置/编辑选项(折叠) -->
<div class="card" aria-label="配置面板">
<details class="options">
<summary>✏️ 编辑选项(点我展开/收起)</summary>
<div class="controls">
<div>
<label for="count">选项数量</label>
<input id="count" type="number" min="2" max="24" value="4" />
</div>
<div>
<label>&nbsp;</label>
<button id="fillDemo" type="button">填入示例</button>
</div>
</div>
<div class="mini">每行一个选项(会自动同步到转盘)</div>
<div id="optList" class="opt-list" role="list"></div>
</details>
</div>
</div>
</div> <script>
// --------- 元素引用 ---------
const optListEl = document.getElementById('optList');
const countEl = document.getElementById('count');
const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d');
const decideBtn = document.getElementById('decide');
const resultEl = document.getElementById('result');
const saveBtn = document.getElementById('saveImg');
// --------- 配色(固定柔和绿黄,无“柔和配色”按钮) ---------
function softPalette(n){
const bases = ['#dff6d7','#e8fad6','#f3fed5','#e6f7c9','#d8f3c9','#ccf1c6','#f2f9dd','#e0f7e7','#e8f9e4','#f6ffe1','#e9f7e9','#f7f9e3'];
const colors=[]; for(let i=0;i<n;i++){ if(i<bases.length) colors.push(bases[i]); else { const h = 90 + (i*11)%40; const s = 50 + (i*3)%16; const l = 80 - (i*2)%12; colors.push(`hsl(${h} ${s}% ${l}%)`);} }
return colors;
}
let colors = softPalette(24);
// --------- 选项编辑 ---------
function createInputRow(idx, value=''){
const wrap = document.createElement('div'); wrap.className='opt-row'; wrap.setAttribute('role','listitem');
const input = document.createElement('input'); input.type='text'; input.placeholder=`选项 ${idx+1}`; input.value=value; input.addEventListener('input', drawWheel);
const del = document.createElement('button'); del.type='button'; del.textContent='✕'; del.addEventListener('click', ()=>{ wrap.remove(); countEl.value = Math.max(2, optListEl.querySelectorAll('input').length); drawWheel(); });
wrap.appendChild(input); wrap.appendChild(del); return wrap;
}
function syncCount(){
let target = parseInt(countEl.value||'2',10); target = Math.min(24, Math.max(2, target)); countEl.value = target;
const current = optListEl.querySelectorAll('input').length;
if(target>current){ for(let i=current;i<target;i++) optListEl.appendChild(createInputRow(i)); }
else if(target<current){ for(let i=current;i>target;i--) optListEl.lastElementChild?.remove(); }
drawWheel();
}
countEl.addEventListener('change', syncCount);
document.getElementById('fillDemo').addEventListener('click', ()=>{
const demo=['吃饭','睡觉','学习','打游戏','看电影','散步'];
countEl.value = demo.length; syncCount();
[...optListEl.querySelectorAll('input')].forEach((el,i)=> el.value = demo[i]||'');
drawWheel();
});
function getOptions(){
return [...optListEl.querySelectorAll('input')].map(i=>i.value.trim()).filter(Boolean);
}
// --------- 绘制与渲染 ---------
let rotation = 0; // 当前弧度
let lastChoice = null; // 上次结果
function renderWheel(drawCtx, w, h, opts, rot, palette){
const n = Math.max(2, opts.length);
const cx = w/2, cy = h/2; const r = Math.min(w,h)/2 - 8;
drawCtx.clearRect(0,0,w,h);
const slice = Math.PI*2 / n;
for(let i=0;i<n;i++){
const start = rot + i*slice - Math.PI/2; const end = start + slice;
drawCtx.beginPath(); drawCtx.moveTo(cx,cy); drawCtx.arc(cx,cy,r,start,end); drawCtx.closePath();
drawCtx.fillStyle = palette[i%palette.length]; drawCtx.fill();
drawCtx.strokeStyle = '#cfead5'; drawCtx.lineWidth=2; drawCtx.stroke();
// 文本
const mid=(start+end)/2; drawCtx.save();
drawCtx.translate(cx + Math.cos(mid)*(r*0.7), cy + Math.sin(mid)*(r*0.7));
drawCtx.rotate(mid + Math.PI/2); drawCtx.fillStyle = '#1b3a2a';
drawCtx.font='600 22px system-ui, -apple-system, Segoe UI, Roboto';
drawCtx.textAlign='center'; drawCtx.textBaseline='middle';
const label = opts[i % opts.length] || `选项${i+1}`;
shrinkTextToWidth(drawCtx,label,r*0.95);
drawCtx.fillText(label,0,0); drawCtx.restore();
}
// 中心圆
drawCtx.beginPath(); drawCtx.arc(cx,cy, r*0.14, 0, Math.PI*2); drawCtx.fillStyle='#ffffffee'; drawCtx.fill();
drawCtx.strokeStyle='#bfe7c8'; drawCtx.lineWidth=3; drawCtx.stroke();
}
function drawWheel(){
const opts = getOptions();
renderWheel(ctx, canvas.width, canvas.height, opts, rotation, colors);
}
function shrinkTextToWidth(c, text, maxWidth){
let size=22; while(size>12){ c.font=`600 ${size}px system-ui, -apple-system, Segoe UI, Roboto`; if(c.measureText(text).width<=maxWidth) return; size--; }
}
// --------- 动画与随机选择 ---------
let spinning=false;
function spinToIndex(index){
const opts = getOptions();
const n = Math.max(2, opts.length);
const slice = Math.PI*2 / n;
const desired = - (index*slice + slice/2);
let delta = desired - rotation; delta = ((delta % (Math.PI*2)) + Math.PI*2) % (Math.PI*2);
const extraTurns = 5 + Math.floor(Math.random()*3); // 5~7 圈
const finalRotation = rotation + delta + extraTurns*Math.PI*2;
animateRotation(rotation, finalRotation, 1600 + Math.random()*700, ()=>{
rotation = ((finalRotation % (Math.PI*2)) + Math.PI*2) % (Math.PI*2); // 归一
drawWheel();
lastChoice = opts[index];
resultEl.textContent = `结果:${lastChoice}`;
spinning=false;
});
}
function easeOutCubic(t){ return 1 - Math.pow(1-t,3); }
function animateRotation(from, to, duration, done){
const start = performance.now(); spinning=true; resultEl.textContent='';
function frame(now){ const t=Math.min(1,(now-start)/duration); const eased=easeOutCubic(t); rotation=from+(to-from)*eased; drawWheel(); if(t<1) requestAnimationFrame(frame); else done(); }
requestAnimationFrame(frame);
}
decideBtn.addEventListener('click', ()=>{
if(spinning) return;
const opts = getOptions(); if(opts.length<2){ alert('请至少输入两个选项'); return; }
const index = Math.floor(Math.random()*opts.length); spinToIndex(index);
});
// --------- 保存结果为图片 ---------
function roundRect(c, x,y,w,h,r){
const rr=Math.min(r, w/2, h/2); c.beginPath();
c.moveTo(x+rr,y); c.arcTo(x+w,y,x+w,y+h,rr); c.arcTo(x+w,y+h,x,y+h,rr); c.arcTo(x,y+h,x,y,rr); c.arcTo(x,y,x+w,y,rr); c.closePath();
}
function saveImage(){
const opts = getOptions();
if(!lastChoice){ alert('请先点击“做决定”抽取结果'); return; }
const size = 1024; const off=document.createElement('canvas'); off.width=size; off.height=size; const octx=off.getContext('2d');
// 背景
const g=octx.createLinearGradient(0,0,size,size); g.addColorStop(0,'#e9f7e9'); g.addColorStop(1,'#f7f9e3'); octx.fillStyle=g; octx.fillRect(0,0,size,size);
// 轮盘
renderWheel(octx, size, size, opts, rotation, colors);
// 指针
const triH=size*0.05, triW=size*0.09; octx.fillStyle='#6ccf6e'; octx.beginPath(); octx.moveTo(size/2, size*0.02); octx.lineTo(size/2 - triW/2, size*0.02 + triH); octx.lineTo(size/2 + triW/2, size*0.02 + triH); octx.closePath(); octx.fill();
// 结果胶囊
const text=`结果:${lastChoice}`; octx.font='800 52px system-ui, -apple-system, Segoe UI, Roboto'; octx.textBaseline='middle'; octx.textAlign='center';
const padX=34, padY=22; const tw=octx.measureText(text).width; const boxW=tw+padX*2; const boxH=92;
const bx=(size-boxW)/2, by=size*0.82; octx.fillStyle='#ffffffee'; roundRect(octx,bx,by,boxW,boxH,24); octx.fill();
octx.lineWidth=3; octx.strokeStyle='#cfead5'; octx.stroke();
octx.fillStyle='#1b3a2a'; octx.fillText(text, size/2, by+boxH/2);
// 下载
const a=document.createElement('a'); a.href=off.toDataURL('image/png'); a.download=`转盘结果_${Date.now()}.png`; document.body.appendChild(a); a.click(); a.remove();
}
saveBtn.addEventListener('click', saveImage);
// --------- 初始化 ---------
(function init(){
const demo=['吃饭','睡觉','学习','打游戏','敲代码'];
countEl.value=demo.length; syncCount();
[...optListEl.querySelectorAll('input')].forEach((el,i)=> el.value=demo[i]||'');
drawWheel();
window.addEventListener('resize', drawWheel);
})();
</script></body>
</html>

View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>白板</title>
<style>
:root {
--grad-start: #dff5e7; /* 淡绿色 */
--grad-end: #e8f7d4; /* 淡黄绿色 */
--accent: #78c6a3; /* 清新绿 */
--accent-2: #a9dba8; /* 柔和绿 */
--text: #2c3e3b;
--soft: rgba(120, 198, 163, 0.15);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background: linear-gradient(135deg, var(--grad-start), var(--grad-end));
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
#app { height: 100vh; display: flex; flex-direction: column; }
.toolbar {
flex: 0 0 auto;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
padding: 10px;
background: linear-gradient(135deg, rgba(223,245,231,0.8), rgba(232,247,212,0.8));
backdrop-filter: blur(8px);
border-bottom: 1px solid rgba(120,198,163,0.25);
box-shadow: 0 6px 16px var(--soft);
}
.group { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.label { font-size: 14px; opacity: 0.9; }
.value { min-width: 36px; text-align: center; font-size: 13px; opacity: 0.85; }
input[type="color"] {
width: 36px; height: 36px; padding: 0; border: 1px solid rgba(0,0,0,0.08);
border-radius: 8px; background: white; box-shadow: 0 2px 6px var(--soft);
}
input[type="range"] { width: 140px; }
.segmented {
display: inline-flex; border: 1px solid rgba(120,198,163,0.35); border-radius: 10px; overflow: hidden;
box-shadow: 0 2px 6px var(--soft);
}
.segmented button {
padding: 8px 12px; font-size: 14px; border: none; background: rgba(255,255,255,0.8); color: var(--text); cursor: pointer;
}
.segmented button + button { border-left: 1px solid rgba(120,198,163,0.25); }
.segmented button.active { background: var(--accent-2); color: #0f3b2f; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.btn {
padding: 8px 14px; font-size: 14px; border-radius: 10px; border: 1px solid rgba(120,198,163,0.35);
background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(240,255,245,0.9));
color: var(--text); cursor: pointer; box-shadow: 0 2px 6px var(--soft);
}
.btn.primary { background: linear-gradient(180deg, var(--accent-2), #d9f4d5); border-color: rgba(120,198,163,0.5); }
.canvas-wrap { flex: 1 1 auto; position: relative; }
canvas#board {
position: absolute; inset: 0; width: 100%; height: 100%;
background: #ffffff; /* 全屏白色背景 */
touch-action: none; display: block;
}
/* 手机竖屏优化 */
@media (max-width: 480px) {
.toolbar { grid-template-columns: 1fr; }
input[type="range"] { width: 100%; }
.actions { justify-content: flex-start; }
}
</style>
</head>
<body>
<div id="app">
<div class="toolbar">
<div class="group">
<span class="label">颜色</span>
<input id="color" type="color" value="#2c3e3b" />
<span class="label">画笔粗细</span>
<input id="brushSize" type="range" min="1" max="64" value="8" />
<span id="brushVal" class="value">8px</span>
</div>
<div class="group">
<div class="segmented" role="tablist" aria-label="绘制模式">
<button id="modeBrush" class="active" role="tab" aria-selected="true">画笔</button>
<button id="modeEraser" role="tab" aria-selected="false">橡皮擦</button>
</div>
<span class="label">橡皮粗细</span>
<input id="eraserSize" type="range" min="4" max="128" value="20" />
<span id="eraserVal" class="value">20px</span>
</div>
<div class="actions">
<button id="saveBtn" class="btn primary">保存为图片</button>
<button id="clearBtn" class="btn">清空画布</button>
</div>
</div>
<div class="canvas-wrap">
<canvas id="board"></canvas>
</div>
</div>
<script>
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const colorInput = document.getElementById('color');
const brushSizeInput = document.getElementById('brushSize');
const brushVal = document.getElementById('brushVal');
const eraserSizeInput = document.getElementById('eraserSize');
const eraserVal = document.getElementById('eraserVal');
const modeBrushBtn = document.getElementById('modeBrush');
const modeEraserBtn = document.getElementById('modeEraser');
const saveBtn = document.getElementById('saveBtn');
const clearBtn = document.getElementById('clearBtn');
let dpr = Math.max(1, window.devicePixelRatio || 1);
let drawing = false;
let last = { x: 0, y: 0 };
let mode = 'brush'; // 'brush' | 'eraser'
function setActiveMode(newMode) {
mode = newMode;
modeBrushBtn.classList.toggle('active', mode === 'brush');
modeEraserBtn.classList.toggle('active', mode === 'eraser');
modeBrushBtn.setAttribute('aria-selected', mode === 'brush');
modeEraserBtn.setAttribute('aria-selected', mode === 'eraser');
}
function cssSize() {
const r = canvas.getBoundingClientRect();
return { w: Math.round(r.width), h: Math.round(r.height) };
}
function resizeCanvas(preserve = true) {
const { w, h } = cssSize();
const snapshot = preserve ? canvas.toDataURL('image/png') : null;
dpr = Math.max(1, window.devicePixelRatio || 1);
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (snapshot) {
const img = new Image();
img.onload = () => {
// 先铺白底,保证保存图片有白色背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, w, h);
};
img.src = snapshot;
} else {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, w, h);
}
}
function pos(e) {
const r = canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
function stroke(from, to) {
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
if (mode === 'eraser') {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = parseInt(eraserSizeInput.value, 10);
} else {
ctx.strokeStyle = colorInput.value;
ctx.lineWidth = parseInt(brushSizeInput.value, 10);
}
ctx.stroke();
}
canvas.addEventListener('pointerdown', (e) => {
canvas.setPointerCapture(e.pointerId);
drawing = true;
last = pos(e);
e.preventDefault();
}, { passive: false });
canvas.addEventListener('pointermove', (e) => {
if (!drawing) return;
const p = pos(e);
stroke(last, p);
last = p;
e.preventDefault();
}, { passive: false });
function endDraw(e) {
drawing = false;
e && e.preventDefault();
}
canvas.addEventListener('pointerup', endDraw);
canvas.addEventListener('pointercancel', endDraw);
canvas.addEventListener('pointerleave', endDraw);
// UI 交互
modeBrushBtn.addEventListener('click', () => setActiveMode('brush'));
modeEraserBtn.addEventListener('click', () => setActiveMode('eraser'));
brushSizeInput.addEventListener('input', () => {
brushVal.textContent = brushSizeInput.value + 'px';
});
eraserSizeInput.addEventListener('input', () => {
eraserVal.textContent = eraserSizeInput.value + 'px';
});
clearBtn.addEventListener('click', () => {
const { w, h } = cssSize();
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, w, h);
});
saveBtn.addEventListener('click', () => {
// 确保白底
const { w, h } = cssSize();
const altCanvas = document.createElement('canvas');
const altCtx = altCanvas.getContext('2d');
altCanvas.width = w * dpr;
altCanvas.height = h * dpr;
altCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
altCtx.fillStyle = '#ffffff';
altCtx.fillRect(0, 0, w, h);
altCtx.drawImage(canvas, 0, 0, w, h);
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const filename = `白板_${y}${m}${d}_${hh}${mm}.png`;
altCanvas.toBlob((blob) => {
if (!blob) return;
const a = document.createElement('a');
const url = URL.createObjectURL(blob);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
}, 'image/png');
});
// 初始化与自适应
function init() {
resizeCanvas(false);
brushVal.textContent = brushSizeInput.value + 'px';
eraserVal.textContent = eraserSizeInput.value + 'px';
}
window.addEventListener('resize', () => resizeCanvas(true));
document.addEventListener('visibilitychange', () => {
if (!document.hidden) resizeCanvas(true);
});
// 禁用默认触控滚动/双击缩放
canvas.style.touchAction = 'none';
init();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,412 @@
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>▶️视频播放器</title>
<style>
:root{
--bg-from:#e9fbb6; /* 淡黄绿色 */
--bg-to:#bff2cf; /* 淡绿色 */
--accent:#6bbf7a; /* 主色 */
--accent-2:#4aa36b; /* 主色深 */
--text:#18412a; /* 文本深色 */
--muted:#2a6b47; /* 次要文本 */
--card:#ffffffcc; /* 半透明卡片 */
--edge:#e3f0e6; /* 边框 */
--shadow: 0 12px 24px rgba(40,120,80,.15);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
color:var(--text);
background: linear-gradient(160deg,var(--bg-from),var(--bg-to));
-webkit-tap-highlight-color: transparent;
}
.wrap{
min-height:100dvh;
display:flex;flex-direction:column;gap:16px;
padding:16px; padding-bottom:24px;
max-width: 960px; margin: 0 auto;
}
header{
display:flex; align-items:center; justify-content:space-between; gap:12px;
}
.title{
font-weight:700; letter-spacing:.2px; font-size:18px;
}
.uploader{
display:grid; grid-template-columns:1fr; gap:10px;
background:var(--card);
border:1px solid var(--edge);
box-shadow:var(--shadow);
border-radius:18px; padding:12px;
}
.row{display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
input[type="url"], .btn, input[type="file"], .speed-select, .range{
border:1px solid var(--edge); border-radius:14px;
background:#fff; color:var(--text);
padding:10px 12px; font-size:14px;
}
input[type="url"]{ flex:1; min-width:0; }
.btn{ background:linear-gradient(180deg,#ffffff,#f7fff5); cursor:pointer; user-select:none; }
.btn.primary{ background:linear-gradient(180deg,#d9fbd8,#bcf2c9); border-color:#b4e6be; }
.btn:hover{ filter:saturate(1.02); }
.btn:active{ transform:translateY(1px); }
.btn[disabled]{ opacity:.5; cursor:not-allowed; }.player-card{
background:var(--card); border:1px solid var(--edge); box-shadow:var(--shadow);
border-radius:22px; overflow:hidden;
}
.video-box{ position:relative; background:#000; aspect-ratio:16/9; }
video{ width:100%; height:auto; display:block; background:#000; }
.overlay-center{
position:absolute; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none;
}
.big-btn{
width:64px; height:64px; border-radius:50%;
background:radial-gradient(circle at 30% 30%, #ffffff, #eaffea);
display:grid; place-items:center; box-shadow:0 8px 24px rgba(0,0,0,.25);
opacity:0; transform:scale(.9); transition:.25s;
}
.overlay-center.show .big-btn{ opacity:1; transform:scale(1); }
.controls{
display:flex; flex-direction:column; gap:8px; padding:12px; background:linear-gradient(180deg,#f3fff3cc,#eafff2cc);
border-top:1px solid var(--edge);
}
.progress-row{ display:flex; align-items:center; gap:10px; }
.range{ -webkit-appearance:none; appearance:none; width:100%; height:14px; padding:0 0; background:transparent; }
.range::-webkit-slider-runnable-track{ height:6px; background:linear-gradient(90deg,#c9efcf,#aee7bb); border-radius:999px; }
.range::-moz-range-track{ height:6px; background:linear-gradient(90deg,#c9efcf,#aee7bb); border-radius:999px; }
.range::-webkit-slider-thumb{ -webkit-appearance:none; margin-top:-6px; width:18px; height:18px; border-radius:50%; background:#fff; border:1px solid #cde6d5; box-shadow:0 2px 8px rgba(51,117,84,.3); }
.range::-moz-range-thumb{ width:18px; height:18px; border:none; border-radius:50%; background:#fff; box-shadow:0 2px 8px rgba(51,117,84,.3); }
.time{ font-variant-tabular-nums:tabular-nums; font-size:12px; color:var(--muted); min-width:96px; text-align:center; }
.bar{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.icon-btn{ border:none; background:#fff; border:1px solid var(--edge); border-radius:12px; width:40px; height:40px; display:grid; place-items:center; cursor:pointer; }
.icon-btn:active{ transform:translateY(1px); }
.speed-select{ padding-right:28px; }
.vol{ display:flex; align-items:center; gap:6px; }
.vol input{ width:110px; }
.name{ font-size:12px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* 手机竖屏优先 */
@media (min-width:720px){
.controls{ padding:14px 16px; }
.icon-btn{ width:42px; height:42px; }
.vol input{ width:140px; }
}
@media (max-width:420px){
.vol input{ width:96px; }
.time{ min-width:70px; }
.uploader .row > .btn,
.uploader .row > input[type="url"] { flex: 1 1 100%; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="title">▶️视频播放器</div>
<button class="btn" id="resetBtn" title="重置播放器">重置</button>
</header><section class="uploader" aria-label="选择视频源">
<div class="row">
<input aria-label="视频链接" type="url" id="urlInput" placeholder="粘贴视频直链(.mp4 / .webm / .ogg / .m3u8后回车…" />
<button class="btn primary" id="loadUrlBtn">加载链接</button>
</div>
<div class="row">
<input aria-label="选择本地视频文件" type="file" id="fileInput" class="file-input" accept="video/*" />
<label for="fileInput" class="btn" id="fileInputLabel">选择本地文件</label>
<span class="name" id="fileName">未选择文件</span>
</div>
</section>
<section class="player-card" aria-label="视频播放器">
<div class="video-box" id="videoBox">
<video id="video" playsinline preload="metadata"></video>
<div class="overlay-center" id="overlay">
<div class="big-btn" aria-hidden="true">▶︎</div>
</div>
</div>
<div class="controls" role="group" aria-label="播放控制">
<div class="progress-row">
<span class="time" id="timeNow">00:00</span>
<input type="range" class="range" id="progress" min="0" max="1000" value="0" step="1" aria-label="进度条" />
<span class="time" id="timeTotal">--:--</span>
</div>
<div class="bar">
<button class="icon-btn" id="playBtn" title="播放/暂停 (空格)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5v14l11-7-11-7z" fill="currentColor"/></svg>
</button>
<button class="icon-btn" id="backwardBtn" title="后退10秒 (←)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M11 5V2L6 7l5 5V9c3.86 0 7 3.14 7 7 0 1.05-.22 2.05-.62 2.95l1.48 1.48A8.96 8.96 0 0020 16c0-4.97-4.03-9-9-9z" fill="currentColor"/><text x="3" y="21" font-size="8" fill="currentColor">10</text></svg>
</button>
<button class="icon-btn" id="forwardBtn" title="快进10秒 (→)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M13 5V2l5 5-5 5V9c-3.86 0-7 3.14-7 7 0 1.05.22 2.05.62 2.95L5.14 20.4A8.96 8.96 0 014 16c0-4.97 4.03-9 9-9z" fill="currentColor"/><text x="14" y="21" font-size="8" fill="currentColor">10</text></svg>
</button>
<label class="vol" title="音量 (↑/↓)">
<button class="icon-btn" id="muteBtn" aria-label="静音切换">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/></svg>
</button>
<input type="range" class="range" id="volume" min="0" max="1" step="0.01" value="1" aria-label="音量" />
</label>
<select id="speed" class="speed-select" title="倍速 ( , / . )" aria-label="倍速">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1.0×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2.0×</option>
</select>
<button class="icon-btn" id="pipBtn" title="画中画">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor"/><rect x="12.5" y="10" width="7" height="5" rx="1" fill="currentColor"/></svg>
</button>
<button class="icon-btn" id="fsBtn" title="全屏 (F)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 3H3v5h2V5h3V3zm10 0h-5v2h3v3h2V3zM5 14H3v7h7v-2H5v-5zm16 0h-2v5h-5v2h7v-7z" fill="currentColor"/></svg>
</button>
<div class="name" id="sourceName" title="当前来源">未加载视频</div>
</div>
</div>
</section>
</div> <!-- 可选HLS 支持(仅当加载 .m3u8 时使用)。静态网页可直接引用 CDN。--> <script src="hls.min.js" integrity="sha384-N7Pzv6j4n0O3+zJVwCyO0n2A7bgb1z47Z4+Z1fH2E0KXpKj9c3n2U6xJQ8P9yq3s" crossorigin="anonymous"></script> <script>
const $ = sel => document.querySelector(sel);
const video = $('#video');
const progress = $('#progress');
const timeNow = $('#timeNow');
const timeTotal = $('#timeTotal');
const playBtn = $('#playBtn');
const backwardBtn = $('#backwardBtn');
const forwardBtn = $('#forwardBtn');
const muteBtn = $('#muteBtn');
const volume = $('#volume');
const speed = $('#speed');
const fsBtn = $('#fsBtn');
const pipBtn = $('#pipBtn');
const overlay = $('#overlay');
const fileInput = $('#fileInput');
const urlInput = $('#urlInput');
const loadUrlBtn = $('#loadUrlBtn');
const sourceName = $('#sourceName');
const fileName = $('#fileName');
const fileInputLabel = $('#fileInputLabel');
const resetBtn = $('#resetBtn');
const videoBox = $('#videoBox');
const fmt = s => {
if (!isFinite(s)) return '--:--';
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = Math.floor(s%60);
return h>0 ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` : `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`;
};
// 恢复偏好
try{
const vol = localStorage.getItem('vp_volume');
if (vol !== null) video.volume = Number(vol);
volume.value = video.volume;
const spd = localStorage.getItem('vp_speed');
if (spd) { video.playbackRate = Number(spd); speed.value = spd; }
}catch{}
// 事件绑定
const togglePlay = () => {
if (video.paused || video.ended) { video.play(); showOverlay(); } else { video.pause(); showOverlay(); }
updatePlayIcon();
};
const updatePlayIcon = () => {
playBtn.innerHTML = video.paused ?
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 5v14l11-7-11-7z" fill="currentColor"/></svg>' :
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 5h4v14H6zM14 5h4v14h-4z" fill="currentColor"/></svg>';
};
const showOverlay = () => {
overlay.classList.add('show');
clearTimeout(showOverlay._t);
showOverlay._t = setTimeout(()=>overlay.classList.remove('show'), 350);
};
const step = (sec) => { video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + sec)); showOverlay(); };
const loadFile = (file) => {
if (!file) return;
const url = URL.createObjectURL(file);
attachSource(url, {name: file.name});
sourceName.textContent = file.name;
fileName.textContent = file.name;
if (fileInputLabel) fileInputLabel.textContent = '已选择:' + file.name;
};
const isHls = (url) => /\.m3u8(\?.*)?$/i.test(url);
const isDirect = (url) => /(\.mp4|\.webm|\.ogg)(\?.*)?$/i.test(url) || url.startsWith('blob:') || url.startsWith('data:');
let hls; // hls.js 实例
const destroyHls = () => { if (hls) { hls.destroy(); hls = null; } };
function attachSource(url, {name='外部链接'}={}){
destroyHls();
video.pause();
video.removeAttribute('src');
video.load();
if (isHls(url)){
if (video.canPlayType('application/vnd.apple.mpegURL')){
video.src = url; // Safari 原生
} else if (window.Hls && window.Hls.isSupported()){
hls = new Hls({ maxBufferLength: 30, enableWorker: true });
hls.loadSource(url);
hls.attachMedia(video);
} else {
alert('该浏览器不支持 HLS 播放(.m3u8。请使用 Safari 或现代 Chromium 浏览器。');
return;
}
} else if (isDirect(url)) {
video.src = url;
} else {
alert('请提供视频“直链”地址(.mp4 / .webm / .ogg / .m3u8。普通网页链接无法直接播放。');
return;
}
sourceName.textContent = name || url;
video.playbackRate = Number(speed.value || 1);
video.volume = Number(volume.value);
updatePlayIcon();
video.addEventListener('loadedmetadata', ()=>{
timeTotal.textContent = fmt(video.duration);
progress.value = 0; timeNow.textContent = '00:00';
}, { once: true });
}
// 选择文件
fileInput.addEventListener('change', e=>{
const file = e.target.files && e.target.files[0];
loadFile(file);
});
// 通过 URL 加载
const loadFromInput = () => {
const url = (urlInput.value || '').trim();
if (!url) return;
attachSource(url, {name: url});
};
loadUrlBtn.addEventListener('click', loadFromInput);
urlInput.addEventListener('keydown', e=>{ if (e.key === 'Enter') loadFromInput(); });
// 播放控制
playBtn.addEventListener('click', togglePlay);
backwardBtn.addEventListener('click', ()=>step(-10));
forwardBtn.addEventListener('click', ()=>step(10));
// 音量与静音
volume.addEventListener('input', ()=>{ video.volume = Number(volume.value); try{localStorage.setItem('vp_volume', volume.value);}catch{} });
muteBtn.addEventListener('click', ()=>{ video.muted = !video.muted; muteBtn.style.opacity = video.muted? .6:1; });
// 倍速
speed.addEventListener('change', ()=>{ video.playbackRate = Number(speed.value); try{localStorage.setItem('vp_speed', speed.value);}catch{} });
// 画中画
pipBtn.addEventListener('click', async ()=>{
try{
if (document.pictureInPictureElement) { await document.exitPictureInPicture(); }
else if (document.pictureInPictureEnabled && !video.disablePictureInPicture) { await video.requestPictureInPicture(); }
}catch(err){ console.warn(err); }
});
// 全屏
const isFullscreen = () => document.fullscreenElement || document.webkitFullscreenElement;
fsBtn.addEventListener('click', ()=>{
if (!isFullscreen()) {
(videoBox.requestFullscreen || videoBox.webkitRequestFullscreen || videoBox.requestFullScreen)?.call(videoBox);
} else {
(document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
}
});
// 进度显示 & 拖动
const syncProgress = () => {
if (!video.duration) return;
const fraction = video.currentTime / video.duration;
progress.value = Math.round(fraction * 1000);
timeNow.textContent = fmt(video.currentTime);
timeTotal.textContent = fmt(video.duration);
};
let rafId;
const loop = () => { syncProgress(); rafId = requestAnimationFrame(loop); };
video.addEventListener('play', ()=>{ updatePlayIcon(); cancelAnimationFrame(rafId); loop(); });
video.addEventListener('pause', ()=>{ updatePlayIcon(); cancelAnimationFrame(rafId); syncProgress(); });
video.addEventListener('ended', ()=>{ updatePlayIcon(); showOverlay(); });
let seeking = false;
const seekTo = (val)=>{
if (!video.duration) return;
const target = (val/1000) * video.duration;
video.currentTime = target;
syncProgress();
};
['input','change'].forEach(evt=>progress.addEventListener(evt, e=>{ seeking = evt==='input'; seekTo(Number(progress.value)); }));
// 键盘快捷键
document.addEventListener('keydown', (e)=>{
if (/input|textarea|select/i.test(document.activeElement.tagName)) return;
switch(e.key){
case ' ': e.preventDefault(); togglePlay(); break;
case 'ArrowLeft': step(-5); break;
case 'ArrowRight': step(5); break;
case 'ArrowUp': video.volume = Math.min(1, video.volume + .05); volume.value = video.volume; break;
case 'ArrowDown': video.volume = Math.max(0, video.volume - .05); volume.value = video.volume; break;
case 'f': case 'F': fsBtn.click(); break;
case 'm': case 'M': video.muted = !video.muted; muteBtn.style.opacity = video.muted? .6:1; break;
case ',': video.playbackRate = Math.max(.25, Math.round((video.playbackRate - .25)*4)/4); speed.value = String(video.playbackRate); break;
case '.': video.playbackRate = Math.min(2, Math.round((video.playbackRate + .25)*4)/4); speed.value = String(video.playbackRate); break;
default:
if (/^[0-9]$/.test(e.key) && video.duration){
const pct = Number(e.key) / 10; video.currentTime = pct * video.duration;
}
}
});
// 双击左右快进/后退(移动端友好)
let lastTap = 0;
videoBox.addEventListener('touchend', (e)=>{
const now = Date.now();
const dt = now - lastTap; lastTap = now;
if (dt>300) return; // 双击窗口
const x = e.changedTouches[0].clientX;
const rect = videoBox.getBoundingClientRect();
if (x - rect.left < rect.width/2) step(-10); else step(10);
});
// 点击视频区域也可播放/暂停
videoBox.addEventListener('click', (e)=>{
// 避免点击控制条触发
if (e.target.closest('.controls')) return;
togglePlay();
});
// 重置
resetBtn.addEventListener('click', ()=>{
try{ localStorage.removeItem('vp_volume'); localStorage.removeItem('vp_speed'); }catch{}
destroyHls();
video.pause();
video.removeAttribute('src');
video.load();
sourceName.textContent = '未加载视频';
fileName.textContent = '未选择文件';
urlInput.value = '';
progress.value = 0; timeNow.textContent = '00:00'; timeTotal.textContent = '--:--';
volume.value = 1; video.volume = 1; speed.value = '1'; video.playbackRate = 1;
updatePlayIcon();
});
// 初始图标
updatePlayIcon();
</script></body>
</html>

View File

@@ -0,0 +1,225 @@
(() => {
const { createApp, ref, computed, watch } = Vue;
// 检测是否可用 Math.js
const hasMath = typeof math !== 'undefined';
if (hasMath) {
math.config({ number: 'BigNumber', precision: 64 });
}
// 保存原始三角函数以便覆盖时调用
const originalSin = hasMath ? math.sin : null;
const originalCos = hasMath ? math.cos : null;
const originalTan = hasMath ? math.tan : null;
// 角度转换因子deg -> rad
const RAD_FACTOR = hasMath ? math.divide(math.pi, math.bignumber(180)) : (Math.PI / 180);
// 动态角度模式变量供三角函数使用
let angleModeVar = 'deg';
function sinWrapper(x) {
try {
if (angleModeVar === 'deg') {
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
return hasMath ? originalSin(xr) : Math.sin(xr);
}
return hasMath ? originalSin(x) : Math.sin(Number(x));
} catch (e) { throw e; }
}
function cosWrapper(x) {
try {
if (angleModeVar === 'deg') {
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
return hasMath ? originalCos(xr) : Math.cos(xr);
}
return hasMath ? originalCos(x) : Math.cos(Number(x));
} catch (e) { throw e; }
}
function tanWrapper(x) {
try {
if (angleModeVar === 'deg') {
const xr = hasMath ? math.multiply(x, RAD_FACTOR) : (Number(x) * RAD_FACTOR);
return hasMath ? originalTan(xr) : Math.tan(xr);
}
return hasMath ? originalTan(x) : Math.tan(Number(x));
} catch (e) { throw e; }
}
// 覆盖三角函数以支持角度模式Math.js 可用时)
if (hasMath) {
math.import({ sin: sinWrapper, cos: cosWrapper, tan: tanWrapper }, { override: true });
}
function formatBig(value) {
try {
if (value == null) return '';
if (hasMath) {
return math.format(value, {
notation: 'auto',
precision: 14,
lowerExp: -6,
upperExp: 15,
});
} else {
const num = typeof value === 'number' ? value : Number(value);
if (!isFinite(num)) return '错误';
const str = num.toFixed(12);
return str.replace(/\.0+$/, '').replace(/(\.[0-9]*?)0+$/, '$1');
}
} catch (e) {
return String(value);
}
}
function normalize(exp) {
// 将显示符号标准化为计算符号,保留原字符不做删除
return exp
.replace(/×/g, '*')
.replace(/÷/g, '/')
.replace(/√/g, 'sqrt');
}
createApp({
setup() {
const expression = ref('');
const result = ref(hasMath ? math.bignumber(0) : 0);
const errorMsg = ref('');
const lastAns = ref(hasMath ? math.bignumber(0) : 0);
const angleMode = ref('deg');
watch(angleMode, (val) => { angleModeVar = val; });
const formattedExpression = computed(() => expression.value || '0');
const formattedResult = computed(() => errorMsg.value ? '' : formatBig(result.value));
function isParenthesesBalanced(s) {
let count = 0;
for (const ch of s) {
if (ch === '(') count++;
else if (ch === ')') count--;
if (count < 0) return false;
}
return count === 0;
}
function safeEvaluate(exp) {
errorMsg.value = '';
try {
const s = normalize(exp);
if (!s) { result.value = hasMath ? math.bignumber(0) : 0; return result.value; }
// 检测非法字符仅允许数字、运算符、括号、字母用于函数和ANS以及空白
if (/[^0-9\.\+\-\*\/\^\(\)a-zA-Z\s]/.test(s)) { throw new Error('错误'); }
if (!isParenthesesBalanced(s)) throw new Error('错误');
if (hasMath) {
const scope = { ANS: lastAns.value };
const res = math.evaluate(s, scope);
// 防止除以零等无效情况
if (res && res.isFinite && !res.isFinite()) { throw new Error('错误'); }
result.value = res;
return res;
} else {
// 原生回退:将表达式映射到安全本地函数
let expr = s
.replace(/\^/g, '**')
.replace(/sin\(/g, '__sin(')
.replace(/cos\(/g, '__cos(')
.replace(/tan\(/g, '__tan(')
.replace(/sqrt\(/g, '__sqrt(')
.replace(/\bANS\b/g, String(lastAns.value));
// 严格校验(只允许安全字符)
if (/[^0-9+\-*/()._^a-zA-Z\s]/.test(expr)) throw new Error('错误');
// 定义本地安全函数
const __sqrt = (x) => Math.sqrt(Number(x));
const __sin = (x) => angleModeVar === 'deg' ? Math.sin(Number(x) * Math.PI / 180) : Math.sin(Number(x));
const __cos = (x) => angleModeVar === 'deg' ? Math.cos(Number(x) * Math.PI / 180) : Math.cos(Number(x));
const __tan = (x) => angleModeVar === 'deg' ? Math.tan(Number(x) * Math.PI / 180) : Math.tan(Number(x));
const res = Function('__sqrt','__sin','__cos','__tan', `"use strict"; return (${expr});`)(__sqrt,__sin,__cos,__tan);
if (!isFinite(res)) throw new Error('错误');
result.value = res;
return res;
}
} catch (err) {
errorMsg.value = '错误';
return null;
}
}
watch(expression, (exp) => { safeEvaluate(exp); });
function press(token) {
// 避免连续两个小数点
if (token === '.' && expression.value.slice(-1) === '.') return;
expression.value += token;
}
function op(opSymbol) {
const last = expression.value.slice(-1);
if (/[\+\-×÷\*\/\^]/.test(last)) {
expression.value = expression.value.slice(0, -1) + opSymbol;
} else {
expression.value += opSymbol;
}
}
function func(fn) {
const map = { sqrt: 'sqrt', sin: 'sin', cos: 'cos', tan: 'tan' };
const f = map[fn] || fn;
expression.value += f + '(';
}
function square() {
expression.value += '^2';
}
function backspace() {
if (!expression.value) return;
expression.value = expression.value.slice(0, -1);
}
function clear() {
expression.value = '';
result.value = math.bignumber(0);
errorMsg.value = '';
}
function equals() {
const res = safeEvaluate(expression.value);
if (res != null) {
lastAns.value = res;
expression.value = formatBig(res);
result.value = res;
}
}
function ans() {
expression.value += 'ANS';
}
function setAngle(mode) {
angleMode.value = mode;
}
// 初始计算
safeEvaluate(expression.value);
return {
expression,
result,
errorMsg,
formattedExpression,
formattedResult,
angleMode,
setAngle,
press,
op,
func,
clear,
backspace,
equals,
square,
ans,
};
},
}).mount('#app');
})();

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<title>🖩网页计算器</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app" class="calculator">
<header class="topbar">
<div class="brand">🖩网页计算器</div>
</header>
<section class="display">
<div class="expression" :title="expression">{{ formattedExpression }}</div>
<div class="result" :class="{ error: !!errorMsg }">{{ errorMsg || formattedResult }}</div>
</section>
<section class="keypad">
<button @click="press('(')">(</button>
<button @click="press(')')">)</button>
<button @click="func('sqrt')"></button>
<button @click="clear()">AC</button>
<button @click="func('sin')">sin</button>
<button @click="func('cos')">cos</button>
<button @click="func('tan')">tan</button>
<button @click="backspace()"></button>
<button @click="press('7')">7</button>
<button @click="press('8')">8</button>
<button @click="press('9')">9</button>
<button @click="op('÷')">÷</button>
<button @click="press('4')">4</button>
<button @click="press('5')">5</button>
<button @click="press('6')">6</button>
<button @click="op('×')">×</button>
<button @click="press('1')">1</button>
<button @click="press('2')">2</button>
<button @click="press('3')">3</button>
<button @click="op('-')">-</button>
<button @click="press('0')">0</button>
<button @click="press('.')">.</button>
<button @click="square()"></button>
<button @click="op('+')">+</button>
<button class="span-2" @click="ans()">ANS</button>
<button class="span-2 action" @click="equals()">=</button>
</section>
</main>
<!-- Frameworks -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjs@11/dist/math.min.js"></script>
<!-- App -->
<script src="./app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,138 @@
:root {
--bg-start: #d9f7d9;
--bg-end: #e9fbd7;
--btn-bg-1: #f7fff0;
--btn-bg-2: #efffe6;
--accent-1: #a6e3a1;
--accent-2: #8fd68b;
--text: #173b2b;
--text-soft: #406a53;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif;
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
color: var(--text);
overflow: auto; /* 保留滚动效果 */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 隐藏滚动条 */
html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; width: 0; height: 0; }
html, body { scrollbar-width: none; }
.calculator {
max-width: 420px;
margin: 0 auto;
min-height: 100vh;
padding: 12px env(safe-area-inset-right) 24px env(safe-area-inset-left);
display: flex;
flex-direction: column;
gap: 10px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
font-weight: 600;
letter-spacing: 0.5px;
}
.angle-toggle {
display: inline-flex;
background: rgba(255,255,255,0.35);
border-radius: 999px;
padding: 3px;
}
.angle-toggle button {
appearance: none;
border: none;
background: transparent;
padding: 8px 12px;
border-radius: 999px;
color: #24543a;
font-weight: 600;
}
.angle-toggle button.active {
background: #b8e2b1;
box-shadow: 0 1px 2px rgba(0,0,0,0.08) inset;
}
.display {
background: rgba(255,255,255,0.6);
border-radius: 14px;
padding: 12px 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.expression {
min-height: 28px;
font-size: 18px;
color: var(--text-soft);
word-break: break-all;
}
.result {
font-size: 32px;
font-weight: 700;
text-align: right;
color: var(--text);
padding-top: 4px;
}
.result.error { color: #d35454; }
.keypad {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.keypad button {
appearance: none;
border: none;
border-radius: 12px;
padding: 14px 0;
min-height: 58px; /* 移动端友好触控 */
font-size: 18px;
font-weight: 600;
color: var(--text);
background: linear-gradient(180deg, var(--btn-bg-1), var(--btn-bg-2));
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
-webkit-tap-highlight-color: transparent;
}
.keypad button:active {
transform: translateY(1px);
box-shadow: 0 1px 4px rgba(0,0,0,0.10);
}
.keypad .action {
background: linear-gradient(180deg, var(--accent-1), var(--accent-2));
color: #0f2a1f;
}
.keypad .span-2 { grid-column: span 2; }
.tips {
opacity: 0.7;
text-align: center;
font-size: 12px;
margin-top: auto;
padding-bottom: 8px;
}
@media (max-width: 360px) {
.keypad button { min-height: 52px; font-size: 16px; }
.result { font-size: 28px; }
}

View File

@@ -0,0 +1,260 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>记事本</title>
<style>
:root{
/* 柔和淡绿—黄绿色系 */
--g1:#effae6; /* very light green */
--g2:#e7f7dc; /* pale spring green */
--g3:#f3f9e1; /* pale yellow-green */
--ink:#1b3a2a; /* 深绿色文字 */
--muted:#2e5a43a8;
--accent:#bfe7b8; /* 轻微按钮底色 */
--accent-press:#a9d7a2;
--ring:#9fd79a88;
}html,body{
height:100%;
-ms-overflow-style: none;
scrollbar-width: none;
}
body{
margin:0;
color:var(--ink);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Noto Sans CJK SC", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif;
background:
radial-gradient(1200px 800px at 10% -10%, var(--g1), transparent 70%),
radial-gradient(1000px 700px at 100% 20%, var(--g2), transparent 60%),
linear-gradient(135deg, var(--g2), var(--g3));
min-height: 100svh;
min-height: 100dvh;
display:flex; flex-direction:column;
}
.app{ display:flex; flex-direction:column; min-height:100vh; min-height:100svh; min-height:100dvh; }
.toolbar{
position:sticky; top:0; z-index:10;
display:flex; gap:.5rem; align-items:center; justify-content:flex-end;
padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) 12px max(12px, env(safe-area-inset-left));
background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,.25));
backdrop-filter: blur(8px);
border-bottom: 1px solid #00000010;
}
.btn{
-webkit-tap-highlight-color: transparent;
appearance:none; border:none; cursor:pointer;
padding: .6rem .9rem; border-radius: 14px;
background: var(--accent);
color: var(--ink); font-weight: 600; letter-spacing:.2px;
box-shadow: 0 1px 0 #00000010, inset 0 0 0 1px #00000010;
transition: transform .05s ease, background-color .15s ease, box-shadow .15s ease;
}
.btn:active{ transform: translateY(1px); background: var(--accent-press); }
.btn:focus-visible{ outline: none; box-shadow: 0 0 0 3px var(--ring); }
.main{
/* 使编辑区纵向充满可视区 */
flex: 1 1 auto;
display:grid;
grid-template-rows: 1fr;
padding: 0 max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
}
/* 编辑器外壳 */
.editor{
align-self: stretch; justify-self: stretch;
width: 100%;
background: rgba(255,255,255,.55);
border-radius: 18px;
box-shadow: 0 10px 30px #00000010, inset 0 0 0 1px #00000010;
display:grid;
grid-template-columns: 1fr;
overflow: visible; /* 因为 textarea 自适应高度,整体由页面滚动 */
}
/* 行号栏 */
.gutter{
--digits: 2; /* 通过 JS 动态更新 */
padding: 14px 10px 14px 14px;
min-width: calc(var(--digits) * 0.75ch + 22px);
text-align: right;
user-select: none;
color: var(--muted);
background: linear-gradient(180deg, rgba(255,255,255,.65), rgba(255,255,255,.35));
border-top-left-radius: 18px; border-bottom-left-radius: 18px;
border-right: 1px dashed #0000001a;
font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
white-space: pre-wrap;
}
/* 记事本文本域 */
.pad{
display:block; resize:none;
padding: 14px 14px 14px 12px; /* 左侧稍小以贴近行号 */
margin:0; border:none; outline:none; background: transparent;
width: 100%; height: auto; min-height: 40vh; /* 初始高度,随后由 JS 自适应 */
font: 15px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
color: var(--ink);
caret-color: #2c7a4b;
tab-size: 2; /* 在移动端保持更短缩进 */
}
.pad::placeholder{ color:#2e5a4377; }
.pad:focus-visible{ outline:none; }
/* 复制成功提示 */
.toast{
position: fixed; left: 50%; bottom: calc(16px + env(safe-area-inset-bottom)); transform: translateX(-50%) translateY(20px);
background: rgba(46, 90, 67, .95);
color: white; padding: 10px 14px; border-radius: 12px; font-size: 14px;
box-shadow: 0 8px 24px #00000030;
opacity: 0; pointer-events:none;
transition: opacity .25s ease, transform .25s ease;
}
.toast.show{ opacity: 1; transform: translateX(-50%) translateY(0); }
html::-webkit-scrollbar, body::-webkit-scrollbar{ width: 0; height: 0; }
/* 小屏优化 */
@media (max-width: 700px){
.toolbar{ justify-content: space-between; }
.btn{ padding:.55rem .8rem; border-radius:12px; }
.gutter{ font-size: 13px; }
.pad{ font-size: 15px; line-height: 1.7; }
}
</style>
</head>
<body>
<div class="app" role="application" aria-label="淡绿记事本">
<div class="toolbar" aria-label="工具栏">
<div style="margin-right:auto;font-weight:700;letter-spacing:.3px;opacity:.8">📝记事本</div>
<button id="copyBtn" class="btn" type="button" aria-label="一键复制">复制</button>
<button id="clearBtn" class="btn" type="button" aria-label="一键清空">清空</button>
<button id="exportBtn" class="btn" type="button" aria-label="导出为TXT">下载</button>
</div><main class="main">
<section class="editor" aria-label="编辑器">
<textarea id="pad" class="pad" spellcheck="false" placeholder="在这里开始记笔记"></textarea>
</section>
</main>
</div> <div id="toast" class="toast" role="status" aria-live="polite">已复制到剪贴板</div> <script>
(function(){
const pad = document.getElementById('pad');
const clearBtn = document.getElementById('clearBtn');
const copyBtn = document.getElementById('copyBtn');
const exportBtn = document.getElementById('exportBtn');
const toast = document.getElementById('toast');
// 自动增高:根据内容调整 textarea 高度
function autoResize(){
pad.style.height = 'auto';
// 在移动端加上一点冗余,避免光标被遮挡
const extra = 8;
pad.style.height = (pad.scrollHeight + extra) + 'px';
}
// 更新行号(以换行符为准,不计算软换行)
function updateLineNumbers(){
const lines = pad.value.split('\n').length || 1;
// 生成 "1\n2\n3..." 的字符串
let nums = '';
// 使用较快的构造方式避免频繁拼接开销
const arr = new Array(lines);
for (let i=0;i<lines;i++){ arr[i] = String(i+1); }
nums = arr.join('\n');
gutter.textContent = nums;
// 动态调整行号栏宽度(按位数估算)
const digits = String(lines).length;
gutter.style.setProperty('--digits', Math.max(2, digits));
}
function refresh(){
autoResize();
}
// 一键清空(确认)
clearBtn.addEventListener('click', () => {
const ok = confirm('确认要清空全部内容吗?');
if(!ok) return;
pad.value = '';
refresh();
pad.focus();
});
// 一键复制
copyBtn.addEventListener('click', async () => {
try{
await navigator.clipboard.writeText(pad.value);
showToast('已复制到剪贴板');
}catch(err){
// 兼容旧环境的回退方案
const sel = document.getSelection();
const range = document.createRange();
range.selectNodeContents(pad);
sel.removeAllRanges(); sel.addRange(range);
const ok = document.execCommand('copy');
sel.removeAllRanges();
showToast(ok ? '已复制到剪贴板' : '复制失败');
}
});
// 导出为TXT
function exportText(){
try{
const text = pad.value || '';
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const now = new Date();
const two = n => String(n).padStart(2, '0');
const filename = `笔记_${now.getFullYear()}${two(now.getMonth()+1)}${two(now.getDate())}_${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}.txt`;
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('已导出 TXT 文件');
}catch(err){
showToast('导出失败');
console.error(err);
}
}
exportBtn.addEventListener('click', exportText);
// 提示小气泡
let toastTimer;
function showToast(text){
toast.textContent = text;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(()=> toast.classList.remove('show'), 1600);
}
// 输入时联动
pad.addEventListener('input', refresh);
// 初次渲染
window.addEventListener('DOMContentLoaded', ()=>{
refresh();
// 让首次点击更丝滑
pad.focus({preventScroll:true});
// iOS 软键盘适配:避免工具栏挡住
window.scrollTo(0,0);
});
// 处理粘贴等突变
pad.addEventListener('paste', ()=> setTimeout(refresh, 0));
// 可选:窗口尺寸变化时保持高度合理
window.addEventListener('resize', () => {
// 仅在可见时更新,避免布局抖动
requestAnimationFrame(refresh);
});
})();
</script></body>
</html>

View File

@@ -0,0 +1,242 @@
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>随机数生成器</title>
<style>
:root{
--bg-start:#e8f8e3; /* 淡绿 */
--bg-end:#f3fbe6; /* 淡黄绿 */
--primary:#2f9e44; /* 主题绿 */
--primary-weak:#94d3a2;
--text:#204030;
--muted:#5d7a67;
--card:#ffffffa6; /* 半透明卡片 */
--shadow:0 8px 20px rgba(34, 139, 34, 0.08);
--radius:18px;
}
html,body{
height:100%;
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, PingFang SC, Noto Sans CJK SC, "Helvetica Neue", Arial, "Noto Sans", "Hiragino Sans GB", sans-serif;
color:var(--text);
background: linear-gradient(160deg, var(--bg-start), var(--bg-end));
}
.app{
max-width: 540px; /* 适配手机竖屏 */
margin: 0 auto;
padding: 20px 16px 36px;
}
header{
display:flex;
align-items:center;
justify-content:space-between;
margin-bottom:14px;
}
h1{
font-size: clamp(20px, 4.8vw, 26px);
margin:12px 0 6px;
letter-spacing: 0.5px;
}
.subtitle{
font-size: 13px;
color:var(--muted);
}.card{
background: var(--card);
backdrop-filter: blur(6px);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 14px;
margin-top: 10px;
}
.grid{ display:grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.field{ display:flex; flex-direction:column; gap:6px; }
label{ font-size: 13px; color: var(--muted);}
input[type="number"]{
-webkit-appearance: none; appearance:none;
width:100%;
padding: 12px 12px;
border-radius: 14px;
border: 1px solid #d7ead9;
background:#ffffff;
outline:none;
font-size:16px;
}
input[type="number"]:focus{ border-color: var(--primary-weak); box-shadow: 0 0 0 3px #2f9e4415; }
.count-row{ display:flex; align-items:center; gap:10px; }
.count-row input[type="range"]{ flex:1; }
input[type="range"]{
width:100%; height: 34px; background:transparent;
}
/* 自定义滑块 */
input[type="range"]::-webkit-slider-runnable-track{ height: 6px; border-radius: 6px; background: linear-gradient(90deg, var(--primary-weak), #d5efcf); }
input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:22px; height:22px; border-radius:50%; background: #fff; border:2px solid var(--primary); margin-top:-8px; box-shadow: 0 1px 4px rgba(0,0,0,.15); }
input[type="range"]::-moz-range-track{ height: 6px; border-radius:6px; background: linear-gradient(90deg, var(--primary-weak), #d5efcf);}
input[type="range"]::-moz-range-thumb{ width:22px; height:22px; border-radius:50%; background:#fff; border:2px solid var(--primary); }
.btns{ display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top:8px; }
button{
-webkit-tap-highlight-color: transparent;
padding: 14px 16px;
border-radius: 15px;
border:none; outline:none;
font-size:16px; font-weight: 600; letter-spacing:.4px;
box-shadow: var(--shadow);
}
.btn-primary{ background: linear-gradient(180deg, #9fe3b0, #62c27b); color:#0b3318; }
.btn-ghost{ background:#ffffff; color:#2f6f3f; border:1px solid #dcefe0; }
.hint{font-size:12px; color:var(--muted); margin-top:6px;}
.results{ margin-top: 14px; display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; }
@media (min-width:380px){ .results{ grid-template-columns: repeat(3, 1fr);} }
@media (min-width:480px){ .results{ grid-template-columns: repeat(4, 1fr);} }
.pill{
background:#ffffff;
border:1px solid #e3f3e6;
border-radius: 14px;
padding: 14px 0;
text-align:center;
font-size:18px;
font-variant-numeric: tabular-nums;
transition: transform .08s ease;
user-select: text;
}
.pill:active{ transform: scale(.98); }
.toolbar{ display:flex; gap:10px; margin-top:10px; }
.toolbar button{ flex:1; }
.small{ font-size:13px; }
</style>
</head>
<body>
<div class="app">
<header>
<div>
<h1>随机数生成器</h1>
<div class="subtitle">设置范围与数量,一键生成(整数,包含最小值与最大值)。</div>
</div>
</header><section class="card">
<div class="grid">
<div class="field">
<label for="min">最小值</label>
<input type="number" id="min" inputmode="numeric" value="0" />
</div>
<div class="field">
<label for="max">最大值</label>
<input type="number" id="max" inputmode="numeric" value="100" />
</div>
</div>
<div class="field" style="margin-top:10px;">
<label for="countRange">生成个数</label>
<div class="count-row">
<input id="countRange" type="range" min="1" max="100" value="10" />
<input id="countNum" type="number" min="1" max="100" value="10" style="width:94px"/>
</div>
<div class="hint">最多一次生成 100 个。若最小值大于最大值,将自动互换。</div>
</div>
<div class="btns">
<button class="btn-ghost" id="clearBtn">清空</button>
<button class="btn-primary" id="genBtn">生成</button>
</div>
<div class="toolbar">
<button class="btn-ghost small" id="copyBtn">复制结果</button>
<button class="btn-ghost small" id="downloadBtn">下载为TXT</button>
</div>
</section>
<section class="card" id="resultCard" style="display:none;">
<div class="results" id="results"></div>
</section>
</div> <script>
const minEl = document.getElementById('min');
const maxEl = document.getElementById('max');
const rangeEl = document.getElementById('countRange');
const countEl = document.getElementById('countNum');
const resultsEl = document.getElementById('results');
const resultCard = document.getElementById('resultCard');
// 双向同步 个数 输入
const sync = (fromRange) => {
if (fromRange) countEl.value = rangeEl.value; else rangeEl.value = Math.min(Math.max(1, Number(countEl.value||1)), Number(countEl.max));
};
rangeEl.addEventListener('input', () => sync(true));
countEl.addEventListener('input', () => sync(false));
const clampInt = (v, def=0) => Number.isFinite(Number(v)) ? Math.trunc(Number(v)) : def;
const randInt = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min; // 含两端
};
function generate(){
let min = clampInt(minEl.value, 0);
let max = clampInt(maxEl.value, 100);
if (min > max) [min, max] = [max, min];
minEl.value = min; maxEl.value = max;
let n = clampInt(countEl.value, 10);
n = Math.max(1, Math.min(100, n));
countEl.value = n; rangeEl.value = n;
const out = Array.from({length:n}, () => randInt(min, max));
// 渲染
resultsEl.innerHTML = '';
out.forEach((num, i) => {
const d = document.createElement('div');
d.className = 'pill';
d.textContent = num;
d.style.opacity = 0;
resultsEl.appendChild(d);
// 简单入场动画
requestAnimationFrame(() => {
d.style.transition = 'opacity .18s ease';
d.style.opacity = 1;
});
});
resultCard.style.display = 'block';
// 保存到剪贴板友好字符串
resultCard.dataset.text = out.join(', ');
}
function clearAll(){
resultsEl.innerHTML = '';
resultCard.style.display = 'none';
}
async function copyResults(){
const text = resultCard.dataset.text || '';
if (!text) return;
try{ await navigator.clipboard.writeText(text); alert('已复制到剪贴板'); }catch(e){
// 兼容不支持的环境
const ta = document.createElement('textarea');
ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
alert('已复制');
}
}
function downloadTxt(){
const text = resultCard.dataset.text || '';
if (!text) return;
const blob = new Blob([text + '\n'], {type:'text/plain;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = '随机数.txt'; a.click();
URL.revokeObjectURL(url);
}
document.getElementById('genBtn').addEventListener('click', generate);
document.getElementById('clearBtn').addEventListener('click', clearAll);
document.getElementById('copyBtn').addEventListener('click', copyResults);
document.getElementById('downloadBtn').addEventListener('click', downloadTxt);
// 方便初次预览
generate();
</script></body>
</html>