不知名提交
This commit is contained in:
42
InfoGenie-frontend/public/toolbox/AI聊天/index.html
Normal file
42
InfoGenie-frontend/public/toolbox/AI聊天/index.html
Normal 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>
|
||||
69
InfoGenie-frontend/public/toolbox/AI聊天/marked.min.js
vendored
Normal file
69
InfoGenie-frontend/public/toolbox/AI聊天/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
InfoGenie-frontend/public/toolbox/AI聊天/purify.min.js
vendored
Normal file
3
InfoGenie-frontend/public/toolbox/AI聊天/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
223
InfoGenie-frontend/public/toolbox/AI聊天/script.js
Normal file
223
InfoGenie-frontend/public/toolbox/AI聊天/script.js
Normal 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();
|
||||
172
InfoGenie-frontend/public/toolbox/AI聊天/style.css
Normal file
172
InfoGenie-frontend/public/toolbox/AI聊天/style.css
Normal 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; }
|
||||
339
InfoGenie-frontend/public/toolbox/Json编辑器/index.html
Normal file
339
InfoGenie-frontend/public/toolbox/Json编辑器/index.html
Normal 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>
|
||||
337
InfoGenie-frontend/public/toolbox/Markdown解析器/index.html
Normal file
337
InfoGenie-frontend/public/toolbox/Markdown解析器/index.html
Normal 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>
|
||||
69
InfoGenie-frontend/public/toolbox/Markdown解析器/marked.min.js
vendored
Normal file
69
InfoGenie-frontend/public/toolbox/Markdown解析器/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
InfoGenie-frontend/public/toolbox/Markdown解析器/purify.min.js
vendored
Normal file
3
InfoGenie-frontend/public/toolbox/Markdown解析器/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
202
InfoGenie-frontend/public/toolbox/人生倒计时/index.html
Normal file
202
InfoGenie-frontend/public/toolbox/人生倒计时/index.html
Normal 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>
|
||||
262
InfoGenie-frontend/public/toolbox/做决定转盘/index.html
Normal file
262
InfoGenie-frontend/public/toolbox/做决定转盘/index.html
Normal 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> </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>
|
||||
380
InfoGenie-frontend/public/toolbox/图片圆角处理/index.html
Normal file
380
InfoGenie-frontend/public/toolbox/图片圆角处理/index.html
Normal 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">上传图片 → 调节四个角的圆角强度(0–100%)→ 预览并下载透明圆角 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>
|
||||
327
InfoGenie-frontend/public/toolbox/图片转Base64编码/index.html
Normal file
327
InfoGenie-frontend/public/toolbox/图片转Base64编码/index.html
Normal 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>
|
||||
295
InfoGenie-frontend/public/toolbox/图片黑白处理/index.html
Normal file
295
InfoGenie-frontend/public/toolbox/图片黑白处理/index.html
Normal 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>
|
||||
136
InfoGenie-frontend/public/toolbox/在线JavaScript执行/app.js
Normal file
136
InfoGenie-frontend/public/toolbox/在线JavaScript执行/app.js
Normal 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'); }
|
||||
})();
|
||||
42
InfoGenie-frontend/public/toolbox/在线JavaScript执行/index.html
Normal file
42
InfoGenie-frontend/public/toolbox/在线JavaScript执行/index.html
Normal 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>
|
||||
176
InfoGenie-frontend/public/toolbox/在线JavaScript执行/styles.css
Normal file
176
InfoGenie-frontend/public/toolbox/在线JavaScript执行/styles.css
Normal 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; }
|
||||
}
|
||||
92
InfoGenie-frontend/public/toolbox/数字时钟/index.html
Normal file
92
InfoGenie-frontend/public/toolbox/数字时钟/index.html
Normal 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>
|
||||
278
InfoGenie-frontend/public/toolbox/白板/index.html
Normal file
278
InfoGenie-frontend/public/toolbox/白板/index.html
Normal 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>
|
||||
2
InfoGenie-frontend/public/toolbox/视频播放器/hls.min.js
vendored
Normal file
2
InfoGenie-frontend/public/toolbox/视频播放器/hls.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
412
InfoGenie-frontend/public/toolbox/视频播放器/index.html
Normal file
412
InfoGenie-frontend/public/toolbox/视频播放器/index.html
Normal 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>
|
||||
225
InfoGenie-frontend/public/toolbox/计算器/app.js
Normal file
225
InfoGenie-frontend/public/toolbox/计算器/app.js
Normal 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');
|
||||
})();
|
||||
63
InfoGenie-frontend/public/toolbox/计算器/index.html
Normal file
63
InfoGenie-frontend/public/toolbox/计算器/index.html
Normal 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()">x²</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>
|
||||
138
InfoGenie-frontend/public/toolbox/计算器/styles.css
Normal file
138
InfoGenie-frontend/public/toolbox/计算器/styles.css
Normal 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; }
|
||||
}
|
||||
260
InfoGenie-frontend/public/toolbox/记事本/index.html
Normal file
260
InfoGenie-frontend/public/toolbox/记事本/index.html
Normal 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>
|
||||
177
InfoGenie-frontend/public/toolbox/随机Emoji表情/index.html
Normal file
177
InfoGenie-frontend/public/toolbox/随机Emoji表情/index.html
Normal 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>
|
||||
242
InfoGenie-frontend/public/toolbox/随机数生成器/index.html
Normal file
242
InfoGenie-frontend/public/toolbox/随机数生成器/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user