不知名提交

This commit is contained in:
2025-12-13 20:53:50 +08:00
parent c147502b4d
commit 1221d6faf1
120 changed files with 11005 additions and 1092 deletions

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.0">
<title>AI 聊天</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chat-container">
<div class="warning-banner" role="alert">⚠️ 重要提示本AI聊天功能为免费体验服务旨在展示不同大语言模型的交互能力。请注意1.不支持上下文记忆功能2.每分钟调用次数有限制3.使用人数过多会出现调用失败情况</div>
<div class="chat-header">
<h2>AI 聊天</h2>
<div class="model-selector">
<label for="model">选择模型: </label>
<select id="model">
<option value="openai/gpt-4.1">gpt-4.1</option>
<option value="openai/gpt-4.1-mini">gpt-4.1-mini</option>
<option value="openai/gpt-4.1-nano">gpt-4.1-nano</option>
<option value="openai/gpt-4o">gpt-4o</option>
<option value="openai/gpt-5">gpt-5</option>
<option value="deepseek-r1">deepseek-r1</option>
<option value="deepseek-v3-0324">deepseek-v3-0324</option>
<option value="xai/grok-3">grok-3</option>
<option value="xai/grok-3-mini">grok-3-mini</option>
</select>
</div>
</div>
<div id="model-info" class="model-info"></div>
<div class="chat-box" id="chat-box">
<!-- 消息将在这里显示 -->
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="输入你的消息...">
<button id="send-btn">发送</button>
</div>
</div>
<script src="marked.min.js"></script>
<script src="purify.min.js"></script>
<script src="script.js"></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,223 @@
// 简单的静态网页调用 GitHub Models API 进行聊天
// 注意:将 token 暴露在前端存在安全风险,仅用于本地演示
const endpoint = "https://models.github.ai/inference";
let apiKey = "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"; // 注意:已硬编码,仅用于本地演示
const chatBox = document.getElementById("chat-box");
const userInput = document.getElementById("user-input");
const sendBtn = document.getElementById("send-btn");
const modelSelect = document.getElementById("model");
// 模型信息映射(中文描述与上下文上限)
const MODEL_INFO = {
"openai/gpt-4.1": {
name: "gpt-4.1",
inputTokens: "1049k",
outputTokens: "33k",
about: "在各方面优于 gpt-4o编码、指令跟随与长上下文理解均有显著提升"
},
"openai/gpt-4.1-mini": {
name: "gpt-4.1-mini",
inputTokens: "1049k",
outputTokens: "33k",
about: "在各方面优于 gpt-4o-mini在编码、指令跟随与长上下文处理上有显著提升"
},
"openai/gpt-4.1-nano": {
name: "gpt-4.1-nano",
inputTokens: "1049k",
outputTokens: "33k",
about: "在编码、指令跟随与长上下文处理上有所提升,同时具备更低延迟与成本"
},
"openai/gpt-4o": {
name: "gpt-4o",
inputTokens: "131k",
outputTokens: "16k",
about: "OpenAI 最先进的多模态 gpt-4o 家族模型,可处理文本与图像输入"
},
"openai/gpt-5": {
name: "gpt-5",
inputTokens: "200k",
outputTokens: "100k",
about: "针对逻辑密集与多步骤任务设计"
},
"deepseek-r1": {
name: "deepseek-r1",
inputTokens: "128k",
outputTokens: "4k",
about: "通过逐步训练过程在推理任务上表现出色,适用于语言、科学推理与代码生成等"
},
"deepseek-v3-0324": {
name: "deepseek-v3-0324",
inputTokens: "128k",
outputTokens: "4k",
about: "相较于 DeepSeek-V3 在关键方面显著提升,包括更强的推理能力、函数调用与代码生成表现"
},
"xai/grok-3": {
name: "grok-3",
inputTokens: "131k",
outputTokens: "4k",
about: "Grok 3 是 xAI 的首发模型,由 Colossus 在超大规模上进行预训练,在金融、医疗和法律等专业领域表现突出。"
},
"xai/grok-3-mini": {
name: "grok-3-mini",
inputTokens: "131k",
outputTokens: "4k",
about: "Grok 3 Mini 是一款轻量级模型,会在答复前进行思考。它针对数学与科学问题进行训练,特别适合逻辑类任务。"
}
};
function renderModelInfo() {
const m = MODEL_INFO[modelSelect.value];
const infoEl = document.getElementById('model-info');
if (!infoEl) return;
if (m) {
infoEl.innerHTML = `<div><span class="name">${m.name}</span> <span class="tokens">上下文 ${m.inputTokens} 输入 · ${m.outputTokens} 输出</span></div><div class="about">简介:${m.about}</div>`;
} else {
infoEl.innerHTML = `<div class="about">未配置该模型的上下文限制信息</div>`;
}
}
// Markdown 解析配置(若库已加载)
if (window.marked) {
marked.setOptions({ gfm: true, breaks: true, headerIds: true, mangle: false });
}
function renderMarkdown(text) {
try {
if (window.marked && window.DOMPurify) {
const html = marked.parse(text || '');
return DOMPurify.sanitize(html);
}
} catch (_) {}
return text || '';
}
function appendMessage(role, text){
const message = document.createElement('div');
message.className = `message ${role}`;
const bubble = document.createElement('div');
bubble.className = 'bubble';
// Markdown 渲染
bubble.innerHTML = renderMarkdown(text);
message.appendChild(bubble);
chatBox.appendChild(message);
chatBox.scrollTop = chatBox.scrollHeight;
}
function appendStreamingMessage(role){
const message = document.createElement('div');
message.className = `message ${role}`;
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble._mdBuffer = '';
bubble.innerHTML = '';
message.appendChild(bubble);
chatBox.appendChild(message);
chatBox.scrollTop = chatBox.scrollHeight;
return bubble;
}
async function sendMessage(){
const content = userInput.value.trim();
if (!content) return;
appendMessage('user', content);
userInput.value = '';
const model = modelSelect.value;
// 令牌已硬编码(本地演示),如未配置则提示
if (!apiKey) {
appendMessage('assistant', '未配置令牌');
return;
}
sendBtn.disabled = true;
const assistantBubble = appendStreamingMessage('assistant');
try {
const res = await fetch(`${endpoint}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model,
temperature: 1,
top_p: 1,
stream: true,
messages: [
{ role: 'system', content: '' },
{ role: 'user', content }
]
})
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`HTTP ${res.status}: ${errText}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let doneStream = false;
while (!doneStream) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split(/\r?\n/);
buffer = parts.pop();
for (const part of parts) {
const line = part.trim();
if (line === '') continue;
if (line.startsWith('data:')) {
const payload = line.slice(5).trim();
if (payload === '[DONE]') {
doneStream = true;
break;
}
try {
const json = JSON.parse(payload);
const delta = json?.choices?.[0]?.delta?.content ?? json?.choices?.[0]?.message?.content ?? '';
if (delta) {
// 累计流式文本并增量渲染 Markdown
assistantBubble._mdBuffer = (assistantBubble._mdBuffer || '') + delta;
const safeHtml = renderMarkdown(assistantBubble._mdBuffer);
assistantBubble.innerHTML = safeHtml;
chatBox.scrollTop = chatBox.scrollHeight;
}
} catch (e) {
// 忽略无法解析的行
}
}
}
}
if (!assistantBubble._mdBuffer || !assistantBubble.textContent) {
assistantBubble.textContent = '(无内容返回)';
}
} catch (err) {
//assistantBubble.textContent = `出错了:${err.message}`;
assistantBubble.textContent = `调用次数过多或者使用人数过多,请稍后再试!`;
} finally {
sendBtn.disabled = false;
}
}
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 切换模型时更新信息面板,初次渲染一次
modelSelect.addEventListener('change', renderModelInfo);
renderModelInfo();

View File

