不知名提交

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

View File

@@ -0,0 +1,11 @@
# React 开发环境变量
# API URL - 开发环境使用本地后端
REACT_APP_API_URL=http://127.0.0.1:5002
# 应用信息
REACT_APP_NAME=InfoGenie
REACT_APP_VERSION=1.0.0
# 调试模式
REACT_APP_DEBUG=true

View File

@@ -0,0 +1,13 @@
# React 构建时环境变量
# 用于 Docker 构建
# API URL - 在 Docker 环境下,前端和后端在同一个容器
# 使用相对路径,这样前端会自动使用当前域名
REACT_APP_API_URL=
# 应用信息
REACT_APP_NAME=InfoGenie
REACT_APP_VERSION=1.0.0
# 调试模式(生产环境关闭)
REACT_APP_DEBUG=false

View File

@@ -4,11 +4,11 @@
body {
background: linear-gradient(
135deg,
#fff8dc 0%,
#ffeaa7 25%,
#fdcb6e 50%,
#e17055 75%,
#d63031 100%
#f1f8e9 0%,
#dcedc8 25%,
#c8e6c8 50%,
#a5d6a7 75%,
#81c784 100%
);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
@@ -24,9 +24,9 @@ body::before {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(212, 175, 55, 0.05) 0%, transparent 50%);
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(102, 187, 106, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 1;
}
@@ -40,11 +40,11 @@ body::after {
width: 100%;
height: 100%;
background-image:
radial-gradient(2px 2px at 20px 30px, rgba(255, 215, 0, 0.3), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255, 223, 0, 0.2), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(212, 175, 55, 0.4), transparent),
radial-gradient(1px 1px at 130px 80px, rgba(255, 215, 0, 0.2), transparent),
radial-gradient(2px 2px at 160px 30px, rgba(255, 223, 0, 0.3), transparent);
radial-gradient(2px 2px at 20px 30px, rgba(129, 199, 132, 0.3), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(165, 214, 167, 0.2), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(102, 187, 106, 0.4), transparent),
radial-gradient(1px 1px at 130px 80px, rgba(129, 199, 132, 0.2), transparent),
radial-gradient(2px 2px at 160px 30px, rgba(165, 214, 167, 0.3), transparent);
background-repeat: repeat;
background-size: 200px 100px;
animation: sparkle 20s linear infinite;
@@ -106,8 +106,8 @@ body::after {
body::before {
background:
radial-gradient(circle at 30% 70%, rgba(255, 215, 0, 0.08) 0%, transparent 40%),
radial-gradient(circle at 70% 30%, rgba(255, 223, 0, 0.08) 0%, transparent 40%);
radial-gradient(circle at 30% 70%, rgba(129, 199, 132, 0.08) 0%, transparent 40%),
radial-gradient(circle at 70% 30%, rgba(165, 214, 167, 0.08) 0%, transparent 40%);
}
body::after {
@@ -121,9 +121,9 @@ body::after {
body {
background: linear-gradient(
135deg,
#fff8dc 0%,
#ffeaa7 50%,
#fdcb6e 100%
#f1f8e9 0%,
#dcedc8 50%,
#c8e6c8 100%
);
background-size: 150% 150%;
}
@@ -138,18 +138,18 @@ body::after {
body {
background: linear-gradient(
135deg,
#2c1810 0%,
#3d2914 25%,
#4a3319 50%,
#5c3e1f 75%,
#6b4423 100%
#1b2e1b 0%,
#2e4a2e 25%,
#3e5e3e 50%,
#4e6e4e 75%,
#5e7e5e 100%
);
}
body::before {
background:
radial-gradient(circle at 20% 80%, rgba(255, 215, 0, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 223, 0, 0.05) 0%, transparent 50%);
radial-gradient(circle at 20% 80%, rgba(129, 199, 132, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(165, 214, 167, 0.05) 0%, transparent 50%);
}
}
@@ -162,6 +162,6 @@ body::after {
}
body {
background: linear-gradient(135deg, #fff8dc 0%, #ffeaa7 50%, #fdcb6e 100%);
background: linear-gradient(135deg, #f1f8e9 0%, #dcedc8 50%, #c8e6c8 100%);
}
}

View File

@@ -8,7 +8,7 @@
body {
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #2c1810;
color: #2e7d32;
overflow-x: hidden;
}
@@ -33,20 +33,20 @@ body {
.title {
font-size: 3rem;
font-weight: 700;
color: #d4af37;
color: #2e7d32;
text-shadow:
0 0 10px rgba(212, 175, 55, 0.8),
0 0 20px rgba(212, 175, 55, 0.6),
0 0 30px rgba(212, 175, 55, 0.4);
0 0 10px rgba(129, 199, 132, 0.8),
0 0 20px rgba(129, 199, 132, 0.6),
0 0 30px rgba(129, 199, 132, 0.4);
margin-bottom: 10px;
animation: titleGlow 3s ease-in-out infinite alternate;
}
.subtitle {
font-size: 1.2rem;
color: #b8860b;
color: #388e3c;
opacity: 0.9;
text-shadow: 0 0 5px rgba(184, 134, 11, 0.5);
text-shadow: 0 0 5px rgba(102, 187, 106, 0.5);
}
/* 主内容区域 */
@@ -58,14 +58,14 @@ body {
/* 一言容器 */
.quote-container {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 223, 0, 0.05));
border: 2px solid rgba(212, 175, 55, 0.3);
background: linear-gradient(135deg, rgba(129, 199, 132, 0.1), rgba(165, 214, 167, 0.05));
border: 2px solid rgba(102, 187, 106, 0.3);
border-radius: 20px;
padding: 40px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
box-shadow:
0 8px 32px rgba(212, 175, 55, 0.2),
0 8px 32px rgba(102, 187, 106, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
@@ -78,7 +78,7 @@ body {
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #ffd700, #ffed4e, #ffd700, #ffed4e);
background: linear-gradient(45deg, #81c784, #a5d6a7, #81c784, #a5d6a7);
border-radius: 22px;
z-index: -1;
animation: borderGlow 4s linear infinite;
@@ -88,7 +88,7 @@ body {
.loading {
display: none;
text-align: center;
color: #d4af37;
color: #2e7d32;
}
.loading.show {
@@ -98,8 +98,8 @@ body {
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(212, 175, 55, 0.3);
border-top: 4px solid #d4af37;
border: 4px solid rgba(102, 187, 106, 0.3);
border-top: 4px solid #2e7d32;
border-radius: 50%;
margin: 0 auto 15px;
animation: spin 1s linear infinite;
@@ -118,15 +118,15 @@ body {
.quote-text {
font-size: 1.8rem;
line-height: 1.8;
color: #2c1810;
color: #2e7d32;
margin-bottom: 20px;
text-shadow: 0 1px 2px rgba(212, 175, 55, 0.1);
text-shadow: 0 1px 2px rgba(102, 187, 106, 0.1);
font-weight: 500;
}
.quote-index {
font-size: 0.9rem;
color: #b8860b;
color: #388e3c;
opacity: 0.8;
}
@@ -134,7 +134,7 @@ body {
.error-message {
display: none;
text-align: center;
color: #cd853f;
color: #66bb6a;
}
.error-message.show {
@@ -157,20 +157,20 @@ body {
}
.refresh-btn {
background: linear-gradient(135deg, #ffd700, #ffed4e);
background: linear-gradient(135deg, #81c784, #a5d6a7);
border: none;
border-radius: 50px;
padding: 15px 30px;
font-size: 1.1rem;
font-weight: 600;
color: #2c1810;
color: #2e7d32;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
box-shadow:
0 4px 15px rgba(212, 175, 55, 0.3),
0 4px 15px rgba(102, 187, 106, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
position: relative;
overflow: hidden;
@@ -179,7 +179,7 @@ body {
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow:
0 6px 20px rgba(212, 175, 55, 0.4),
0 6px 20px rgba(102, 187, 106, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
@@ -206,7 +206,7 @@ body {
.footer {
margin-top: 40px;
text-align: center;
color: #b8860b;
color: #388e3c;
opacity: 0.8;
font-size: 0.9rem;
}
@@ -215,15 +215,15 @@ body {
@keyframes titleGlow {
0% {
text-shadow:
0 0 10px rgba(212, 175, 55, 0.8),
0 0 20px rgba(212, 175, 55, 0.6),
0 0 30px rgba(212, 175, 55, 0.4);
0 0 10px rgba(129, 199, 132, 0.8),
0 0 20px rgba(129, 199, 132, 0.6),
0 0 30px rgba(129, 199, 132, 0.4);
}
100% {
text-shadow:
0 0 15px rgba(212, 175, 55, 1),
0 0 25px rgba(212, 175, 55, 0.8),
0 0 35px rgba(212, 175, 55, 0.6);
0 0 15px rgba(129, 199, 132, 1),
0 0 25px rgba(129, 199, 132, 0.8),
0 0 35px rgba(129, 199, 132, 0.6);
}
}

View File

@@ -6,9 +6,9 @@ body {
transition: background 0.5s ease;
}
/* Hand-drawn Comic Theme Background - NEW VIBRANT VERSION */
/* Hand-drawn Comic Theme Background - FRESH GREEN VERSION */
body.theme-comic {
background: linear-gradient(-45deg, #ff7e5f, #feb47b, #ffcc80, #ffecb3);
background: linear-gradient(-45deg, #c8e6c9, #dcedc8, #f1f8e9, #e8f5e8);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
}

View File

@@ -33,7 +33,7 @@
border: 2px solid transparent;
}
.theme-icon.active {
border-color: #ff7043;
border-color: #66bb6a;
transform: scale(1.1);
}
@@ -41,26 +41,26 @@
.theme-comic header h1 {
font-family: 'Zhi Mang Xing', cursive;
font-size: 4em;
color: #d84315; /* Deep Orange */
color: #2e7d32; /* Fresh Green */
text-shadow: 2px 2px 0 #fff;
margin: 0.2em 0;
}
.theme-comic .divider {
height: 3px;
background: linear-gradient(90deg, #ffca28, #ff7043, #29b6f6, #66bb6a);
background: linear-gradient(90deg, #81c784, #a5d6a7, #c8e6c9, #66bb6a);
border-radius: 3px;
margin: 20px auto;
width: 80%;
}
.theme-comic .joke-card {
background: rgba(255, 255, 255, 0.85); /* White with transparency */
background: rgba(248, 255, 248, 0.9); /* Light green tinted white */
backdrop-filter: blur(5px);
border-radius: 15px;
padding: 40px;
min-height: 200px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
box-shadow: 0 8px 25px rgba(102, 187, 106, 0.15);
display: flex;
justify-content: center;
align-items: center;
@@ -68,6 +68,7 @@
margin-bottom: 20px;
transform: rotate(-1deg);
transition: transform 0.2s ease;
border: 1px solid rgba(129, 199, 132, 0.3);
}
.theme-comic .joke-card:hover {
transform: rotate(1deg) scale(1.02);
@@ -77,11 +78,11 @@
font-family: 'Zhi Mang Xing', cursive;
font-size: 2em;
line-height: 1.6;
color: #5d4037;
color: #1b5e20;
}
.theme-comic .new-joke-btn {
background: #1e88e5; /* Vibrant Blue */
background: linear-gradient(135deg, #66bb6a, #81c784); /* Fresh Green Gradient */
color: white;
font-family: 'Zhi Mang Xing', cursive;
font-size: 2.5em;
@@ -89,7 +90,7 @@
border-radius: 50px;
padding: 10px 30px;
cursor: pointer;
box-shadow: 0 5px 0 #1565c0; /* Darker Blue */
box-shadow: 0 5px 0 #388e3c; /* Darker Green */
transition: all 0.1s ease-in-out;
}
.theme-comic .new-joke-btn:active {
@@ -120,8 +121,8 @@
margin-top: -27.5px;
}
.book-page {
background: #ffca28;
border: 1px solid #ff7043;
background: #a5d6a7;
border: 1px solid #66bb6a;
border-radius: 3px;
transform-origin: left;
}

View File

@@ -11,8 +11,6 @@
<div class="theme-switcher">
<div class="theme-icon" data-theme="theme-comic" title="手绘漫画">✏️</div>
<div class="theme-icon" data-theme="theme-emoji" title="表情包狂欢">😂</div>
<div class="theme-icon" data-theme="theme-retro" title="复古电视">📺</div>
</div>
<div class="container">

View File

@@ -0,0 +1,163 @@
/* 随机答案之书 - 淡绿色清新风格样式(与随机唱歌音频一致) */
/* 重置样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
min-height: 100vh;
color: #2d5016;
line-height: 1.6;
overflow-x: hidden;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
/* 头部 */
.header {
text-align: center;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.85);
border-radius: 20px;
padding: 24px;
box-shadow: 0 8px 25px rgba(45, 80, 22, 0.08);
backdrop-filter: blur(10px);
}
.header h1 {
font-size: 2rem;
color: #2d5016;
margin-bottom: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.header p {
color: #5a7c65;
font-size: 1rem;
}
/* 按钮 */
.btn {
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
color: white;
border: none;
padding: 10px 18px;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.35);
text-decoration: none;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(129, 199, 132, 0.45);
}
/* 加载与错误 */
.loading, .error {
text-align: center;
padding: 30px;
background: rgba(255, 255, 255, 0.85);
border-radius: 15px;
box-shadow: 0 5px 20px rgba(45, 80, 22, 0.08);
}
.spinner {
width: 36px;
height: 36px;
border: 4px solid #e8f5e8;
border-top: 4px solid #81c784;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 18px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 动画 */
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 答案卡片 */
.answer-card {
background: rgba(255, 255, 255, 0.9);
padding: 16px;
border-radius: 15px;
box-shadow: 0 4px 18px rgba(45, 80, 22, 0.08);
margin-bottom: 15px;
text-align: center;
}
.answer-text {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 8px;
color: #1b5e20;
word-break: break-word;
}
.answer-en {
color: #5a7c65;
font-size: 1rem;
margin-bottom: 10px;
}
.meta {
color: #5a7c65;
font-size: 0.95rem;
}
.actions {
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
margin-top: 12px;
}
/* 手机端优先优化 */
@media (max-width: 767px) {
.container { padding: 12px; }
.header { padding: 18px; }
.header h1 { font-size: 1.6rem; gap: 8px; }
.answer-card { padding: 16px; }
.answer-text { font-size: 1.3rem; }
.answer-en { font-size: 0.95rem; }
.actions {
flex-direction: column;
gap: 15px;
}
.btn {
width: 100%;
max-width: 220px;
text-align: center;
}
}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>📘真理之道</title>
<meta name="description" content="当你踌躇不定,犹豫不决时,不妨来这里看看吧。" />
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<div class="container">
<header class="header">
<h1>📘真理之道</h1>
<p>当你踌躇不定,犹豫不决时,不妨来这里看看吧</p>
</header>
<!-- 加载与错误状态 -->
<section id="loading" class="loading">
<div class="spinner"></div>
<p>正在加载中,请稍候…</p>
</section>
<section id="error" class="error" style="display: none;">
<p>获取数据失败,请稍后重试</p>
</section>
<!-- 内容区域 -->
<main id="content" style="display: none;" class="fade-in">
<div class="answer-card">
<div class="answer-text" id="answer">-</div>
<div class="answer-en" id="answer-en" style="display: none;">-</div>
<div class="meta">编号:<span id="index">-</span></div>
<div class="actions">
<button class="btn" id="refresh-btn">换一个</button>
<button class="btn" id="copy-btn">复制</button>
</div>
</div>
</main>
</div>
<script src="./js/script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,224 @@
// 随机答案之书 页面脚本
(function () {
'use strict';
const API = {
endpoints: [],
currentIndex: 0,
params: {
encoding: 'json'
},
localFallback: '返回接口.json',
// 初始化API接口列表
async init() {
try {
const res = await fetch('./接口集合.json');
const endpoints = await res.json();
this.endpoints = endpoints.map(endpoint => `${endpoint}/v2/answer`);
} catch (e) {
// 如果无法加载接口集合,使用默认接口
this.endpoints = ['https://60s.api.shumengya.top/v2/answer'];
}
},
// 获取当前接口URL
getCurrentUrl() {
if (this.endpoints.length === 0) return null;
const url = new URL(this.endpoints[this.currentIndex]);
Object.entries(this.params).forEach(([k, v]) => url.searchParams.append(k, v));
return url.toString();
},
// 切换到下一个接口
switchToNext() {
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
return this.currentIndex < this.endpoints.length;
},
// 重置到第一个接口
reset() {
this.currentIndex = 0;
}
};
// DOM 元素引用
const els = {
loading: null,
error: null,
container: null,
answer: null,
answerEn: null,
indexEl: null,
refreshBtn: null,
copyBtn: null,
};
function initDom() {
els.loading = document.getElementById('loading');
els.error = document.getElementById('error');
els.container = document.getElementById('content');
els.answer = document.getElementById('answer');
els.answerEn = document.getElementById('answer-en');
els.indexEl = document.getElementById('index');
els.refreshBtn = document.getElementById('refresh-btn');
els.copyBtn = document.getElementById('copy-btn');
}
function showLoading() {
els.loading.style.display = 'block';
els.error.style.display = 'none';
els.container.style.display = 'none';
}
function showError(msg) {
els.loading.style.display = 'none';
els.error.style.display = 'block';
els.container.style.display = 'none';
els.error.querySelector('p').textContent = msg || '获取数据失败,请稍后重试';
}
function showContent() {
els.loading.style.display = 'none';
els.error.style.display = 'none';
els.container.style.display = 'block';
}
function safeText(text) {
const div = document.createElement('div');
div.textContent = text == null ? '' : String(text);
return div.innerHTML;
}
async function fetchFromAPI() {
// 初始化API接口列表
await API.init();
// 重置API索引到第一个接口
API.reset();
// 尝试所有API接口
for (let i = 0; i < API.endpoints.length; i++) {
try {
const url = API.getCurrentUrl();
console.log(`尝试接口 ${i + 1}/${API.endpoints.length}: ${url}`);
const resp = await fetch(url, {
cache: 'no-store',
timeout: 10000 // 10秒超时兼容同目录页面风格
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
const data = await resp.json();
if (data && data.code === 200) {
console.log(`接口 ${i + 1} 请求成功`);
return data;
}
throw new Error(data && data.message ? data.message : '接口返回异常');
} catch (e) {
console.warn(`接口 ${i + 1} 失败:`, e.message);
// 如果不是最后一个接口,切换到下一个
if (i < API.endpoints.length - 1) {
API.switchToNext();
continue;
}
// 所有接口都失败了
console.warn('所有远程接口都失败,尝试本地数据');
return null;
}
}
}
async function fetchFromLocal() {
try {
const resp = await fetch(API.localFallback + `?t=${Date.now()}`);
if (!resp.ok) throw new Error(`本地文件HTTP ${resp.status}`);
const data = await resp.json();
return data;
} catch (e) {
console.error('读取本地返回接口.json失败:', e);
return null;
}
}
function render(data) {
const d = data?.data || {};
const cn = d.answer || '';
const en = d.answer_en || '';
const idx = d.index != null ? d.index : d.id != null ? d.id : '-';
els.answer.innerHTML = safeText(cn || '-');
if (en) {
els.answerEn.style.display = 'block';
els.answerEn.innerHTML = safeText(en);
} else {
els.answerEn.style.display = 'none';
els.answerEn.innerHTML = '';
}
els.indexEl.textContent = idx;
showContent();
}
async function load() {
showLoading();
try {
// 先尝试远程API
const data = await fetchFromAPI();
if (data) {
render(data);
return;
}
// 远程API失败尝试本地数据
const localData = await fetchFromLocal();
if (localData) {
render(localData);
return;
}
// 都失败了
showError('获取数据失败,请稍后重试');
} catch (e) {
console.error('加载数据时发生错误:', e);
showError('获取数据失败,请稍后重试');
}
}
function bindEvents() {
if (els.refreshBtn) {
els.refreshBtn.addEventListener('click', load);
}
if (els.copyBtn) {
els.copyBtn.addEventListener('click', async () => {
const textParts = [];
const cn = els.answer?.textContent?.trim();
const en = els.answerEn?.textContent?.trim();
if (cn) textParts.push(cn);
if (en) textParts.push(en);
const finalText = textParts.join('\n');
try {
await navigator.clipboard.writeText(finalText);
const old = els.copyBtn.textContent;
els.copyBtn.textContent = '已复制';
setTimeout(() => { els.copyBtn.textContent = old; }, 1200);
} catch (e) {
alert('复制失败,请手动选择文本复制');
}
});
}
}
document.addEventListener('DOMContentLoaded', () => {
initDom();
bindEvents();
load();
});
})();

View File

@@ -0,0 +1,3 @@
[
"https://60s.api.shumengya.top"
]

View File

@@ -0,0 +1,10 @@
{
"code": 200,
"message": "获取成功。数据来自官方/权威源头,以确保稳定与实时。开源地址 https://github.com/vikiboss/60s反馈群 595941841",
"data": {
"id": "63",
"answer": "那不值得纠结",
"answer_en": "It's not worth worrying about",
"index": 62
}
}

View File

@@ -1,8 +1,8 @@
body {
background: linear-gradient(-45deg, #0a021a, #2a0d3f, #4a1a6c, #7b2f8f);
background: linear-gradient(-45deg, #f1f8e9, #e8f5e8, #c8e6c9, #dcedc8);
background-size: 400% 400%;
animation: gradientBG 20s ease infinite;
color: #ffffff;
color: #2e7d32;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;

View File

@@ -8,14 +8,14 @@
header h1 {
font-size: 2.8em;
color: #f0e6ff;
text-shadow: 0 0 10px #d1a9ff, 0 0 20px #d1a9ff;
color: #2e7d32;
text-shadow: 0 0 10px #81c784, 0 0 20px #a5d6a7;
margin-bottom: 0.2em;
}
header p {
font-size: 1.2em;
color: #e0c8ff;
color: #388e3c;
margin-bottom: 40px;
}
@@ -27,11 +27,11 @@ header p {
.crystal-ball {
width: 200px;
height: 200px;
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 180, 255, 0.1));
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), rgba(200, 230, 201, 0.3));
border-radius: 50%;
margin: 0 auto;
position: relative;
box-shadow: 0 0 30px #c390ff, 0 0 60px #a060e0, inset 0 0 20px rgba(255, 220, 255, 0.3);
box-shadow: 0 0 30px #81c784, 0 0 60px #66bb6a, inset 0 0 20px rgba(220, 255, 220, 0.3);
animation: float 6s ease-in-out infinite;
transform-style: preserve-3d;
}
@@ -54,7 +54,7 @@ header p {
left: 50%;
width: 120%;
height: 120%;
background: linear-gradient(45deg, rgba(255, 192, 203, 0.1), rgba(128, 0, 128, 0.2));
background: linear-gradient(45deg, rgba(200, 230, 201, 0.2), rgba(129, 199, 132, 0.3));
border-radius: 50%;
animation: swirl 10s linear infinite;
transform: translate(-50%, -50%);
@@ -71,7 +71,7 @@ header p {
}
.fortune-card {
background: rgba(255, 255, 255, 0.05);
background: rgba(248, 255, 248, 0.8);
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
@@ -80,8 +80,8 @@ header p {
justify-content: center;
align-items: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(129, 199, 132, 0.3);
box-shadow: 0 8px 32px 0 rgba(102, 187, 106, 0.2);
transition: opacity 0.5s ease-in-out;
}
@@ -96,13 +96,13 @@ header p {
#luck-desc {
font-size: 2em;
color: #ffc0cb;
color: #2e7d32;
margin: 0 0 10px;
}
#luck-tip {
font-size: 1.1em;
color: #e0e0e0;
color: #388e3c;
margin: 0;
padding-bottom: 20px; /* Add some space before the new details */
}
@@ -121,7 +121,7 @@ header p {
.detail-item h3 {
font-size: 0.9em;
color: #ffc0cb;
color: #66bb6a;
margin: 0 0 5px;
font-weight: normal;
}
@@ -151,8 +151,8 @@ header p {
.tarot-container h2 {
font-size: 1.5em;
color: #f0e6ff;
text-shadow: 0 0 8px #d1a9ff;
color: #2e7d32;
text-shadow: 0 0 8px #81c784;
margin-bottom: 20px;
}
@@ -188,23 +188,23 @@ header p {
}
.tarot-card-back {
background: linear-gradient(135deg, #4a1a6c, #2a0d3f);
border: 2px solid #d1a9ff;
background: linear-gradient(135deg, #66bb6a, #388e3c);
border: 2px solid #81c784;
display: flex;
justify-content: center;
align-items: center;
font-size: 3em;
color: #d1a9ff;
color: #c8e6c9;
}
.tarot-card-back::after {
content: '✧'; /* A simple star symbol */
text-shadow: 0 0 10px #f0e6ff;
text-shadow: 0 0 10px #e8f5e8;
}
.tarot-card-front {
background: linear-gradient(135deg, #3e165b, #592883);
border: 2px solid #d1a9ff;
background: linear-gradient(135deg, #4caf50, #66bb6a);
border: 2px solid #81c784;
color: white;
transform: rotateY(180deg);
padding: 20px;
@@ -217,7 +217,7 @@ header p {
#tarot-name {
font-size: 1.4em;
color: #ffc0cb;
color: #e8f5e8;
margin: 0 0 10px;
}
@@ -248,8 +248,8 @@ header p {
.decor-symbol {
position: absolute;
color: rgba(209, 169, 255, 0.5);
text-shadow: 0 0 10px rgba(240, 230, 255, 0.7);
color: rgba(129, 199, 132, 0.5);
text-shadow: 0 0 10px rgba(200, 230, 201, 0.7);
animation: floatSymbol 20s infinite ease-in-out;
}
@@ -268,7 +268,7 @@ header p {
}
#get-fortune-btn {
background: linear-gradient(45deg, #da70d6, #8a2be2);
background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
border: none;
border-radius: 50px;
@@ -276,12 +276,12 @@ header p {
font-size: 1.1em;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 0 15px #c390ff;
box-shadow: 0 0 15px #81c784;
}
#get-fortune-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 25px #d1a9ff;
box-shadow: 0 0 25px #a5d6a7;
}
#get-fortune-btn:active {
@@ -289,8 +289,8 @@ header p {
}
.loading-spinner {
border: 4px solid rgba(255, 255, 255, 0.2);
border-left-color: #ffc0cb;
border: 4px solid rgba(129, 199, 132, 0.3);
border-left-color: #66bb6a;
border-radius: 50%;
width: 40px;
height: 40px;
@@ -308,7 +308,7 @@ header p {
footer {
margin-top: 40px;
color: rgba(255, 255, 255, 0.6);
color: rgba(46, 125, 50, 0.7);
}
/* Responsive Design */

View File

@@ -6,7 +6,7 @@ body::before {
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
z-index: -2;
}
@@ -18,9 +18,9 @@ body::after {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
z-index: -1;
animation: backgroundMove 20s ease-in-out infinite;
}
@@ -28,27 +28,27 @@ body::after {
@keyframes backgroundMove {
0%, 100% {
background:
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
radial-gradient(circle at 20% 80%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
}
25% {
background:
radial-gradient(circle at 60% 30%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 30% 70%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
radial-gradient(circle at 60% 30%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 30% 70%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
}
50% {
background:
radial-gradient(circle at 80% 60%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 30%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 60% 70%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
radial-gradient(circle at 80% 60%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 20% 30%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 60% 70%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
}
75% {
background:
radial-gradient(circle at 40% 90%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 70% 10%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 60%, rgba(120, 219, 255, 0.3) 0%, transparent 50%);
radial-gradient(circle at 40% 90%, rgba(144, 238, 144, 0.2) 0%, transparent 50%),
radial-gradient(circle at 70% 10%, rgba(173, 255, 173, 0.2) 0%, transparent 50%),
radial-gradient(circle at 20% 60%, rgba(152, 251, 152, 0.2) 0%, transparent 50%);
}
}

View File

@@ -11,6 +11,22 @@ body {
color: #333;
min-height: 100vh;
overflow-x: hidden;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
body::-webkit-scrollbar,
html::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none;
}
/* 全局隐藏滚动条但保留滚动功能 */
html {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 容器样式 */
@@ -26,7 +42,7 @@ body {
.header {
text-align: center;
padding: 3rem 2rem 2rem;
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(80, 200, 120, 0.1));
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(152, 251, 152, 0.15));
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
@@ -34,7 +50,7 @@ body {
.header h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #4a90e2, #50c878);
background: linear-gradient(135deg, #228B22, #32CD32);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -44,7 +60,7 @@ body {
.header h1 i {
margin-right: 0.5rem;
background: linear-gradient(135deg, #4a90e2, #50c878);
background: linear-gradient(135deg, #228B22, #32CD32);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -72,7 +88,7 @@ body {
}
.query-btn {
background: linear-gradient(135deg, #4a90e2, #50c878);
background: linear-gradient(135deg, #228B22, #32CD32);
color: white;
border: none;
padding: 1rem 2rem;
@@ -81,7 +97,7 @@ body {
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3);
display: inline-flex;
align-items: center;
gap: 0.5rem;
@@ -91,8 +107,8 @@ body {
.query-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
background: linear-gradient(135deg, #3a7bc8, #40a868);
box-shadow: 0 6px 20px rgba(34, 139, 34, 0.4);
background: linear-gradient(135deg, #1e7e1e, #2eb82e);
}
.query-btn:active {

View File

@@ -51,11 +51,6 @@
<span class="label">查询时间:</span>
<span id="query-time" class="value">--</span>
</div>
<div class="detail-item">
<i class="fas fa-server"></i>
<span class="label">数据来源:</span>
<span class="value">60s.viki.moe</span>
</div>
<div class="detail-item">
<i class="fas fa-map-marker-alt"></i>
<span class="label">位置信息:</span>
@@ -129,9 +124,6 @@
</main>
<!-- 页脚 -->
<footer class="footer">
<p>&copy; 2024 公网IP地址查询工具 | 数据来源: 60s.viki.moe</p>
</footer>
</div>
<script src="js/script.js"></script>

View File

@@ -1,18 +1,33 @@
/* 农历主题背景样式 - 柔和版本 */
/* 全局滚动条隐藏样式 */
html, body {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none; /* Webkit浏览器 */
}
/* 农历主题背景样式 - 淡黄绿色到淡绿色清新渐变 */
body {
background: linear-gradient(135deg,
#f8f9fa 0%, /* 浅灰白 */
#fff3e0 20%, /* 淡橙色 */
#fef7e0 40%, /* 极淡黄 */
#f3e5ab 60%, /* 柔和金色 */
#e8dcc6 80%, /* 色 */
#f8f9fa 100% /* 浅灰白 */
#f0f8e8 0%, /* 淡黄绿色 */
#e8f5e8 20%, /* 浅绿色 */
#d4f4dd 40%, /* 淡绿色 */
#c8f2d4 60%, /* 清新绿色 */
#b8f0c8 80%, /* 柔和绿色 */
#e8f5e8 100% /* 浅绿色 */
);
background-size: 400% 400%;
animation: gentleShift 30s ease infinite;
background-attachment: fixed;
min-height: 100vh;
position: relative;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
@keyframes gentleShift {
@@ -23,7 +38,7 @@ body {
100% { background-position: 0% 50%; }
}
/* 动态颜色调节系统 - 柔和版本 */
/* 动态颜色调节系统 - 绿色主题版本 */
.adaptive-overlay {
position: fixed;
top: 0;
@@ -31,9 +46,9 @@ body {
width: 100%;
height: 100%;
background:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.25) 0%, transparent 50%),
linear-gradient(45deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.3) 100%);
radial-gradient(circle at 20% 30%, rgba(200, 242, 212, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(184, 240, 200, 0.25) 0%, transparent 50%),
linear-gradient(45deg, rgba(232, 245, 232, 0.2) 0%, rgba(212, 244, 221, 0.3) 100%);
pointer-events: none;
z-index: 1;
animation: adaptiveShift 60s ease infinite;
@@ -42,33 +57,33 @@ body {
@keyframes adaptiveShift {
0% {
background:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
radial-gradient(circle at 20% 30%, rgba(232, 245, 232, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(212, 244, 221, 0.15) 0%, transparent 50%),
linear-gradient(45deg, rgba(240, 248, 232, 0.1) 0%, rgba(232, 245, 232, 0.2) 100%);
}
25% {
background:
radial-gradient(circle at 70% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 50%),
radial-gradient(circle at 30% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.25) 100%);
radial-gradient(circle at 70% 20%, rgba(200, 242, 212, 0.2) 0%, transparent 50%),
radial-gradient(circle at 30% 80%, rgba(184, 240, 200, 0.1) 0%, transparent 50%),
linear-gradient(135deg, rgba(212, 244, 221, 0.15) 0%, rgba(200, 242, 212, 0.25) 100%);
}
50% {
background:
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
radial-gradient(circle at 10% 90%, rgba(255, 255, 255, 0.12) 0%, transparent 50%),
linear-gradient(225deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.22) 100%);
radial-gradient(circle at 50% 50%, rgba(220, 246, 228, 0.15) 0%, transparent 50%),
radial-gradient(circle at 10% 90%, rgba(232, 245, 232, 0.12) 0%, transparent 50%),
linear-gradient(225deg, rgba(240, 248, 232, 0.12) 0%, rgba(212, 244, 221, 0.22) 100%);
}
75% {
background:
radial-gradient(circle at 90% 60%, rgba(255, 255, 255, 0.18) 0%, transparent 50%),
radial-gradient(circle at 40% 10%, rgba(255, 255, 255, 0.08) 0%, transparent 50%),
linear-gradient(315deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
radial-gradient(circle at 90% 60%, rgba(184, 240, 200, 0.18) 0%, transparent 50%),
radial-gradient(circle at 40% 10%, rgba(240, 248, 232, 0.08) 0%, transparent 50%),
linear-gradient(315deg, rgba(232, 245, 232, 0.1) 0%, rgba(200, 242, 212, 0.2) 100%);
}
100% {
background:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.15) 0%, transparent 50%),
linear-gradient(45deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.2) 100%);
radial-gradient(circle at 20% 30%, rgba(232, 245, 232, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(212, 244, 221, 0.15) 0%, transparent 50%),
linear-gradient(45deg, rgba(240, 248, 232, 0.1) 0%, rgba(232, 245, 232, 0.2) 100%);
}
}

View File

@@ -266,12 +266,12 @@ body {
.date-input:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.15);
border-color: #228B22;
background: rgba(255, 255, 255, 0.95);
box-shadow:
0 6px 20px rgba(31, 38, 135, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 0 0 3px rgba(255, 255, 255, 0.1);
0 6px 20px rgba(34, 139, 34, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 0 0 3px rgba(34, 139, 34, 0.1);
transform: translateY(-2px);
}
@@ -282,11 +282,11 @@ body {
}
.query-btn {
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
color: #1a1a1a;
border: 1px solid rgba(0, 0, 0, 0.2);
color: #ffffff;
border: 1px solid rgba(34, 139, 34, 0.3);
padding: 12px 28px;
border-radius: 20px;
cursor: pointer;
@@ -298,10 +298,10 @@ body {
gap: 8px;
position: relative;
overflow: hidden;
text-shadow: 0 1px 3px rgba(255, 255, 255, 0.8);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
box-shadow:
0 4px 15px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
0 4px 15px rgba(34, 139, 34, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* 移除按钮颜色动画,保持稳定的可读性 */
@@ -326,12 +326,12 @@ body {
}
.query-btn:hover {
background: linear-gradient(135deg, #e8e8e8, #d8d8d8);
border-color: rgba(0, 0, 0, 0.3);
background: linear-gradient(135deg, #1e7e1e, #2eb82e);
border-color: rgba(34, 139, 34, 0.5);
transform: translateY(-2px);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
0 8px 25px rgba(34, 139, 34, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.btn-icon {

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🌙 农历信息查询</title>
<title>🌙农历信息查询</title>
<link rel="stylesheet" href="css/background.css">
<link rel="stylesheet" href="css/style.css">
</head>
@@ -48,8 +48,7 @@
<div class="container">
<header class="header">
<div class="header-icon">🏮</div>
<h1 class="title">🌙 农历信息查询 📅</h1>
<h1 class="title">🌙农历信息查询</h1>
<p class="subtitle">传统文化 · 时光转换 · 节气查询</p>
<div class="date-selector">

View File

@@ -287,15 +287,9 @@ function displayLunarInfo(lunarData) {
<div class="item-label">本月进度</div>
<div class="item-value">${lunarData.stats.percents_formatted.month}</div>
</div>
<div class="info-item">
<div class="item-icon">🗓️</div>
<div class="item-label">本周第几天</div>
<div class="item-value">第${lunarData.stats.week_of_month}周</div>
</div>
<div class="info-item">
<div class="item-icon">⏰</div>
<div class="item-label">今日进度</div>
<div class="item-value">${lunarData.stats.percents_formatted.day}</div>
</div>
</div>
</div>

View File

@@ -7,10 +7,26 @@
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
min-height: 100vh;
color: #333;
overflow-x: hidden;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
body::-webkit-scrollbar,
html::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none;
}
/* 全局隐藏滚动条但保留滚动功能 */
html {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.container {
@@ -65,7 +81,7 @@ body {
.logo i {
font-size: 48px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
background: linear-gradient(45deg, #228B22, #32CD32);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -174,7 +190,7 @@ body {
.card-header i {
font-size: 24px;
color: #667eea;
color: #228B22;
}
.card-header h2 {
@@ -202,8 +218,8 @@ body {
#inputText:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
border-color: #228B22;
box-shadow: 0 0 0 3px rgba(34, 139, 34, 0.1);
background: rgba(255, 255, 255, 0.95);
}
@@ -247,14 +263,14 @@ body {
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 15px rgba(34, 139, 34, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
box-shadow: 0 8px 25px rgba(34, 139, 34, 0.4);
}
.btn-secondary {
@@ -306,7 +322,7 @@ body {
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
background: linear-gradient(90deg, #228B22, #32CD32, #90EE90, #98FB98);
}
.result-card:hover {
@@ -356,7 +372,7 @@ body {
.result-value:hover {
background: rgba(248, 250, 252, 0.95);
border-color: #667eea;
border-color: #228B22;
}
.result-value .placeholder {
@@ -367,7 +383,7 @@ body {
.copy-btn {
background: none;
border: none;
color: #667eea;
color: #228B22;
cursor: pointer;
padding: 8px;
border-radius: 6px;
@@ -377,8 +393,8 @@ body {
}
.copy-btn:hover {
background: rgba(102, 126, 234, 0.1);
color: #5a67d8;
background: rgba(34, 139, 34, 0.1);
color: #1e7e1e;
}
/* Loading Overlay */
@@ -429,7 +445,7 @@ body {
position: fixed;
bottom: 30px;
right: 30px;
background: linear-gradient(135deg, #4ecdc4, #44a08d);
background: linear-gradient(135deg, #228B22, #32CD32);
color: white;
padding: 16px 24px;
border-radius: 12px;

View File

@@ -5,12 +5,27 @@
box-sizing: border-box;
}
/* 隐藏滚动条但保留滚动功能 */
html {
scrollbar-width: none;
-ms-overflow-style: none;
}
body::-webkit-scrollbar,
html::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #2c3e50;
min-height: 100vh;
overflow-x: hidden;
background: linear-gradient(135deg, #f0f8e8 0%, #e8f5e8 50%, #d4f4dd 100%);
scrollbar-width: none;
-ms-overflow-style: none;
}
/* 容器布局 */
@@ -28,9 +43,9 @@ body {
text-align: center;
margin-bottom: 40px;
padding: 40px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
box-shadow: 0 10px 30px rgba(34, 139, 34, 0.3);
color: white;
}
@@ -95,9 +110,9 @@ body {
.password-input:focus {
outline: none;
border-color: #667eea;
border-color: #228B22;
background: #ffffff;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 4px rgba(34, 139, 34, 0.1);
}
.password-input::placeholder {
@@ -140,7 +155,7 @@ body {
/* 检测按钮 */
.check-btn {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #228B22 0%, #32CD32 100%);
color: white;
border: none;
padding: 18px 32px;
@@ -149,7 +164,7 @@ body {
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 20px rgba(34, 139, 34, 0.3);
display: flex;
align-items: center;
justify-content: center;
@@ -160,7 +175,7 @@ body {
.check-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.4);
box-shadow: 0 6px 25px rgba(34, 139, 34, 0.4);
}
.check-btn:active {
@@ -284,7 +299,7 @@ body {
.bar-fill {
height: 100%;
background: linear-gradient(90deg, #ef4444, #f97316, #eab308, #22c55e);
background: linear-gradient(90deg, #90EE90, #98FB98, #32CD32, #228B22);
border-radius: 6px;
width: 0%;
transition: width 0.8s ease;
@@ -383,13 +398,13 @@ body {
}
.char-type.has-type {
background: #dcfce7;
border-color: #bbf7d0;
color: #166534;
background: #f0f8e8;
border-color: #d4f4dd;
color: #1e7e1e;
}
.char-type.has-type .type-icon {
color: #22c55e;
color: #228B22;
}
.type-icon {
@@ -555,11 +570,11 @@ body {
position: fixed;
top: 20px;
right: 20px;
background: #22c55e;
background: #228B22;
color: white;
padding: 16px 24px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.3);
box-shadow: 0 4px 20px rgba(34, 139, 34, 0.3);
z-index: 1000;
animation: toastSlide 0.3s ease-out;
font-weight: 500;
@@ -586,11 +601,11 @@ body {
}
.strength-strong {
color: #059669 !important;
color: #228B22 !important;
}
.strength-very-strong {
color: #047857 !important;
color: #1e7e1e !important;
}
/* 分数圆圈颜色 */
@@ -603,11 +618,11 @@ body {
}
.score-strong {
background: conic-gradient(from 0deg, #059669 0deg, #059669 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
background: conic-gradient(from 0deg, #228B22 0deg, #228B22 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
}
.score-very-strong {
background: conic-gradient(from 0deg, #047857 0deg, #047857 var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
background: conic-gradient(from 0deg, #1e7e1e 0deg, #1e7e1e var(--score-deg), #e2e8f0 var(--score-deg), #e2e8f0 360deg) !important;
}
/* 平板端适配 (768px - 1024px) */

View File

@@ -0,0 +1,25 @@
// 环境配置文件 - AI中国亲戚称呼计算器
// 复用 InfoGenie 的全局 ENV_CONFIG支持独立打开的回退地址
const DEFAULT_API = (window.ENV_CONFIG && window.ENV_CONFIG.API_URL) || 'http://127.0.0.1:5002';
window.API_CONFIG = {
baseUrl: window.parent?.ENV_CONFIG?.API_URL || DEFAULT_API,
endpoints: {
kinshipCalculator: '/api/aimodelapp/kinship-calculator'
}
};
window.AUTH_CONFIG = {
tokenKey: 'token',
getToken: () => localStorage.getItem('token'),
isAuthenticated: () => !!localStorage.getItem('token')
};
window.APP_CONFIG = {
name: 'InfoGenie 中国亲戚称呼计算器',
version: '1.0.0',
debug: false
};
console.log('中国亲戚称呼计算器 环境配置已加载');

View File

@@ -0,0 +1,59 @@
<!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>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="app-container">
<header class="app-header">
<h1>中国亲戚称呼计算器</h1>
<p class="subtitle">输入亲属关系链(如“妈妈的爸爸”、“爸爸的姐姐的儿子”),快速得到标准普通话称呼与各地方言称呼</p>
</header>
<main class="card">
<section class="input-section">
<label for="relationInput" class="label">亲属关系链</label>
<textarea id="relationInput" class="textarea" rows="2" placeholder="例如:妈妈的爸爸"></textarea>
<div class="hint">
· 使用“的”连接每一层关系,例如:
<span class="chip" onclick="setExample('妈妈的爸爸')">妈妈的爸爸</span>
<span class="chip" onclick="setExample('爸爸的姐姐的儿子')">爸爸的姐姐的儿子</span>
<span class="chip" onclick="setExample('妈妈的弟弟的女儿')">妈妈的弟弟的女儿</span>
</div>
<button id="calcBtn" class="button primary">计算称呼</button>
<div id="loading" class="loading" style="display:none">正在计算,请稍候…</div>
<div id="error" class="error" style="display:none"></div>
</section>
<section id="resultSection" class="result-section" style="display:none">
<h2>计算结果</h2>
<div class="result-block">
<div class="result-title">标准普通话称呼</div>
<div id="mandarinTitle" class="result-value"></div>
<div class="actions">
<button id="copyMandarinBtn" class="button">复制称呼</button>
</div>
</div>
<div class="result-block">
<div class="result-title">各地方言称呼</div>
<div id="dialectList" class="dialect-list"></div>
</div>
<div id="notesBlock" class="result-block" style="display:none">
<div class="result-title">说明</div>
<div id="notes" class="notes"></div>
</div>
</section>
</main>
</div>
<script src="env.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,135 @@
// 环境与认证在 env.js 中定义
const relationInput = document.getElementById('relationInput');
const calcBtn = document.getElementById('calcBtn');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
const resultSection = document.getElementById('resultSection');
const mandarinTitleEl = document.getElementById('mandarinTitle');
const dialectListEl = document.getElementById('dialectList');
const copyMandarinBtn = document.getElementById('copyMandarinBtn');
const notesBlock = document.getElementById('notesBlock');
const notesEl = document.getElementById('notes');
function setExample(text) {
relationInput.value = text;
}
window.setExample = setExample;
function showLoading(show) {
loadingDiv.style.display = show ? 'block' : 'none';
calcBtn.disabled = show;
}
function showError(msg) {
errorDiv.textContent = msg || '';
errorDiv.style.display = msg ? 'block' : 'none';
}
function clearResults() {
resultSection.style.display = 'none';
mandarinTitleEl.textContent = '';
dialectListEl.innerHTML = '';
notesBlock.style.display = 'none';
notesEl.textContent = '';
}
async function callKinshipAPI(relationChain) {
const token = window.AUTH_CONFIG.getToken();
if (!token) throw new Error('未登录请先登录后使用AI功能');
const url = `${window.API_CONFIG.baseUrl}${window.API_CONFIG.endpoints.kinshipCalculator}`;
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ relation_chain: relationChain })
});
if (!resp.ok) {
if (resp.status === 402) throw new Error('您的萌芽币余额不足,无法使用此功能');
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || `API请求失败: ${resp.status} ${resp.statusText}`);
}
const data = await resp.json();
if (!data.success) throw new Error(data.error || 'API响应异常');
return data;
}
function renderDialects(dialectTitles) {
dialectListEl.innerHTML = '';
const order = ['粤语','闽南语','上海话','四川话','东北话','客家话'];
const names = order.concat(Object.keys(dialectTitles || {}).filter(k => !order.includes(k)));
names.forEach(name => {
const info = dialectTitles?.[name];
if (!info || (!info.title && !info.romanization && !info.notes)) return;
const item = document.createElement('div');
item.className = 'dialect-item';
const title = (info.title || '').toString();
const roman = (info.romanization || '').toString();
const notes = (info.notes || '').toString();
item.innerHTML = `
<div class="dialect-name">${name}</div>
<div class="dialect-title">${title}</div>
${roman ? `<div class="dialect-roman">${roman}</div>` : ''}
${notes ? `<div class="dialect-notes">${notes}</div>` : ''}
`;
dialectListEl.appendChild(item);
});
}
async function doCalculate() {
const relation = (relationInput.value || '').trim();
if (!relation) {
showError('请输入亲属关系链');
return;
}
showError('');
showLoading(true);
clearResults();
try {
const data = await callKinshipAPI(relation);
mandarinTitleEl.textContent = data.mandarin_title || '';
renderDialects(data.dialect_titles || {});
if (data.notes) {
notesEl.textContent = data.notes;
notesBlock.style.display = 'block';
}
resultSection.style.display = 'block';
} catch (e) {
console.error('计算失败:', e);
showError(`计算失败: ${e.message}`);
} finally {
showLoading(false);
}
}
function copyText(text) {
try {
navigator.clipboard.writeText(text);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
copyMandarinBtn.addEventListener('click', () => {
const t = mandarinTitleEl.textContent || '';
if (!t) return;
copyText(t);
});
calcBtn.addEventListener('click', doCalculate);
document.addEventListener('DOMContentLoaded', () => {
showError('');
});

View File

@@ -0,0 +1,189 @@
/* 渐变背景与毛玻璃风格,参考 AI文章排版 */
:root {
--green: #a8e6cf;
--lime: #dcedc1;
--dark: #2e7d32;
--text: #1b5e20;
--muted: #558b2f;
--white: #ffffff;
--shadow: rgba(0, 0, 0, 0.08);
}
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(--green) 0%, var(--lime) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
}
.app-header {
text-align: center;
margin: 8px 0 16px;
}
.app-header h1 {
font-size: 22px;
margin: 0;
color: var(--dark);
}
.subtitle {
margin-top: 8px;
font-size: 13px;
color: var(--muted);
}
.card {
width: 100%;
max-width: 720px;
background: rgba(255,255,255,0.75);
backdrop-filter: blur(10px);
border-radius: 14px;
box-shadow: 0 8px 24px var(--shadow);
padding: 16px;
}
.label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
.textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(0,0,0,0.05);
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
color: var(--text);
outline: none;
}
.textarea:focus {
border-color: rgba(46,125,50,0.35);
box-shadow: 0 0 0 3px rgba(46,125,50,0.12);
}
.hint {
margin: 10px 0 12px;
font-size: 12px;
color: var(--muted);
}
.chip {
display: inline-block;
background: rgba(255,255,255,0.9);
border: 1px solid rgba(46,125,50,0.2);
color: var(--dark);
border-radius: 999px;
padding: 4px 10px;
margin-right: 6px;
cursor: pointer;
user-select: none;
}
.chip:hover { filter: brightness(0.98); }
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12px;
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
background: rgba(46,125,50,0.15);
color: var(--dark);
}
.button.primary {
background: linear-gradient(135deg, #81c784, #aed581);
color: #fff;
}
.button:disabled { opacity: 0.6; cursor: not-allowed; }
.loading {
margin-top: 10px;
font-size: 13px;
color: var(--muted);
}
.error {
margin-top: 8px;
padding: 8px 10px;
border-left: 3px solid #e53935;
background: rgba(229,57,53,0.08);
border-radius: 8px;
color: #c62828;
font-size: 13px;
}
.result-section { margin-top: 14px; }
.result-section h2 {
font-size: 16px;
margin: 0 0 8px;
color: var(--dark);
}
.result-block {
background: rgba(255,255,255,0.9);
border: 1px solid rgba(0,0,0,0.05);
border-radius: 12px;
padding: 10px;
margin-bottom: 10px;
}
.result-title {
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
.result-value {
font-size: 18px;
color: var(--text);
}
.actions { margin-top: 8px; }
.dialect-list {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
@media (min-width: 540px) {
.dialect-list { grid-template-columns: 1fr 1fr; }
}
.dialect-item {
border: 1px solid rgba(46,125,50,0.18);
border-radius: 10px;
padding: 8px;
background: rgba(255,255,255,0.95);
}
.dialect-item .dialect-name {
font-weight: 600;
color: var(--dark);
margin-bottom: 4px;
}
.dialect-item .dialect-title { font-size: 15px; }
.dialect-item .dialect-roman { font-size: 12px; color: var(--muted); }
.dialect-item .dialect-notes { font-size: 12px; color: var(--muted); margin-top: 4px; }
.notes { font-size: 13px; color: var(--muted); }
.app-footer {
margin-top: 12px;
font-size: 12px;
color: var(--muted);
text-align: center;
}

View File

@@ -8,13 +8,31 @@
/* 主体样式 - iOS风格 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 100%);
background: linear-gradient(135deg, #F0FFF0 0%, #98FB98 50%, #90EE90 100%);
min-height: 100vh;
padding: 20px;
color: #1D1D1F;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 隐藏滚动条但保留滚动功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏Webkit浏览器的滚动条 */
body::-webkit-scrollbar {
display: none;
}
/* 全局滚动条隐藏 */
* {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
*::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* 容器样式 - iOS毛玻璃效果 */
@@ -81,9 +99,9 @@ body {
.form-input:focus {
outline: none;
border-color: #007AFF;
border-color: #32CD32;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.1);
box-shadow: 0 0 0 4px rgba(50, 205, 50, 0.1);
}
.textarea {
@@ -105,7 +123,7 @@ body {
.btn {
width: 100%;
padding: 16px;
background: #007AFF;
background: #32CD32;
color: white;
border: none;
border-radius: 12px;
@@ -114,18 +132,18 @@ body {
cursor: pointer;
transition: all 0.2s ease;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
box-shadow: 0 2px 8px rgba(50, 205, 50, 0.25);
}
.btn:hover {
background: #0056CC;
background: #228B22;
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.35);
box-shadow: 0 4px 16px rgba(50, 205, 50, 0.35);
}
.btn:active {
transform: translateY(0);
background: #004499;
background: #006400;
}
.btn:disabled {
@@ -151,7 +169,7 @@ body {
.loading {
display: none;
text-align: center;
color: #007AFF;
color: #32CD32;
font-style: normal;
padding: 24px;
font-weight: 500;
@@ -161,9 +179,12 @@ body {
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 16px;
padding: 24px;
padding: 20px;
min-height: 150px;
backdrop-filter: blur(10px);
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.placeholder {
@@ -176,15 +197,16 @@ body {
/* 分组标题样式 - iOS风格 */
.convention-group-title {
font-size: 1.0625rem;
font-size: 1rem;
font-weight: 600;
color: white;
margin: 20px 0 12px 0;
padding: 12px 16px;
background: #007AFF;
margin: 16px 0 12px 0;
padding: 10px 16px;
background: #32CD32;
border-radius: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 122, 255, 0.25);
box-shadow: 0 2px 8px rgba(50, 205, 50, 0.25);
grid-column: 1 / -1;
}
.convention-group-title:first-child {
@@ -196,17 +218,21 @@ body {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
padding: 12px;
margin-bottom: 0;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
backdrop-filter: blur(10px);
min-height: 80px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.suggestion-item:hover {
border-color: rgba(0, 122, 255, 0.3);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.1);
border-color: rgba(50, 205, 50, 0.3);
box-shadow: 0 4px 16px rgba(50, 205, 50, 0.1);
background: rgba(255, 255, 255, 0.95);
}
@@ -216,33 +242,35 @@ body {
.variable-name {
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 1.0625rem;
font-size: 1rem;
font-weight: 600;
color: #1D1D1F;
margin-bottom: 6px;
margin-bottom: 4px;
word-break: break-all;
}
.variable-description {
font-size: 0.9375rem;
font-size: 0.875rem;
color: #86868B;
line-height: 1.47;
line-height: 1.4;
flex-grow: 1;
}
.copy-btn {
position: absolute;
top: 12px;
right: 12px;
background: #007AFF;
top: 8px;
right: 8px;
background: #32CD32;
color: white;
border: none;
border-radius: 8px;
padding: 6px 12px;
font-size: 0.8125rem;
border-radius: 6px;
padding: 4px 8px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 122, 255, 0.25);
box-shadow: 0 1px 3px rgba(50, 205, 50, 0.25);
}
.suggestion-item:hover .copy-btn {
@@ -250,7 +278,7 @@ body {
}
.copy-btn:hover {
background: #0056CC;
background: #228B22;
transform: translateY(-1px);
}
@@ -314,17 +342,30 @@ body {
.suggestions-container {
padding: 15px;
grid-template-columns: 1fr;
gap: 12px;
}
.suggestion-item {
padding: 12px;
padding: 10px;
min-height: 70px;
}
.copy-btn {
position: static;
opacity: 1;
margin-top: 8px;
margin-top: 6px;
width: 100%;
padding: 6px 8px;
font-size: 0.75rem;
}
.variable-name {
font-size: 0.9rem;
}
.variable-description {
font-size: 0.8rem;
}
}
@@ -342,16 +383,30 @@ body {
padding: 10px;
}
.suggestions-container {
padding: 12px;
gap: 10px;
}
.suggestion-item {
padding: 10px;
padding: 8px;
min-height: 60px;
}
.variable-name {
font-size: 1rem;
font-size: 0.85rem;
margin-bottom: 3px;
}
.variable-description {
font-size: 0.85rem;
font-size: 0.75rem;
line-height: 1.3;
}
.convention-group-title {
font-size: 0.9rem;
padding: 8px 12px;
margin: 12px 0 8px 0;
}
}

View File

@@ -0,0 +1,29 @@
// 环境配置文件 - AI文章排版
// 复用 InfoGenie 的全局 ENV_CONFIG
// 本地/独立打开页面的API回退地址优先使用父窗口ENV_CONFIG
const DEFAULT_API = (window.ENV_CONFIG && window.ENV_CONFIG.API_URL) || 'http://127.0.0.1:5002';
// API配置
window.API_CONFIG = {
baseUrl: window.parent?.ENV_CONFIG?.API_URL || DEFAULT_API,
endpoints: {
markdownFormatting: '/api/aimodelapp/markdown_formatting'
}
};
// 认证配置
window.AUTH_CONFIG = {
tokenKey: 'token',
getToken: () => localStorage.getItem('token'),
isAuthenticated: () => !!localStorage.getItem('token')
};
// 应用配置
window.APP_CONFIG = {
name: 'InfoGenie AI文章排版',
version: '1.0.0',
debug: false
};
console.log('AI文章排版 环境配置已加载');

View File

@@ -0,0 +1,92 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI文章排版助手</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1 class="title">AI文章排版助手</h1>
<p class="subtitle">保持原文不变 · 智能转为Markdown并点缀Emoji</p>
</div>
<div class="form-section">
<div class="form-group">
<label class="form-label" for="articleText">请输入文章内容:</label>
<textarea
id="articleText"
class="form-input textarea"
placeholder="粘贴或输入原文内容点击开始排版即可生成Markdown并通过Emoji增强可读性..."
></textarea>
</div>
<div class="form-row">
<div class="form-group half-width">
<label class="form-label" for="emojiStyle">Emoji风格</label>
<select id="emojiStyle" class="form-input select">
<option value="balanced">适中(推荐)</option>
<option value="light">清爽少量Emoji</option>
<option value="rich">丰富较多Emoji</option>
</select>
</div>
<div class="form-group half-width">
<label class="form-label" for="markdownOption">排版偏好:</label>
<select id="markdownOption" class="form-input select">
<option value="standard">标准Markdown</option>
<option value="compact">紧凑排版</option>
<option value="readable">易读增强</option>
</select>
</div>
</div>
<button id="formatBtn" class="btn">开始排版</button>
</div>
<div class="result-section">
<h3 class="result-title">排版结果</h3>
<div id="loading" class="loading">正在排版中,请稍候...</div>
<div id="resultContainer" class="conversion-container">
<div class="placeholder">输入文章后点击“开始排版”AI将把原文转换为规范的Markdown并智能添加合适的Emoji</div>
</div>
<div id="previewSection" class="preview-section" style="display:none;">
<div class="preview-header">
<span class="label">Markdown预览</span>
<button class="copy-btn" id="copyHtmlBtn">复制HTML</button>
</div>
<div id="markdownPreview" class="markdown-preview"></div>
</div>
<div id="rawSection" class="raw-section" style="display:none;">
<div class="raw-header">
<span class="label">Markdown源文本</span>
<button class="copy-btn" id="copyMdBtn">复制Markdown</button>
</div>
<pre id="markdownRaw" class="markdown-raw"></pre>
</div>
</div>
</div>
<!-- 环境配置与功能脚本 -->
<script src="env.js"></script>
<!-- Markdown 渲染与安全过滤CDN -->
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.9/dist/purify.min.js"></script>
<script>
// 检查库是否正确加载
document.addEventListener('DOMContentLoaded', function() {
if (typeof marked === 'undefined') {
console.error('marked库加载失败');
document.getElementById('resultContainer').innerHTML = '<div class="placeholder error">Markdown渲染库加载失败请检查网络连接</div>';
}
if (typeof DOMPurify === 'undefined') {
console.warn('DOMPurify库加载失败将使用不安全的HTML渲染');
}
});
</script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,152 @@
// 配置已在 env.js 中定义
// DOM元素
const articleTextInput = document.getElementById('articleText');
const emojiStyleSelect = document.getElementById('emojiStyle');
const markdownOptionSelect = document.getElementById('markdownOption');
const formatBtn = document.getElementById('formatBtn');
const loadingDiv = document.getElementById('loading');
const resultContainer = document.getElementById('resultContainer');
const previewSection = document.getElementById('previewSection');
const markdownPreview = document.getElementById('markdownPreview');
const rawSection = document.getElementById('rawSection');
const markdownRaw = document.getElementById('markdownRaw');
const copyMdBtn = document.getElementById('copyMdBtn');
const copyHtmlBtn = document.getElementById('copyHtmlBtn');
// 加载器控制
function showLoading(show) {
loadingDiv.style.display = show ? 'block' : 'none';
formatBtn.disabled = show;
}
// 错误提示
function showErrorMessage(msg) {
resultContainer.innerHTML = `<div class="placeholder">${msg}</div>`;
}
// 调用后端API
async function callBackendAPI(articleText, emojiStyle, markdownOption) {
try {
const token = window.AUTH_CONFIG.getToken();
if (!token) throw new Error('未登录请先登录后使用AI功能');
const url = `${window.API_CONFIG.baseUrl}${window.API_CONFIG.endpoints.markdownFormatting}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
article_text: articleText,
emoji_style: emojiStyle,
markdown_option: markdownOption
})
});
if (!response.ok) {
if (response.status === 402) throw new Error('您的萌芽币余额不足,无法使用此功能');
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `API请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.success && data.formatted_markdown) return data.formatted_markdown;
throw new Error(data.error || 'API响应格式异常');
} catch (error) {
console.error('API调用错误:', error);
throw error;
}
}
// 显示结果
function displayFormattingResult(markdownText) {
// 源Markdown
markdownRaw.textContent = markdownText || '';
rawSection.style.display = markdownText ? 'block' : 'none';
// 预览渲染使用marked + DOMPurify
let html = '';
try {
// 兼容新旧版本的marked库
if (typeof marked === 'function') {
// 旧版本marked直接调用
html = marked(markdownText || '');
} else if (marked && typeof marked.parse === 'function') {
// 新版本marked使用parse方法
html = marked.parse(markdownText || '');
} else {
throw new Error('marked库未正确加载');
}
// 使用DOMPurify清理HTML如果可用
const safeHtml = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html) : html;
markdownPreview.innerHTML = safeHtml;
} catch (error) {
console.error('Markdown渲染失败:', error);
markdownPreview.innerHTML = `<div class="error">Markdown渲染失败: ${error.message}</div>`;
}
previewSection.style.display = markdownText ? 'block' : 'none';
// 顶部结果容器状态
resultContainer.innerHTML = '';
resultContainer.classList.add('conversion-result');
}
// 复制功能
function copyToClipboard(text) {
try {
navigator.clipboard.writeText(text);
} catch (e) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
}
copyMdBtn.addEventListener('click', () => copyToClipboard(markdownRaw.textContent || ''));
copyHtmlBtn.addEventListener('click', () => copyToClipboard(markdownPreview.innerHTML || ''));
// 执行排版
async function performFormatting() {
const articleText = articleTextInput.value.trim();
const emojiStyle = emojiStyleSelect.value;
const markdownOption = markdownOptionSelect.value;
if (!articleText) {
showErrorMessage('请输入需要排版的文章内容');
return;
}
showLoading(true);
resultContainer.innerHTML = '';
previewSection.style.display = 'none';
rawSection.style.display = 'none';
try {
const markdown = await callBackendAPI(articleText, emojiStyle, markdownOption);
displayFormattingResult(markdown);
} catch (error) {
console.error('排版失败:', error);
showErrorMessage(`排版失败: ${error.message}`);
} finally {
showLoading(false);
}
}
// 事件绑定
formatBtn.addEventListener('click', performFormatting);
// 页面初始化
document.addEventListener('DOMContentLoaded', () => {
resultContainer.innerHTML = '<div class="placeholder">请输入文章内容选择Emoji风格与排版偏好然后点击开始排版</div>';
});
// 导出函数供HTML调用
window.performFormatting = performFormatting;
window.copyToClipboard = copyToClipboard;

View File

@@ -0,0 +1,84 @@
/* 全局样式重置 */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* 主体样式 - 清新渐变 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
min-height: 100vh;
padding: 20px;
color: #2e7d32;
line-height: 1.47;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 容器样式 - 毛玻璃效果 */
.container {
max-width: 900px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.85);
border-radius: 24px;
padding: 32px;
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.15), 0 2px 8px rgba(76, 175, 80, 0.1);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(76, 175, 80, 0.2);
}
/* 头部样式 */
.header { text-align: center; margin-bottom: 32px; }
.title { font-size: 2.25rem; color: #1b5e20; margin-bottom: 8px; font-weight: 600; letter-spacing: -0.02em; }
.subtitle { color: #4caf50; font-size: 1.0625rem; margin-bottom: 24px; font-weight: 400; }
/* 表单区域 */
.form-section { margin-bottom: 32px; }
.form-group { margin-bottom: 24px; }
.form-row { display: flex; gap: 16px; margin-bottom: 24px; }
.half-width { flex: 1; }
.form-label { display: block; margin-bottom: 8px; font-weight: 600; color: #2e7d32; }
.form-input { width: 100%; border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 12px 14px; outline: none; background: rgba(255, 255, 255, 0.75); color: #1b5e20; font-size: 1rem; transition: all 0.2s ease; }
.form-input:focus { border-color: rgba(76, 175, 80, 0.4); box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15); }
.textarea { min-height: 160px; resize: vertical; line-height: 1.6; }
.select { appearance: none; background-image: linear-gradient(135deg, #f1f8e9 0%, #e8f5e9 100%); }
/* 操作按钮 */
.btn { width: 100%; padding: 14px 18px; border: none; border-radius: 14px; font-weight: 600; font-size: 1.0625rem; color: #fff; background: linear-gradient(135deg, #43a047 0%, #66bb6a 50%, #81c784 100%); box-shadow: 0 4px 16px rgba(76, 175, 80, 0.3), 0 2px 8px rgba(76, 175, 80, 0.2); cursor: pointer; transition: all 0.2s ease; }
.btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(76, 175, 80, 0.35); }
.btn:active { transform: translateY(0); background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; background: #86868B; }
/* 结果区域 */
.result-section { margin-top: 32px; }
.result-title { font-size: 1.25rem; color: #1b5e20; margin-bottom: 16px; text-align: center; font-weight: 600; }
.loading { display: none; text-align: center; color: #4caf50; padding: 24px; font-weight: 500; }
.conversion-container { background: rgba(255, 255, 255, 0.6); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 16px; padding: 24px; min-height: 140px; backdrop-filter: blur(10px); }
.placeholder { text-align: center; color: #86868B; padding: 32px 20px; font-weight: 400; }
.placeholder.error { color: #d32f2f; background: rgba(244, 67, 54, 0.1); border: 1px solid rgba(244, 67, 54, 0.2); border-radius: 8px; }
.error { color: #d32f2f; background: rgba(244, 67, 54, 0.1); padding: 12px; border-radius: 8px; border: 1px solid rgba(244, 67, 54, 0.2); }
/* 预览与原文区域 */
.preview-section, .raw-section { margin-top: 24px; }
.preview-header, .raw-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.preview-header .label, .raw-header .label { font-weight: 600; color: #2e7d32; font-size: 1rem; }
.copy-btn { padding: 6px 10px; border: none; border-radius: 10px; font-weight: 600; font-size: 0.9375rem; color: #fff; background: linear-gradient(135deg, #4caf50 0%, #81c784 100%); box-shadow: 0 2px 8px rgba(76, 175, 80, 0.25); cursor: pointer; }
.copy-btn:hover { filter: brightness(1.05); }
.markdown-preview { background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 12px; padding: 20px; color: #2e7d32; line-height: 1.8; }
.markdown-raw { background: rgba(255, 255, 255, 0.85); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 12px; padding: 16px; color: #1b5e20; font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace; font-size: 0.9375rem; white-space: pre-wrap; word-break: break-word; }
/* Markdown渲染细节 */
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3 { color: #1b5e20; margin: 10px 0; }
.markdown-preview p { margin: 10px 0; }
.markdown-preview ul, .markdown-preview ol { padding-left: 24px; margin: 10px 0; }
.markdown-preview blockquote { border-left: 4px solid rgba(76, 175, 80, 0.4); padding-left: 12px; color: #4caf50; background: rgba(76, 175, 80, 0.08); border-radius: 6px; }
.markdown-preview code { background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 6px; font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace; }
/* 移动端优化 */
@media (max-width: 480px) {
.container { padding: 18px; border-radius: 18px; }
.title { font-size: 1.75rem; }
.subtitle { font-size: 0.95rem; }
.form-row { flex-direction: column; gap: 12px; }
.textarea { min-height: 200px; }
.btn { font-size: 1rem; padding: 12px 16px; }
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -323,9 +323,9 @@
<img class="loading-logo" src="%PUBLIC_URL%/assets/logo.png" alt="万象口袋" />
</div>
<div class="loading-text-container">
<div class="loading-text">万象口袋</div>
<strong class="loading-text">万象口袋</strong>
</div>
<div class="loading-desc">🎨 一个跨平台的多功能聚合应用(´。• ω •。`) 💬</div>
<strong class="loading-desc">🎨 一个跨平台的多功能聚合应用(´。• ω •。`) 💬</strong>
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>

View File

@@ -30,6 +30,122 @@ class Game2048 {
// 开始计时
this.startTimer();
}
// 依据分数计算权重0.1 ~ 0.95
calculateWeightByScore(score) {
const w = score / 4000; // 4000分约接近满权重
return Math.max(0.1, Math.min(0.95, w));
}
// 按权重偏向生成0~10的随机整数权重越高越偏向更大值
biasedRandomInt(maxInclusive, weight) {
const rand = Math.random();
const biased = Math.pow(rand, 1 - weight); // weight越大biased越接近1
const val = Math.floor(biased * (maxInclusive + 1));
return Math.max(0, Math.min(maxInclusive, val));
}
// 附加结束信息到界面
appendEndInfo(text, type = 'info') {
const message = document.getElementById('game-message');
if (!message) return;
const info = document.createElement('div');
info.style.marginTop = '10px';
info.style.fontSize = '16px';
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#776e65');
info.textContent = text;
message.appendChild(info);
}
// 游戏结束时尝试给当前登录账户加“萌芽币”
async tryAwardCoinsOnGameOver() {
try {
const token = localStorage.getItem('token');
if (!token) {
this.appendEndInfo('未登录,无法获得萌芽币');
return;
}
let email = null;
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const userObj = JSON.parse(userStr);
email = userObj && (userObj.email || userObj['邮箱']);
}
} catch (e) {
// 忽略解析错误
}
if (!email) {
this.appendEndInfo('未找到账户信息email无法加币', 'error');
return;
}
// 根据分数计算权重与概率
const weight = this.calculateWeightByScore(this.score);
let awardProbability = weight; // 默认用权重作为概率
let guaranteed = false;
// 分数≥500时必定触发奖励
if (this.score >= 500) {
awardProbability = 1;
guaranteed = true;
}
const roll = Math.random();
if (roll > awardProbability) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 生成0~10随机萌芽币数量权重越高越偏向更大值
let coins = this.biasedRandomInt(5, weight);
// 保底至少 1 个仅当分数≥500时
if (guaranteed) {
coins = Math.max(1, coins);
}
coins = Math.max(0, Math.min(10, coins));
if (coins <= 0) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 后端 API base URL从父窗口ENV_CONFIG获取回退到本地默认
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
? window.parent.ENV_CONFIG.API_URL
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ email, amount: coins })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
this.appendEndInfo(`加币失败:${msg}`, 'error');
return;
}
const data = await resp.json();
if (data && data.success) {
const newCoins = data.data && data.data.new_coins;
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
} else {
const msg = (data && (data.message || data.error)) || '未知错误';
this.appendEndInfo(`加币失败:${msg}`, 'error');
}
} catch (e) {
console.error('加币流程发生错误:', e);
this.appendEndInfo('加币失败:网络或系统错误', 'error');
}
}
initializeGrid() {
this.grid = [];
@@ -315,6 +431,16 @@ class Game2048 {
message.className = 'game-message game-won';
message.style.display = 'flex';
message.querySelector('p').textContent = '你赢了!';
// 胜利也尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 显示最终统计
setTimeout(() => {
if (window.gameStats) {
window.gameStats.showFinalStats();
}
}, 1000);
}
showGameOver() {
@@ -323,6 +449,16 @@ class Game2048 {
message.style.display = 'flex';
message.querySelector('p').textContent = '游戏结束!';
// 渲染排行榜
try {
this.renderLeaderboard();
} catch (e) {
console.error('渲染排行榜时发生错误:', e);
}
// 尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 显示最终统计
setTimeout(() => {
if (window.gameStats) {
@@ -377,6 +513,92 @@ class Game2048 {
}, 1000);
}
// 构建并渲染排行榜
renderLeaderboard() {
const container = document.getElementById('leaderboard');
if (!container) return;
// 生成当前玩家数据
const today = this.formatDate(new Date());
const currentPlayer = {
"名称": "我",
"账号": "guest-local",
"分数": this.score,
"时间": today,
_current: true
};
// 合并并排序数据(分数由高到低)
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
const merged = [...baseData.map(d => ({...d})), currentPlayer]
.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
// 计算当前玩家排名
const currentIndex = merged.findIndex(d => d._current);
const rank = currentIndex >= 0 ? currentIndex + 1 : '-';
// 仅展示前10条
const topN = merged.slice(0, 10);
// 生成 HTML
const summaryHtml = `
<div class="leaderboard-summary">
<span>本局分数:<strong>${this.score}</strong></span>
<span>用时:<strong>${this.stats.gameTime}</strong> 秒</span>
<span>你的排名:<strong>${rank}</strong></span>
</div>
`;
const headerHtml = `
<div class="leaderboard-header">
<div class="leaderboard-col rank">排名</div>
<div class="leaderboard-col name">名称</div>
<div class="leaderboard-col score">分数</div>
<div class="leaderboard-col time">日期</div>
</div>
`;
const rowsHtml = topN.map((d, i) => {
const isCurrent = !!d._current;
const rowClass = `leaderboard-row${isCurrent ? ' current' : ''}`;
return `
<div class="${rowClass}">
<div class="leaderboard-col rank">${i + 1}</div>
<div class="leaderboard-col name">${this.escapeHtml(d["名称"] || '未知')}</div>
<div class="leaderboard-col score">${d["分数"] ?? 0}</div>
<div class="leaderboard-col time">${this.escapeHtml(d["时间"] || '-')}</div>
</div>
`;
}).join('');
container.innerHTML = `
<div class="leaderboard-title">排行榜</div>
${summaryHtml}
<div class="leaderboard-table">
${headerHtml}
<div class="leaderboard-body">${rowsHtml}</div>
</div>
`;
}
// 工具:日期格式化 YYYY-MM-DD
formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
// 工具:简单转义以避免 XSS
escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&#039;');
}
bindEvents() {
// 重试按钮
document.getElementById('retry-btn').addEventListener('click', () => {

View File

@@ -22,6 +22,8 @@
<div class="game-container">
<div class="game-message" id="game-message">
<p></p>
<!-- 排行榜容器:游戏结束后动态填充 -->
<div id="leaderboard" class="leaderboard" aria-live="polite"></div>
<div class="lower">
<a class="retry-button" id="retry-btn">重新开始</a>
</div>
@@ -64,6 +66,7 @@
<script src="gamedata.js"></script>
<script src="game-logic.js"></script>
<script src="controls.js"></script>
</body>

View File

@@ -237,6 +237,91 @@ body {
transition: all 0.3s ease;
}
/* 排行榜样式(与 2048 主题一致) */
.leaderboard {
width: 100%;
max-width: 440px;
background: rgba(250, 248, 239, 0.95); /* #faf8ef */
border-radius: 12px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
color: #776e65;
}
.leaderboard-title {
font-size: 22px;
font-weight: 700;
color: #8f7a66;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.leaderboard-summary {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 14px;
color: #8f7a66;
margin-bottom: 10px;
}
.leaderboard-summary strong {
color: #8f7a66;
}
.leaderboard-table {
border: 1px solid rgba(187, 173, 160, 0.3); /* #bbada0 */
border-radius: 10px;
overflow: hidden;
background: rgba(238, 228, 218, 0.4); /* #eee4da */
}
.leaderboard-header,
.leaderboard-row {
display: grid;
grid-template-columns: 64px 1fr 90px 120px; /* 排名/名称/分数/日期 */
align-items: center;
}
.leaderboard-header {
background: #eee4da;
color: #776e65;
font-weight: 700;
padding: 8px 10px;
border-bottom: 1px solid rgba(187, 173, 160, 0.3);
}
.leaderboard-body {
max-height: 220px;
overflow-y: auto;
background: rgba(238, 228, 218, 0.25);
}
.leaderboard-row {
padding: 8px 10px;
border-top: 1px solid rgba(187, 173, 160, 0.15);
}
.leaderboard-row:nth-child(odd) {
background: rgba(238, 228, 218, 0.22);
}
.leaderboard-row.current {
background: #f3e9d4;
box-shadow: inset 0 0 0 2px rgba(143, 122, 102, 0.35);
}
.leaderboard-col.rank {
text-align: center;
font-weight: 700;
color: #8f7a66;
}
.leaderboard-col.score {
text-align: right;
font-weight: 700;
}
.leaderboard-col.time {
text-align: right;
color: #776e65;
}
.retry-button:hover {
background: #9f8a76;
transform: translateY(-2px);

View File

@@ -1,338 +1,96 @@
// 游戏统计和成就系统
class GameStats {
constructor() {
this.achievements = [
{
id: 'first_game',
name: '初次体验',
description: '完成第一次游戏',
condition: (stats) => true
},
{
id: 'score_1000',
name: '小试牛刀',
description: '单局得分达到1000分',
condition: (stats) => stats.score >= 1000
},
{
id: 'score_5000',
name: '游戏达人',
description: '单局得分达到5000分',
condition: (stats) => stats.score >= 5000
},
{
id: 'score_10000',
name: '方块大师',
description: '单局得分达到10000分',
condition: (stats) => stats.score >= 10000
},
{
id: 'level_5',
name: '步步高升',
description: '达到第5级',
condition: (stats) => stats.level >= 5
},
{
id: 'level_10',
name: '速度之王',
description: '达到第10级',
condition: (stats) => stats.level >= 10
},
{
id: 'lines_50',
name: '消除专家',
description: '累计消除50行',
condition: (stats) => stats.lines >= 50
},
{
id: 'lines_100',
name: '清理大师',
description: '累计消除100行',
condition: (stats) => stats.lines >= 100
},
{
id: 'tetris',
name: 'Tetris!',
description: '一次消除4行',
condition: (stats) => stats.maxCombo >= 4
},
{
id: 'time_10min',
name: '持久战士',
description: '单局游戏时间超过10分钟',
condition: (stats) => stats.playTime >= 600000
},
{
id: 'efficiency',
name: '效率专家',
description: '平均每分钟得分超过500',
condition: (stats) => stats.avgScore >= 500
}
];
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
const playAgainBtn = document.getElementById('playAgainBtn');
playAgainBtn.addEventListener('click', () => {
this.hideStats();
game.restart();
});
}
showStats(gameData) {
const playTimeMinutes = gameData.playTime / 60000;
const avgScore = playTimeMinutes > 0 ? Math.round(gameData.score / playTimeMinutes) : 0;
const stats = {
...gameData,
avgScore: avgScore
};
// 更新统计显示
document.getElementById('finalScore').textContent = stats.score.toLocaleString();
document.getElementById('finalLevel').textContent = stats.level;
document.getElementById('finalLines').textContent = stats.lines;
document.getElementById('playTime').textContent = this.formatTime(stats.playTime);
document.getElementById('maxCombo').textContent = stats.maxCombo;
document.getElementById('avgScore').textContent = stats.avgScore;
// 检查成就
const achievement = this.checkAchievements(stats);
this.displayAchievement(achievement);
// 显示统计界面
document.getElementById('gameStats').style.display = 'flex';
document.getElementById('gameStats').classList.add('fade-in');
}
hideStats() {
document.getElementById('gameStats').style.display = 'none';
document.getElementById('gameStats').classList.remove('fade-in');
}
formatTime(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
checkAchievements(stats) {
// 获取已获得的成就
const earnedAchievements = this.getEarnedAchievements();
// 检查新成就
for (let achievement of this.achievements) {
if (!earnedAchievements.includes(achievement.id) &&
achievement.condition(stats)) {
// 保存新成就
this.saveAchievement(achievement.id);
return achievement;
}
}
return null;
}
displayAchievement(achievement) {
const achievementEl = document.getElementById('achievement');
if (achievement) {
achievementEl.innerHTML = `
🏆 <strong>成就解锁!</strong><br>
<strong>${achievement.name}</strong><br>
${achievement.description}
`;
achievementEl.classList.add('pulse');
} else {
// 显示随机鼓励话语
const encouragements = [
'继续努力,你会变得更强!',
'每一次游戏都是进步的机会!',
'方块世界需要你的智慧!',
'熟能生巧,加油!',
'下一局一定会更好!',
'坚持就是胜利!',
'你的反应速度在提升!',
'策略思维正在增强!'
];
const randomEncouragement = encouragements[Math.floor(Math.random() * encouragements.length)];
achievementEl.innerHTML = `💪 ${randomEncouragement}`;
achievementEl.classList.remove('pulse');
}
}
getEarnedAchievements() {
const saved = localStorage.getItem('tetris_achievements');
return saved ? JSON.parse(saved) : [];
}
saveAchievement(achievementId) {
const earned = this.getEarnedAchievements();
if (!earned.includes(achievementId)) {
earned.push(achievementId);
localStorage.setItem('tetris_achievements', JSON.stringify(earned));
}
}
// 获取历史最佳记录
getBestStats() {
const saved = localStorage.getItem('tetris_best_stats');
return saved ? JSON.parse(saved) : {
score: 0,
level: 0,
lines: 0,
maxCombo: 0
};
}
// 保存最佳记录
saveBestStats(stats) {
const best = this.getBestStats();
let updated = false;
if (stats.score > best.score) {
best.score = stats.score;
updated = true;
}
if (stats.level > best.level) {
best.level = stats.level;
updated = true;
}
if (stats.lines > best.lines) {
best.lines = stats.lines;
updated = true;
}
if (stats.maxCombo > best.maxCombo) {
best.maxCombo = stats.maxCombo;
updated = true;
}
if (updated) {
localStorage.setItem('tetris_best_stats', JSON.stringify(best));
}
return updated;
}
// 显示排行榜
showLeaderboard() {
const best = this.getBestStats();
const earned = this.getEarnedAchievements();
console.log('最佳记录:', best);
console.log('已获得成就:', earned.length + '/' + this.achievements.length);
}
}
// 游戏结束排行榜展示
const gameStats = {
showStats({ score, playTime }) {
// 将毫秒转为 mm:ss
const formatDuration = (ms) => {
const totalSec = Math.max(0, Math.floor(ms / 1000));
const m = String(Math.floor(totalSec / 60)).padStart(2, '0');
const s = String(totalSec % 60).padStart(2, '0');
return `${m}:${s}`;
};
// 高级特效系统
class GameEffects {
constructor(game) {
this.game = game;
this.particles = [];
this.effects = [];
this.init();
}
init() {
// 创建特效canvas
this.effectsCanvas = document.createElement('canvas');
this.effectsCanvas.width = this.game.canvas.width;
this.effectsCanvas.height = this.game.canvas.height;
this.effectsCanvas.style.position = 'absolute';
this.effectsCanvas.style.top = '0';
this.effectsCanvas.style.left = '0';
this.effectsCanvas.style.pointerEvents = 'none';
this.effectsCanvas.style.zIndex = '10';
this.effectsCtx = this.effectsCanvas.getContext('2d');
// 将特效canvas添加到游戏板容器中
this.game.canvas.parentElement.style.position = 'relative';
this.game.canvas.parentElement.appendChild(this.effectsCanvas);
}
// 行消除特效
lineCleared(row) {
for (let i = 0; i < 20; i++) {
this.particles.push({
x: Math.random() * this.game.canvas.width,
y: row * this.game.CELL_SIZE + this.game.CELL_SIZE / 2,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
life: 1,
decay: 0.02,
color: `hsl(${Math.random() * 360}, 100%, 50%)`
});
}
}
// 方块锁定特效
pieceLocked(piece) {
const centerX = (piece.x + piece.matrix[0].length / 2) * this.game.CELL_SIZE;
const centerY = (piece.y + piece.matrix.length / 2) * this.game.CELL_SIZE;
for (let i = 0; i < 10; i++) {
this.particles.push({
x: centerX,
y: centerY,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
life: 0.8,
decay: 0.03,
color: piece.color
});
}
}
// 更新特效
update() {
// 更新粒子
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
particle.x += particle.vx;
particle.y += particle.vy;
particle.life -= particle.decay;
if (particle.life <= 0) {
this.particles.splice(i, 1);
}
}
}
// 绘制特效
draw() {
this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height);
// 绘制粒子
for (let particle of this.particles) {
this.effectsCtx.save();
this.effectsCtx.globalAlpha = particle.life;
this.effectsCtx.fillStyle = particle.color;
this.effectsCtx.beginPath();
this.effectsCtx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
this.effectsCtx.fill();
this.effectsCtx.restore();
}
}
}
// 构造排行榜数据(模拟),将当前成绩与 gamedata.js 合并
const todayStr = (() => {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
})();
// 创建统计系统实例
const gameStats = new GameStats();
// 当前玩家信息(可根据实际项目替换为真实用户)
const currentEntry = {
名称: localStorage.getItem('tetris_player_name') || '我',
账号: localStorage.getItem('tetris_player_account') || 'guest@local',
分数: score,
时间: formatDuration(playTime), // 排行榜展示“游戏时长”
isCurrent: true,
};
// 在适当的地方创建特效系统
// const gameEffects = new GameEffects(game);
// 注意:在浏览器中,使用 const 声明的全局变量不会挂载到 window 上
// 因此这里直接使用 playerdata而不是 window.playerdata
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
// 为基础数据模拟“游戏时长”mm:ss以满足展示需求
const simulateDuration = (scoreVal) => {
const sec = Math.max(30, Math.min(30 * 60, Math.round((Number(scoreVal) || 0) * 1.2)));
return formatDuration(sec * 1000);
};
const merged = [...baseData.map((d) => ({
...d,
// 使用已有分数推导一个模拟时长
时间: simulateDuration(d.分数),
isCurrent: false,
})), currentEntry]
.sort((a, b) => (b.分数 || 0) - (a.分数 || 0));
// 3) 渲染排行榜取前10
const tbody = document.getElementById('leaderboardBody');
tbody.innerHTML = '';
const topN = merged.slice(0, 10);
topN.forEach((item, idx) => {
const tr = document.createElement('tr');
if (item.isCurrent) {
tr.classList.add('current-row');
}
const rankCell = document.createElement('td');
const nameCell = document.createElement('td');
const scoreCell = document.createElement('td');
const timeCell = document.createElement('td');
const rankBadge = document.createElement('span');
rankBadge.className = 'rank-badge';
rankBadge.textContent = String(idx + 1);
rankCell.appendChild(rankBadge);
nameCell.textContent = item.名称 || '未知';
scoreCell.textContent = item.分数 || 0;
timeCell.textContent = item.时间 || formatDuration(playTime);
tr.appendChild(rankCell);
tr.appendChild(nameCell);
tr.appendChild(scoreCell);
tr.appendChild(timeCell);
tbody.appendChild(tr);
});
// 4) 展示排行榜界面
const statsEl = document.getElementById('gameStats');
statsEl.style.display = 'flex';
// 5) 再玩一次按钮
const playAgainBtn = document.getElementById('playAgainBtn');
if (playAgainBtn) {
playAgainBtn.onclick = () => {
statsEl.style.display = 'none';
if (window.game && typeof window.game.restart === 'function') {
window.game.restart();
}
};
}
},
};
// 暴露到全局
window.gameStats = gameStats;

View File

@@ -61,40 +61,32 @@
<!-- 游戏结束统计界面 -->
<div class="game-stats" id="gameStats">
<div class="stats-content">
<h2>游戏结束</h2>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">最终分数</span>
<span class="stat-value" id="finalScore">0</span>
</div>
<div class="stat-item">
<span class="stat-label">达到等级</span>
<span class="stat-value" id="finalLevel">1</span>
</div>
<div class="stat-item">
<span class="stat-label">消除行数</span>
<span class="stat-value" id="finalLines">0</span>
</div>
<div class="stat-item">
<span class="stat-label">游戏时长</span>
<span class="stat-value" id="playTime">00:00</span>
</div>
<div class="stat-item">
<span class="stat-label">单次消除最大行数</span>
<span class="stat-value" id="maxCombo">0</span>
</div>
<div class="stat-item">
<span class="stat-label">平均每分钟分数</span>
<span class="stat-value" id="avgScore">0</span>
<h2>游戏结束排行榜</h2>
<!-- 排行榜 -->
<div class="leaderboard" id="leaderboard">
<div class="leaderboard-title">本局排行榜</div>
<div class="leaderboard-wrap">
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>名称</th>
<th>分数</th>
<th>游戏时长</th>
</tr>
</thead>
<tbody id="leaderboardBody"></tbody>
</table>
</div>
<div class="leaderboard-tip">仅显示前10名“游戏时长”为模拟数据已与您的成绩合并</div>
</div>
<div class="achievement" id="achievement"></div>
<button class="game-btn" id="playAgainBtn">再玩一次</button>
</div>
</div>
<script src="tetris.js"></script>
<script src="game-controls.js"></script>
<script src="gamedata.js"></script>
<script src="game-stats.js"></script>
</body>
</html>

View File

@@ -319,6 +319,84 @@ body {
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.3);
}
/* 排行榜样式 */
.leaderboard {
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
color: #2e7d32;
border: 1px solid rgba(46, 125, 50, 0.3);
border-radius: 16px;
box-shadow: 0 6px 18px rgba(46, 125, 50, 0.25);
padding: 16px;
margin-bottom: 20px;
}
.leaderboard-title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 12px;
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 50%, #cddc39 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.leaderboard-wrap {
max-height: 260px;
overflow: auto;
border-radius: 12px;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table thead tr {
background: linear-gradient(135deg, #66bb6a 0%, #8bc34a 100%);
color: #fff;
}
.leaderboard-table th,
.leaderboard-table td {
text-align: left;
padding: 10px 12px;
border-bottom: 1px solid rgba(46, 125, 50, 0.15);
font-size: 0.95rem;
}
.leaderboard-table tbody tr {
background: linear-gradient(135deg, rgba(46,125,50,0.08) 0%, rgba(46,125,50,0.03) 100%);
transition: background 0.2s ease, transform 0.2s ease;
}
.leaderboard-table tbody tr:hover {
background: linear-gradient(135deg, rgba(46,125,50,0.12) 0%, rgba(46,125,50,0.06) 100%);
transform: translateY(-1px);
}
.rank-badge {
display: inline-block;
min-width: 32px;
text-align: center;
padding: 4px 8px;
border-radius: 12px;
background: linear-gradient(45deg, #66bb6a, #8bc34a);
color: #fff;
font-weight: 700;
}
.current-row {
outline: 2px solid rgba(76, 175, 80, 0.7);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15) inset;
}
.leaderboard-tip {
margin-top: 10px;
font-size: 0.85rem;
color: #388e3c;
opacity: 0.85;
}
/* 响应式设计 */
@media (max-width: 768px) {
.game-container {
@@ -378,6 +456,15 @@ body {
padding: 20px;
width: 95%;
}
.leaderboard-wrap {
max-height: 200px;
}
.leaderboard-table th,
.leaderboard-table td {
padding: 8px 10px;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
@@ -449,3 +536,39 @@ body {
.pulse {
animation: pulse 2s infinite;
}
/* 摘要卡片 */
.leaderboard-summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.summary-item {
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
color: #fff;
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(46, 125, 50, 0.3);
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
}
.summary-label {
display: block;
font-size: 0.9rem;
opacity: 0.9;
}
.summary-value {
display: block;
font-size: 1.3rem;
font-weight: 700;
margin-top: 4px;
}
@media (max-width: 768px) {
.leaderboard-summary {
grid-template-columns: 1fr;
gap: 10px;
}
}

View File

@@ -84,6 +84,8 @@ function gameOver(){
// 显示最终得分和达到的最高速度
document.getElementById('final-score-value').innerHTML = myScore;
document.getElementById('final-speed-value').innerHTML = gameSpeed.toFixed(1);
// 渲染排行榜
renderLeaderboard();
// 显示游戏结束弹窗
document.getElementById('game-over-modal').style.display = 'flex';
@@ -250,6 +252,78 @@ function handleClick(e) {
checkHit(x, y);
}
// ===== 排行榜逻辑 =====
function formatDateYYYYMMDD() {
var d = new Date();
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderLeaderboard(){
var nowStr = formatDateYYYYMMDD();
// 当前玩家数据(模拟)
var me = {
"名称": "我",
"账号": "guest",
"分数": myScore,
"时间": nowStr,
"__isMe": true
};
// 合并现有数据与当前玩家
var data = (typeof playerdata !== 'undefined' && Array.isArray(playerdata))
? playerdata.slice() : [];
data.push(me);
// 按分数降序排序
data.sort(function(a, b){
return (b["分数"] || 0) - (a["分数"] || 0);
});
var tbody = document.getElementById('leaderboard-body');
if (!tbody) return;
tbody.innerHTML = '';
var myRank = -1;
for (var i = 0; i < data.length; i++){
var row = data[i];
var tr = document.createElement('tr');
if (row.__isMe){
myRank = i + 1;
tr.className = 'leaderboard-row-me';
}
tr.innerHTML =
'<td>' + (i + 1) + '</td>' +
'<td>' + escapeHtml(row["名称"] || '') + '</td>' +
'<td>' + (row["分数"] || 0) + '</td>' +
'<td>' + escapeHtml(row["时间"] || '') + '</td>';
// 只展示前10名
if (i < 10) tbody.appendChild(tr);
}
// 更新我的数据摘要
var rankEl = document.getElementById('my-rank');
var scoreEl = document.getElementById('my-score');
var timeEl = document.getElementById('my-time');
if (rankEl) rankEl.textContent = myRank > 0 ? myRank : '-';
if (scoreEl) scoreEl.textContent = myScore;
if (timeEl) timeEl.textContent = nowStr;
}
// 处理触摸事件
function handleTouch(e) {

View File

@@ -182,6 +182,56 @@
background: linear-gradient(45deg, #4caf50, #388e3c);
box-shadow: 0 6px 16px rgba(76,175,80,0.4);
}
/* 排行榜样式 */
.leaderboard {
margin-top: 15px;
background: rgba(255,255,255,0.6);
border: 1px solid rgba(129,199,132,0.3);
border-radius: 10px;
overflow: hidden;
}
.leaderboard-title {
background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
font-weight: bold;
font-size: 16px;
padding: 8px 12px;
text-align: left;
box-shadow: inset 0 -1px 0 rgba(255,255,255,0.2);
}
.leaderboard-meta {
color: #2e7d32;
font-size: 13px;
padding: 8px 12px;
border-bottom: 1px solid rgba(129,199,132,0.2);
display: flex;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table th, .leaderboard-table td {
padding: 8px 6px;
font-size: 13px;
border-bottom: 1px solid rgba(129,199,132,0.2);
color: #1b5e20;
text-align: center;
}
.leaderboard-table th {
background: rgba(129,199,132,0.2);
font-weight: bold;
color: #1b5e20;
}
.leaderboard-row-me {
background: rgba(198,40,40,0.08);
border-left: 3px solid #c62828;
}
.leaderboard-table tr:nth-child(even) {
background: rgba(129,199,132,0.1);
}
/* 移动端适配 */
@media (max-width: 768px) {
@@ -253,11 +303,32 @@
<h2 class="modal-title">游戏结束</h2>
<div class="final-score">最终得分: <span id="final-score-value">0</span></div>
<div class="final-speed">最高速度: <span id="final-speed-value">1.0</span>x</div>
<!-- 排行榜区域 -->
<div class="leaderboard">
<div class="leaderboard-title">排行榜</div>
<div class="leaderboard-meta">
<span>我的排名:第 <span id="my-rank">-</span></span>
<span>我的分数:<span id="my-score">0</span></span>
<span>时间:<span id="my-time">--</span></span>
</div>
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>名称</th>
<th>分数</th>
<th>时间</th>
</tr>
</thead>
<tbody id="leaderboard-body"></tbody>
</table>
</div>
<button id="restart-btn" class="modal-btn restart-btn">重新开始</button>
</div>
</div>
<audio id="music" src="MUSIC.mp3" loop></audio>
<script src="gamedata.js"></script>
<script src="game.js"></script>
</body>
</html>

View File

@@ -0,0 +1,309 @@
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('scoreVal');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');
const startOverlay = document.getElementById('startOverlay');
const startBtn = document.getElementById('startBtn');
const overOverlay = document.getElementById('overOverlay');
const againBtn = document.getElementById('againBtn');
const finalScoreEl = document.getElementById('finalScore');
let width = 0, height = 0;
let running = false, paused = false, gameOver = false;
let player, bullets = [], enemies = [], particles = [];
let score = 0, elapsed = 0, spawnTimer = 0, fireTimer = 0;
function fitCanvas(){
const w = canvas.clientWidth | 0;
const h = canvas.clientHeight | 0;
if (canvas.width !== w || canvas.height !== h){
canvas.width = w;
canvas.height = h;
}
width = canvas.width; height = canvas.height;
}
function clamp(v,min,max){ return v < min ? min : (v > max ? max : v); }
function rand(min,max){ return Math.random()*(max-min)+min; }
function initGame(){
fitCanvas();
score = 0;
elapsed = 0;
spawnTimer = 0;
fireTimer = 0;
bullets.length = 0;
enemies.length = 0;
particles.length = 0;
gameOver = false;
paused = false;
player = {
x: width/2,
y: height*0.82,
r: Math.max(14, Math.min(width,height)*0.02),
speed: Math.max(350, Math.min(width,height)*0.9),
alive: true
};
scoreEl.textContent = '0';
pauseBtn.textContent = '暂停';
}
function startGame(){
running = true;
startOverlay.classList.add('hide');
overOverlay.classList.add('hide');
initGame();
requestAnimationFrame(loop);
}
function restartGame(){
startOverlay.classList.add('hide');
startGame();
}
pauseBtn.addEventListener('click', ()=>{
if (!running) return;
paused = !paused;
pauseBtn.textContent = paused ? '继续' : '暂停';
});
restartBtn.addEventListener('click', ()=>{ initGame(); });
startBtn.addEventListener('click', startGame);
againBtn.addEventListener('click', ()=>{ startOverlay.classList.add('hide'); startGame(); });
window.addEventListener('resize', fitCanvas);
let pointerActive = false;
canvas.addEventListener('pointerdown', (e)=>{
pointerActive = true;
if (!running) startGame();
movePlayer(e);
canvas.setPointerCapture && canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove', (e)=>{ if (pointerActive) movePlayer(e); });
canvas.addEventListener('pointerup', ()=>{ pointerActive = false; });
function movePlayer(e){
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left);
const y = (e.clientY - rect.top);
const minY = height * 0.45;
player.x = clamp(x, player.r, width - player.r);
player.y = clamp(y, minY, height - player.r);
}
function spawnEnemy(){
const d = Math.min(6, 1 + elapsed/10);
let r, x, speed, hp, color, type;
const roll = Math.random();
if (roll < 0.5 - Math.min(0.2, elapsed*0.02)) { // 普通
type = 'normal';
r = rand(12, 18 + d*1.8);
x = rand(r, width - r);
speed = rand(60 + d*20, 110 + d*30);
hp = 1; color = 'rgba(70,160,80,0.9)';
enemies.push({x, y: -r, r, speed, hp, color, type});
} else if (roll < 0.75) { // 快速
type = 'fast';
r = rand(10, 14 + d);
x = rand(r, width - r);
speed = rand(130 + d*35, 220 + d*40);
hp = 1; color = 'rgba(120,200,90,0.95)';
enemies.push({x, y: -r, r, speed, hp, color, type});
} else if (roll < 0.92) { // 之字形
type = 'zigzag';
r = rand(12, 18 + d*1.5);
x = rand(r, width - r);
speed = rand(90 + d*20, 140 + d*25);
hp = 1; color = 'rgba(90,180,110,0.95)';
const vxAmp = rand(40, 80);
const freq = rand(2, 4);
const phase = rand(0, Math.PI*2);
enemies.push({x, y: -r, r, speed, hp, color, type, vxAmp, freq, phase});
} else if (roll < 0.98) { // 坦克型(耐久)
type = 'tough';
r = rand(20, 26 + d);
x = rand(r, width - r);
speed = rand(60, 100 + d*10);
hp = 3; color = 'rgba(50,140,70,0.9)';
enemies.push({x, y: -r, r, speed, hp, color, type});
} else { // 分裂型
type = 'splitter';
r = rand(22, 28 + d);
x = rand(r, width - r);
speed = rand(70 + d*15, 100 + d*20);
hp = 2; color = 'rgba(80,170,90,0.95)';
enemies.push({x, y: -r, r, speed, hp, color, type});
}
}
function spawnChildren(parent){
const count = 2;
for (let k=0; k<count; k++){
const r = Math.max(8, parent.r*0.45);
const x = clamp(parent.x + rand(-r, r), r, width - r);
const speed = rand(120, 180);
const vx = rand(-60, 60);
enemies.push({ x, y: parent.y + 6, r, speed, hp: 1, color: 'rgba(140,220,110,0.95)', type: 'mini', vx });
}
}
function fireBullet(){
const br = Math.max(3, player.r*0.22);
bullets.push({x: player.x, y: player.y - player.r - br, r: br, vy: -420});
}
function update(dt){
if (!running || paused || gameOver) return;
elapsed += dt;
// difficulty & spawn interval decreases over time
const interval = Math.max(0.16, 0.72 - elapsed*0.018);
spawnTimer -= dt;
if (spawnTimer <= 0){ spawnEnemy(); spawnTimer = interval; }
// auto fire
const fireInterval = Math.max(0.08, 0.14 - elapsed*0.002);
fireTimer -= dt;
if (fireTimer <= 0){ fireBullet(); fireTimer = fireInterval; }
// bullets
for (let i=bullets.length-1; i>=0; i--){
const b = bullets[i];
b.y += b.vy * dt;
if (b.y + b.r < 0){ bullets.splice(i,1); }
}
// enemies
const speedBoost = Math.min(2.2, 1 + elapsed*0.015);
for (let i=enemies.length-1; i>=0; i--){
const e = enemies[i];
// 不同类型的移动方式
if (e.type === 'zigzag'){
e.y += e.speed * speedBoost * dt;
e.phase += (e.freq || 3) * dt;
e.x += Math.sin(e.phase) * (e.vxAmp || 60) * dt;
e.x = clamp(e.x, e.r, width - e.r);
} else if (e.type === 'mini'){
e.y += e.speed * speedBoost * dt;
e.x += (e.vx || 0) * dt;
e.x = clamp(e.x, e.r, width - e.r);
} else {
e.y += e.speed * speedBoost * dt;
}
// 与玩家碰撞
const dx = e.x - player.x, dy = e.y - player.y;
const rr = e.r + player.r;
if (dx*dx + dy*dy < rr*rr){ endGame(); break; }
if (e.y - e.r > height){ enemies.splice(i,1); }
}
// bullet-enemy collisions
for (let i=enemies.length-1; i>=0; i--){
const e = enemies[i];
for (let j=bullets.length-1; j>=0; j--){
const b = bullets[j];
const dx = e.x - b.x, dy = e.y - b.y;
const rr = e.r + b.r;
if (dx*dx + dy*dy <= rr*rr){
bullets.splice(j,1);
e.hp -= 1;
addBurst(e.x, e.y, e.r);
if (e.hp <= 0){
if (e.type === 'splitter'){ spawnChildren(e); }
enemies.splice(i,1);
score += (e.type === 'tough' ? 2 : 1);
scoreEl.textContent = score;
}
break;
}
}
}
// particles
for (let i=particles.length-1; i>=0; i--){
const p = particles[i];
p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt;
if (p.life <= 0) particles.splice(i,1);
}
}
function addBurst(x,y,r){
for (let i=0; i<6; i++){
const a = Math.random() * Math.PI * 2;
const speed = rand(40, 140);
particles.push({ x, y, vx: Math.cos(a)*speed, vy: Math.sin(a)*speed, life: rand(0.15, 0.4) });
}
}
function draw(){
fitCanvas();
ctx.clearRect(0,0,width,height);
// soft overlay for depth
const grd = ctx.createLinearGradient(0,0,0,height);
grd.addColorStop(0, 'rgba(255,255,255,0.0)');
grd.addColorStop(1, 'rgba(255,255,255,0.05)');
ctx.fillStyle = grd; ctx.fillRect(0,0,width,height);
// player
drawPlayer();
// bullets
ctx.fillStyle = 'rgba(80,180,90,0.9)';
for (let i=0; i<bullets.length; i++){
const b = bullets[i];
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill();
}
// enemies
for (let i=0; i<enemies.length; i++){
const e = enemies[i];
ctx.fillStyle = e.color; drawEnemy(e);
}
// particles
ctx.fillStyle = 'rgba(160,220,140,0.9)';
for (let i=0; i<particles.length; i++){
const p = particles[i];
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2); ctx.fill();
}
}
function drawPlayer(){
const x = player.x, y = player.y, r = player.r;
ctx.save(); ctx.translate(x, y);
ctx.fillStyle = 'rgba(60,150,80,0.95)';
ctx.strokeStyle = 'rgba(40,120,60,0.9)'; ctx.lineWidth = 2;
// body
ctx.beginPath();
ctx.moveTo(0, -r*1.2);
ctx.quadraticCurveTo(r*0.3, -r*0.4, r*0.25, r*0.3);
ctx.lineTo(0, r*1.1);
ctx.lineTo(-r*0.25, r*0.3);
ctx.quadraticCurveTo(-r*0.3, -r*0.4, 0, -r*1.2);
ctx.closePath(); ctx.fill(); ctx.stroke();
// wings
ctx.beginPath(); ctx.fillStyle = 'rgba(90,180,110,0.95)';
ctx.moveTo(-r*0.9, r*0.1);
ctx.lineTo(r*0.9, r*0.1);
ctx.lineTo(r*0.5, r*0.4);
ctx.lineTo(-r*0.5, r*0.4);
ctx.closePath(); ctx.fill();
ctx.restore();
}
function drawEnemy(e){
const r = e.r;
ctx.beginPath(); ctx.arc(e.x, e.y, r, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.22)';
ctx.beginPath(); ctx.arc(e.x - r*0.3, e.y - r*0.3, r*0.4, 0, Math.PI*2); ctx.fill();
}
function endGame(){
gameOver = true; running = false;
finalScoreEl.textContent = score;
overOverlay.classList.remove('hide');
}
let last = 0;
function loop(ts){
if (!last) last = ts;
const dt = Math.min(0.033, (ts - last) / 1000);
last = ts;
update(dt);
draw();
if (running) requestAnimationFrame(loop);
}
// 初始显示开始覆盖层
fitCanvas();

View File

@@ -0,0 +1,44 @@
<!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">
<meta name="theme-color" content="#d8f5c3">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>打飞机 · 清新休闲</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<header class="topbar">
<div class="brand">打飞机</div>
<div class="score">得分:<span id="scoreVal">0</span></div>
<div class="actions">
<button id="pauseBtn" aria-label="暂停">暂停</button>
<button id="restartBtn" aria-label="重来">重来</button>
</div>
</header>
<main class="game-wrap">
<canvas id="game" aria-label="打飞机游戏画布"></canvas>
<div id="startOverlay" class="overlay">
<div class="panel">
<h1>打飞机</h1>
<p>轻触屏幕开始,无尽模式。</p>
<p>操作:手指拖动战机移动。</p>
<button id="startBtn">开始游戏</button>
</div>
</div>
<div id="overOverlay" class="overlay hide">
<div class="panel">
<h2>游戏结束</h2>
<p>本次得分:<span id="finalScore">0</span></p>
<button id="againBtn">再来一局</button>
</div>
</div>
</main>
<script src="./game.js"></script>
</body>
</html>

View File

@@ -0,0 +1,65 @@
:root {
--header-h: 56px;
--bg-start: #d7f6d2;
--bg-end: #ecf7c8;
--accent: #6bb86f;
--accent2: #a5d67e;
--text: #274b2f;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
height: 100dvh;
width: 100vw;
color: var(--text);
background: linear-gradient(180deg, var(--bg-start), var(--bg-end));
font-family: system-ui, -apple-system, Segoe UI, Roboto, "Noto Sans SC", Arial, sans-serif;
-webkit-tap-highlight-color: transparent;
}
.topbar {
position: fixed; top:0; left:0; right:0; height: var(--header-h);
display: flex; align-items: center; justify-content: space-between;
padding: 0 12px;
background: rgba(255,255,255,0.35);
border-bottom: 1px solid rgba(0,0,0,0.06);
backdrop-filter: saturate(120%) blur(10px);
}
.brand { font-weight: 700; letter-spacing: .5px; }
.score { font-weight: 600; }
.actions button {
background: var(--accent2); border: none; color: #1e3c27;
border-radius: 999px; padding: 6px 12px; margin-left: 8px;
font-weight: 600; box-shadow: 0 2px 6px rgba(0,0,0,0.08);
}
.actions button:active { transform: translateY(1px); }
.game-wrap { position: absolute; inset: var(--header-h) 0 0 0; }
#game {
width: 100vw; height: calc(100dvh - var(--header-h));
display: block; touch-action: none; cursor: crosshair;
}
.overlay {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
background: rgba(240, 250, 236, 0.6);
backdrop-filter: blur(6px);
}
.overlay.hide { display: none; }
.panel {
background: rgba(255,255,255,0.75);
border: 1px solid rgba(0,0,0,0.05);
border-radius: 16px; padding: 16px;
width: min(420px, 92vw); text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.06);
}
.panel h1, .panel h2 { margin: 6px 0 8px; }
.panel p { margin: 4px 0; }
.panel button {
background: var(--accent); color: #fff; border: none;
border-radius: 12px; padding: 10px 16px; font-size: 16px; margin-top: 8px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
}
.panel button:active { transform: translateY(1px); }

View File

@@ -35,6 +35,114 @@ class SnakeGame {
this.init();
}
// 根据分数计算权重(权重越高,越容易触发且数量偏大)
calculateWeightByScore(score) {
const w = score / 100; // 1000分趋近高权重
return Math.max(0.1, Math.min(0.95, w));
}
// 权重偏向的随机整数weight越大越偏向更大值
biasedRandomInt(maxInclusive, weight) {
const r = Math.random();
const biased = Math.pow(r, 1 - weight);
const val = Math.floor(biased * (maxInclusive + 1));
return Math.max(0, Math.min(maxInclusive, val));
}
// 在排行榜弹层追加结束信息
appendEndInfo(text, type = 'info') {
const summary = document.getElementById('leaderboardSummary');
if (!summary) return;
const info = document.createElement('div');
info.style.marginTop = '8px';
info.style.fontSize = '14px';
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#333');
info.textContent = text;
summary.appendChild(info);
}
// 游戏结束后尝试加“萌芽币”
async tryAwardCoinsOnGameOver() {
try {
const token = localStorage.getItem('token');
if (!token) {
this.appendEndInfo('未登录,无法获得萌芽币');
return;
}
let email = null;
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const userObj = JSON.parse(userStr);
email = userObj && (userObj.email || userObj['邮箱']);
}
} catch (_) {}
if (!email) {
this.appendEndInfo('未找到账户信息email无法加币', 'error');
return;
}
const weight = this.calculateWeightByScore(this.score);
let coins = 0;
let guaranteed = false;
// 得分大于400必定触发获得1-5个萌芽币
if (this.score > 5) {
guaranteed = true;
coins = Math.floor(Math.random() * 5) + 1; // 1~5
} else {
// 使用权重作为概率
const roll = Math.random();
if (roll > weight) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 生成0~10随机数量权重越高越偏向更大
coins = this.biasedRandomInt(10, weight);
coins = Math.max(0, Math.min(10, coins));
if (coins <= 0) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
}
// 后端 API base优先父窗口ENV_CONFIG
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
? window.parent.ENV_CONFIG.API_URL
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ email, amount: coins })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
this.appendEndInfo(`加币失败:${msg}`, 'error');
return;
}
const data = await resp.json();
if (data && data.success) {
const newCoins = data.data && data.data.new_coins;
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
} else {
const msg = (data && (data.message || data.error)) || '未知错误';
this.appendEndInfo(`加币失败:${msg}`, 'error');
}
} catch (e) {
console.error('加币流程发生错误:', e);
this.appendEndInfo('加币失败:网络或系统错误', 'error');
}
}
init() {
this.generateFood();
@@ -310,10 +418,94 @@ class SnakeGame {
this.dy = dy;
}
// 工具:格式化日期为 YYYY-MM-DD
formatDate(date = new Date()) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
showGameOver() {
// 游戏结束时只记录最终状态,不显示弹窗
// 构建并展示排行榜弹层
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
console.log(`游戏结束! 分数: ${this.score}, 长度: ${this.snake.length}, 等级: ${this.level}, 时间: ${gameTime}`);
const overlay = document.getElementById('leaderboardOverlay');
const listEl = document.getElementById('leaderboardList');
const lbScore = document.getElementById('lbScore');
const lbLength = document.getElementById('lbLength');
const lbLevel = document.getElementById('lbLevel');
const lbGameTime = document.getElementById('lbGameTime');
const lbRank = document.getElementById('lbRank');
if (!overlay || !listEl) {
console.warn('排行榜容器不存在');
return;
}
// 汇总当前玩家数据
lbScore.textContent = this.score;
lbLength.textContent = this.snake.length;
lbLevel.textContent = this.level;
lbGameTime.textContent = `${gameTime}`;
const currentEntry = {
"名称": localStorage.getItem('snakePlayerName') || '我',
"账号": localStorage.getItem('snakePlayerAccount') || 'guest@local',
"分数": this.score,
"时间": this.formatDate(new Date()),
__isCurrent: true,
__duration: gameTime
};
// 合并并排序数据(使用 gamedata.js 中的 playerdata
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
const merged = [...baseData, currentEntry];
merged.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
const playerIndex = merged.findIndex(e => e.__isCurrent);
lbRank.textContent = playerIndex >= 0 ? `#${playerIndex + 1}` : '—';
// 生成排行榜TOP 10
const topList = merged.slice(0, 10).map((entry, idx) => {
const isCurrent = !!entry.__isCurrent;
const name = entry["名称"] ?? '未知玩家';
const score = entry["分数"] ?? 0;
const dateStr = entry["时间"] ?? '';
const timeStr = isCurrent ? `时长:${entry.__duration}` : `时间:${dateStr}`;
return `
<div class="leaderboard-item ${isCurrent ? 'current-player' : ''}">
<span class="rank">#${idx + 1}</span>
<span class="player-name">${name}</span>
<span class="player-score">${score}分</span>
<span class="player-time">${timeStr}</span>
</div>
`;
}).join('');
listEl.innerHTML = topList;
overlay.style.display = 'flex';
// 结束时尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 触发游戏结束事件(供统计模块使用)
const gameOverEvent = new CustomEvent('gameOver', {
detail: {
score: this.score,
length: this.snake.length,
level: this.level,
gameTime: gameTime,
foodEaten: this.foodEaten
}
});
document.dispatchEvent(gameOverEvent);
// 绑定重新开始按钮
const restartBtn = document.getElementById('leaderboardRestartBtn');
if (restartBtn) {
restartBtn.onclick = () => {
overlay.style.display = 'none';
this.restart();
};
}
}
@@ -334,6 +526,10 @@ class SnakeGame {
this.foodEaten = 0;
this.specialFood = null;
// 隐藏排行榜弹层(若可见)
const overlay = document.getElementById('leaderboardOverlay');
if (overlay) overlay.style.display = 'none';
this.generateFood();
this.updateUI();

View File

@@ -86,9 +86,6 @@ class GameStatistics {
// 保存到本地存储
localStorage.setItem('snakeHighScores', JSON.stringify(this.highScores));
// 显示高分榜
this.displayHighScores();
}
displaySessionStats() {
@@ -175,31 +172,7 @@ class GameStatistics {
}
}
// 扩展游戏核心类,添加统计事件触发
SnakeGame.prototype.showGameOver = function() {
const modal = document.getElementById('gameOverModal');
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
document.getElementById('finalScore').textContent = this.score;
document.getElementById('finalLength').textContent = this.snake.length;
document.getElementById('finalLevel').textContent = this.level;
document.getElementById('gameTime').textContent = gameTime;
document.getElementById('foodEaten').textContent = this.foodEaten;
// 触发游戏结束事件
const gameOverEvent = new CustomEvent('gameOver', {
detail: {
score: this.score,
length: this.snake.length,
level: this.level,
gameTime: gameTime,
foodEaten: this.foodEaten
}
});
document.dispatchEvent(gameOverEvent);
modal.style.display = 'flex';
};
// 原游戏结束界面已移除,保留统计模块以便响应 'gameOver' 事件
// 初始化统计模块
let gameStats;

View File

@@ -5,18 +5,6 @@ const playerdata = [
"分数":1568,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":245,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":1123,
"时间":"2025-09-25"
},
{
"名称":"风行者",
"账号":"4456723190@qq.com",

View File

@@ -27,12 +27,32 @@
</div>
</div>
<div class="game-instructions">
<p>使用方向键或拖动手势控制蛇的方向</p>
</div>
<!-- 游戏结束排行榜弹层(替换旧的游戏结束界面) -->
<div id="leaderboardOverlay" class="modal">
<div class="modal-content">
<h2>游戏结束排行榜</h2>
<div class="leaderboard-summary" id="leaderboardSummary">
<p>
分数: <span id="lbScore">0</span>
|长度: <span id="lbLength">3</span>
|等级: <span id="lbLevel">1</span>
</p>
<p>
游戏时长: <span id="lbGameTime">0秒</span>
|你的排名: <span id="lbRank"></span>
</p>
</div>
<div class="leaderboard">
<h3>TOP 10</h3>
<div id="leaderboardList" class="leaderboard-list"></div>
</div>
<button id="leaderboardRestartBtn" class="restart-btn">重新开始</button>
</div>
</div>
<script src="gamedata.js"></script>
<script src="game-core.js"></script>

View File

@@ -184,6 +184,20 @@ body {
border: 1px solid rgba(46, 125, 50, 0.2);
}
.leaderboard-summary {
margin: 10px 0 15px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
color: #1b5e20;
text-align: center;
border: 1px solid rgba(46, 125, 50, 0.2);
}
.leaderboard-summary p {
margin: 6px 0;
}
.leaderboard h3 {
color: #1b5e20;
margin-bottom: 15px;
@@ -234,6 +248,13 @@ body {
text-align: right;
}
.leaderboard-item .player-time {
color: #4a5568;
font-size: 0.8rem;
min-width: 80px;
text-align: right;
}
/* 手机端优化 */
@media (max-width: 768px) {
.game-container {

View File

@@ -0,0 +1,314 @@
// 清新跑酷 - Endless Runner (Mobile Portrait, Touch-friendly)
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const restartBtn = document.getElementById('restartBtn');
const overlay = document.getElementById('overlay');
const overlayRestart = document.getElementById('overlayRestart');
const finalScoreEl = document.getElementById('finalScore');
let dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
let running = true;
let gameOver = false;
let lastTime = performance.now();
let elapsed = 0; // seconds
let score = 0;
const world = {
width: 360,
height: 640,
groundH: 90, // 地面高度CSS像素
baseSpeed: 240, // 初始速度px/s
speed: 240, // 当前速度(随难度提升)
gravity: 1800, // 重力px/s^2
jumpV: -864, // 跳跃初速度px/s
};
const player = {
x: 72,
y: 0, // 通过 resetPlayer 设置
w: 44,
h: 54,
vy: 0,
grounded: false,
color: '#2f7d5f'
};
const obstacles = [];
const coins = [];
let obstacleTimer = 0; // ms 到下一个障碍
let coinTimer = 0; // ms 到下一个道具
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(min, max) { return Math.random() * (max - min) + min; }
function resizeCanvas() {
dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
const cssWidth = Math.min(480, document.documentElement.clientWidth);
const cssHeight = document.documentElement.clientHeight;
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
canvas.width = Math.floor(cssWidth * dpr);
canvas.height = Math.floor(cssHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 使用CSS像素绘制
world.width = cssWidth;
world.height = cssHeight;
world.groundH = Math.max(64, Math.floor(world.height * 0.14));
resetPlayer();
}
function resetPlayer() {
player.y = world.height - world.groundH - player.h;
player.vy = 0;
player.grounded = true;
}
function spawnObstacle() {
const w = rand(28, 56);
const h = rand(40, clamp(world.height * 0.28, 80, 140));
const y = world.height - world.groundH - h;
obstacles.push({ x: world.width + w, y, w, h, color: '#3ea573' });
// 以一定概率在障碍上方生成一个金币
if (Math.random() < 0.6) {
const cx = world.width + w + rand(10, 40);
const cy = y - rand(28, 56);
coins.push({ x: cx, y: cy, r: 10, color: '#f6c453' });
}
}
function spawnCoin() {
const r = 10;
const yTop = world.height * 0.35; // 道具浮在中上区域
const y = rand(yTop, world.height - world.groundH - 80);
coins.push({ x: world.width + 60, y, r, color: '#f6c453' });
}
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function circleRectOverlap(cx, cy, r, rx, ry, rw, rh) {
const closestX = clamp(cx, rx, rx + rw);
const closestY = clamp(cy, ry, ry + rh);
const dx = cx - closestX;
const dy = cy - closestY;
return (dx * dx + dy * dy) <= r * r;
}
function jump() {
if (gameOver) return;
if (player.grounded) {
player.vy = world.jumpV;
player.grounded = false;
}
}
function update(dt) {
// 难度递增:速度随时间上涨,生成间隔缩短
elapsed += dt;
world.speed = world.baseSpeed + elapsed * 22; // 每秒加速
obstacleTimer -= dt * 1000;
coinTimer -= dt * 1000;
const minInterval = clamp(1400 - elapsed * 20, 700, 1600); // 障碍间隔(更远)
const coinInterval = clamp(1200 - elapsed * 25, 500, 1200); // 金币间隔
if (obstacleTimer <= 0) {
spawnObstacle();
obstacleTimer = rand(minInterval, minInterval * 1.35);
}
if (coinTimer <= 0) {
spawnCoin();
coinTimer = rand(coinInterval * 0.6, coinInterval);
}
// 玩家物理
player.vy += world.gravity * dt;
player.y += player.vy * dt;
const groundY = world.height - world.groundH - player.h;
if (player.y >= groundY) {
player.y = groundY;
player.vy = 0;
player.grounded = true;
}
// 移动障碍与金币
const dx = world.speed * dt;
for (let i = obstacles.length - 1; i >= 0; i--) {
const ob = obstacles[i];
ob.x -= dx;
if (ob.x + ob.w < 0) obstacles.splice(i, 1);
}
for (let i = coins.length - 1; i >= 0; i--) {
const c = coins[i];
c.x -= dx;
if (c.x + c.r < 0) coins.splice(i, 1);
}
// 碰撞检测:障碍
for (const ob of obstacles) {
if (rectsOverlap(player.x, player.y, player.w, player.h, ob.x, ob.y, ob.w, ob.h)) {
endGame();
return;
}
}
// 拾取金币
for (let i = coins.length - 1; i >= 0; i--) {
const c = coins[i];
if (circleRectOverlap(c.x, c.y, c.r, player.x, player.y, player.w, player.h)) {
score += 100; // 金币加分
coins.splice(i, 1);
}
}
// 距离积分(随速度)
score += Math.floor(world.speed * dt * 0.2);
scoreEl.textContent = String(score);
}
function drawGround() {
const y = world.height - world.groundH;
// 地面阴影渐变
const grad = ctx.createLinearGradient(0, y, 0, world.height);
grad.addColorStop(0, 'rgba(60, 150, 110, 0.35)');
grad.addColorStop(1, 'rgba(60, 150, 110, 0.05)');
ctx.fillStyle = grad;
ctx.fillRect(0, y, world.width, world.groundH);
// 地面纹理线
ctx.strokeStyle = 'rgba(47, 79, 63, 0.25)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(world.width, y);
ctx.stroke();
}
function drawPlayer() {
ctx.fillStyle = player.color;
const r = 8; // 圆角
const x = player.x, y = player.y, w = player.w, h = player.h;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fill();
// 前进指示条
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(x + 6, y + 10, 6, 12);
ctx.fillRect(x + 18, y + 10, 6, 12);
}
function drawObstacles() {
for (const ob of obstacles) {
// 渐变柱体
const g = ctx.createLinearGradient(ob.x, ob.y, ob.x, ob.y + ob.h);
g.addColorStop(0, '#52b985');
g.addColorStop(1, '#3ea573');
ctx.fillStyle = g;
ctx.fillRect(ob.x, ob.y, ob.w, ob.h);
// 顶部高亮
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fillRect(ob.x, ob.y, ob.w, 4);
}
}
function drawCoins() {
for (const c of coins) {
ctx.beginPath();
ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
ctx.fillStyle = c.color;
ctx.fill();
// 外圈高光
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function draw(now) {
// 清屏CSS负责背景渐变这里仅清理
ctx.clearRect(0, 0, world.width, world.height);
drawGround();
drawPlayer();
drawObstacles();
drawCoins();
// 速度指示(右上角小提示)
ctx.fillStyle = 'rgba(47,79,63,0.35)';
ctx.font = '12px system-ui';
ctx.textAlign = 'right';
ctx.fillText(`速度 ${Math.round(world.speed)}px/s`, world.width - 8, 18);
}
function endGame() {
running = false;
gameOver = true;
finalScoreEl.textContent = String(score);
overlay.hidden = false;
}
function resetGame() {
running = true;
gameOver = false;
obstacles.length = 0;
coins.length = 0;
obstacleTimer = 0;
coinTimer = rand(400, 900);
score = 0;
elapsed = 0;
resetPlayer();
overlay.hidden = true;
lastTime = performance.now();
}
function loop(now) {
const dt = Math.min(0.033, (now - lastTime) / 1000); // 限制最大步长
lastTime = now;
if (running) {
update(dt);
draw(now);
}
requestAnimationFrame(loop);
}
// 输入事件
function onKey(e) {
if (e.code === 'Space' || e.code === 'ArrowUp') {
e.preventDefault();
jump();
}
}
function onPointer() { jump(); }
restartBtn.addEventListener('click', () => {
resetGame();
});
overlayRestart.addEventListener('click', () => {
resetGame();
});
window.addEventListener('keydown', onKey, { passive: false });
window.addEventListener('mousedown', onPointer);
window.addEventListener('touchstart', onPointer, { passive: true });
window.addEventListener('resize', () => {
resizeCanvas();
});
// 初始化并启动
resizeCanvas();
requestAnimationFrame(loop);
})();

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover" />
<title>清新跑酷 - InfoGenie</title>
<meta name="theme-color" content="#bde8c7" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="game-shell">
<header class="hud">
<div class="score">分数 <span id="score">0</span></div>
<button id="restartBtn" class="restart" aria-label="重新开始">重新开始</button>
</header>
<main class="stage">
<canvas id="gameCanvas" aria-label="跑酷游戏画布"></canvas>
<div class="hint">轻触屏幕或按空格跳跃</div>
<div id="overlay" class="overlay" hidden>
<div class="overlay-card">
<div class="gameover-title">游戏结束</div>
<div class="summary">分数 <span id="finalScore">0</span></div>
<button class="overlay-restart" id="overlayRestart">重新开始</button>
</div>
</div>
</main>
</div>
<script src="./game.js" defer></script>
</body>
</html>

View File

@@ -0,0 +1,130 @@
/* 清新淡绿色渐变风格与移动端适配 */
:root {
--green-1: #a8e6cf; /* 淡绿色 */
--green-2: #dcedc1; /* 淡黄绿色 */
--accent: #58c48b; /* 按钮主色 */
--accent-dark: #3ca16c;
--text: #2f4f3f; /* 深绿文字 */
--card: #ffffffd9; /* 半透明卡片 */
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
color: var(--text);
background: linear-gradient(180deg, var(--green-1) 0%, var(--green-2) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.game-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.hud {
position: sticky;
top: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: color-mix(in oklab, var(--green-2) 65%, white 35%);
backdrop-filter: saturate(1.4) blur(6px);
box-shadow: 0 2px 10px rgb(0 0 0 / 6%);
}
.score {
font-weight: 700;
font-size: 18px;
letter-spacing: 0.5px;
}
.restart {
appearance: none;
border: none;
outline: none;
background: var(--accent);
color: #fff;
font-weight: 600;
font-size: 14px;
padding: 8px 12px;
border-radius: 999px;
box-shadow: 0 4px 12px rgb(88 196 139 / 30%);
transition: transform .05s ease, background .2s ease;
}
.restart:active { transform: scale(0.98); }
.restart:hover { background: var(--accent-dark); }
.stage {
position: relative;
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.hint {
position: absolute;
bottom: max(10px, env(safe-area-inset-bottom));
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: color-mix(in oklab, var(--text), white 35%);
opacity: 0.85;
pointer-events: none;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgb(0 0 0 / 10%);
}
.overlay[hidden] { display: none; }
.overlay-card {
width: min(88vw, 420px);
background: var(--card);
border-radius: 16px;
box-shadow: 0 10px 30px rgb(0 0 0 / 15%);
padding: 18px 18px 16px;
text-align: center;
}
.gameover-title {
font-size: 20px;
font-weight: 800;
}
.summary {
margin: 12px 0 16px;
font-size: 16px;
}
.overlay-restart {
appearance: none;
border: none;
outline: none;
background: var(--accent);
color: #fff;
font-weight: 700;
font-size: 16px;
padding: 10px 16px;
border-radius: 12px;
box-shadow: 0 6px 16px rgb(88 196 139 / 38%);
}
.overlay-restart:active { transform: scale(0.98); }
.overlay-restart:hover { background: var(--accent-dark); }
@media (prefers-reduced-motion: reduce) {
.restart, .overlay-restart { transition: none; }
}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
<title>清新躲避 · 无尽模式</title>
<meta name="theme-color" content="#d8f7c2" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="hud">
<div class="score">分数 <span id="score">0</span></div>
<button id="pauseBtn" class="btn" aria-label="暂停"></button>
</div>
<canvas id="game" aria-label="清新躲避游戏画布"></canvas>
<div id="startOverlay" class="overlay show" role="dialog" aria-modal="true">
<div class="card">
<h1>清新躲避</h1>
<p>按住并左右拖动,躲避下落的叶片。</p>
<p>无尽模式,难度会随时间提升。</p>
<button id="startBtn" class="btn primary">开始游戏</button>
</div>
</div>
<div id="gameOverOverlay" class="overlay" role="dialog" aria-modal="true">
<div class="card">
<h2>游戏结束</h2>
<p>本局分数:<strong id="finalScore">0</strong></p>
<button id="restartBtn" class="btn primary">再来一次</button>
</div>
</div>
<div id="rotateOverlay">请将手机竖屏以获得最佳体验</div>
<script src="./script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,261 @@
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hudScoreEl = document.getElementById('score');
const pauseBtn = document.getElementById('pauseBtn');
const startOverlay = document.getElementById('startOverlay');
const startBtn = document.getElementById('startBtn');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const finalScoreEl = document.getElementById('finalScore');
const restartBtn = document.getElementById('restartBtn');
const rotateOverlay = document.getElementById('rotateOverlay');
let width = 0, height = 0, DPR = 1;
let running = false, paused = false;
let lastTime = 0, timeElapsed = 0, score = 0, spawnTimer = 0;
const player = { x: 0, y: 0, r: 18, vx: 0, targetX: null };
const obstacles = [];
let pointerActive = false;
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
function rand(min, max){ return Math.random()*(max-min)+min; }
function updateOrientationOverlay(){
const landscape = window.innerWidth > window.innerHeight;
rotateOverlay.style.display = landscape ? 'flex' : 'none';
}
function resize(){
updateOrientationOverlay();
DPR = Math.min(2, window.devicePixelRatio || 1);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * DPR);
canvas.height = Math.floor(height * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
if (!running){
player.x = width * 0.5;
player.y = height - Math.max(80, height * 0.12);
} else {
player.y = height - Math.max(80, height * 0.12);
player.x = clamp(player.x, player.r + 8, width - player.r - 8);
}
}
window.addEventListener('resize', resize);
resize();
function drawBackground(){
// 轻微的顶部高光,让画面更通透
const g = ctx.createLinearGradient(0,0,0,height);
g.addColorStop(0,'rgba(255,255,255,0.10)');
g.addColorStop(1,'rgba(255,255,255,0.00)');
ctx.fillStyle = g;
ctx.fillRect(0,0,width,height);
}
function drawPlayer(){
ctx.save();
ctx.translate(player.x, player.y);
const r = player.r;
const grad = ctx.createRadialGradient(-r*0.3, -r*0.3, r*0.2, 0, 0, r);
grad.addColorStop(0, '#5fca7e');
grad.addColorStop(1, '#3a9e5a');
ctx.fillStyle = grad;
// 圆形带小叶柄的简化“叶子”角色
ctx.beginPath();
ctx.arc(0,0,r,0,Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(58,158,90,0.7)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -r*0.9);
ctx.quadraticCurveTo(r*0.2, -r*1.3, r*0.5, -r*1.0);
ctx.stroke();
ctx.restore();
}
function spawnObstacle(){
const difficulty = 1 + timeElapsed * 0.08; // 随时间慢慢提升
const r = rand(10, 22);
const x = rand(r+8, width - r - 8);
const speed = rand(90, 140) * (0.9 + difficulty * 0.5);
const rot = rand(-Math.PI*0.5, Math.PI*0.5);
obstacles.push({ x, y: -r - 20, r, speed, rot, swayPhase: Math.random()*Math.PI*2, swayAmp: rand(6,12) });
}
function drawObstacle(o){
ctx.save();
ctx.translate(o.x, o.y);
ctx.rotate(o.rot);
const r = o.r;
ctx.beginPath();
ctx.ellipse(0, 0, r*0.9, r*0.6, 0, 0, Math.PI*2);
const grad = ctx.createLinearGradient(-r, -r, r, r);
grad.addColorStop(0, '#d8f7c2');
grad.addColorStop(0.5, '#b9ef9f');
grad.addColorStop(1, '#9edf77');
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = 'rgba(90,150,90,0.5)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-r*0.5, 0);
ctx.quadraticCurveTo(0, -r*0.3, r*0.5, 0);
ctx.stroke();
ctx.restore();
}
function update(dt){
const difficulty = 1 + timeElapsed * 0.08;
const spawnInterval = Math.max(0.12, 0.9 / difficulty);
spawnTimer -= dt;
if (spawnTimer <= 0){
spawnObstacle();
spawnTimer = spawnInterval;
}
// 障碍移动 + 轻微左右摆动
for (let i = 0; i < obstacles.length; i++){
const o = obstacles[i];
o.y += o.speed * dt;
o.x += Math.sin(o.swayPhase + timeElapsed * 1.6) * (o.swayAmp * dt);
}
// 清除离开屏幕的障碍
for (let i = obstacles.length - 1; i >= 0; i--){
const o = obstacles[i];
if (o.y > height + o.r + 60){
obstacles.splice(i, 1);
}
}
// 键盘轻推(桌面端备用)
if (player.targetX != null){
const dir = player.targetX - player.x;
player.vx = clamp(dir, -500, 500);
player.x += player.vx * dt;
if (Math.abs(dir) < 2){
player.targetX = null;
player.vx = 0;
}
}
// 限制玩家范围
player.x = clamp(player.x, player.r + 8, width - player.r - 8);
// 碰撞检测(近似圆形)
for (const o of obstacles){
const dx = o.x - player.x;
const dy = o.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist < player.r + o.r * 0.65){
endGame();
return;
}
}
// 计分:按生存时间累计
score += dt * 10; // 每秒约10分
hudScoreEl.textContent = Math.floor(score);
}
function render(){
ctx.clearRect(0,0,width,height);
drawBackground();
for (const o of obstacles) drawObstacle(o);
drawPlayer();
}
function loop(t){
if (!running){ return; }
if (paused){
lastTime = t;
requestAnimationFrame(loop);
return;
}
if (!lastTime) lastTime = t;
const dt = Math.min(0.033, (t - lastTime)/1000);
lastTime = t;
timeElapsed += dt;
update(dt);
render();
requestAnimationFrame(loop);
}
function startGame(){
obstacles.length = 0;
score = 0;
timeElapsed = 0;
spawnTimer = 0;
running = true;
paused = false;
lastTime = 0;
startOverlay.classList.remove('show');
gameOverOverlay.classList.remove('show');
resize();
requestAnimationFrame(loop);
}
function endGame(){
running = false;
paused = false;
finalScoreEl.textContent = Math.floor(score);
gameOverOverlay.classList.add('show');
}
function pointerToCanvasX(e){
const rect = canvas.getBoundingClientRect();
return clamp(e.clientX - rect.left, 0, rect.width);
}
// 触控与指针事件:按住并左右拖动
canvas.addEventListener('pointerdown', e => {
pointerActive = true;
player.targetX = null;
player.x = pointerToCanvasX(e);
});
canvas.addEventListener('pointermove', e => {
if (!pointerActive) return;
player.x = pointerToCanvasX(e);
});
canvas.addEventListener('pointerup', () => { pointerActive = false; });
canvas.addEventListener('pointercancel', () => { pointerActive = false; });
// 轻点屏幕:向左/右轻推一段距离
canvas.addEventListener('click', e => {
const x = pointerToCanvasX(e);
const center = width / 2;
const dir = x < center ? -1 : 1;
player.targetX = clamp(player.x + dir * Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
});
// 键盘备用控制(桌面端)
window.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'a'){
player.targetX = clamp(player.x - Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
} else if (e.key === 'ArrowRight' || e.key === 'd'){
player.targetX = clamp(player.x + Math.max(50, width * 0.12), player.r + 8, width - player.r - 8);
} else if (e.key === ' ') {
togglePause();
} else if (e.key === 'Enter' && !running){
startGame();
}
});
// 按钮
pauseBtn.addEventListener('click', togglePause);
startBtn.addEventListener('click', startGame);
restartBtn.addEventListener('click', startGame);
function togglePause(){
if (!running) return;
paused = !paused;
pauseBtn.textContent = paused ? '▶' : 'Ⅱ';
}
// 避免滚动与系统手势干扰
['touchstart','touchmove','touchend'].forEach(type => {
window.addEventListener(type, e => { if (pointerActive) e.preventDefault(); }, { passive: false });
});
})();

View File

@@ -0,0 +1,106 @@
/* 主题色:淡绿色到淡黄绿色的清新渐变 */
:root {
--bg-start: #dff9d3;
--bg-mid: #eaffd1;
--bg-end: #e9fbb5;
--accent: #4fb66d;
--accent-dark: #3a9e5a;
--text: #2f4f3f;
--hud-bg: rgba(255, 255, 255, 0.65);
--overlay-bg: rgba(255, 255, 255, 0.55);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, PingFang SC, Microsoft YaHei, sans-serif;
color: var(--text);
background: linear-gradient(180deg, var(--bg-start), var(--bg-mid), var(--bg-end));
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: none;
}
/* 顶部HUD */
#hud {
position: fixed;
top: env(safe-area-inset-top, 12px);
left: env(safe-area-inset-left, 12px);
right: env(safe-area-inset-right, 12px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
margin: 8px;
border-radius: 14px;
background: var(--hud-bg);
backdrop-filter: blur(8px);
box-shadow: 0 6px 16px rgba(0,0,0,0.08);
}
#hud .score { font-weight: 600; letter-spacing: 0.5px; }
/* 画布填充屏幕,适配竖屏 */
#game {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
display: block;
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
/* 通用按钮样式 */
.btn {
appearance: none;
border: none;
outline: none;
padding: 8px 12px;
border-radius: 12px;
color: #fff;
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
box-shadow: 0 6px 14px rgba(79,182,109,0.35);
cursor: pointer;
}
.btn:active { transform: translateY(1px); }
.btn.primary { font-weight: 600; }
/* 覆盖层样式 */
.overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
}
.overlay.show { display: flex; }
.card {
width: min(520px, 92vw);
padding: 20px 18px;
border-radius: 16px;
background: var(--overlay-bg);
backdrop-filter: blur(8px);
box-shadow: 0 10px 22px rgba(0,0,0,0.12);
text-align: center;
}
.card h1, .card h2 { margin: 8px 0 12px; }
.card p { margin: 6px 0 12px; }
/* 横屏提示覆盖层 */
#rotateOverlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
font-weight: 600;
color: var(--accent-dark);
background: rgba(255,255,255,0.6);
backdrop-filter: blur(6px);
}

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 聊天</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chat-container">
<div class="warning-banner" role="alert">⚠️ 重要提示本AI聊天功能为免费体验服务旨在展示不同大语言模型的交互能力。请注意1.不支持上下文记忆功能2.每分钟调用次数有限制3.使用人数过多会出现调用失败情况</div>
<div class="chat-header">
<h2>AI 聊天</h2>
<div class="model-selector">
<label for="model">选择模型: </label>
<select id="model">
<option value="openai/gpt-4.1">gpt-4.1</option>
<option value="openai/gpt-4.1-mini">gpt-4.1-mini</option>
<option value="openai/gpt-4.1-nano">gpt-4.1-nano</option>
<option value="openai/gpt-4o">gpt-4o</option>
<option value="openai/gpt-5">gpt-5</option>
<option value="deepseek-r1">deepseek-r1</option>
<option value="deepseek-v3-0324">deepseek-v3-0324</option>
<option value="xai/grok-3">grok-3</option>
<option value="xai/grok-3-mini">grok-3-mini</option>
</select>
</div>
</div>
<div id="model-info" class="model-info"></div>
<div class="chat-box" id="chat-box">
<!-- 消息将在这里显示 -->
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="输入你的消息...">
<button id="send-btn">发送</button>
</div>
</div>
<script src="marked.min.js"></script>
<script src="purify.min.js"></script>
<script src="script.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<title>数字时钟</title>
<style>
/* 基础:黑底白字、全屏居中 */
html, body {
height: 100%;
margin: 0;
background: #000;
color: #fff;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
overscroll-behavior: none;
touch-action: manipulation;
}
.stage {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: env(safe-area-inset);
}
/* 大号等宽数字,保证各位宽度一致,避免跳动 */
.clock {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-variant-numeric: tabular-nums lining-nums;
line-height: 1;
letter-spacing: 0.02em;
white-space: nowrap;
user-select: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 自适应字号:在不同屏幕上都足够大 */
font-size: clamp(12vmin, 18vmin, 22vmin);
/* 防止 iOS 浏览器工具栏出现时的轻微抖动 */
max-width: 100vw;
}
/* 横屏:按宽度再放大一些(更横向铺满) */
@media (orientation: landscape) {
.clock { font-size: min(20vmin, 22vw); }
}
/* 竖屏:把数字整体顺时针旋转 90°看起来依旧是“横屏”的排布 */
@media (orientation: portrait) {
.clock { transform: rotate(90deg); }
}
</style>
</head>
<body>
<div class="stage">
<div id="clock" class="clock" aria-live="polite" aria-label="当前时间">00:00:00</div>
</div> <script>
(function() {
const el = document.getElementById('clock');
function two(n) { return (n < 10 ? '0' : '') + n; }
function renderNow() {
const d = new Date();
el.textContent = `${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`;
}
// 与下一整秒对齐,尽量避免定时器漂移
function startAlignedClock() {
renderNow();
const drift = 1000 - (Date.now() % 1000);
setTimeout(function tick() {
renderNow();
setTimeout(tick, 1000);
}, drift);
}
// 处理移动端 100vh 问题:设置 CSS 变量供需要时使用(当前样式未直接用到,但保留以便扩展)
function setViewportVars() {
document.documentElement.style.setProperty('--dvw', window.innerWidth + 'px');
document.documentElement.style.setProperty('--dvh', window.innerHeight + 'px');
}
setViewportVars();
window.addEventListener('resize', setViewportVars);
window.addEventListener('orientationchange', setViewportVars);
startAlignedClock();
})();
</script></body>
</html>

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,177 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>随机Emoji表情</title>
<meta name="description" content="清新风格随机Emoji表情包展示支持复制与刷新移动端与电脑端自适应。" />
<style>
:root {
--bg-start: #eaf9e9;
--bg-end: #f4ffe5;
--panel-bg: rgba(255,255,255,0.60);
--panel-bd: rgba(255,255,255,0.85);
--accent: #79c86b;
--text: #253525;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
background-attachment: fixed;
/* 隐藏滚动条但保留滚动功能 */
overflow-x: hidden;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
/* 隐藏 Webkit 浏览器的滚动条 */
body::-webkit-scrollbar {
display: none;
}
html {
/* 隐藏滚动条但保留滚动功能 */
overflow-x: hidden;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
html::-webkit-scrollbar {
display: none;
}
.wrap {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.panel {
width: min(92vw, 1100px);
background: var(--panel-bg);
border: 1px solid var(--panel-bd);
border-radius: 16px;
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
backdrop-filter: saturate(1.1) blur(2px);
padding: 1rem 1rem 1.25rem;
}
.header { display:flex; justify-content:space-between; align-items:baseline; gap:.75rem; margin-bottom:.75rem; }
.title { font-weight:600; font-size: clamp(1.1rem, 2.6vw, 1.6rem); letter-spacing:.3px; }
.hint { font-size: clamp(.85rem, 2vw, 1rem); color:#4a6f4a; opacity:.9; }
table { width: 100%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; overflow: hidden; border-radius: 12px; background: rgba(255,255,255,0.38); }
tbody { width: 100%; }
td {
border: 1px solid rgba(255,255,255,0.75);
text-align: center; vertical-align: middle; user-select: none; cursor: pointer;
background: rgba(255,255,255,0.50);
transition: transform 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
font-size: var(--cell-font);
height: var(--cell-size);
line-height: 1;
}
td:hover { background: rgba(255,255,255,0.80); transform: scale(1.06); box-shadow: inset 0 0 0 2px rgba(121,200,107,0.18); }
.controls { display:flex; justify-content:center; align-items:center; gap:.8rem; margin-top:1rem; }
.refresh { border:none; padding:.6rem 1.15rem; border-radius:999px; font-weight:600; letter-spacing:.3px; color:#1f351f; background: linear-gradient(180deg, #c9f6c9, #c9f2a8); box-shadow: 0 6px 14px rgba(121,200,107,0.28), inset 0 1px 0 rgba(255,255,255,0.85); transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; }
.refresh:hover { transform: translateY(-1px); filter: saturate(1.06); }
.refresh:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(121,200,107,0.28); }
.toast {
position: fixed; left:50%; bottom: 24px; transform: translateX(-50%) translateY(80px);
background: rgba(255,255,255,0.95); color:#1f351f; border:1px solid rgba(255,255,255,0.95);
border-radius: 10px; box-shadow: 0 8px 18px rgba(0,0,0,0.12);
padding: .5rem .85rem; font-weight:600; opacity:0; pointer-events:none; transition: opacity 160ms ease, transform 160ms ease;
}
.toast.show { opacity:1; transform: translateX(-50%) translateY(0); }
/* 基础尺寸JS 会根据 10×10 或 20×20 自动适配 */
:root { --cell-size: 48px; --cell-font: 28px; }
@media (max-width: 768px) and (orientation: portrait) {
:root { --cell-size: clamp(40px, 7.2vw, 58px); --cell-font: clamp(22px, 5.8vw, 32px); }
}
@media (min-width: 769px) {
:root { --cell-size: clamp(44px, 3.6vw, 58px); --cell-font: clamp(22px, 2.2vw, 32px); }
}
</style>
</head>
<body>
<div class="wrap">
<div class="panel">
<div class="header">
<div class="title">随机 Emoji 表情</div>
<div class="hint">点击任意 Emoji 复制</div>
</div>
<table id="emoji-table" aria-label="随机 Emoji 表格"><tbody id="emoji-body"></tbody></table>
<div class="controls"><button class="refresh" id="refresh-btn">刷新</button></div>
</div>
</div>
<div class="toast" id="toast">已复制!</div>
<script>
// 随机生成 Emoji从常用的 Unicode 区间中抽取)
function randomEmoji() {
const ranges = [
[0x1F600, 0x1F64F], // Emoticons
[0x1F300, 0x1F5FF], // Misc Symbols and Pictographs
[0x1F680, 0x1F6FF], // Transport & Map
[0x2600, 0x26FF], // Misc symbols
[0x2700, 0x27BF], // Dingbats
[0x1F900, 0x1F9FF], // Supplemental Symbols and Pictographs
[0x1FA70, 0x1FAFF] // Symbols & Pictographs Extended-A
];
const [start, end] = ranges[Math.floor(Math.random() * ranges.length)];
const code = start + Math.floor(Math.random() * (end - start + 1));
return String.fromCodePoint(code);
}
const tableBody = document.getElementById('emoji-body');
const refreshBtn = document.getElementById('refresh-btn');
const toastEl = document.getElementById('toast');
function isPortraitMobile() {
return window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches;
}
function getGridSize() { return isPortraitMobile() ? 10 : 20; }
function showToast(text) {
toastEl.textContent = text;
toastEl.classList.add('show');
clearTimeout(showToast._timer);
showToast._timer = setTimeout(() => toastEl.classList.remove('show'), 1200);
}
async function copyToClipboard(text) {
try { await navigator.clipboard.writeText(text); }
catch (e) {
const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove();
}
showToast(`已复制: ${text}`);
}
function generateTable() {
const size = getGridSize();
tableBody.innerHTML = '';
for (let r = 0; r < size; r++) {
const tr = document.createElement('tr');
for (let c = 0; c < size; c++) {
const td = document.createElement('td');
const e = randomEmoji();
td.textContent = e; td.title = '点击复制';
td.addEventListener('click', () => copyToClipboard(e));
tr.appendChild(td);
}
tableBody.appendChild(tr);
}
}
refreshBtn.addEventListener('click', generateTable);
window.addEventListener('resize', () => {
const newSize = getGridSize();
if (tableBody.children.length !== newSize) generateTable();
});
generateTable();
</script>
</body>
</html>

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -101,7 +101,7 @@ const Footer = () => {
<FooterBottom>
<Copyright>
蜀ICP备2025151694号 | Copyright © 2025-{currentYear}
<strong>蜀ICP备2025151694号 | Copyright © 2025-{currentYear}</strong>
</Copyright>
</FooterBottom>
</FooterContent>

View File

@@ -197,7 +197,13 @@ const MobileMenuContent = styled.div.withConfig({
right: 0;
width: 280px;
height: 100vh;
background: white;
background: linear-gradient(135deg,
rgba(255, 240, 245, 0.95) 0%, /* 淡粉红色 */
rgba(255, 253, 240, 0.95) 35%, /* 淡黄色 */
rgba(240, 255, 240, 0.95) 70%, /* 淡绿色 */
rgba(248, 250, 252, 0.95) 100% /* 接近白色 */
);
backdrop-filter: blur(10px);
transform: translateX(${props => props.isOpen ? '0' : '100%'});
transition: transform 0.3s ease;
padding: 20px;

View File

@@ -28,7 +28,7 @@ export const AI_MODEL_APPS = [
IsShow: true
},
{
title: 'AI翻译助手',
title: 'AI翻译',
description: '基于AI的翻译工具',
link: '/aimodelapp/AI语言翻译助手/index.html',
gradient: 'linear-gradient(135deg,rgb(80, 77, 243) 0%,rgb(30, 211, 111) 100%)',
@@ -59,13 +59,29 @@ export const AI_MODEL_APPS = [
icon: '🐧',
IsShow: true
},
{
title: 'AI文章排版',
description: '将您的文章添加Emoji美化并排版成markdown格式',
link: '/aimodelapp/AI文章排版/index.html',
gradient: 'linear-gradient(135deg,rgba(238, 252, 113, 1) 0%,rgba(9, 185, 103, 1) 100%)',
icon: '📕',
IsShow: true
},
{
title: 'AI亲戚称呼计算器',
description: '基于AI的中国亲戚关系查询工具',
link: '/aimodelapp/AI中国亲戚称呼计算器/index.html',
gradient: 'linear-gradient(135deg,rgba(48, 155, 255, 1) 0%,rgba(230, 107, 255, 1) 100%)',
icon: '👨‍👩‍👧',
IsShow: true
},
];
//休闲游戏
export const SMALL_GAMES = [
{
title: '2048',
description: '经典数字合并游戏,挑战你的策略思维',
description: '经典数字合并游戏,挑战你的数学思维',
link: '/smallgame/2048/index.html',
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
icon: '🔢',
@@ -81,7 +97,7 @@ export const SMALL_GAMES = [
},
{
title: '俄罗斯方块',
description: '经典落块消除游戏,永恒的经典之作',
description: '十分解压的方块消除游戏,永恒的经典之作',
link: '/smallgame/俄罗斯方块/index.html',
gradient: 'linear-gradient(135deg, #4ade80 0%, #22c55e 100%)',
icon: '🧩',
@@ -89,20 +105,44 @@ export const SMALL_GAMES = [
},
{
title: '贪吃蛇',
description: '经典贪吃蛇游戏,考验你的反应速度和手指协调',
description: '有趣的贪吃蛇游戏,考验你的操作速度和策略',
link: '/smallgame/贪吃蛇/index.html',
gradient: 'linear-gradient(135deg,rgb(37, 132, 240) 0%, #f5576c 100%)',
icon: '🐍',
IsShow: true
},
{
{
title: '扫雷',
description: '经典扫雷游戏,考验你的反应速度和手指协调',
description: '经典扫雷游戏,考验你的思维能力和策略',
link: '/smallgame/扫雷/index.html',
gradient: 'linear-gradient(135deg,rgb(37, 132, 240) 0%, #f5576c 100%)',
icon: '💣',
IsShow: true
},
{
title: '躲树叶',
description: '躲避掉落的树叶,加油坚持到最后',
link: '/smallgame/躲树叶/index.html',
gradient: 'linear-gradient(135deg,rgba(26, 231, 70, 1) 0%, #14d9fcff 100%)',
icon: '🌳',
IsShow: true
},
{
title: '打飞机',
description: '用子弹击落飞来的敌机,坚持到最后',
link: '/smallgame/打飞机/index.html',
gradient: 'linear-gradient(135deg,rgba(240, 96, 204, 1) 0%, #ff405aff 100%)',
icon: '✈️',
IsShow: true
},
{
title: '跑酷',
description: '躲避障碍物,吃掉更多的金币',
link: '/smallgame/跑酷/index.html',
gradient: 'linear-gradient(135deg,rgba(64, 61, 255, 1) 0%, #28eaf8ff 100%)',
icon: '🏃‍♂️',
IsShow: true
},
];
//聚合应用
@@ -111,71 +151,95 @@ export const API_60S_CATEGORIES = [
title: '热搜榜单',
icon: '🔥',
color: '#ff6b6b',
description: '实时追踪各大平台热门话题,掌握最新网络动态和流行趋势',
apis: [
{ title: '哔哩哔哩热搜', link: '/60sapi/热搜榜单/哔哩哔哩热搜榜/index.html', icon: '📺', IsShow: true },
{ title: '哔哩哔哩热搜', link: '/60sapi/热搜榜单/哔哩哔哩热搜榜/index.html', icon: '📺', IsShow: true },
{ title: '抖音热搜榜', link: '/60sapi/热搜榜单/抖音热搜榜/index.html', icon: '🎵', IsShow: true },
{ title: '小红书热搜榜', link: '/60sapi/热搜榜单/小红书热点/index.html', icon: '📖', IsShow: true },
{ title: '猫眼票房排行榜', link: '/60sapi/热搜榜单/猫眼票房排行榜/index.html', icon: '🎬', IsShow: true },
{ title: '猫眼电视收视率排行榜', link: '/60sapi/热搜榜单/猫眼电视收视排行/index.html', icon: '📺', IsShow: true },
{ title: '猫眼电影实时票房', link: '/60sapi/热搜榜单/猫眼电影实时票房/index.html', icon: '🎬', IsShow: true },
{ title: '猫眼网剧实时热搜榜', link: '/60sapi/热搜榜单/猫眼网剧实时热度/index.html', icon: '💻', IsShow: true },
{ title: '今日头条热搜榜', link: '/60sapi/热搜榜单/头条热搜榜/index.html', icon: '📰', IsShow: true },
{ title: '网易云榜单', link: '/60sapi/热搜榜单/网易云榜单/index.html', icon: '🎶', IsShow: true },
{ title: '小红书热搜榜', link: '/60sapi/热搜榜单/小红书热点/index.html', icon: '🍠', IsShow: true },
{ title: '百度热搜榜', link: '/60sapi/热搜榜单/百度实时热搜/index.html', icon: '🔍', IsShow: true },
{ title: '微博热搜榜', link: '/60sapi/热搜榜单/微博热搜榜/index.html', icon: '📱', IsShow: true },
{ title: '知乎热门话题', link: '/60sapi/热搜榜单/知乎热门话题/index.html', icon: '💡', IsShow: true },
{ title: 'HackerNews HotRanks', link: '/60sapi/热搜榜单/Hacker News 榜单/index.html', icon: '💻', IsShow: true },
{ title: '百度实时热搜榜', link: '/60sapi/热搜榜单/百度实时热搜/index.html', icon: '🔍', IsShow: true },
{ title: '百度电视剧热搜榜', link: '/60sapi/热搜榜单/百度电视剧榜/index.html', icon: '📺', IsShow: true },
{ title: '百度贴吧话题榜', link: '/60sapi/热搜榜单/百度贴吧话题榜/index.html', icon: '💬', IsShow: true },
{ title: '今日头条热搜', link: '/60sapi/热搜榜单/头条热搜榜/index.html', icon: '📰', IsShow: true },
{ title: '懂车帝热搜榜', link: '/60sapi/热搜榜单/懂车帝热搜/index.html', icon: '🚗', IsShow: true },
{ title: '知乎热门话题', link: '/60sapi/热搜榜单/知乎热门话题/index.html', icon: '💡', IsShow: true },
{ title: '猫眼票房排行', link: '/60sapi/热搜榜单/猫眼票房排行榜/index.html', icon: '🎬', IsShow: true },
{ title: '猫眼电视热搜', link: '/60sapi/热搜榜单/猫眼电视收视排行/index.html', icon: '📺', IsShow: true },
{ title: '猫眼电影票房', link: '/60sapi/热搜榜单/猫眼电影实时票房/index.html', icon: '🎬', IsShow: true },
{ title: '猫眼网剧热搜', link: '/60sapi/热搜榜单/猫眼网剧实时热度/index.html', icon: '💻', IsShow: true },
{ title: '网易云榜单', link: '/60sapi/热搜榜单/网易云榜单/index.html', icon: '🎶', IsShow: true },
{ title: 'HackerNews', link: '/60sapi/热搜榜单/Hacker News 榜单/index.html', icon: '💻', IsShow: true },
{ title: '百度电视剧热搜', link: '/60sapi/热搜榜单/百度电视剧榜/index.html', icon: '📺', IsShow: true },
{ title: '百度贴吧热搜', link: '/60sapi/热搜榜单/百度贴吧话题榜/index.html', icon: '💬', IsShow: true },
]
},
{
title: '日更资讯',
icon: '📰',
color: '#81c784',
description: '每日精选优质内容,提供最新资讯和实用信息',
apis: [
{ title: '必应每日壁纸', link: '/60sapi/日更资讯/必应每日壁纸/index.html', icon: '🖼️', IsShow: true },
{ title: '每日必应壁纸', link: '/60sapi/日更资讯/必应每日壁纸/index.html', icon: '🖼️', IsShow: true },
{ title: '历史上的今天', link: '/60sapi/日更资讯/历史上的今天/index.html', icon: '📅', IsShow: true },
{ title: '每日国际汇率', link: '/60sapi/日更资讯/每日国际汇率/index.html', icon: '💱', IsShow: true },
{ title: '每60s读懂世界', link: '/60sapi/日更资讯/每天60s读懂世界/index.html', icon: '🌍', IsShow: true }
{ title: '每60s新闻', link: '/60sapi/日更资讯/每天60s读懂世界/index.html', icon: '🌍', IsShow: true }
]
},
{
title: '实用功能',
icon: '🛠️',
color: '#45b7d1',
description: '集成多种便民工具,让生活和工作更加便捷高效',
apis: [
{ title: '百度百科词条', link: '/60sapi/实用功能/百度百科词条/index.html', icon: '📚', IsShow: true },
{ title: '公网IP地址', link: '/60sapi/实用功能/公网IP地址/index.html', icon: '🌐', IsShow: true },
{ title: '查询公网IP', link: '/60sapi/实用功能/公网IP地址/index.html', icon: '🌐', IsShow: true },
{ title: '哈希解压压缩', link: '/60sapi/实用功能/哈希解压压缩/index.html', icon: '🗜️', IsShow: true },
{ title: '链接OG信息', link: '/60sapi/实用功能/链接OG信息/index.html', icon: '🔗', IsShow: false },
{ title: '密码强度检测', link: '/60sapi/实用功能/密码强度检测/index.html', icon: '🔐', IsShow: true },
{ title: '农历信息', link: '/60sapi/实用功能/农历信息/index.html', icon: '📅', IsShow: true },
{ title: '配色方案', link: '/60sapi/实用功能/配色方案/index.html', icon: '🎨', IsShow: true },
{ title: '身体健康分析', link: '/60sapi/实用功能/身体健康分析/index.html', icon: '🏥', IsShow: true },
{ title: '生成二维码', link: '/60sapi/实用功能/生成二维码/index.html', icon: '📱', IsShow: true },
{ title: '二维码生成', link: '/60sapi/实用功能/生成二维码/index.html', icon: '📱', IsShow: true },
{ title: '随机密码生成器', link: '/60sapi/实用功能/随机密码生成器/index.html', icon: '🔒', IsShow: true },
{ title: '随机颜色', link: '/60sapi/实用功能/随机颜色/index.html', icon: '🌈', IsShow: true },
{ title: '随机颜色生成器', link: '/60sapi/实用功能/随机颜色/index.html', icon: '🌈', IsShow: true },
{ title: '天气预报', link: '/60sapi/实用功能/天气预报/index.html', icon: '🌤️', IsShow: true },
{ title: 'EpicGames免费游戏', link: '/60sapi/实用功能/EpicGames免费游戏/index.html', icon: '🎮', IsShow: true },
{ title: '在线机器翻译', link: '/60sapi/实用功能/在线翻译/index.html', icon: '🌍', IsShow: false },
//新增的
{ title: '在线白板', link: '/toolbox/白板/index.html', icon: '🀆', IsShow: true },
{ title: '记事本', link: '/toolbox/记事本/index.html', icon: '🗒️', IsShow: true },
{ title: '视频播放器', link: '/toolbox/视频播放器/index.html', icon: '▶️', IsShow: true },
{ title: '图片转Base64编码', link: '/toolbox/图片转Base64编码/index.html', icon: '🔀', IsShow: true },
{ title: '在线JavaScript执行', link: '/toolbox/在线JavaScript执行/index.html', icon: '🟩', IsShow: true },
{ title: 'Json编辑器', link: '/toolbox/Json编辑器/index.html', icon: '📑', IsShow: true },
{ title: 'Markdown解析器', link: '/toolbox/Markdown解析器/index.html', icon: '⌨︎', IsShow: true },
{ title: 'AI聊天', link: '/toolbox/AI聊天/index.html', icon: '🤖', IsShow: true },
{ title: '随机数生成器', link: '/toolbox/随机数生成器/index.html', icon: '🎲', IsShow: true },
{ title: '图片黑白处理', link: '/toolbox/图片黑白处理/index.html', icon: '🖻', IsShow: true },
{ title: '图片圆角处理', link: '/toolbox/图片圆角处理/index.html', icon: '🖼', IsShow: true },
]
},
{
title: '娱乐消遣',
icon: '🎉',
color: '#f7b731',
description: '轻松有趣的娱乐内容,为您的闲暇时光增添乐趣',
apis: [
{ title: '随机唱歌音频', link: '/60sapi/娱乐消遣/随机唱歌音频/index.html', icon: '🎤', IsShow: true },
{ title: '哼歌一曲', link: '/60sapi/娱乐消遣/随机唱歌音频/index.html', icon: '🎤', IsShow: true },
{ title: '随机发病文学', link: '/60sapi/娱乐消遣/随机发病文学/index.html', icon: '📖', IsShow: false },
{ title: '随机搞笑段子', link: '/60sapi/娱乐消遣/随机搞笑段子/index.html', icon: '😂', IsShow: true },
{ title: '随机冷笑话', link: '/60sapi/娱乐消遣/随机冷笑话/index.html', icon: '😄', IsShow: true },
{ title: '段子游乐场', link: '/60sapi/娱乐消遣/随机搞笑段子/index.html', icon: '😂', IsShow: true },
{ title: '冷笑话大全', link: '/60sapi/娱乐消遣/随机冷笑话/index.html', icon: '😄', IsShow: true },
{ title: '随机一言', link: '/60sapi/娱乐消遣/随机一言/index.html', icon: '💭', IsShow: true },
{ title: '随机运势', link: '/60sapi/娱乐消遣/随机运势/index.html', icon: '⭐', IsShow: true },
{ title: '随机JavaScript趣味题', link: '/60sapi/娱乐消遣/随机JavaScript趣味题/index.html', icon: '💻', IsShow: true },
{ title: '随机KFC文案', link: '/60sapi/娱乐消遣/随机KFC文案/index.html', icon: '🍗', IsShow: true }
{ title: '水晶球占卜', link: '/60sapi/娱乐消遣/随机运势/index.html', icon: '⭐', IsShow: true },
{ title: 'JavaScript趣味题', link: '/60sapi/娱乐消遣/随机JavaScript趣味题/index.html', icon: '💻', IsShow: true },
{ title: '疯狂星期四', link: '/60sapi/娱乐消遣/随机KFC文案/index.html', icon: '🍗', IsShow: true },
{ title: '真理之道', link: '/60sapi/娱乐消遣/随机答案之书/index.html', icon: '📘', IsShow: true },
{ title: '随机Emoji', link: '/toolbox/随机Emoji表情/index.html', icon: '😊', IsShow: true },
{ title: '人生倒计时', link: '/toolbox/人生倒计时/index.html', icon: '⏲️', IsShow: true },
{ title: '数字时钟', link: '/toolbox/数字时钟/index.html', icon: '⏲︎', IsShow: true },
{ title: '做决定转盘', link: '/toolbox/做决定转盘/index.html', icon: '🎡', IsShow: true },
]
}
];

View File

@@ -1,11 +1,26 @@
// 环境配置文件
// 统一管理所有环境变量配置
// 获取环境变量中的 API URL如果为空则使用当前域名
const getApiUrl = () => {
// 优先使用环境变量
const envApiUrl = process.env.REACT_APP_API_URL;
// 如果环境变量存在且不为空,使用环境变量
if (envApiUrl && envApiUrl.trim() !== '') {
return envApiUrl;
}
// 否则使用当前域名Docker 环境)
return window.location.origin;
};
// 统一环境配置
const config = {
//API_URL: 'https://infogenie.api.shumengya.top',
API_URL: 'http://127.0.0.1:5002', // 确保本地开发环境正常工作
DEBUG: true,
API_URL: getApiUrl(), // Docker 环境: 使用当前域名,开发环境可通过 .env 配置
//API_URL: 'http://127.0.0.1:5002', // 本地开发环境(在 .env.development 中配置)
//API_URL: 'https://infogenie.api.shumengya.top', // 原生产环境(在 .env.production 中配置)
DEBUG: process.env.REACT_APP_DEBUG === 'true',
LOG_LEVEL: 'debug'
};

View File

@@ -34,7 +34,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 44.8px;
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -224,7 +224,7 @@ const AboutPage = () => {
<PageHeader>
<PageTitle>关于我们</PageTitle>
<PageDescription>
<strong>了解万象口袋的更多信息和功能特色(,,ω,,)</strong>
<strong style={{ color: '#ffffff' }}>了解万象口袋的更多信息和功能特色(,,ω,,)</strong>
</PageDescription>
</PageHeader>
@@ -251,7 +251,6 @@ const AboutPage = () => {
</LinkIcon>
<LinkContent>
<LinkTitle>Web端在线体验</LinkTitle>
<LinkUrl>https://infogenie.shumengya.top</LinkUrl>
</LinkContent>
</LinkCard>
@@ -261,7 +260,6 @@ const AboutPage = () => {
</LinkIcon>
<LinkContent>
<LinkTitle>最新版下载地址</LinkTitle>
<LinkUrl>https://work.shumengya.top/#/work/InfoGenie</LinkUrl>
</LinkContent>
</LinkCard>
</LinksSection>

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { FiCpu, FiUser, FiExternalLink, FiArrowLeft } from 'react-icons/fi';
import { useUser } from '../contexts/UserContext';
@@ -13,6 +14,7 @@ const AiContainer = styled.div`
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
position: relative;
@keyframes pageEnter {
0% {
@@ -39,7 +41,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 44.8px;
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -62,11 +64,20 @@ const PageDescription = styled.p`
const LoginPrompt = styled.div`
background: white;
border-radius: 16px;
border-radius: 0;
padding: 60px 40px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-bottom: 40px;
box-shadow: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const AppGrid = styled.div`
@@ -74,6 +85,18 @@ const AppGrid = styled.div`
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 40px;
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
@media (max-width: 480px) {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
`;
const AppCard = styled.div`
@@ -91,6 +114,16 @@ const AppCard = styled.div`
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: #4ade80;
}
@media (max-width: 768px) {
padding: 18px;
border-radius: 12px;
}
@media (max-width: 480px) {
padding: 16px;
border-radius: 10px;
}
`;
const AppHeader = styled.div`
@@ -105,11 +138,27 @@ const AppTitle = styled.h3`
font-weight: bold;
color: #1f2937;
margin: 0;
@media (max-width: 768px) {
font-size: 18px;
}
@media (max-width: 480px) {
font-size: 16px;
}
`;
const AppIcon = styled.div`
font-size: 24px;
color: #4ade80;
@media (max-width: 768px) {
font-size: 22px;
}
@media (max-width: 480px) {
font-size: 20px;
}
`;
const AppDescription = styled.p`
@@ -117,6 +166,18 @@ const AppDescription = styled.p`
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
@media (max-width: 768px) {
font-size: 13px;
margin-bottom: 14px;
line-height: 1.4;
}
@media (max-width: 480px) {
font-size: 12px;
margin-bottom: 12px;
line-height: 1.3;
}
`;
const AppFooter = styled.div`
@@ -135,6 +196,20 @@ const AppTheme = styled.div`
font-size: 24px;
background: rgba(74, 222, 128, 0.1);
border: 1px solid rgba(74, 222, 128, 0.3);
@media (max-width: 768px) {
width: 36px;
height: 36px;
font-size: 20px;
border-radius: 6px;
}
@media (max-width: 480px) {
width: 32px;
height: 32px;
font-size: 18px;
border-radius: 5px;
}
`;
const LaunchButton = styled.button`
@@ -155,6 +230,20 @@ const LaunchButton = styled.button`
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(74, 222, 128, 0.3);
}
@media (max-width: 768px) {
padding: 7px 14px;
font-size: 13px;
border-radius: 6px;
gap: 5px;
}
@media (max-width: 480px) {
padding: 6px 12px;
font-size: 12px;
border-radius: 5px;
gap: 4px;
}
`;
const LoginIcon = styled.div`
@@ -196,68 +285,237 @@ const LoginButton = styled.button`
}
`;
const EmbeddedContainer = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
`;
// 独立全屏嵌套网页组件
const FullscreenEmbeddedPage = ({ app, onClose }) => {
useEffect(() => {
// 禁用页面滚动
document.body.style.overflow = 'hidden';
// 键盘事件监听
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
// 恢复页面滚动
document.body.style.overflow = 'auto';
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
const EmbeddedContent = styled.div`
background: white;
border-radius: 0;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
box-shadow: none;
`;
const fullscreenStyles = {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: '#ffffff',
zIndex: 999999,
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
margin: 0,
padding: 0,
boxSizing: 'border-box',
// 重置所有可能的继承样式
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const EmbeddedHeader = styled.div`
background: linear-gradient(135deg, #4ade80, #22c55e);
color: white;
padding: 15px 20px;
padding-top: max(15px, env(safe-area-inset-top));
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1001;
`;
const headerStyles = {
backgroundColor: '#4ade80',
color: '#ffffff',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
flexShrink: 0,
minHeight: '56px',
boxSizing: 'border-box',
margin: 0,
border: 'none',
borderRadius: 0,
fontSize: '16px',
fontWeight: 'normal',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible'
};
const BackButton = styled.button`
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
const titleStyles = {
fontSize: '18px',
fontWeight: '500',
margin: 0,
padding: 0,
color: '#ffffff',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const EmbeddedFrame = styled.iframe`
width: 100%;
height: calc(100% - 60px);
border: none;
background: white;
position: relative;
z-index: 1000;
`;
const backButtonStyles = {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
color: '#ffffff',
border: 'none',
padding: '8px 16px',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '14px',
fontWeight: '500',
transition: 'background-color 0.2s ease',
margin: 0,
textAlign: 'center',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
outline: 'none',
transform: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const iframeStyles = {
width: '100%',
height: 'calc(100vh - 56px)',
border: 'none',
backgroundColor: '#ffffff',
flexGrow: 1,
margin: 0,
padding: 0,
boxSizing: 'border-box',
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const handleBackButtonHover = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';
};
const handleBackButtonLeave = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
};
// 在iframe加载时注入token
const handleIframeLoad = (e) => {
try {
const iframe = e.target;
const token = localStorage.getItem('token');
if (iframe && iframe.contentWindow && token) {
// 将token传递给iframe
iframe.contentWindow.localStorage.setItem('token', token);
}
} catch (error) {
console.error('iframe通信错误:', error);
}
};
return (
<div style={fullscreenStyles}>
<div style={headerStyles}>
<h1 style={titleStyles}>{app.title}</h1>
<button
style={backButtonStyles}
onClick={onClose}
onMouseEnter={handleBackButtonHover}
onMouseLeave={handleBackButtonLeave}
>
<FiArrowLeft size={16} />
返回
</button>
</div>
<iframe
src={app.link}
title={app.title}
style={iframeStyles}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="lazy"
onLoad={handleIframeLoad}
/>
</div>
);
};
@@ -303,23 +561,6 @@ const AiModelPage = () => {
const closeEmbedded = () => {
setEmbeddedApp(null);
};
// 在iframe加载时注入token
const handleIframeLoad = (e) => {
try {
const iframe = e.target;
const token = localStorage.getItem('token');
if (iframe && iframe.contentWindow && token) {
// 将token传递给iframe
iframe.contentWindow.localStorage.setItem('token', token);
// Token已传递给iframe
}
} catch (error) {
console.error('iframe通信错误:', error);
}
};
@@ -336,20 +577,11 @@ const AiModelPage = () => {
);
}
return (
<AiContainer>
<Container>
<PageHeader>
<PageTitle>
AI工具
</PageTitle>
<PageDescription>
<strong>AI大模型工具提供一些生成式大语言模型的小功能(´,,ω,,)</strong>
</PageDescription>
</PageHeader>
{!isLoggedIn ? (
<LoginPrompt>
if (!isLoggedIn) {
return (
<AiContainer>
<LoginPrompt>
<div>
<LoginIcon>🔒</LoginIcon>
<LoginTitle>需要登录访问Σ(°°)</LoginTitle>
<LoginText>
@@ -361,37 +593,58 @@ const AiModelPage = () => {
<FiUser />
立即登录
</LoginButton>
</LoginPrompt>
) : loadingApps ? (
</div>
</LoginPrompt>
</AiContainer>
);
}
return (
<AiContainer>
<Container>
<PageHeader>
<PageTitle>
AI工具
</PageTitle>
<PageDescription>
<strong style={{ color: '#ffffff' }}>AI大模型工具提供一些生成式大语言模型的小功能(´,,ω,,)</strong>
</PageDescription>
</PageHeader>
{loadingApps ? (
<LoginPrompt>
<LoginIcon>🤖</LoginIcon>
<LoginTitle>加载AI应用中...</LoginTitle>
<LoginText>
正在为您准备强大的AI工具请稍候...
</LoginText>
<div>
<LoginIcon>🤖</LoginIcon>
<LoginTitle>加载AI应用中...</LoginTitle>
<LoginText>
正在为您准备强大的AI工具请稍候...
</LoginText>
</div>
</LoginPrompt>
) : error ? (
<LoginPrompt>
<LoginIcon>😅</LoginIcon>
<LoginTitle>加载失败</LoginTitle>
<LoginText>
{error}
<br />
<button
onClick={fetchApps}
style={{
background: 'linear-gradient(135deg, #4ade80 0%, #22c55e 100%)',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '8px',
cursor: 'pointer',
marginTop: '16px'
}}
>
重新加载
</button>
</LoginText>
<div>
<LoginIcon>😅</LoginIcon>
<LoginTitle>加载失败</LoginTitle>
<LoginText>
{error}
<br />
<button
onClick={fetchApps}
style={{
background: 'linear-gradient(135deg, #4ade80 0%, #22c55e 100%)',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '8px',
cursor: 'pointer',
marginTop: '16px'
}}
>
重新加载
</button>
</LoginText>
</div>
</LoginPrompt>
) : apps.length > 0 ? (
<AppGrid>
@@ -419,11 +672,13 @@ const AiModelPage = () => {
</AppGrid>
) : (
<LoginPrompt>
<LoginIcon>🎯</LoginIcon>
<LoginTitle>暂无AI应用</LoginTitle>
<LoginText>
目前还没有可用的AI应用请稍后再来查看
</LoginText>
<div>
<LoginIcon>🎯</LoginIcon>
<LoginTitle>暂无AI应用</LoginTitle>
<LoginText>
目前还没有可用的AI应用请稍后再来查看
</LoginText>
</div>
</LoginPrompt>
)}
@@ -445,36 +700,25 @@ const AiModelPage = () => {
marginTop: 0
}}>
<span style={{ fontSize: '24px' }}>💰</span>
萌芽币消费提示
AI工具使用提示
</h3>
<p style={{ lineHeight: '1.6', color: '#374151' }}>
每次使用AI功能将消耗<b>100萌芽币</b>使AI
每次使用AI功能将消耗<b>100萌芽币</b>使AI
</p>
<p style={{ lineHeight: '1.6', color: '#374151' }}>
您可以通过<b>每日签到</b>300
您可以通过<b>每日签到</b><b></b>
</p>
</div>
)}
{/* 内嵌显示组件 */}
{embeddedApp && (
<EmbeddedContainer onClick={closeEmbedded}>
<EmbeddedContent onClick={(e) => e.stopPropagation()}>
<EmbeddedHeader>
<h3>{embeddedApp.title}</h3>
<BackButton onClick={closeEmbedded}>
<FiArrowLeft />
返回
</BackButton>
</EmbeddedHeader>
<EmbeddedFrame
src={embeddedApp.link}
title={embeddedApp.title}
onLoad={handleIframeLoad}
/>
</EmbeddedContent>
</EmbeddedContainer>
)}
{/* 使用Portal渲染独立的全屏嵌套网页 */}
{embeddedApp && createPortal(
<FullscreenEmbeddedPage
app={embeddedApp}
onClose={closeEmbedded}
/>,
document.body
)}
</Container>

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { FiExternalLink, FiArrowLeft } from 'react-icons/fi';
import { API_60S_CATEGORIES } from '../config/StaticPageConfig';
@@ -37,7 +38,7 @@ const Header = styled.div`
const Title = styled.h1`
color: white;
font-size: 32px;
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -104,7 +105,7 @@ const CategoryDescription = styled.p`
font-size: 17px;
margin: 0 0 20px 0;
line-height: 1.4;
font-weight: 400;
font-weight: 700;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
`;
@@ -257,17 +258,23 @@ const CardIcon = styled.div`
`;
const CardTitle = styled.h3`
font-size: 15px;
font-size: calc(15px * 1.05);
font-weight: 600;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
color: #2e7d32;
margin: 0;
flex: 1;
line-height: 1.3;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
transition: all 0.2s ease;
/* 强化加粗在中文上的可见度 */
strong {
font-weight: 800;
}
@media (max-width: 768px) {
font-size: 14px;
font-size: calc(14px * 1.05);
}
${ApiCard}:hover & {
@@ -297,68 +304,220 @@ const ExternalIcon = styled.div`
const EmbeddedContainer = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
`;
// 独立全屏嵌套网页组件
const FullscreenEmbeddedPage = ({ api, onClose }) => {
useEffect(() => {
// 禁用页面滚动
document.body.style.overflow = 'hidden';
// 键盘事件监听
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
// 恢复页面滚动
document.body.style.overflow = 'auto';
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
const EmbeddedContent = styled.div`
background: white;
border-radius: 0;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
box-shadow: none;
`;
const fullscreenStyles = {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: '#ffffff',
zIndex: 999999,
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
margin: 0,
padding: 0,
boxSizing: 'border-box',
// 重置所有可能的继承样式
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const EmbeddedHeader = styled.div`
background: linear-gradient(135deg, #4caf50, #66bb6a);
color: white;
padding: 15px 20px;
padding-top: max(15px, env(safe-area-inset-top));
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1001;
`;
const headerStyles = {
backgroundColor: '#81C784',
color: '#ffffff',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
flexShrink: 0,
minHeight: '56px',
boxSizing: 'border-box',
margin: 0,
border: 'none',
borderRadius: 0,
fontSize: '16px',
fontWeight: 'normal',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible'
};
const BackButton = styled.button`
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
const titleStyles = {
fontSize: '18px',
fontWeight: '500',
margin: 0,
padding: 0,
color: '#ffffff',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const EmbeddedFrame = styled.iframe`
width: 100%;
height: calc(100% - 60px);
border: none;
background: white;
position: relative;
z-index: 1000;
`;
const backButtonStyles = {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
color: '#ffffff',
border: 'none',
padding: '8px 16px',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '14px',
fontWeight: '500',
transition: 'background-color 0.2s ease',
margin: 0,
textAlign: 'center',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
outline: 'none',
transform: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const iframeStyles = {
width: '100%',
height: 'calc(100vh - 56px)',
border: 'none',
backgroundColor: '#ffffff',
flexGrow: 1,
margin: 0,
padding: 0,
boxSizing: 'border-box',
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const handleBackButtonHover = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';
};
const handleBackButtonLeave = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
};
return (
<div style={fullscreenStyles}>
<div style={headerStyles}>
<h1 style={titleStyles}>{api.title}</h1>
<button
style={backButtonStyles}
onClick={onClose}
onMouseEnter={handleBackButtonHover}
onMouseLeave={handleBackButtonLeave}
>
返回
</button>
</div>
<iframe
src={api.link}
title={api.title}
style={iframeStyles}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
loading="lazy"
/>
</div>
);
};
//================css样式================
const Api60sPage = () => {
@@ -367,16 +526,7 @@ const Api60sPage = () => {
const [loading, setLoading] = useState(true);
const [embeddedApi, setEmbeddedApi] = useState(null);
// 获取分类描述文字
const getCategoryDescription = (categoryTitle) => {
const descriptions = {
'热搜榜单': '实时追踪各大平台热门话题,掌握最新网络动态和流行趋势',
'日更资讯': '每日精选优质内容,提供最新资讯和实用信息',
'实用功能': '集成多种便民工具,让生活和工作更加便捷高效',
'娱乐消遣': '轻松有趣的娱乐内容,为您的闲暇时光增添乐趣'
};
return descriptions[categoryTitle] || '';
};
// 从配置文件获取60s API数据
const scanApiModules = async () => {
@@ -437,7 +587,7 @@ const Api60sPage = () => {
<Header>
<Title>聚合应用</Title>
<Subtitle>
<strong>提供各大社交平台最新的实时数据让您摆脱平台大数据算法的干扰走出信息茧房涵盖热搜榜单日更资讯实用工具和娱乐消遣四大板块(˘ω˘)</strong>
<strong style={{ color: '#ffffff' }}>提供各大社交平台最新的实时数据让您摆脱平台大数据算法的干扰走出信息茧房涵盖热搜榜单日更资讯实用工具和娱乐消遣四大板块(˘ω˘)</strong>
</Subtitle>
</Header>
@@ -456,7 +606,7 @@ const Api60sPage = () => {
{category.title}
</CategoryTitle>
<CategoryDescription>
{getCategoryDescription(category.title)}
{category.description}
</CategoryDescription>
<CategoryGrid>
{category.apis.map((api, apiIndex) => (
@@ -469,10 +619,8 @@ const Api60sPage = () => {
<CardIcon color={category.color}>
{api.icon || category.icon}
</CardIcon>
<CardTitle>{api.title}</CardTitle>
<ExternalIcon>
<FiExternalLink />
</ExternalIcon>
<CardTitle><strong>{api.title}</strong></CardTitle>
</CardHeader>
</ApiCard>
))}
@@ -482,23 +630,13 @@ const Api60sPage = () => {
)}
</Container>
{/* 内嵌显示组件 */}
{embeddedApi && (
<EmbeddedContainer onClick={closeEmbedded}>
<EmbeddedContent onClick={(e) => e.stopPropagation()}>
<EmbeddedHeader>
<h3>{embeddedApi.title}</h3>
<BackButton onClick={closeEmbedded}>
<FiArrowLeft />
返回
</BackButton>
</EmbeddedHeader>
<EmbeddedFrame
src={embeddedApi.link}
title={embeddedApi.title}
/>
</EmbeddedContent>
</EmbeddedContainer>
{/* 使用Portal渲染独立的全屏嵌套网页 */}
{embeddedApi && createPortal(
<FullscreenEmbeddedPage
api={embeddedApi}
onClose={closeEmbedded}
/>,
document.body
)}
</Api60sContainer>
);

View File

@@ -41,16 +41,19 @@ const HeroTitle = styled.h1`
font-weight: bold;
color: #1f2937;
margin-bottom: 16px;
text-shadow:
/* 添加柔和的金色光晕效果(不强烈) */
text-shadow:
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(129, 199, 132, 0.2);
0 0 8px rgba(255, 215, 0, 0.18),
0 0 14px rgba(255, 215, 0, 0.12);
position: relative;
.title-emoji {
margin: 0 8px;
display: inline-block;
animation: float 3s ease-in-out infinite;
filter: drop-shadow(0 2px 4px rgba(129, 199, 132, 0.3));
/* 与标题保持一致的金色氛围 */
filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.3));
}
&::after {
@@ -78,13 +81,35 @@ const HeroTitle = styled.h1`
const HeroSubtitle = styled.p`
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
color: rgba(255, 255, 255, 0.95);
margin-bottom: 32px;
line-height: 1.6;
line-height: 1.7;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 16px 20px 16px 24px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.22);
box-shadow:
0 8px 24px rgba(129, 199, 132, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
backdrop-filter: blur(6px);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
border-radius: 12px 0 0 12px;
background: linear-gradient(180deg, #81c784, #a5d6a7);
box-shadow: 0 0 0 1px rgba(129, 199, 132, 0.2);
}
@media (max-width: 768px) {
font-size: 16px;
padding: 12px 16px 12px 20px;
}
`;
@@ -387,42 +412,72 @@ const ModuleDescription = styled.p`
}
`;
const ModuleFeatures = styled.ul`
list-style: none;
padding: 0;
const ModuleFeatures = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 16px;
margin: 0;
padding: 0;
@media (max-width: 480px) {
grid-template-columns: 1fr;
gap: 10px;
}
`;
const ModuleFeature = styled.li`
const ModuleFeature = styled.div`
color: #374151;
font-size: 14px;
margin-bottom: 8px;
padding-left: 20px;
position: relative;
padding: 8px 12px;
background: rgba(129, 199, 132, 0.08);
border-radius: 8px;
border: 1px solid rgba(129, 199, 132, 0.15);
transition: all 0.3s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
&:before {
content: '';
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
color: #10b981;
font-weight: bold;
text-shadow: 0 1px 2px rgba(16, 185, 129, 0.3);
width: 3px;
height: 100%;
background: linear-gradient(180deg, #81c784, #a5d6a7);
transition: all 0.3s ease;
}
&:last-child {
margin-bottom: 0;
&::after {
content: '';
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
width: 6px;
height: 6px;
background: #10b981;
border-radius: 50%;
opacity: 0.6;
transition: all 0.3s ease;
}
${ModuleCard}:hover & {
color: #1f2937;
transform: translateX(2px);
background: rgba(129, 199, 132, 0.12);
border-color: rgba(129, 199, 132, 0.25);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(129, 199, 132, 0.15);
&:before {
color: #059669;
transform: scale(1.1);
&::before {
width: 4px;
background: linear-gradient(180deg, #66bb6a, #81c784);
}
&::after {
background: #059669;
opacity: 1;
transform: translateY(-50%) scale(1.2);
}
}
`;
@@ -460,7 +515,7 @@ const HomePage = () => {
path: '/aimodel',
icon: FiCpu,
title: 'AI工具',
description: '智能AI工具和模型应用',
description: '智能AI工具和模型应用',
features: [
'🌍翻译助手',
'📝写诗达人',
@@ -482,13 +537,13 @@ const HomePage = () => {
<HeroSubtitle>
<strong>💡一个跨平台的聚合式应用</strong>
<br />
集成了热搜榜单资讯休闲游戏AI大模型工具等丰富功能
像微信小程序一样把分散的功能汇聚在一个应用中让你无需下载一个又一个app
热搜榜单日资讯轻松小游戏以及 AI 大模型工具等常用功能全部装进一个 App
告别满屏 App 的零碎与冗余释放存储空间让你的时间与注意力回归真正重要的事
<br />
<strong>🎯功能繁多却不显得臃肿</strong>
<strong>🎯丰富不臃肿强大而精巧</strong>
<br />
Windows端仅约18MBAndroid端仅约15MB平均内存占用仅48MB
相比市面上的软件更小巧更轻便却能承载更多可能
每项功能都小而美精而全像一把随身携带的瑞士军刀
应对从信息娱乐到效率的各种场景需求无需来回切换一切尽在掌控之中
</HeroSubtitle>
<HeroButton to="/60sapi">
<FiTrendingUp />

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { FiPlay, FiExternalLink, FiArrowLeft } from 'react-icons/fi';
import { SMALL_GAMES } from '../config/StaticPageConfig';
@@ -35,7 +36,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 44.8px;
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -61,6 +62,18 @@ const GameGrid = styled.div`
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 40px;
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
@media (max-width: 480px) {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
`;
const GameCard = styled.div`
@@ -77,6 +90,16 @@ const GameCard = styled.div`
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: #4ade80;
}
@media (max-width: 768px) {
padding: 18px;
border-radius: 12px;
}
@media (max-width: 480px) {
padding: 16px;
border-radius: 10px;
}
`;
const GameHeader = styled.div`
@@ -91,11 +114,27 @@ const GameTitle = styled.h3`
font-weight: bold;
color: #1f2937;
margin: 0;
@media (max-width: 768px) {
font-size: 18px;
}
@media (max-width: 480px) {
font-size: 16px;
}
`;
const GameIcon = styled.div`
font-size: 24px;
color: #4ade80;
@media (max-width: 768px) {
font-size: 22px;
}
@media (max-width: 480px) {
font-size: 20px;
}
`;
const GameDescription = styled.p`
@@ -103,6 +142,18 @@ const GameDescription = styled.p`
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
@media (max-width: 768px) {
font-size: 13px;
margin-bottom: 14px;
line-height: 1.4;
}
@media (max-width: 480px) {
font-size: 12px;
margin-bottom: 12px;
line-height: 1.3;
}
`;
const GameFooter = styled.div`
@@ -121,6 +172,20 @@ const GameTheme = styled.div`
font-size: 24px;
background: rgba(74, 222, 128, 0.1);
border: 1px solid rgba(74, 222, 128, 0.3);
@media (max-width: 768px) {
width: 36px;
height: 36px;
font-size: 20px;
border-radius: 6px;
}
@media (max-width: 480px) {
width: 32px;
height: 32px;
font-size: 18px;
border-radius: 5px;
}
`;
const PlayButton = styled.button`
@@ -141,6 +206,20 @@ const PlayButton = styled.button`
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(74, 222, 128, 0.3);
}
@media (max-width: 768px) {
padding: 7px 14px;
font-size: 13px;
border-radius: 6px;
gap: 5px;
}
@media (max-width: 480px) {
padding: 6px 12px;
font-size: 12px;
border-radius: 5px;
gap: 4px;
}
`;
const LoadingCard = styled.div`
@@ -171,68 +250,226 @@ const LoadingText = styled.p`
margin-bottom: 24px;
`;
const EmbeddedContainer = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
`;
// 独立全屏嵌套网页组件
const FullscreenEmbeddedPage = ({ game, onClose }) => {
useEffect(() => {
// 禁用页面滚动
document.body.style.overflow = 'hidden';
// 键盘事件监听
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
// 恢复页面滚动
document.body.style.overflow = 'auto';
document.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]);
const EmbeddedContent = styled.div`
background: white;
border-radius: 0;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
box-shadow: none;
`;
const fullscreenStyles = {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: '#ffffff',
zIndex: 999999,
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
margin: 0,
padding: 0,
boxSizing: 'border-box',
// 重置所有可能的继承样式
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const EmbeddedHeader = styled.div`
background: linear-gradient(135deg, #4ade80, #22c55e);
color: white;
padding: 15px 20px;
padding-top: max(15px, env(safe-area-inset-top));
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1001;
`;
const headerStyles = {
backgroundColor: '#4ade80',
color: '#ffffff',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
flexShrink: 0,
minHeight: '56px',
boxSizing: 'border-box',
margin: 0,
border: 'none',
borderRadius: 0,
fontSize: '16px',
fontWeight: 'normal',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible'
};
const BackButton = styled.button`
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
`;
const titleStyles = {
fontSize: '18px',
fontWeight: '500',
margin: 0,
padding: 0,
color: '#ffffff',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
border: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const EmbeddedFrame = styled.iframe`
width: 100%;
height: calc(100% - 60px);
border: none;
background: white;
position: relative;
z-index: 1000;
`;
const backButtonStyles = {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
color: '#ffffff',
border: 'none',
padding: '8px 16px',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '14px',
fontWeight: '500',
transition: 'background-color 0.2s ease',
margin: 0,
textAlign: 'center',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
outline: 'none',
transform: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'visible',
boxSizing: 'border-box'
};
const iframeStyles = {
width: '100%',
height: 'calc(100vh - 56px)',
border: 'none',
backgroundColor: '#ffffff',
flexGrow: 1,
margin: 0,
padding: 0,
boxSizing: 'border-box',
fontSize: '16px',
lineHeight: '1.5',
color: '#333',
textAlign: 'left',
textDecoration: 'none',
textTransform: 'none',
letterSpacing: 'normal',
wordSpacing: 'normal',
textShadow: 'none',
boxShadow: 'none',
borderRadius: 0,
outline: 'none',
transform: 'none',
transition: 'none',
animation: 'none',
filter: 'none',
backdropFilter: 'none',
opacity: 1,
visibility: 'visible',
overflow: 'hidden'
};
const handleBackButtonHover = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.25)';
};
const handleBackButtonLeave = (e) => {
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
};
return (
<div style={fullscreenStyles}>
<div style={headerStyles}>
<h1 style={titleStyles}>{game.title}</h1>
<button
style={backButtonStyles}
onClick={onClose}
onMouseEnter={handleBackButtonHover}
onMouseLeave={handleBackButtonLeave}
>
<FiArrowLeft size={16} />
返回
</button>
</div>
<iframe
src={game.link}
title={game.title}
style={iframeStyles}
allow="keyboard-map *"
allowFullScreen
loading="lazy"
tabIndex="0"
onLoad={(e) => {
// 确保iframe获得焦点以接收键盘事件
e.target.focus();
}}
/>
</div>
);
};
@@ -281,7 +518,7 @@ const SmallGamePage = () => {
休闲游戏
</PageTitle>
<PageDescription>
<strong>好玩的休闲小游戏合集即点即玩无需下载支持离线游玩(,,ω,,)</strong>
<strong style={{ color: '#ffffff' }}>好玩的休闲小游戏合集即点即玩无需下载支持离线游玩(,,ω,,)</strong>
</PageDescription>
</PageHeader>
@@ -350,29 +587,13 @@ const SmallGamePage = () => {
</LoadingCard>
)}
{/* 内嵌显示组件 */}
{embeddedGame && (
<EmbeddedContainer onClick={closeEmbedded}>
<EmbeddedContent onClick={(e) => e.stopPropagation()}>
<EmbeddedHeader>
<h3>{embeddedGame.title}</h3>
<BackButton onClick={closeEmbedded}>
<FiArrowLeft />
返回
</BackButton>
</EmbeddedHeader>
<EmbeddedFrame
src={embeddedGame.link}
title={embeddedGame.title}
allow="keyboard-map *"
tabIndex="0"
onLoad={(e) => {
// 确保iframe获得焦点以接收键盘事件
e.target.focus();
}}
/>
</EmbeddedContent>
</EmbeddedContainer>
{/* 使用Portal渲染独立的全屏嵌套网页 */}
{embeddedGame && createPortal(
<FullscreenEmbeddedPage
game={embeddedGame}
onClose={closeEmbedded}
/>,
document.body
)}

View File

@@ -12,6 +12,7 @@ const ProfileContainer = styled.div`
opacity: 0;
transform: translateY(20px);
animation: pageEnter 0.8s ease-out forwards;
position: relative;
@keyframes pageEnter {
0% {
@@ -38,7 +39,7 @@ const PageHeader = styled.div`
const PageTitle = styled.h1`
color: white;
font-size: 44.8px;
font-size: 40px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
@@ -283,14 +284,22 @@ const SuccessMessage = styled.div`
`;
const LoginPrompt = styled.div`
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
background: white;
border-radius: 0;
padding: 60px 40px;
text-align: center;
box-shadow: 0 8px 32px rgba(168, 230, 207, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(168, 230, 207, 0.2);
margin-bottom: 40px;
box-shadow: none;
border: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const LoginIcon = styled.div`
@@ -436,14 +445,8 @@ const UserProfilePage = () => {
if (!isLoggedIn) {
return (
<ProfileContainer>
<Container>
<PageHeader>
<PageTitle>个人中心</PageTitle>
<PageDescription>
<strong>管理您的个人信息查看萌芽币余额和签到记录(,,ω,,)</strong>
</PageDescription>
</PageHeader>
<LoginPrompt>
<LoginPrompt>
<div>
<LoginIcon>🔒</LoginIcon>
<LoginTitle>需要登录才能访问Σ(°°)</LoginTitle>
<LoginText>
@@ -455,8 +458,8 @@ const UserProfilePage = () => {
<FiUser />
立即登录
</LoginButton>
</LoginPrompt>
</Container>
</div>
</LoginPrompt>
</ProfileContainer>
);
}
@@ -494,10 +497,12 @@ const UserProfilePage = () => {
return (
<ProfileContainer>
<Container>
<PageHeader>
<PageTitle>个人中心</PageTitle>
<PageDescription>管理您的个人信息查看萌芽币余额和签到记录</PageDescription>
</PageHeader>
<PageHeader>
<PageTitle>个人中心</PageTitle>
<PageDescription>
<strong style={{ color: '#ffffff' }}>管理您的个人信息查看萌芽币余额和签到记录(,,ω,,)</strong>
</PageDescription>
</PageHeader>
<ProfileHeader>
<Avatar>
{qqAvatarUrl && !avatarError ? (
@@ -528,7 +533,7 @@ const UserProfilePage = () => {
<FiTrendingUp />
</StatIcon>
<StatValue>{gameData?.experience || 0}</StatValue>
<StatLabel>经验值 ({expProgress}%)</StatLabel>
<StatLabel>经验值</StatLabel>
</StatCard>
<StatCard>
@@ -544,7 +549,7 @@ const UserProfilePage = () => {
<FiCalendar />
</StatIcon>
<StatValue>{consecutiveDays}</StatValue>
<StatLabel>连续签到天数</StatLabel>
<StatLabel>签到天数</StatLabel>
</StatCard>
</StatsGrid>
@@ -561,20 +566,6 @@ const UserProfilePage = () => {
{checkinLoading ? '签到中...' : isCheckedInToday ? '今日已签到' : '立即签到'}
</CheckinButton>
<CheckinInfo>
<CheckinText>
签到奖励300 萌芽币 + 200 经验
</CheckinText>
<CheckinText>
升级公式100 × 1.2^(等级)
</CheckinText>
{consecutiveDays > 0 && (
<CheckinText>
当前连续签到{consecutiveDays}
</CheckinText>
)}
</CheckinInfo>
{checkinMessage && (
checkinSuccess ? (
<SuccessMessage>{checkinMessage}</SuccessMessage>