@@ -0,0 +1,172 @@
/* 淡绿色淡黄绿色渐变清新柔和风格,移动端适配 */
:root {
--bg-start: #d9f7be; /* 淡绿 */
--bg-end: #f4f9d2; /* 淡黄绿 */
--primary: #4caf50; /* 绿色强调 */
--secondary: #8bc34a;
--text: #2f3b2f;
--muted: #6b7a6b;
--white: #ffffff;
--shadow: rgba(76, 175, 80, 0.2);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.chat-container {
width: 100%;
max-width: 720px;
background: var(--white);
border-radius: 16px;
box-shadow: 0 10px 30px var(--shadow);
overflow: hidden;
}
/* 顶部警示通知样式 */
.warning-banner {
padding: 10px 16px;
background: #fff8d6; /* 柔和黄色背景 */
border: 1px solid #ffec99; /* 黄色边框 */
border-left: 4px solid #faad14; /* 左侧强调条 */
color: #5c4a00; /* 深色文字保证可读性 */
font-size: 14px;
line-height: 1.6;
}
@media (max-width: 480px) {
.warning-banner { font-size: 13px; padding: 8px 12px; }
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(135deg, #eafbe6, #f9ffe6);
border-bottom: 1px solid #e1f3d8;
}
.chat-header h2 {
margin: 0;
font-size: 18px;
color: var(--primary);
}
.model-selector {
display: flex;
align-items: center;
gap: 8px;
}
.model-selector select {
padding: 6px 8px;
border: 1px solid #cfe8c9;
border-radius: 8px;
background: #f7fff2;
color: var(--text);
}
.chat-box {
height: 60vh;
padding: 16px;
overflow-y: auto;
background: #fbfff5;
}
.message { display: flex; margin-bottom: 12px; gap: 8px; }
.message .bubble {
padding: 10px 12px;
border-radius: 12px;
max-width: 85%;
box-shadow: 0 2px 10px var(--shadow);
}
.message.user .bubble {
background: #e2f7d8;
align-self: flex-end;
}
.message.assistant .bubble {
background: #fffef0;
}
.message.user { justify-content: flex-end; }
.message.assistant { justify-content: flex-start; }
.chat-input {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid #e1f3d8;
background: #fafff0;
}
#user-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #cfe8c9;
border-radius: 12px;
font-size: 16px;
background: #ffffff;
}
#send-btn {
padding: 10px 16px;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, var(--secondary), var(--primary));
color: var(--white);
font-weight: 600;
cursor: pointer;
}
#send-btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* 模型信息面板样式 */
.model-info {
padding: 10px 16px;
background: #f9fff0;
border-bottom: 1px solid #e1f3d8;
color: var(--muted);
font-size: 14px;
}
.model-info .name { color: var(--primary); font-weight: 600; }
.model-info .tokens { color: var(--text); font-weight: 600; margin-left: 8px; }
.model-info .about { margin-top: 6px; line-height: 1.5; }
/* 移动端优化 */
@media (max-width: 480px) {
.chat-box { height: 62vh; }
#user-input { font-size: 15px; }
#send-btn { padding: 10px 14px; }
.model-info { font-size: 13px; padding: 8px 12px; }
}
/* 全局隐藏滚动条,保留滚动效果 */
html, body {
-ms-overflow-style: none; /* IE/旧版 Edge */
scrollbar-width: none; /* Firefox */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar { display: none; } /* Chrome/Safari/新 Edge */
/* 隐藏所有元素滚动条(覆盖常见浏览器) */
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
*::-webkit-scrollbar { display: none; }
/* 保持聊天框可滚动并优化移动端滚动体验 */
.chat-box { -webkit-overflow-scrolling: touch; }
/* 代码块允许横向滚动但隐藏滚动条 */
.message .bubble pre { overflow-x: auto; }
.message .bubble pre { scrollbar-width: none; -ms-overflow-style: none; }
.message .bubble pre::-webkit-scrollbar { display: none; }

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,202 @@
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>人生倒计时</title>
<style>
:root{
--bg-start:#e8f8e4; /* 淡绿 */
--bg-end:#f5fbdc; /* 淡黄绿 */
--text:#1f3830;
--muted:#5c7a68;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0; min-height:100svh; color:var(--text);
font-family: system-ui,-apple-system,"PingFang SC","Microsoft YaHei",Segoe UI,Roboto,Helvetica,Arial,"Noto Sans CJK SC","Noto Sans",sans-serif;
background: linear-gradient(135deg,var(--bg-start),var(--bg-end));
background-size:200% 200%;
animation: flow 12s ease-in-out infinite;
}
@keyframes flow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}.container{max-width:680px;margin:0 auto;padding:24px 16px 80px} .header{text-align:center;margin-bottom:16px} .title{font-size:clamp(28px,7vw,40px);font-weight:900;letter-spacing:2px; background:linear-gradient(90deg,#4caf50,#8bc34a,#aed581);-webkit-background-clip:text;color:transparent; filter:drop-shadow(0 2px 6px rgba(76,175,80,.18));} .subtitle{color:var(--muted);font-size:clamp(13px,3.5vw,15px)} .grid{display:grid;gap:14px;grid-template-columns:1fr} .card{position:relative;padding:16px 14px;border-radius:20px;background:rgba(255,255,255,.55); box-shadow:0 10px 25px rgba(93,125,106,.18), inset 0 1px 0 rgba(255,255,255,.6); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,.6); } .card::after{content:""; position:absolute; inset:-2px; border-radius:22px; pointer-events:none; background:linear-gradient(120deg, rgba(124,179,66,.35), rgba(255,255,255,0) 40%, rgba(85,204,170,.3)); mask: linear-gradient(#000,#000) exclude, linear-gradient(#000 0 0); } .section-title{display:flex;align-items:center;gap:8px;font-weight:800;font-size:16px;letter-spacing:.2px} .kbd{font-size:11px;color:var(--muted);border:1px solid rgba(0,0,0,.05);padding:2px 6px;border-radius:8px;background:rgba(255,255,255,.7)} .row{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:10px;flex-wrap:wrap} .metric{font-size:14px;color:var(--muted)} .value{font-feature-settings:"tnum" 1,"lnum" 1;font-variant-numeric:tabular-nums;font-weight:800;font-size:clamp(18px,5.5vw,24px)} .progress{width:100%;height:14px;background:rgba(0,0,0,.06);border-radius:999px;overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.12)} .bar{height:100%;width:0%;border-radius:inherit;transition:width .8s cubic-bezier(.22,.61,.36,1);position:relative} .bar::after{content:"";position:absolute;inset:0;background:linear-gradient(to right,rgba(255,255,255,0) 0%,rgba(255,255,255,.35) 30%,rgba(255,255,255,0) 60%);transform:translateX(-100%);animation:shimmer 2.4s infinite} @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}} .bar.c1{background:linear-gradient(90deg,#7bc67b,#cde77f)} .bar.c2{background:linear-gradient(90deg,#77d0b5,#b5e48c)} .bar.c3{background:linear-gradient(90deg,#bade6d,#e6f39a)} .bar.c4{background:linear-gradient(90deg,#6ac26a,#a3e07a)} .badge{display:inline-flex;align-items:center;gap:6px;font-size:13px;padding:8px 10px;border-radius:999px;background:rgba(255,255,255,.6);border:1px solid rgba(0,0,0,.06);box-shadow:0 4px 12px rgba(0,0,0,.06)} .badge b{font-feature-settings:"tnum" 1,"lnum" 1;font-variant-numeric:tabular-nums} .holidays{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:12px} .holiday{padding:12px;border-radius:18px;background:linear-gradient(135deg,rgba(139,195,74,.25),rgba(205,220,57,.2));border:1px dashed rgba(139,195,74,.35);box-shadow:inset 0 1px 0 rgba(255,255,255,.5)} .holiday h4{margin:0 0 8px;font-size:14px} .holiday .days{font-weight:900;font-size:24px} .footer{text-align:center;margin-top:22px;color:var(--muted);font-size:12px} .small{font-size:12px;color:var(--muted)}
/* 轻微装饰粒子效果 */ .particles{position:fixed;inset:0;overflow:hidden;pointer-events:none} .particles span{position:absolute;width:clamp(6px,1.8vw,10px);height:clamp(6px,1.8vw,10px); background:radial-gradient(circle at 30% 30%,rgba(255,255,255,.9),rgba(255,255,255,.2));border-radius:50%;filter:blur(.2px); animation:float 14s linear infinite;opacity:.6} @keyframes float{0%{transform:translateY(110vh) translateX(0) scale(1) rotate(0)}100%{transform:translateY(-10vh) translateX(40px) scale(1.4) rotate(180deg)}} .particles span:nth-child(odd){animation-duration:18s} .particles span:nth-child(3n){animation-direction:reverse} .particles span:nth-child(4n){animation-duration:22s}
@media (min-width:760px){.grid{grid-template-columns:1fr 1fr}.card{padding:18px 16px}} </style>
</head>
<body>
<div class="particles" aria-hidden="true"></div>
<div class="container">
<header class="header">
<h1 class="title">人生倒计时</h1>
</header> <main class="grid">
<section class="card" id="today">
<div class="section-title">今天 · <span class="kbd">剩余小时</span></div>
<div class="row">
<div class="metric">距离今天结束还有</div>
<div class="value"><span id="todayHours">--</span> 小时 <span class="small" id="todayHMS"></span></div>
</div>
<div class="progress" aria-label="今日剩余百分比">
<div class="bar c1" id="barToday"></div>
</div>
</section><section class="card" id="week">
<div class="section-title">本周 · <span class="kbd">剩余天数</span></div>
<div class="row">
<div class="metric">(周一为一周起点)</div>
<div class="value"><span id="weekDays">--</span></div>
</div>
<div class="progress" aria-label="本周剩余百分比">
<div class="bar c2" id="barWeek"></div>
</div>
</section>
<section class="card" id="month">
<div class="section-title">本月 · <span class="kbd">剩余天数</span></div>
<div class="row">
<div class="metric">当前月份还剩</div>
<div class="value"><span id="monthDays">--</span></div>
</div>
<div class="progress" aria-label="本月剩余百分比">
<div class="bar c3" id="barMonth"></div>
</div>
</section>
<section class="card" id="year">
<div class="section-title">2025 · <span class="kbd">剩余天数</span></div>
<div class="row">
<div class="metric">距离今年结束还有</div>
<div class="value"><span id="yearDays">--</span></div>
</div>
<div class="progress" aria-label="本年剩余百分比">
<div class="bar c4" id="barYear"></div>
</div>
</section>
<section class="card" id="holidays">
<div class="section-title">重要节日倒计时</div>
<div class="holidays">
<div class="holiday">
<h4>五一劳动节</h4>
<div class="days"><span id="d51">--</span></div>
<div class="small" id="d51Date"></div>
</div>
<div class="holiday">
<h4>国庆节</h4>
<div class="days"><span id="dGQ">--</span></div>
<div class="small" id="dGQDate"></div>
</div>
<div class="holiday">
<h4>圣诞节</h4>
<div class="days"><span id="dXmas">--</span></div>
<div class="small" id="dXmasDate"></div>
</div>
</div>
</section>
</main>
</div><script>
(function(){
// ===== 常用工具函数 =====
const pad = n => String(n).padStart(2,'0');
const isLeapYear = y => (y%4===0 && y%100!==0) || (y%400===0);
const daysInYear = y => isLeapYear(y) ? 366 : 365;
const daysInMonth = (y,m)=> new Date(y, m+1, 0).getDate(); // m:0-11
// 结束时间以下一单位0点为界方便计算剩余比例
const endOfToday = now => new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);
// 周一为一周的起点周末结束到下周一0点
function endOfWeek(now){
const day = (now.getDay()+6)%7; // 0..6 (Mon..Sun)
const daysToNextMon = 7 - day;
return new Date(now.getFullYear(), now.getMonth(), now.getDate()+daysToNextMon);
}
const endOfMonth = now => new Date(now.getFullYear(), now.getMonth()+1, 1);
const endOfYear = now => new Date(now.getFullYear()+1, 0, 1);
const msToHMS = ms => {
const sec = Math.max(0, Math.floor(ms/1000));
const s = sec % 60; const m = Math.floor(sec/60)%60; const h = Math.floor(sec/3600);
return h+":"+pad(m)+":"+pad(s);
}
const niceDays = ms => Math.max(0, (ms/86400000));
const setBar = (id, percent) => { document.getElementById(id).style.width = Math.max(0,Math.min(100,percent)).toFixed(2)+"%"; };
function update(){
const now = new Date();
// 今天剩余小时
const eToday = endOfToday(now);
const msLeftToday = eToday - now;
const hoursLeft = msLeftToday / 3600000;
document.getElementById("todayHours").textContent = hoursLeft.toFixed(2);
document.getElementById("todayHMS").textContent = "(约 "+ msToHMS(msLeftToday) +"";
setBar("barToday", (hoursLeft/24)*100);
// 本周剩余天数(周一为起点)
const eWeek = endOfWeek(now);
const msLeftWeek = eWeek - now;
const daysLeftWeek = niceDays(msLeftWeek);
document.getElementById("weekDays").textContent = daysLeftWeek.toFixed(2);
setBar("barWeek", (daysLeftWeek/7)*100);
// 本月剩余天数
const eMonth = endOfMonth(now);
const msLeftMonth = eMonth - now;
const dInMonth = daysInMonth(now.getFullYear(), now.getMonth());
const daysLeftMonth = niceDays(msLeftMonth);
document.getElementById("monthDays").textContent = daysLeftMonth.toFixed(2);
setBar("barMonth", (daysLeftMonth/dInMonth)*100);
// 今年剩余天数(以 2025 年为展示基准)
const eYear = endOfYear(now);
const msLeftYear = eYear - now;
const dInYear = daysInYear(now.getFullYear());
const daysLeftYear = niceDays(msLeftYear);
document.getElementById("yearDays").textContent = daysLeftYear.toFixed(2);
setBar("barYear", (daysLeftYear/dInYear)*100);
// 重要节日
const thisYear = now.getFullYear();
function nextDate(mm, dd){
let d = new Date(thisYear, mm-1, dd);
if (now >= endOfToday(d)) d.setFullYear(thisYear+1);
return d;
}
function daysUntil(target){
// 以当天0点为日界向上取整到“天”
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return Math.ceil((target - start)/86400000);
}
const d51 = nextDate(5,1);
const dGQ = nextDate(10,1);
const dXmas = nextDate(12,25);
document.getElementById("d51").textContent = daysUntil(d51);
document.getElementById("dGQ").textContent = daysUntil(dGQ);
document.getElementById("dXmas").textContent = daysUntil(dXmas);
document.getElementById("d51Date").textContent = d51.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
document.getElementById("dGQDate").textContent = dGQ.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
document.getElementById("dXmasDate").textContent = dXmas.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric',weekday:'short'});
// 底部时间
const tn = now.toLocaleString(undefined,{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
}
// 装饰粒子生成
(function spawnParticles(){
const wrap = document.querySelector('.particles');
for(let i=0;i<28;i++){
const s = document.createElement('span');
s.style.left = Math.random()*100 + 'vw';
s.style.animationDelay = (-Math.random()*20) + 's';
s.style.opacity = (0.35 + Math.random()*0.4).toFixed(2);
wrap.appendChild(s);
}
})();
update();
setInterval(update, 1000);
})();
</script></body>
</html>

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,380 @@
<!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:#eaf8e4; /* 淡绿色 */
--bg-to:#f5ffd8; /* 淡黄绿色 */
--card:#ffffffcc; /* 卡片半透明 */
--accent:#78c67e; /* 绿色主色 */
--accent-2:#99d78c; /* 次要 */
--text:#234; /* 深色文字 */
--muted:#5b6b63; /* 次级文字 */
--shadow:0 8px 28px rgba(34, 102, 60, 0.15);
--radius:18px;
}html, body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
color: var(--text);
background: linear-gradient(160deg, var(--bg-from), var(--bg-to));
}
.wrap {
min-height: 100%;
display: grid;
place-items: start center;
padding: 18px 14px 28px;
}
.card {
width: 100%;
max-width: 520px; /* 适配手机竖屏 */
background: var(--card);
backdrop-filter: blur(8px);
border-radius: 22px;
box-shadow: var(--shadow);
padding: 16px;
}
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
header .dot {
width: 10px; height: 10px; border-radius: 999px; background: var(--accent);
box-shadow: 0 0 0 6px rgba(120,198,126,0.15);
}
h1 { font-size: 18px; margin: 0; font-weight: 700; }
p.sub { margin: 4px 0 10px; color: var(--muted); font-size: 13px; }
.uploader {
border: 1.5px dashed #a9d6ab;
border-radius: var(--radius);
background: #ffffffb3;
padding: 12px;
display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center;
}
.uploader input[type=file] {
width: 100%;
border: none; outline: none; background: transparent;
}
.controls { margin-top: 12px; display: grid; gap: 12px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.slider {
background: #ffffff;
border: 1px solid #e3f2e1;
border-radius: var(--radius);
padding: 10px;
box-shadow: 0 4px 14px rgba(0,0,0,0.05);
}
.slider label { display:flex; align-items:center; justify-content:space-between; gap:6px; font-size: 14px; font-weight:600; }
.slider output { font-variant-numeric: tabular-nums; color: var(--accent); min-width: 3ch; text-align: right; }
input[type=range] {
-webkit-appearance: none; appearance: none; width: 100%; height: 32px; background: transparent;
}
input[type=range]::-webkit-slider-runnable-track { height: 8px; background: linear-gradient(90deg, #cfeecf, #e9ffd4); border-radius: 999px; }
input[type=range]::-moz-range-track { height: 8px; background: linear-gradient(90deg, #cfeecf, #e9ffd4); border-radius: 999px; }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 22px; height: 22px; border-radius: 50%; background: var(--accent);
border: 2px solid #fff; margin-top: -7px; box-shadow: 0 2px 8px rgba(37,106,63,.3);
}
input[type=range]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--accent); border: 2px solid #fff; box-shadow: 0 2px 8px rgba(37,106,63,.3); }
.row { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.row .chk { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--muted); }
.preview {
margin-top: 12px;
background: #ffffff;
border: 1px solid #e3f2e1;
border-radius: 24px;
overflow: hidden;
position: relative;
}
canvas { width: 100%; height: auto; display: block; background: repeating-conic-gradient(from 45deg, #f8fff1 0 10px, #f1ffe4 10px 20px); }
.actions { display:flex; gap:10px; margin-top: 12px; }
button, .btn {
appearance: none; border: none; cursor: pointer; font-weight: 700; letter-spacing: .2px; transition: transform .05s ease, box-shadow .2s ease, background .2s ease;
border-radius: 14px; padding: 12px 14px; box-shadow: 0 6px 18px rgba(120,198,126,.25);
}
.btn-primary { background: linear-gradient(180deg, var(--accent), var(--accent-2)); color: #fff; }
.btn-ghost { background: #ffffffb5; color: #2c4432; border: 1px solid #d9efda; }
button:active { transform: translateY(1px); }
footer { text-align:center; color: #6a7; font-size: 12px; margin-top: 10px; }
.hidden { display:none; }
</style>
</head>
<body>
<div class="wrap">
<div class="card" role="region" aria-label="图片圆角处理工具">
<header>
<div class="dot" aria-hidden="true"></div>
<h1>图片圆角处理(四角独立,最高至圆形)</h1>
</header>
<p class="sub">上传图片 → 调节四个角的圆角强度0100%)→ 预览并下载透明圆角 PNG。已针对手机竖屏优化。</p><div class="uploader" aria-label="上传图片">
<input id="file" type="file" accept="image/*" />
<small style="color:var(--muted);">支持 JPG / PNG / WebP 等常见格式</small>
</div>
<div class="controls">
<div class="row">
<label class="chk"><input type="checkbox" id="linkAll" checked /> 联动四角</label>
<button id="resetBtn" class="btn btn-ghost" type="button">重置</button>
</div>
<div class="grid-2">
<div class="slider">
<label>左上角 <output id="o_tl">20%</output></label>
<input id="r_tl" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>右上角 <output id="o_tr">20%</output></label>
<input id="r_tr" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>右下角 <output id="o_br">20%</output></label>
<input id="r_br" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>左下角 <output id="o_bl">20%</output></label>
<input id="r_bl" type="range" min="0" max="100" step="1" value="20" />
</div>
</div>
</div>
<div class="preview" aria-live="polite">
<canvas id="previewCanvas" aria-label="预览画布"></canvas>
</div>
<div class="actions">
<button id="downloadBtn" class="btn btn-primary" type="button" disabled>下载处理后的 PNG</button>
<button id="fitBtn" class="btn btn-ghost" type="button" disabled>适配预览尺寸</button>
</div>
<footer>小贴士:将四个角都拉到 <b>100%</b>,在方形图片上会得到完全圆形效果。</footer>
</div>
</div> <!-- 脚本:处理圆角、预览与下载 --> <script>
const fileInput = document.getElementById('file');
const linkAll = document.getElementById('linkAll');
const previewCanvas = document.getElementById('previewCanvas');
const downloadBtn = document.getElementById('downloadBtn');
const fitBtn = document.getElementById('fitBtn');
const resetBtn = document.getElementById('resetBtn');
const sliders = {
tl: document.getElementById('r_tl'),
tr: document.getElementById('r_tr'),
br: document.getElementById('r_br'),
bl: document.getElementById('r_bl'),
};
const outputs = {
tl: document.getElementById('o_tl'),
tr: document.getElementById('o_tr'),
br: document.getElementById('o_br'),
bl: document.getElementById('o_bl'),
};
// 工作画布(按原图尺寸绘制,导出用)
const workCanvas = document.createElement('canvas');
const workCtx = workCanvas.getContext('2d');
const prevCtx = previewCanvas.getContext('2d');
let img = new Image();
let imageLoaded = false;
const state = {
percent: { tl: 20, tr: 20, br: 20, bl: 20 },
fitToPreview: false,
};
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
function updateOutputs(){
for(const k of ['tl','tr','br','bl']) outputs[k].textContent = state.percent[k] + '%';
}
function setAllPercents(v){ for(const k of ['tl','tr','br','bl']) state.percent[k] = v; updateSliders(); }
function updateSliders(){ for(const k in sliders) sliders[k].value = state.percent[k]; updateOutputs(); render(); }
// 根据百分比换算到像素半径(以较短边的一半为 100%
function percentToRadiusPx(p){
const base = Math.min(img.naturalWidth, img.naturalHeight) / 2; // 100% 对应的像素半径
return (clamp(p,0,100) / 100) * base;
}
// 绘制带四角独立圆角的路径
function roundedRectPath(ctx, x, y, w, h, r){
// 约束:每个角半径不能超过对应边长度的一半
const rTL = clamp(r.tl, 0, Math.min(w, h) / 2);
const rTR = clamp(r.tr, 0, Math.min(w, h) / 2);
const rBR = clamp(r.br, 0, Math.min(w, h) / 2);
const rBL = clamp(r.bl, 0, Math.min(w, h) / 2);
ctx.beginPath();
ctx.moveTo(x + rTL, y);
ctx.lineTo(x + w - rTR, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rTR);
ctx.lineTo(x + w, y + h - rBR);
ctx.quadraticCurveTo(x + w, y + h, x + w - rBR, y + h);
ctx.lineTo(x + rBL, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - rBL);
ctx.lineTo(x, y + rTL);
ctx.quadraticCurveTo(x, y, x + rTL, y);
ctx.closePath();
}
// 渲染到工作画布(原尺寸)
function renderWork(){
if(!imageLoaded) return;
workCanvas.width = img.naturalWidth;
workCanvas.height = img.naturalHeight;
workCtx.clearRect(0,0,workCanvas.width, workCanvas.height);
workCtx.save();
const r = {
tl: percentToRadiusPx(state.percent.tl),
tr: percentToRadiusPx(state.percent.tr),
br: percentToRadiusPx(state.percent.br),
bl: percentToRadiusPx(state.percent.bl),
};
roundedRectPath(workCtx, 0, 0, workCanvas.width, workCanvas.height, r);
workCtx.clip();
workCtx.drawImage(img, 0, 0, workCanvas.width, workCanvas.height);
workCtx.restore();
}
// 渲染到预览画布(自适应容器宽度,保持清晰度)
function renderPreview(){
const container = previewCanvas.parentElement.getBoundingClientRect();
const targetW = Math.min(container.width, 1000);
const scale = targetW / workCanvas.width;
const dpr = window.devicePixelRatio || 1;
const canvasW = Math.round(targetW * dpr);
const canvasH = Math.round(workCanvas.height * scale * dpr);
previewCanvas.width = canvasW;
previewCanvas.height = canvasH;
previewCanvas.style.height = Math.round(canvasH / dpr) + 'px';
previewCanvas.style.width = Math.round(canvasW / dpr) + 'px';
prevCtx.clearRect(0,0,canvasW,canvasH);
prevCtx.imageSmoothingEnabled = true;
prevCtx.drawImage(workCanvas, 0, 0, canvasW, canvasH);
}
function render(){
if(!imageLoaded) return;
renderWork();
renderPreview();
}
// 事件:上传图片
fileInput.addEventListener('change', (e)=>{
const file = e.target.files && e.target.files[0];
if(!file) return;
const url = URL.createObjectURL(file);
const temp = new Image();
temp.onload = ()=>{
img = temp;
imageLoaded = true;
render();
downloadBtn.disabled = false;
fitBtn.disabled = false;
};
temp.onerror = ()=>{
alert('图片加载失败,请更换文件重试');
imageLoaded = false;
downloadBtn.disabled = true;
fitBtn.disabled = true;
};
temp.src = url;
});
// 事件:滑块变更
for(const key of Object.keys(sliders)){
sliders[key].addEventListener('input', (e)=>{
const val = parseInt(e.target.value, 10) || 0;
if(linkAll.checked){
setAllPercents(val);
}else{
state.percent[key] = val;
updateOutputs();
render();
}
});
}
// 重置
resetBtn.addEventListener('click', ()=>{
linkAll.checked = true;
setAllPercents(20);
});
// 适配预览尺寸(导出较小尺寸,便于快速分享)
fitBtn.addEventListener('click', ()=>{
if(!imageLoaded) return;
const container = previewCanvas.parentElement.getBoundingClientRect();
const targetW = Math.min(container.width, 1080); // 限制到 1080 宽
const scale = targetW / img.naturalWidth;
const targetH = Math.round(img.naturalHeight * scale);
// 临时缩放导出画布
const temp = document.createElement('canvas');
temp.width = Math.round(targetW);
temp.height = Math.round(targetH);
const tctx = temp.getContext('2d');
// 先把当前工作画布绘好
renderWork();
tctx.drawImage(workCanvas, 0, 0, temp.width, temp.height);
const a = document.createElement('a');
a.href = temp.toDataURL('image/png');
a.download = 'rounded-image-fit.png';
a.click();
});
// 下载原始尺寸 PNG
downloadBtn.addEventListener('click', ()=>{
if(!imageLoaded) return;
renderWork();
const a = document.createElement('a');
a.href = workCanvas.toDataURL('image/png');
a.download = 'rounded-image.png';
a.click();
});
// 初始输出数字
updateOutputs();
// 自适应:窗口尺寸变动时重绘预览
window.addEventListener('resize', ()=>{ if(imageLoaded) renderPreview(); });
// 支持 PWA 风:阻止 iOS 双击缩放(改善滑块体验)
let lastTouch = 0;
document.addEventListener('touchend', (e)=>{
const now = Date.now();
if(now - lastTouch <= 300){ e.preventDefault(); }
lastTouch = now;
}, {passive:false});
</script></body>
</html>

View File

@@ -0,0 +1,327 @@
<!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>图片转化 base64 编码</title>
<meta name="description" content="将图片快速转换为 Base64 / Data URL支持一键复制与清空适配手机竖屏。" />
<style>
:root{
--bg1:#dff7d6; /* 淡绿色 */
--bg2:#eef8c9; /* 淡黄绿色 */
--card:#ffffffcc;
--text:#0f2f1a;
--muted:#3d5f46;
--accent:#6ebf75;
--accent-2:#a6d98d;
--danger:#b55252;
--radius:18px;
--shadow:0 10px 24px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04);
}
html,body{height:100%;}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
color:var(--text);
background: linear-gradient(135deg,var(--bg1),var(--bg2));
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
}
.container{
max-width: 860px;
padding: 16px clamp(14px,4vw,28px) 28px;
margin: 0 auto;
display:flex;
flex-direction:column;
gap:14px;
}
header{
text-align:center;
padding-top: 8px;
padding-bottom: 6px;
}
h1{
margin:0 0 6px;
font-size: clamp(20px, 5.5vw, 32px);
letter-spacing: .5px;
font-weight: 800;
background: linear-gradient(90deg, #3a7f43, #79c26f);
-webkit-background-clip: text;
background-clip:text;
color: transparent;
}
.subtitle{
font-size: clamp(12px, 3.6vw, 14px);
color: color-mix(in oklab, var(--muted) 80%, white);
}
.card{
background: var(--card);
border: 1px solid rgba(99, 135, 102, .15);
backdrop-filter: blur(6px);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 14px;
}
.uploader{
display:grid;
gap:12px;
}
.dropzone{
position:relative;
border:2px dashed rgba(59, 120, 74, .35);
border-radius: calc(var(--radius) - 6px);
padding: 18px;
background: linear-gradient(180deg, rgba(255,255,255,.85), rgba(255,255,255,.65));
transition: .2s ease;
cursor: pointer;
display:flex;
align-items:center;
gap:14px;
}
.dropzone:hover{ border-color: rgba(59,120,74,.6); }
.dropzone.dragover{ box-shadow: inset 0 0 0 3px rgba(110,191,117,.35); background: rgba(255,255,255,.9); }
.drop-icon{ width:40px; height:40px; flex: 0 0 40px; }
.dz-text{ display:flex; flex-direction:column; gap:4px; }
.dz-title{ font-weight:700; font-size: 15px; }
.dz-sub{ font-size:12px; color: color-mix(in oklab, var(--muted) 75%, white); }input[type="file"]{
position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%;
}
.preview{
display:grid; grid-template-columns: 1fr; gap:10px; align-items:start;
}
.preview img{
width:100%; height:auto; max-height: 55vh; object-fit: contain;
border-radius: 12px;
background: #f8fff2;
border:1px solid rgba(59, 120, 74, .18);
}
.controls{
display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;
}
.left-controls, .right-controls{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.btn{
appearance:none; border:0; border-radius: 999px;
padding: 10px 14px; font-weight: 700; letter-spacing:.2px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
color:#05370f; box-shadow: var(--shadow); cursor:pointer; transition:.15s ease; display:flex; gap:8px; align-items:center;
}
.btn:active{ transform: translateY(1px); }
.btn.secondary{ background: #ffffff; color:#2a5532; border:1px solid rgba(59,120,74,.2); }
.btn.danger{ background: #ffecec; color:#7a2222; border:1px solid rgba(181,82,82,.35); }
.format{
display:flex; gap:8px; align-items:center; background:#ffffff; border:1px solid rgba(59,120,74,.15);
padding:6px 10px; border-radius:999px; box-shadow: var(--shadow);
font-size: 13px;
}
.format label{ display:flex; gap:6px; align-items:center; padding:6px 8px; border-radius: 999px; cursor:pointer; }
.format input{ accent-color:#6ebf75; }
.info{
font-size:12px; color: color-mix(in oklab, var(--muted) 70%, white);
display:flex; gap:10px; flex-wrap:wrap; align-items:center; line-height:1.4;
}
.output card{
display:block;
}
textarea{
width:100%; min-height: 36vh; resize: vertical; padding:12px 12px; line-height:1.35; border-radius: 12px;
border:1px solid rgba(59,120,74,.2); background: #fbfff6; outline: none; box-shadow: inset 0 2px 4px rgba(0,0,0,.03);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 13px;
}
footer{ text-align:center; font-size:12px; color: color-mix(in oklab, var(--muted) 64%, white); padding: 10px 0 0; }
/* 小屏优化(手机竖屏) */
@media (max-width: 480px){
.controls{ gap:8px; }
.btn{ padding: 10px 12px; }
.format{ width:100%; justify-content:center; }
.left-controls{ width:100%; justify-content:center; }
.right-controls{ width:100%; justify-content:center; }
}
/* toast */
.toast{ position: fixed; z-index: 50; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(16px);
background: #103e1b; color: #e9ffe9; padding: 10px 14px; border-radius: 999px; opacity:0; pointer-events:none;
transition: .25s ease; box-shadow: var(--shadow); font-size: 13px; }
.toast.show{ opacity:1; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<div class="container">
<header>
<h1>图片转化 base64 编码</h1>
<div class="subtitle">上传或拖拽图片,立即生成 Base64 / Data URL支持一键复制与清空。已针对手机竖屏优化。</div>
</header><section class="card uploader">
<div class="dropzone" id="dropZone" tabindex="0" aria-label="点击或拖拽图片到此处上传">
<svg class="drop-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 16v-8M8 8l4-4 4 4" stroke="#568f5b" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="3" y="12" width="18" height="8" rx="3" stroke="#85c076" stroke-width="1.4" />
</svg>
<div class="dz-text">
<div class="dz-title">点击选择图片或直接拖拽到这里</div>
<div class="dz-sub">支持 PNG / JPG / GIF / WebP / SVG 等常见格式</div>
</div>
<input id="fileInput" type="file" accept="image/*" />
</div>
<div class="preview" id="previewWrap" hidden>
<img id="preview" alt="图片预览" />
<div class="info" id="info"></div>
<div class="controls">
<div class="left-controls format" role="radiogroup" aria-label="输出格式">
<label><input type="radio" name="format" value="dataurl" checked> Data URL含前缀</label>
<label><input type="radio" name="format" value="base64"> 仅 Base64不含前缀</label>
</div>
<div class="right-controls">
<button class="btn" id="copyBtn" type="button" title="复制到剪贴板">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 9h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2Z" stroke="#234b2d" stroke-width="1.6"/><path d="M7 15H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" stroke="#234b2d" stroke-width="1.6"/></svg>
复制
</button>
<button class="btn danger" id="clearBtn" type="button" title="清空当前内容">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 7h16M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2M6 7l1.2 12.1A2 2 0 0 0 9.2 21h5.6a2 2 0 0 0 2-1.9L18 7" stroke="#8a3737" stroke-width="1.6" stroke-linecap="round"/></svg>
清空
</button>
</div>
</div>
<div class="output card">
<textarea id="output" placeholder="这里将显示转换后的内容……" readonly></textarea>
</div>
</div>
</section>
<footer>本工具在浏览器本地完成转换,不会上传图片或保存数据。</footer>
</div> <div class="toast" id="toast" role="status" aria-live="polite"></div> <script>
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const previewWrap = document.getElementById('previewWrap');
const previewImg = document.getElementById('preview');
const infoEl = document.getElementById('info');
const output = document.getElementById('output');
const copyBtn = document.getElementById('copyBtn');
const clearBtn = document.getElementById('clearBtn');
const formatRadios = document.querySelectorAll('input[name="format"]');
const toast = document.getElementById('toast');
let originalDataUrl = '';
function showToast(text){
toast.textContent = text;
toast.classList.add('show');
setTimeout(()=> toast.classList.remove('show'), 1800);
}
function humanSize(bytes){
if(bytes < 1024) return bytes + ' B';
const units = ['KB','MB','GB'];
let i = -1; do { bytes = bytes / 1024; i++; } while(bytes >= 1024 && i < units.length-1);
return bytes.toFixed(bytes < 10 ? 2 : 1) + ' ' + units[i];
}
function setOutputByFormat(){
if(!originalDataUrl) return;
const base64 = originalDataUrl.split(',')[1] || '';
const format = document.querySelector('input[name="format"]:checked')?.value || 'dataurl';
output.value = format === 'base64' ? base64 : originalDataUrl;
}
function updateInfo(file){
const base64Len = (originalDataUrl.split(',')[1] || '').length;
const approxBytes = Math.floor(base64Len * 3/4); // 估算
const tip = base64Len > 3_000_000 ? '(较大,复制可能稍慢)' : '';
infoEl.innerHTML = `
<span><strong>文件:</strong>${file.name}</span>
<span><strong>类型:</strong>${file.type || '未知'}</span>
<span><strong>原大小:</strong>${humanSize(file.size)}</span>
<span><strong>Base64 长度:</strong>${base64Len.toLocaleString()} 字符 ≈ ${humanSize(approxBytes)}</span>
<span>${tip}</span>
`;
}
function handleFile(file){
if(!file) return;
if(!file.type.startsWith('image/')){
showToast('请选择图片文件');
return;
}
const reader = new FileReader();
reader.onload = (e)=>{
originalDataUrl = String(e.target.result || '');
previewImg.src = originalDataUrl;
previewWrap.hidden = false;
setOutputByFormat();
updateInfo(file);
};
reader.onerror = ()=> showToast('读取文件失败');
reader.readAsDataURL(file);
}
// 文件选择
fileInput.addEventListener('change', (e)=> handleFile(e.target.files?.[0]));
// 拖拽上传
['dragenter','dragover'].forEach(ev=> dropZone.addEventListener(ev, (e)=>{ e.preventDefault(); e.dataTransfer.dropEffect='copy'; dropZone.classList.add('dragover'); }));
;['dragleave','drop'].forEach(ev=> dropZone.addEventListener(ev, (e)=>{ e.preventDefault(); dropZone.classList.remove('dragover'); }));
dropZone.addEventListener('drop', (e)=>{
const file = e.dataTransfer.files?.[0];
handleFile(file);
});
// 键盘无障碍:回车打开文件选择
dropZone.addEventListener('keydown', (e)=>{
if(e.key === 'Enter' || e.key === ' '){
e.preventDefault();
fileInput.click();
}
});
// 切换输出格式
formatRadios.forEach(r => r.addEventListener('change', setOutputByFormat));
// 复制
copyBtn.addEventListener('click', async ()=>{
if(!output.value){ showToast('没有可复制的内容'); return; }
try{
await navigator.clipboard.writeText(output.value);
showToast('已复制到剪贴板');
}catch(err){
// 兼容:选中文本让用户手动复制
output.select();
const ok = document.execCommand?.('copy');
showToast(ok ? '已复制到剪贴板' : '复制失败,请手动复制');
}
});
// 清空
clearBtn.addEventListener('click', ()=>{
originalDataUrl = '';
output.value = '';
previewImg.removeAttribute('src');
previewWrap.hidden = true;
fileInput.value = '';
showToast('已清空');
});
// 粘贴图片(可选加分功能)
window.addEventListener('paste', (e)=>{
const items = e.clipboardData?.items || [];
for(const it of items){
if(it.type.startsWith('image/')){
const file = it.getAsFile();
handleFile(file);
showToast('已从剪贴板粘贴图片');
break;
}
}
});
</script></body>
</html>

View File

@@ -0,0 +1,295 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>图片黑白处理</title>
<style>
:root{
--bg-1:#E9F9E7; /* 淡绿色 */
--bg-2:#F5FBE7; /* 淡黄绿色 */
--fg:#0f5132; /* 深一点的绿色文字 */
--muted:#5c7a66;
--card:#ffffffcc;
--accent:#6cc870;
--accent-2:#90d47f;
--shadow: 0 8px 24px rgba(39, 115, 72, .15);
--radius: 18px;
}
html,body{
height:100%;
}
body{
margin:0;
font-family: -apple-system,BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
color:var(--fg);
background: linear-gradient(160deg,var(--bg-1) 0%, var(--bg-2) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display:flex; align-items:stretch; justify-content:center;
}
.wrap{
width:min(100%, 760px);
padding:16px;
}
header{
text-align:center;
margin: 8px 0 14px;
}
h1{
margin:0 0 6px;
font-weight:800;
letter-spacing:.3px;
font-size: clamp(20px, 4.5vw, 28px);
}
.sub{
margin:0 auto;
max-width: 32em;
color:var(--muted);
font-size: clamp(12px, 3.4vw, 14px);
}.card{
background: var(--card);
backdrop-filter: blur(6px) saturate(120%);
border: 1px solid rgba(108,200,112,.18);
border-radius: var(--radius);
padding: 14px;
box-shadow: var(--shadow);
}
.controls{
display:grid;
grid-template-columns: 1fr;
gap:12px;
}
.row{
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
}
label{
font-weight:600; font-size:14px;
}
input[type="file"]{
width:100%;
padding:12px;
border: 1px dashed #a8d7a3;
border-radius: 14px;
background: #ffffffb0;
}
input[type="range"]{
width:100%;
accent-color: var(--accent);
height: 28px;
}
output{ font-variant-numeric: tabular-nums; min-width:3ch; text-align:right; display:inline-block; }
.buttons{
display:grid; grid-template-columns: 1fr 1fr; gap:10px;
}
button{
appearance:none; border:0; cursor:pointer;
padding:12px 14px; border-radius: 14px; font-weight:700;
background: linear-gradient(180deg, var(--accent), var(--accent-2));
color:white; box-shadow: 0 6px 16px rgba(16,123,62,.25);
}
button.secondary{
background:#eaf8ea; color:#245b35; box-shadow:none; border:1px solid #cfe9ce;
}
button:disabled{ opacity:.5; cursor:not-allowed; }
.preview{
margin-top: 12px;
display:grid; gap:10px;
}
.canvas-wrap{
background: repeating-linear-gradient( 45deg, #f3fbf0, #f3fbf0 14px, #eef8ec 14px, #eef8ec 28px);
border:1px solid #dcefd7; border-radius: 14px; overflow:hidden;
display:flex; align-items:center; justify-content:center;
min-height: 240px;
}
canvas{ max-width:100%; height:auto; display:block; }
.placeholder{ color:#7da287; padding:20px; text-align:center; }
footer{
text-align:center; color:var(--muted); font-size:12px; margin:14px auto 6px;
}
/* 更偏向手机竖屏的合理布局 */
@media (min-width: 720px){
.controls{ grid-template-columns: 1.2fr 1fr; align-items:end; }
.buttons{ grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>图片黑白处理 · 轻柔绿意版</h1>
<p class="sub">上传一张图片,拖动滑块设置<span style="font-weight:600">黑白程度</span>0% 原图 → 100% 全黑白),一键下载处理结果。完全本地处理,无需联网。</p>
</header><section class="card">
<div class="controls">
<div>
<label for="file">选择图片</label>
<input id="file" type="file" accept="image/*" />
</div>
<div>
<div class="row" style="justify-content:space-between">
<label for="amount">黑白程度</label>
<div><output id="val">100</output>%</div>
</div>
<input id="amount" type="range" min="0" max="100" step="1" value="100" />
<div class="buttons">
<button id="download" disabled>下载处理后的图片</button>
<button id="reset" class="secondary" disabled>重置</button>
</div>
</div>
</div>
<div class="preview">
<div class="canvas-wrap">
<canvas id="canvas" aria-label="预览画布"></canvas>
<div id="ph" class="placeholder">⬆️ 请选择一张图片开始…</div>
</div>
</div>
</section>
<footer>© 黑白处理在您的设备本地完成 · 支持手机竖屏友好显示</footer>
</div><script>
(() => {
const fileInput = document.getElementById('file');
const amount = document.getElementById('amount');
const valOut = document.getElementById('val');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const downloadBtn = document.getElementById('download');
const resetBtn = document.getElementById('reset');
const placeholder = document.getElementById('ph');
let originalPixels = null; // Uint8ClampedArray当前画布的原始像素用于反复处理
let imgNatural = { w: 0, h: 0 };
let currentFileName = 'processed.png';
let rafId = null;
// 将图像绘制到画布,并根据屏幕宽度进行适度缩放,避免超大图导致内存压力
const MAX_DIM = 4096; // 上限,兼顾清晰度与移动端内存
function drawImageToCanvas(img) {
// 处理超大图片:在不改变比例的前提下收敛到 MAX_DIM 以内
let w = img.naturalWidth;
let h = img.naturalHeight;
const scale = Math.min(1, MAX_DIM / Math.max(w, h));
w = Math.round(w * scale);
h = Math.round(h * scale);
// 为了保证下载清晰度canvas 使用实际像素尺寸;显示层用 CSS 自适应
canvas.width = w;
canvas.height = h;
ctx.clearRect(0,0,w,h);
ctx.drawImage(img, 0, 0, w, h);
// 保存原始像素用于反复应用不同程度
originalPixels = ctx.getImageData(0,0,w,h);
imgNatural = { w, h };
}
function lerp(a,b,t){ return a + (b-a) * t; }
// 将原图按给定强度转为黑白(灰度)
function applyGrayscale(strength01){
if(!originalPixels) return;
const { data: src } = originalPixels;
const copy = new ImageData(new Uint8ClampedArray(src), imgNatural.w, imgNatural.h);
const out = copy.data;
// 加权灰度(符合 sRGB 感知权重)
for(let i=0; i<out.length; i+=4){
const r = src[i], g = src[i+1], b = src[i+2];
const y = 0.2126*r + 0.7152*g + 0.0722*b;
out[i] = lerp(r, y, strength01);
out[i+1] = lerp(g, y, strength01);
out[i+2] = lerp(b, y, strength01);
// 保留 alpha
}
ctx.putImageData(copy, 0, 0);
}
// 防抖 + rAF流畅响应滑杆
function scheduleRender(){
if(rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const t = Number(amount.value) / 100; // 0~1
valOut.textContent = amount.value;
applyGrayscale(t);
});
}
// 处理文件加载
fileInput.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
if(!file) return;
currentFileName = (file.name ? file.name.replace(/\.[^.]+$/, '') : 'processed') + '.png';
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
placeholder.style.display = 'none';
drawImageToCanvas(img);
// 初始按当前滑杆值处理
scheduleRender();
downloadBtn.disabled = false;
resetBtn.disabled = false;
URL.revokeObjectURL(url);
};
img.onerror = () => {
alert('无法加载该图片,请更换文件试试。');
URL.revokeObjectURL(url);
};
img.src = url;
});
amount.addEventListener('input', scheduleRender);
resetBtn.addEventListener('click', () => {
if(!originalPixels) return;
amount.value = 100;
scheduleRender();
});
// 下载当前画布
function downloadCanvas(filename){
// toBlob 在少数旧版浏览器可能不可用,做个兼容
if(canvas.toBlob){
canvas.toBlob((blob) => {
if(!blob){ fallback(); return; }
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
}, 'image/png');
} else {
fallback();
}
function fallback(){
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}
}
downloadBtn.addEventListener('click', () => {
if(!originalPixels) return;
downloadCanvas(currentFileName);
});
})();
</script></body>
</html>

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,92 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<title>数字时钟</title>
<style>
/* 基础:黑底白字、全屏居中 */
html, body {
height: 100%;
margin: 0;
background: #000;
color: #fff;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
overscroll-behavior: none;
touch-action: manipulation;
}
.stage {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: env(safe-area-inset);
}
/* 大号等宽数字,保证各位宽度一致,避免跳动 */
.clock {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-variant-numeric: tabular-nums lining-nums;
line-height: 1;
letter-spacing: 0.02em;
white-space: nowrap;
user-select: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 自适应字号:在不同屏幕上都足够大 */
font-size: clamp(12vmin, 18vmin, 22vmin);
/* 防止 iOS 浏览器工具栏出现时的轻微抖动 */
max-width: 100vw;
}
/* 横屏:按宽度再放大一些(更横向铺满) */
@media (orientation: landscape) {
.clock { font-size: min(20vmin, 22vw); }
}
/* 竖屏:把数字整体顺时针旋转 90°看起来依旧是“横屏”的排布 */
@media (orientation: portrait) {
.clock { transform: rotate(90deg); }
}
</style>
</head>
<body>
<div class="stage">
<div id="clock" class="clock" aria-live="polite" aria-label="当前时间">00:00:00</div>
</div> <script>
(function() {
const el = document.getElementById('clock');
function two(n) { return (n < 10 ? '0' : '') + n; }
function renderNow() {
const d = new Date();
el.textContent = `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
}
// 与下一整秒对齐,尽量避免定时器漂移
function startAlignedClock() {
renderNow();
const drift = 1000 - (Date.now() % 1000);
setTimeout(function tick() {
renderNow();
setTimeout(tick, 1000);
}, drift);
}
// 处理移动端 100vh 问题:设置 CSS 变量供需要时使用(当前样式未直接用到,但保留以便扩展)
function setViewportVars() {
document.documentElement.style.setProperty('--dvw', window.innerWidth + 'px');
document.documentElement.style.setProperty('--dvh', window.innerHeight + 'px');
}
setViewportVars();
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
startAlignedClock();
})();
</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,177 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>随机Emoji表情</title>
<meta name="description" content="清新风格随机Emoji表情包展示支持复制与刷新移动端与电脑端自适应。" />
<style>
:root {
--bg-start: #eaf9e9;
--bg-end: #f4ffe5;
--panel-bg: rgba(255,255,255,0.60);
--panel-bd: rgba(255,255,255,0.85);
--accent: #79c86b;
--text: #253525;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
background-attachment: fixed;
/* 隐藏滚动条但保留滚动功能 */
overflow-x: hidden;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
body::-webkit-scrollbar {
display: none;
}
html {
/* 隐藏滚动条但保留滚动功能 */
overflow-x: hidden;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
html::-webkit-scrollbar {
display: none;
}
.wrap {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.panel {
width: min(92vw, 1100px);
background: var(--panel-bg);
border: 1px solid var(--panel-bd);
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
backdrop-filter: saturate(1.1) blur(2px);
padding: 1rem 1rem 1.25rem;
}
.header { display:flex; justify-content:space-between; align-items:baseline; gap:.75rem; margin-bottom:.75rem; }
.title { font-weight:600; font-size: clamp(1.1rem, 2.6vw, 1.6rem); letter-spacing:.3px; }
.hint { font-size: clamp(.85rem, 2vw, 1rem); color:#4a6f4a; opacity:.9; }
table { width: 100%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; overflow: hidden; border-radius: 12px; background: rgba(255,255,255,0.38); }
tbody { width: 100%; }
td {
border: 1px solid rgba(255,255,255,0.75);
text-align: center; vertical-align: middle; user-select: none; cursor: pointer;
background: rgba(255,255,255,0.50);
transition: transform 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
font-size: var(--cell-font);
height: var(--cell-size);
line-height: 1;
}
td:hover { background: rgba(255,255,255,0.80); transform: scale(1.06); box-shadow: inset 0 0 0 2px rgba(121,200,107,0.18); }
.controls { display:flex; justify-content:center; align-items:center; gap:.8rem; margin-top:1rem; }
.refresh { border:none; padding:.6rem 1.15rem; border-radius:999px; font-weight:600; letter-spacing:.3px; color:#1f351f; background: linear-gradient(180deg, #c9f6c9, #c9f2a8); box-shadow: 0 6px 14px rgba(121,200,107,0.28), inset 0 1px 0 rgba(255,255,255,0.85); transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; }
.refresh:hover { transform: translateY(-1px); filter: saturate(1.06); }
.refresh:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(121,200,107,0.28); }
.toast {
position: fixed; left:50%; bottom: 24px; transform: translateX(-50%) translateY(80px);
background: rgba(255,255,255,0.95); color:#1f351f; border:1px solid rgba(255,255,255,0.95);
border-radius: 10px; box-shadow: 0 8px 18px rgba(0,0,0,0.12);
padding: .5rem .85rem; font-weight:600; opacity:0; pointer-events:none; transition: opacity 160ms ease, transform 160ms ease;
}
.toast.show { opacity:1; transform: translateX(-50%) translateY(0); }
/* 基础尺寸JS 会根据 10×10 或 20×20 自动适配 */
:root { --cell-size: 48px; --cell-font: 28px; }
@media (max-width: 768px) and (orientation: portrait) {
:root { --cell-size: clamp(40px, 7.2vw, 58px); --cell-font: clamp(22px, 5.8vw, 32px); }
}
@media (min-width: 769px) {
:root { --cell-size: clamp(44px, 3.6vw, 58px); --cell-font: clamp(22px, 2.2vw, 32px); }
}
</style>
</head>
<body>
<div class="wrap">
<div class="panel">
<div class="header">
<div class="title">随机 Emoji 表情</div>
<div class="hint">点击任意 Emoji 复制</div>
</div>
<table id="emoji-table" aria-label="随机 Emoji 表格"><tbody id="emoji-body"></tbody></table>
<div class="controls"><button class="refresh" id="refresh-btn">刷新</button></div>
</div>
</div>
<div class="toast" id="toast">已复制!</div>
<script>
// 随机生成 Emoji从常用的 Unicode 区间中抽取)
function randomEmoji() {
const ranges = [
[0x1F600, 0x1F64F], // Emoticons
[0x1F300, 0x1F5FF], // Misc Symbols and Pictographs
[0x1F680, 0x1F6FF], // Transport & Map
[0x2600, 0x26FF], // Misc symbols
[0x2700, 0x27BF], // Dingbats
[0x1F900, 0x1F9FF], // Supplemental Symbols and Pictographs
[0x1FA70, 0x1FAFF] // Symbols & Pictographs Extended-A
];
const [start, end] = ranges[Math.floor(Math.random() * ranges.length)];
const code = start + Math.floor(Math.random() * (end - start + 1));
return String.fromCodePoint(code);
}
const tableBody = document.getElementById('emoji-body');
const refreshBtn = document.getElementById('refresh-btn');
const toastEl = document.getElementById('toast');
function isPortraitMobile() {
return window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches;
}
function getGridSize() { return isPortraitMobile() ? 10 : 20; }
function showToast(text) {
toastEl.textContent = text;
toastEl.classList.add('show');
clearTimeout(showToast._timer);
showToast._timer = setTimeout(() => toastEl.classList.remove('show'), 1200);
}
async function copyToClipboard(text) {
try { await navigator.clipboard.writeText(text); }
catch (e) {
const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove();
}
showToast(`已复制: ${text}`);
}
function generateTable() {
const size = getGridSize();
tableBody.innerHTML = '';
for (let r = 0; r < size; r++) {
const tr = document.createElement('tr');
for (let c = 0; c < size; c++) {
const td = document.createElement('td');
const e = randomEmoji();
td.textContent = e; td.title = '点击复制';
td.addEventListener('click', () => copyToClipboard(e));
tr.appendChild(td);
}
tableBody.appendChild(tr);
}
}
refreshBtn.addEventListener('click', generateTable);
window.addEventListener('resize', () => {
const newSize = getGridSize();
if (tableBody.children.length !== newSize) generateTable();
});
generateTable();
</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>